HEEx
Conditional rendering
Read time: 11 minutes
This guide is a direct continuation of the previous guide
git clone https://github.com/adopt-liveview/v2-myapp.git --branch events-done.
Let's learn some ways to render HTML depending on certain conditions. Let's update our PageLive:
#Using if-else for simple cases
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def mount(_params, _session, socket) do
socket = assign(socket, show_information?: false)
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<%= if @show_information? do %>
<p>You're an amazing person!</p>
<% else %>
<p>You can't see this message!</p>
<% end %>
</div>
<input type="button" value="Toggle" phx-click="toggle" />
"""
end
def handle_event("toggle", _params, socket) do
socket = assign(socket, show_information?: !socket.assigns.show_information?)
{:noreply, socket}
end
end
Let's break down this code. The only assign we have here is called show_information? with an initial value of false. The "toggle" event sent by the input simply reverses the value between true and false. What's really new here is our if-else block.
Question mark in the middle of the code? Is that even possible?
if @show_information? elegant?
Inside a LiveView you can do an if-else as follows:
-
Add a
<%= if condition do %>. It is important that you use the tag that contains=otherwise HEEx will understand that this should not be rendered! - Write any HTML that will be in the case that should be rendered.
-
Add an
<% else %>. Note that there is no=this time. If you add it, the code continues to work but a warning will ask to remove it. -
Write any HTML for the
elsecase. -
Add a
<% end %>. Again, without=.
If you don't want to show an else case there are two ways to do this. The first is simple: just remove the <% else %> and its contents! Update your page_live.ex:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def mount(_params, _session, socket) do
socket = assign(socket, show_information?: false)
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<%= if @show_information? do %>
<p>You're an amazing person!</p>
<% end %>
</div>
<input type="button" value="Toggle" phx-click="toggle" />
"""
end
def handle_event("toggle", _params, socket) do
socket = assign(socket, show_information?: !socket.assigns.show_information?)
{:noreply, socket}
end
end
#The special attribute :if
For cases where you only have if, HEEx has a special attribute called :if that you can place directly in the HTML tag. Once more update page_live.ex:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def mount(_params, _session, socket) do
socket = assign(socket, show_information?: false)
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<p :if={@show_information?}>You're an amazing person!</p>
</div>
<input type="button" value="Toggle" phx-click="toggle" />
"""
end
def handle_event("toggle", _params, socket) do
socket = assign(socket, show_information?: !socket.assigns.show_information?)
{:noreply, socket}
end
end
At the moment there is no special attribute for else so if you only need if it is recommended to use :if when you can put it in a parent tag of the things that enter the condition, otherwise use the first example with if-else as shown above.
#Using case for complex cases
It's only a matter of time before you end up in a situation where there are more than two possibilities for rendering something. Elixir does not support else if and for good reason: the preference is case which is much more powerful!
Let's create a simple tab system in LiveView.
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def mount(_params, _session, socket) do
socket = assign(socket, tab: "home")
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<%= case @tab do %>
<% "home" -> %>
<p>You're on my personal page!</p>
<% "about" -> %>
<p>Hi, I'm a LiveView developer!</p>
<% "contact" -> %>
<p>Mail me to bot [at] company [dot] com</p>
<% end %>
</div>
<input disabled={@tab == "home"} type="button" value="Open Home" phx-click="show_home" />
<input disabled={@tab == "about"} type="button" value="Open About" phx-click="show_about" />
<input disabled={@tab == "contact"} type="button" value="Open Contact" phx-click="show_contact" />
"""
end
def handle_event("show_" <> tab, _params, socket) do
socket = assign(socket, tab: tab)
{:noreply, socket}
end
end
This time our assign became tab which can be a string between "home", "about" or "contact". Each input contains a phx-click="show_TAB_NAME" so our handle_event/3 will use pattern matching in Elixir to accept any event that starts with show_ and save the rest of the event name in a variable. Another simple but interesting bit in our code is that we use the HTML disabled property to prevent the button from being clickable if you are already on the correct tab.
Pattern matching?!
Now let's talk about what's important for this lesson: case. Just like if you need to start the conditional with <%= case (condition here) do %>, emphasis on = because without it nothing will be rendered. Since our condition under case was @tab, each condition will essentially check @tab == 'value'. For each condition we do an <% "expected value" -> %> (without the need for = in the interpolation tag) and end the block with <% end %>.
It is worth mentioning that in our case statement we handled all possible scenarios. What if we forgot a possibility? Let's try this out:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def mount(_params, _session, socket) do
socket = assign(socket, tab: "home")
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
<%= case @tab do %>
<% "home" -> %>
<p>You're on my personal page!</p>
<% "about" -> %>
<p>Hi, I'm a LiveView developer!</p>
<% "contact" -> %>
<p>Mail me to bot [at] company [dot] com</p>
<% end %>
</div>
<input disabled={@tab == "home"} type="button" value="Open Home" phx-click="show_home" />
<input disabled={@tab == "about"} type="button" value="Open About" phx-click="show_about" />
<input disabled={@tab == "contact"} type="button" value="Open Contact" phx-click="show_contact" />
<input disabled={@tab == "blog"} type="button" value="Open Blog" phx-click="show_blog" />
"""
end
def handle_event("show_" <> tab, _params, socket) do
socket = assign(socket, tab: tab)
{:noreply, socket}
end
end
In this example we added a new button to show a blog tab but we did not add a clause in our case to handle this value from our assign. When you click on "Open Blog" you should notice that your LiveView resets to its original state and an exception appears in your terminal:
[debug] HANDLE EVENT "show_blog" in MyappWeb.PageLive
Parameters: %{"value" => "Open Blog"}
[debug] Replied in 78µs
[error] GenServer #PID<0.802.0> terminating
** (CaseClauseError) no case clause matching:
"blog"
(myapp 0.1.0) lib/myapp_web/live/page_live.ex:12: anonymous fn/2 in MyappWeb.PageLive.render/1
(phoenix_live_view 1.1.16) lib/phoenix_live_view/diff.ex:399: Phoenix.LiveView.Diff.traverse/6
(phoenix_live_view 1.1.16) lib/phoenix_live_view/diff.ex:146: Phoenix.LiveView.Diff.render/4
(phoenix_live_view 1.1.16) lib/phoenix_live_view/channel.ex:1018: anonymous fn/4 in Phoenix.LiveView.Channel.render_diff/3
(telemetry 1.3.0) /Users/joaoferreira/workspace/adopt-liveview-apps/myapp/deps/telemetry/src/telemetry.erl:324: :telemetry.span/3
(phoenix_live_view 1.1.16) lib/phoenix_live_view/channel.ex:1011: Phoenix.LiveView.Channel.render_diff/3
(phoenix_live_view 1.1.16) lib/phoenix_live_view/channel.ex:841: Phoenix.LiveView.Channel.handle_changed/4
(stdlib 7.0.1) gen_server.erl:2434: :gen_server.try_handle_info/3
(stdlib 7.0.1) gen_server.erl:2420: :gen_server.handle_msg/3
(stdlib 7.0.1) proc_lib.erl:333: :proc_lib.init_p_do_apply/3
Process Label: {Phoenix.LiveView, MyappWeb.PageLive, "lv:phx-GKk86LOWj7JUBQPB"}
Last message: %Phoenix.Socket.Message{topic: "lv:phx-GKk86LOWj7JUBQPB", event: "event", payload: %{"event" => "show_blog", "type" => "click", "value" => %{"value" => "Open Blog"}}, ref: "14", join_ref: "4"}
State: %{socket: #Phoenix.LiveView.Socket<id: "phx-GKk86LOWj7JUBQPB", endpoint: MyappWeb.Endpoint, view: MyappWeb.PageLive, parent_pid: nil, root_pid: #PID<0.802.0>, router: MyappWeb.Router, assigns: %{__changed__: %{}, flash: %{}, live_action: :home, tab: "contact"}, transport_pid: #PID<0.794.0>, sticky?: false, ...>, components: {%{}, %{}, 1}, topic: "lv:phx-GKk86LOWj7JUBQPB", serializer: Phoenix.Socket.V2.JSONSerializer, join_ref: "4", fingerprints: {87600012899249314488331332113278855451, %{0 => {124361009764341041662904844712747910554, %{}}}}, redirect_count: 0, upload_names: %{}, upload_pids: %{}}
The message could not be more explicit! Let's analyze each piece:
-
The exception is
CaseClauseErrormaking it obvious that acaseclause is missing. - The error message itself makes it clear that the missing clause is called "blog".
-
If you look at "Last message" you can see that the event that caused the problem was
"show_blog". This makes it easier for you to understand which part of your LiveView initiated the issue so that you can reproduce locally and handle that.
To add a default clause simply use the format <% _ -> %>. In Elixir the _ in the context of pattern matching means "anything". We could add default content like <p>Tab does not exist</p>.
What to do when we don't know how to handle all cases?
#Condition chains with cond
In the previous example we used case to compare the exact value of the @tab variable in each clause. If you need to render something based on a condition that isn't about equality, cond is perfect for this. Let's try this out:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def mount(_params, _session, socket) do
socket = assign(socket, temperature_celsius: 30)
{:ok, socket}
end
def render(assigns) do
~H"""
<div>
Current temperature: {@temperature_celsius}C
</div>
<div>
<%= cond do %>
<% @temperature_celsius > 40 -> %>
<p>🔥 Impossible to live 🔥</p>
<% @temperature_celsius > 30 -> %>
<p>Its hot</p>
<% @temperature_celsius > 20 -> %>
<p>Kinda cool</p>
<% @temperature_celsius > 10 -> %>
<p>Chill</p>
<% @temperature_celsius > 0 -> %>
<p>Chill</p>
<% true -> %>
<p>❄️⛄️</p>
<% end %>
</div>
<input type="button" value="Increase" phx-click="increase" />
<input type="button" value="Decrease" phx-click="decrease" />
"""
end
def handle_event("increase", _params, socket) do
socket = assign(socket, temperature_celsius: socket.assigns.temperature_celsius + 10)
{:noreply, socket}
end
def handle_event("decrease", _params, socket) do
socket = assign(socket, temperature_celsius: socket.assigns.temperature_celsius - 10)
{:noreply, socket}
end
end
In the this code we manage the temperature in degrees Celsius increasing/decreasing by 10. The really important part of the code is precisely our cond. Once again do note that only the first tag has = while the others doesn't. The first difference between cond and case is that in cond you always start with cond do without passing an expression to be evaluated immediately, each clause is independent and may well use different variables.
Each cond clause follows the predicate format (an expression that returns true or false) and the first condition that is true ends the flow and renders its corresponding HEEx. As the order of checking the clauses is from top to bottom we do not need to do checks like @temperature_celsius > 30 && @temperature_celsius < 40 -> because if the condition @temperature_celsius > 40 -> did not return true we already know that in the next clause we already have a temperature below 40. Unlike case, to add a default clause we do true -> at the end because as true is hardcoded and this is the last clause it will always pass there.
#Recap!
-
For
if-elseyou must explicitly use the<%= if condition do %>and<% else %>blocks. -
For
ifwithoutelseyou can use the<%= if condition do %>block format or the special HEEx attribute:if={condition}in an HTML tag. -
For multiple comparisons of a value you can use
<%= case value do %>. -
For multiple conditions that don't just involve comparing whether a value matches something you can use
<%= cond do %>. -
In all cases, the first tag will always need
=and the others won't. If you add=to the other tags LiveView will generate warnings but everything will work normally. -
If in the first tag you do not add
=the HTML code will not be rendered.
Feedback
Got any feedback about this page? Let us know!