Layouts and LiveEEx templates

Course Index page

Code

Links


We’ve previously seen in detail how Phoenix LiveView works. In this lesson we are going to see:

  • Layouts, how they work with regular views and LiveViews.
  • How to update the page title in LiveView.
  • .leex LiveEEx template file

Layouts

We’ve seen that when a browser connects to a LiveView route, it initially makes a normal HTTP GET request to get a fully rendered HTML page from the server.

But our LiveView template is made just by few lines of code, so where does all this html come from? It comes from the layout! Like regular views, LiveViews are also rendered in a layout.

Regular views

When rendering regular views, for example the :index action in ProductController, Phoenix wraps the view using the root (lib/poeticoins_web/templates/layout/root.html.leex) layout

<!-- root.html.leex -->
<!DOCTYPE html>
<html lang="en">
  <head>
    ...
    <%= csrf_meta_tag() %>
    <%= live_title_tag assigns[:page_title] || "Poeticoins", suffix: " · Phoenix Framework" %>
    <link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
    <script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </head>
  <body>
    <header>
      <section class="container">
        <nav role="navigation"> ... </nav>
      </section>
    </header>

    <%= @inner_content %>
  
  </body>
</html>

and app (lib/poeticoins_web/templates/layout/app.html.leex) layout

<!-- app.html.eex -->
<main role="main" class="container">
  <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
  <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
  <%= @inner_content %>
</main>

The root.html.leex layout is used by both LiveViews and regular views. The .leex extension stays for LiveEEx template and the syntax is the same as a regular EEx template.

app.html.eex is the default application layout, and it’s used only when rendering regular views. By default, the app.html.eex template has some extra code to render flash messages, but we can customize it as we want.

By calling <%= @inner_content %> we inject the rendered content inside a layout. In case of a regular view, the rendered action html is then injected inside app.html.eex layout and the resulting html is injected in the root.html.leex layout.

root, app and index
root, app and index

By default the :root layout is defined in router.ex, in the :browser pipeline.

defmodule PoeticoinsWeb.Router do
  use PoeticoinsWeb, :router

  pipeline :browser do
    ...
    plug :put_root_layout, {PoeticoinsWeb.LayoutView, :root}
  end
  ...
end

It can be also passed to a live route with the :layout option.

live "/", CryptoDashboardLive, layout: {PoeticoinsWeb.LayoutView, :root}

LiveViews

While regular views use root and app layouts, LiveViews go with root.html.leex and live.html.leex. The live.html.leex layout wraps the LiveView and it’s rendered as part of the LiveView life-cycle. This means that the root layout is fixed, while the content in the live layout can be updated by LiveView.

By default, the live.html.leex template has some extra code to render live flash messages.

<!-- live.html.leex -->
<main role="main" class="container">
  <p class="alert alert-info" role="alert"
    phx-click="lv:clear-flash"
    phx-value-key="info"><%= live_flash(@flash, :info) %></p>

  <p class="alert alert-danger" role="alert"
    phx-click="lv:clear-flash"
    phx-value-key="error"><%= live_flash(@flash, :error) %></p>

  <%= @inner_content %>
</main>

Let’s take the browser, connect to the live "/" route and inspect the HTML generated by the HTTP request. We notice that live.html.leex is rendered inside the special LiveView tag.

DOM showing live template inside live tag
DOM showing live template inside live tag

Then, when the browser connects to a stateful LiveView process via websocket, in the phx_reply message it receives the dynamic and static parts of the rendered live layout.

Static and Dynamic parts of *live* layout
Static and Dynamic parts of *live* layout

Update the page title

Since the root layout is fixed, its content can’t be changed by LiveView, with the exception of the <title>.

In the root.html.leex template we see that <title> tag is set by calling the live_title_tag helper.

<html lang="en">
  <head>
    ...
    <%= live_title_tag assigns[:page_title] || "Poeticoins", suffix: " · Phoenix Framework" %>
    ...
  </head>
  ...
</html>

By assigning a new :page_title to the socket, for example setting the updated price of a product, we immediately see that the page title gets updated.

defmodule PoeticoinsWeb.CryptoDashboardLive do

  def mount(_params, _session, socket) do
    products = [Poeticoins.Product.new("coinbase", "BTC-USD")]

    ...
  end

  def handle_info({:new_trade, trade}, socket) do
    socket =
      socket
      |> update(:trades, &Map.put(&1, trade.product, trade))
      |> assign(:page_title, "#{trade.price}")

    {:noreply, socket}
  end
end

Since LiveView can’t change the root layout’s content, how can it be possible? Because LiveView uses a special t key in the diff message, which instructs the frontend LiveView JS to update the <title> tag.

`diff` message with `t` key

LiveEEx templates with ~L sigil and .leex file

To define a LiveEEx template, we’ve used the render/1 callback and the ~L sigil, to return the template. When the template is big, it can be better to move it to a separate LiveEEx template file. It needs to be in the same directory of the LiveView and with the same name, in our case crypto_dashboard_live.html.leex. After moving the template to this file (just the html, without the ~L sigil), we can remove the render/1 callback in the LiveView.