HEEx
HEEx Basics
Read time: 7 minutes
This guide is a direct continuation of the previous guide
git clone https://github.com/adopt-liveview/v2-myapp.git --branch events-done.
To make your future easier on LiveView, let's learn some simple things about how HEEx works that will make your daily life more productive.
#Elixir rendering
Let's update page_live.ex to this:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def render(assigns) do
~H"""
<h2>Hello {"Lubien"}</h2>
<h2>Hello {1 + 1}</h2>
<h2>Hello {"Chris" <> " " <> "McCord"}</h2>
<h2>Hello <% "King Crimson" |> IO.puts() %></h2>
"""
end
end
In this file we have 3 HEEx interpolations and a tag in our code. HEEx supports rendering any type of code that implements the Phoenix.HTML.Safe protocol.
-
The first case renders the string
"Lubien". - The second case renders the integer 2.
-
The third case just uses the string concatenation operator
<>whose result is "Chris McCord".
But what about the fourth case? Nothing appears on your screen. The reason is simple: we use the <% %> tag, do note that there is no = after the first %. In HEEx this means "execute this code but do not render the result". As it uses the IO.puts/2 function, you can see the result in your terminal.
Then I can add logic to my HEEx!
#Rendering something that cannot be converted to string
Now let's try this:
defmodule User do
defstruct id: nil, name: nil
end
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def render(assigns) do
~H"""
<h2>Hello {%User{id: 1, name: "Lubien"}}</h2>
"""
end
end
You will notice an "Internal Server Error" and the exception in your terminal:
[info] GET /
[debug] Processing with MyappWeb.PageLive.__live__/0
Parameters: %{}
Pipelines: [:browser]
[info] Sent 500 in 50ms
[error] ** (Protocol.UndefinedError) protocol Phoenix.HTML.Safe not implemented for User (a struct). This protocol is implemented for: Atom, BitString, Date, DateTime, Decimal, Duration, Float, Integer, List, NaiveDateTime, Phoenix.LiveComponent.CID, Phoenix.LiveView.Component, Phoenix.LiveView.Comprehension, Phoenix.LiveView.JS, Phoenix.LiveView.Rendered, Time, Tuple, URI
Got value:
%User{id: 1, name: "Lubien"}
(phoenix_html 4.3.0) lib/phoenix_html/safe.ex:1: Phoenix.HTML.Safe.impl_for!/1
(phoenix_html 4.3.0) lib/phoenix_html/safe.ex:15: Phoenix.HTML.Safe.to_iodata/1
(myapp 0.1.0) lib/myapp_web/live/page_live.ex:10: anonymous fn/2 in MyappWeb.PageLive.render/1
(phoenix_live_view 1.1.16) lib/phoenix_live_view/diff.ex:420: 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/static.ex:291: Phoenix.LiveView.Static.to_rendered_content_tag/4
(phoenix_live_view 1.1.16) lib/phoenix_live_view/static.ex:171: Phoenix.LiveView.Static.do_render/4
(phoenix_live_view 1.1.16) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
(phoenix 1.8.1) lib/phoenix/router.ex:416: Phoenix.Router.__call__/5
(myapp 0.1.0) lib/myapp_web/endpoint.ex:1: MyappWeb.Endpoint.plug_builder_call/2
(myapp 0.1.0) deps/plug/lib/plug/debugger.ex:155: MyappWeb.Endpoint."call (overridable 3)"/2
(myapp 0.1.0) lib/myapp_web/endpoint.ex:1: MyappWeb.Endpoint.call/2
(phoenix 1.8.1) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
(bandit 1.8.0) lib/bandit/pipeline.ex:131: Bandit.Pipeline.call_plug!/2
(bandit 1.8.0) lib/bandit/pipeline.ex:42: Bandit.Pipeline.run/5
(bandit 1.8.0) lib/bandit/http1/handler.ex:13: Bandit.HTTP1.Handler.handle_data/3
(bandit 1.8.0) lib/bandit/delegating_handler.ex:18: Bandit.DelegatingHandler.handle_data/3
(bandit 1.8.0) lib/bandit/delegating_handler.ex:8: Bandit.DelegatingHandler.handle_continue/2
(stdlib 7.0.1) gen_server.erl:2424: :gen_server.try_handle_continue/3
(stdlib 7.0.1) gen_server.erl:2291: :gen_server.loop/4
As mentioned previously, the Phoenix.HTML.Safe protocol is necessary for us to render Elixir data. The reason this protocol exists is that the Phoenix team converts the original Elixir data into a structure called iodata which is more efficient in being sent to its user.
If you just want to quickly debug data that cannot be rendered by HEEx, the recommendation would be to use inspect: <h2>Hello {inspect(%User{id: 1, name: "Lubien"})}</h2>. If you really need to teach HEEx to interpret your struct you can also implement the protocol yourself. Updating the example from before:
defmodule User do
defstruct id: nil, name: nil
end
defimpl Phoenix.HTML.Safe, for: User do
def to_iodata(user) do
"User #{user.id} is named #{user.name}"
end
end
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def render(assigns) do
~H"""
<h2>Hello {%User{id: 1, name: "Lubien"}}</h2>
"""
end
end
Using defimpl/3 we were able to define the protocol's to_iodata/1 callback and convert the user to string (something that HEEx can render).
It is worth mentioning that if you decide to return any type of HTML here, you are responsible for ensuring that there is no vulnerability such as XSS. Imagine if your user has a name with <svg onload=alert(1)> and you didn't escape this data? Therefore, avoid this practice whenever possible.
#Rendering of nil
Let's simplify our code to just this:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def render(assigns) do
~H"""
<h2>Hello {"Lubien"}</h2>
<h2>Hello {nil}</h2>
"""
end
end
In this scenario we see that when the interpolation {} receives nil the result is to render absolutely nothing. This will come in handy soon!
#HTML attribute rendering
Now try this out:
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
def render(assigns) do
bg_for_hello_phoenix = "bg-black"
multiple_attributes = %{style: "color: yellow", class: "bg-black"}
~H"""
<style>
.color-red { color: red }
.bg-black { background-color: black }
.bg-red { background-color: red }
</style>
<h2 style="color: red" class="bg-black">Hello World</h2>
<h2 style="color: white" class={"bg-" <> "red"}>Hello Elixir</h2>
<h2 class={[bg_for_hello_phoenix, nil, "color-red"]}>Hello Phoenix</h2>
<h2 {multiple_attributes}>Hello LiveView</h2>
"""
end
end
There are multiple ways we can add HTML attributes in HEEx for developer convenience. Let's check each of them.
In the first case (Hello World) we add style="color: red" which works like any other HTML in the world. In this format there are no type of extra processing steps. In class={"bg-black"} when using brackets we are saying that the content inside them is Elixir code. Any Elixir code like class={calculate_class()} (assuming the function exists) or class={"bg-#{@my_background}"} (assuming the assign exists) will be valid!
In the second case (Hello Elixir) we just demonstrate once again what was explained in the previous case. In class={"bg-" <> "red"} you can see an example of using the <> operator to calculate the final class.
In the third example (Hello Phoenix) there is a golden tip: you can pass a list with multiple strings to an attribute and at the end it will be automatically joined and values that are nil will be ignored. The reason this technique is powerful is that it makes it easier to work with variables, as we can see bg_for_hello_phoenix being used.
The last case (Hello LiveView) adds one more way of working with attributes. If you ever need to add attributes dynamically, that is, you don't know exactly which attributes will or won't be included in advance, you can use the syntax of adding an elixir map within the opening HTML tag and HEEx will understand that each key in your map represents an attribute.
Can I use variables in my render function?
#A sidenote for an old syntax
Before interpolations like {this} existed the default way for HEEx to render things in HTML was the tag <%= this %>. Similar to the <% %> tag (which doesn't render), this one comes with a =. Nowadays if you use formatters on new projects they will be automatically converted to interpolations unless they fall into very specific cases. We will still be using <%= %> in the future for control structures though!
#Recap!
-
Using the
{}tag renders Elixir code that thePhoenix.HTML.Safeprotocol accepts. -
Using the
<% %>tag just executes Elixir code and does not render anything. -
You can implement
Phoenix.HTML.Safefor structs but you should be aware of the security risks this might bring. -
HEEx considers
nilas something that should not be rendered, this is useful if you want to work with optional variables. - In HEEx, HTML attributes with the value around curly braces execute any valid Elixir code to generate the attribute value.
- In HEEx, you can also pass lists to attributes to simplify mixing strings and variables.
- In HEEx, you can pass a map between braces in the HTML tag so that multiple attributes are added dynamically.
-
Old projects would have the tag interpolation syntax
<%= %>instead. -
<%= %>Is mostly used for control structures (if,else...)
Feedback
Got any feedback about this page? Let us know!