HEEx
Renderização condicional
Read time: 11 minutes
Este guia é uma continuação direta do guia anterior
git clone https://github.com/adopt-liveview/v2-myapp.git --branch events-done.
Vamos aprender algumas formas de renderizar HTML dependendo de certas condições. Vamos atualizar nossa PageLive:
#Usando if-else para casos simples
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
Vamos destrinchar este código. O único assign que temos aqui se chama show_information? com valor inicial de false. O evento "toggle" enviado pelo input simplesmente inverte o valor entre true e false. O que realmente é novo aqui é nosso bloco de if-else.
Ponto de interrogação no meio do código? Isso é possível?
if @show_information??
Dentro de uma LiveView você pode fazer um if-else da seguinte forma:
-
Adicione um
<%= if condição do %>. É importante usar a tag que contém=, caso contrário o HEEx entenderá que isso não deve ser renderizado! - Escreva qualquer HTML que ficará no caso que deve ser renderizado.
-
Adicione um
<% else %>. Note que não há=desta vez. Se você adicionar, o código continuará funcionando mas um warning pedirá para removê-lo. -
Escreva qualquer HTML para o caso
else. -
Adicione um
<% end %>. Novamente, sem=.
Se você não quiser mostrar um caso else, existem duas formas de fazer isso. A primeira é simples: basta remover o <% else %> e seu conteúdo! Atualize seu 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
#O atributo especial :if
Para casos onde você só tem if, HEEx possui um atributo especial chamado :if que você pode colocar diretamente na tag HTML. Atualize mais uma vez o 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
No momento não existe um atributo especial para else, então se você precisar apenas de if recomenda-se usar :if quando puder colocá-lo em uma tag pai das coisas que entram na condição; caso contrário, use o primeiro exemplo com if-else como mostrado acima.
#Usando case para casos complexos
É só uma questão de tempo até você se encontrar em uma situação onde existem mais de duas possibilidades para renderizar algo. Elixir não suporta else if e por um bom motivo: a preferência é o case, que é muito mais poderoso!
Vamos criar um sistema simples de abas no 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
Desta vez nosso assign virou tab, que pode ser uma string entre "home", "about" ou "contact". Cada input contém um phx-click="show_NOME_DA_ABA" para que nosso handle_event/3 use pattern matching do Elixir para aceitar qualquer evento que começa com show_ e salvar o restante do nome do evento em uma variável. Outro detalhe simples mas interessante no nosso código é que usamos a propriedade HTML disabled para impedir que o botão seja clicável se você já estiver na aba correta.
Pattern matching?!
Agora vamos falar sobre o que é importante para esta aula: case. Assim como o if, você precisa iniciar o condicional com <%= case (condição aqui) do %>, com ênfase no = porque sem ele nada será renderizado. Como nossa condição no case era @tab, cada condição essencialmente verifica @tab == 'valor'. Para cada condição fazemos um <% "valor esperado" -> %> (sem necessidade de = na tag de interpolação) e encerramos o bloco com <% end %>.
Vale mencionar que no nosso case tratamos todos os cenários possíveis. E se esquecêssemos uma possibilidade? Vamos tentar isso:
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
Neste exemplo adicionamos um novo botão para mostrar uma aba de blog, mas não adicionamos uma cláusula no nosso case para tratar esse valor do nosso assign. Quando você clicar em "Open Blog" deve notar que sua LiveView reinicia para o estado original e uma exceção aparece no seu 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: %{}}
A mensagem não poderia ser mais explícita! Vamos analisar cada parte:
-
A exceção é
CaseClauseError, deixando óbvio que uma cláusulacaseestá faltando. - A própria mensagem de erro deixa claro que a cláusula faltante se chama "blog".
-
Se você olhar "Last message" pode ver que o evento que causou o problema foi
"show_blog". Isso facilita entender qual parte da sua LiveView iniciou o problema para que você possa reproduzi-lo localmente e tratá-lo.
Para adicionar uma cláusula padrão basta usar o formato <% _ -> %>. Em Elixir o _ no contexto de pattern matching significa "qualquer coisa". Poderíamos adicionar um conteúdo padrão como <p>Aba não existe</p>.
O que fazer quando não sabemos como tratar todos os casos?
#Cadeias de condições com cond
No exemplo anterior usamos case para comparar o valor exato da variável @tab em cada cláusula. Se você precisar renderizar algo com base em uma condição que não é sobre igualdade, cond é perfeito para isso. Vamos tentar:
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
Neste código gerenciamos a temperatura em graus Celsius aumentando/diminuindo 10. A parte realmente importante do código é justamente o nosso cond. Mais uma vez observe que apenas a primeira tag tem = enquanto as outras não têm. A primeira diferença entre cond e case é que em cond você sempre começa com cond do sem passar uma expressão para ser avaliada imediatamente; cada cláusula é independente e pode muito bem usar variáveis diferentes.
Cada cláusula cond segue o formato predicado (uma expressão que retorna true ou false) e a primeira condição que for verdadeira encerra o fluxo e renderiza o HEEx correspondente. Como a ordem de verificação das cláusulas é de cima para baixo, não precisamos fazer verificações como @temperature_celsius > 30 && @temperature_celsius < 40 -> porque se a condição @temperature_celsius > 40 -> não retornou true, já sabemos que na próxima cláusula a temperatura está abaixo de 40. Ao contrário do case, para adicionar uma cláusula padrão fazemos true -> no final porque, como true é fixo e esta é a última cláusula, sempre passará por ali.
#Resumindo!
-
Para
if-elsevocê deve usar explicitamente os blocos<%= if condição do %>e<% else %>. -
Para
ifsemelsevocê pode usar o formato de bloco<%= if condição do %>ou o atributo especial HEEx:if={condição}em uma tag HTML. -
Para múltiplas comparações de um valor você pode usar
<%= case valor do %>. -
Para múltiplas condições que não envolvem apenas comparar se um valor corresponde a algo, você pode usar
<%= cond do %>. -
Em todos os casos, a primeira tag sempre precisará de
=e as outras não. Se você adicionar=nas outras tags, o LiveView gerará warnings mas tudo funcionará normalmente. -
Se na primeira tag você não adicionar
=, o código HTML não será renderizado.
Feedback
Got any feedback about this page? Let us know!