View Source Components and HEEx

Requirement: This guide expects that you have gone through the introductory guides and got a Phoenix application up and running.

Requirement: This guide expects that you have gone through the request life-cycle guide.

The Phoenix endpoint pipeline takes a request, routes it to a controller, and calls a view module to render a template. The view interface from the controller is simple – the controller calls a view function with the connections assigns, and the function's job is to return a HEEx template. We call any function that accepts an assigns parameter and returns a HEEx template a function component. Function components are defined with the help of the Phoenix.Component module.

Function components are the essential building block for any kind of markup-based template rendering you'll perform in Phoenix. They serve as a shared abstraction for the standard MVC controller-based applications, LiveView applications, layouts, and smaller UI definitions you'll use throughout other templates.

In this chapter, we will recap how components were used in previous chapters and find new use cases for them.

Function components

At the end of the Request life-cycle chapter, we created a template at lib/hello_web/controllers/hello_html/show.html.heex, let's open it up:

<section>
  <h2>Hello World, from {@messenger}!</h2>
</section>

This template, is embedded as part of HelloHTML, at lib/hello_web/controllers/hello_html.ex:

defmodule HelloWeb.HelloHTML do
  use HelloWeb, :html

  embed_templates "hello_html/*"
end

That's simple enough. There's only two lines, use HelloWeb, :html. This line calls the html/0 function defined in HelloWeb which sets up the basic imports and configuration for our function components and templates.

All of the imports and aliases we make in our module will also be available in our templates. That's because templates are effectively compiled into functions inside their respective module. For example, if you define a function in your module, you will be able to invoke it directly from the template. Let's see this in practice.

Imagine we want to refactor our show.html.heex to move the rendering of <h2>Hello World, from {@messenger}!</h2> to its own function. We can move it to a function component inside HelloHTML, let's do so:

defmodule HelloWeb.HelloHTML do
  use HelloWeb, :html

  embed_templates "hello_html/*"

  attr :messenger, :string, required: true

  def greet(assigns) do
    ~H"""
    <h2>Hello World, from {@messenger}!</h2>
    """
  end
end

We declared the attributes we accept via the attr/3 macro provided by Phoenix.Component, then we defined our greet/1 function which returns the HEEx template.

Next we need to update show.html.heex:

<section>
  <.greet messenger={@messenger} />
</section>

When we reload http://localhost:4000/hello/Frank, we should see the same content as before.

Since templates are embedded inside the HelloHTML module, we were able to invoke the view function simply as <.greet messenger="..." />.

If the component was defined elsewhere, we can also type <HelloWeb.HelloHTML.greet messenger="..." />.

By declaring attributes as required, Phoenix will warn at compile time if we call the <.greet /> component without passing attributes. If an attribute is optional, you can specify the :default option with a value:

attr :messenger, :string, default: nil

Although this is a quick example, it shows the different roles function components play in Phoenix:

  • Function components can be defined as functions that receive assigns as argument and call the ~H sigil, as we did in greet/1

  • Function components can be embedded from template files, that's how we load show.html.heex into HelloWeb.HelloHTML

  • Function components can declare which attributes are expected, which are validated at compilation time

  • Function components can be directly rendered from controllers

  • Function components can be directly rendered from other function components, as we called <.greet messenger={@messenger} /> from show.html.heex

And there's more. Before we go deeper, let's fully understand the expressive power behind the HEEx template language.

HEEx

Function components and templates files are powered by the HEEx template language, which stands for "HTML+EEx". EEx is an Elixir library that uses <%= expression %> to execute Elixir expressions and interpolate their results into arbitrary text templates. HEEx extends EEx for writing HTML templates mixed with Elixir interpolation. We can write Elixir code inside {...} for HTML-aware interpolation inside tag attributes and the body. We can also interpolate arbitrary HEEx blocks using EEx interpolation (<%= ... %>). We use @name to access the key name defined inside assigns.

This is frequently used to display assigns we have set by way of the @ shortcut. In your controller, if you invoke:

  render(conn, :show, username: "joe")

Then you can access said username in the templates as {@username}. In addition to displaying assigns and functions, we can use pretty much any Elixir expression. For example, in order to have conditionals:

<%= if some_condition? do %>
  <p>Some condition is true for user: {@username}</p>
<% else %>
  <p>Some condition is false for user: {@username}</p>
<% end %>

or even loops:

<table>
  <tr>
    <th>Number</th>
    <th>Power</th>
  </tr>
  <%= for number <- 1..10 do %>
    <tr>
      <td>{number}</td>
      <td>{number * number}</td>
    </tr>
  <% end %>
</table>

Did you notice the use of <%= %> versus <% %> above? All expressions that output something to the template must use the equals sign (=). If this is not included the code will still be executed but nothing will be inserted into the template.

HEEx also comes with handy HTML extensions we will learn next.

HTML extensions

Besides allowing interpolation of Elixir expressions via <%= %>, .heex templates come with HTML-aware extensions. For example, let's see what happens if you try to interpolate a value with "<" or ">" in it, which would lead to HTML injection:

{"<b>Bold?</b>"}

Once you render the template, you will see the literal <b> on the page. This means users cannot inject HTML content on the page. If you want to allow them to do so, you can call raw, but do so with extreme care:

{raw "<b>Bold?</b>"}

Another super power of HEEx templates is validation of HTML and interpolation syntax of attributes. You can write:

<div title="My div" class={@class}>
  <p>Hello {@username}</p>
</div>

Notice how you could simply use key={value}. HEEx will automatically handle special values such as false to remove the attribute or a list of classes.

To interpolate a dynamic number of attributes in a keyword list or map, do:

<div title="My div" {@many_attributes}>
  <p>Hello {@username}</p>
</div>

Also, try removing the closing </div> or renaming it to </div-typo>. HEEx templates will let you know about your error.

HEEx also supports shorthand syntax for if and for expressions via the special :if and :for attributes. For example, rather than this:

<%= if @some_condition do %>
  <div>...</div>
<% end %>

You can write:

<div :if={@some_condition}>...</div>

Likewise, for comprehensions may be written as:

<ul>
  <li :for={item <- @items}>{item.name}</li>
</ul>

Layouts

Layouts are just function components. They are defined in a module, just like all other function component templates. In a newly generated app, this is lib/hello_web/components/layouts.ex. You will also find a layouts folder with two built-in layouts generated by Phoenix. The default root layout is called root.html.heex, and it is the layout into which all templates will be rendered by default. The second is the app layout, called app.html.heex, which is rendered within the root layout and includes our contents.

You may be wondering how the string resulting from a rendered view ends up inside a layout. That's a great question! If we look at lib/hello_web/components/layouts/root.html.heex, just about at the end of the <body>, we will see this.

{@inner_content}

In other words, after rendering your page, the result is placed in the @inner_content assign.

Phoenix provides all kinds of conveniences to control which layout should be rendered. For example, the Phoenix.Controller module provides the put_root_layout/2 function for us to switch root layouts. This takes conn as its first argument and a keyword list of formats and their layouts. You can set it to false to disable the layout altogether.

You can edit the index action of HelloController in lib/hello_web/controllers/hello_controller.ex to look like this.

def index(conn, _params) do
  conn
  |> put_root_layout(html: false)
  |> render(:index)
end

After reloading http://localhost:4000/hello, we should see a very different page, one with no title or CSS styling at all.

To customize the application layout, we invoke a similar function named put_layout/2. Let's actually create another layout and render the index template into it. As an example, let's say we had a different layout for the admin section of our application which didn't have the logo image. To do this, copy the existing app.html.heex to a new file admin.html.heex in the same directory lib/hello_web/components/layouts. Then remove everything inside the <header>...</header> tags (or change it to whatever you desire) in the new file.

Now, in the index action of the controller of lib/hello_web/controllers/hello_controller.ex, add the following:

def index(conn, _params) do
  conn
  |> put_layout(html: :admin)
  |> render(:index)
end

When we load the page, we should be rendering the admin layout without the header (or a custom one that you wrote).

At this point, you may be wondering, why does Phoenix have two layouts?

First of all, it gives us flexibility. In practice, we will hardly have multiple root layouts, as they often contain only HTML headers. This allows us to focus on different application layouts with only the parts that changes between them. Second of all, Phoenix ships with a feature called LiveView, which allows us to build rich and real-time user experiences with server-rendered HTML. LiveView is capable of dynamically changing the contents of the page, but it only ever changes the app layout, never the root layout. Check out the LiveView documentation to learn more.

CoreComponents

In a new Phoenix application, you will also find a core_components.ex module inside the components folder. This module is a great example of defining function components to be reused throughout our application. This guarantees that, as our application evolves, our components will look consistent.

If you look inside def html in HelloWeb placed at lib/hello_web.ex, you will see that CoreComponents are automatically imported into all HTML views via use HelloWeb, :html. This is also the reason why CoreComponents itself performs use Phoenix.Component instead use HelloWeb, :html at the top: doing the latter would cause a deadlock as we would try to import CoreComponents into itself.

CoreComponents also play an important role in Phoenix code generators, as the code generators assume those components are available in order to quickly scaffold your application. In case you want to learn more about all of these pieces, you may: