Templates

Templates are what they sound like they should be: files into which we pass data to form complete HTTP responses. For a web application these responses would typically be full HTML documents. For an API, they would most often be JSON or possibly XML. The majority of the code in template files is often markup, but there will also be sections of Elixir code for Phoenix to compile and evaluate. The fact that Phoenix templates are pre-compiled makes them extremely fast.

EEx is the default template system in Phoenix, and it is quite similar to ERB in Ruby. It is actually part of Elixir itself, and Phoenix uses EEx templates to create files like the router and the main application view while generating a new application.

As we learned in the View Guide, by default, templates live in the lib/hello_web/templates directory, organized into directories named after a view. Each directory has its own view module to render the templates in it.

Examples

We've already seen several ways in which templates are used, notably in the Adding Pages Guide and the Views Guide. We may cover some of the same territory here, but we will certainly add some new information.

hello_web.ex

Phoenix generates a lib/hello_web.ex file that serves as place to group common imports and aliases. All declarations here within the view block apply to all your templates.

Let's make some additions to our application so we can experiment a little.

First, let's define a new route in lib/hello_web/router.ex.

defmodule HelloWeb.Router do
  ...

  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/test", PageController, :test
  end

  # Other scopes may use custom stacks.
  # scope "/api", Hello do
  #   pipe_through :api
  # end
end

Now, let's define the controller action we specified in the route. We'll add a test/2 action in the lib/hello_web/controllers/page_controller.ex file.

defmodule HelloWeb.PageController do
  ...

  def test(conn, _params) do
    render(conn, "test.html")
  end
end

We're going to create a function that tells us which controller and action are handling our request.

To do that, we need to import the action_name/1 and controller_module/1 functions from Phoenix.Controller in lib/hello_web.ex.

  def view do
    quote do
      use Phoenix.View, root: "lib/hello_web/templates",
                        namespace: HelloWeb

      # Import convenience functions from controllers
      import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1,
                                        action_name: 1, controller_module: 1]

      ...
    end
  end

Next, let's define a handler_info/1 function at the bottom of the lib/hello_web/views/page_view.ex which makes use of the controller_module/1 and action_name/1 functions we just imported. We'll also define a connection_keys/1 function that we'll use in a moment.

defmodule HelloWeb.PageView do
  use HelloWeb, :view

  def handler_info(conn) do
    "Request Handled By: #{controller_module(conn)}.#{action_name(conn)}"
  end

  def connection_keys(conn) do
    conn
    |> Map.from_struct()
    |> Map.keys()
  end
end

We have a route. We created a new controller action. We have made modifications to the main application view. Now all we need is a new template to display the string we get from handler_info/1. Let's create a new one at lib/hello_web/templates/page/test.html.eex.

<div class="phx-hero">
  <p><%= handler_info(@conn) %></p>
</div>

Notice that @conn is available to us in the template for free via the assigns map.

If we visit localhost:4000/test, we will see that our page is brought to us by Elixir.HelloWeb.PageController.test.

We can define functions in any individual view in lib/hello_web/views. Functions defined in an individual view will only be available to templates which that view renders. For example, functions like our handler_info above, will only be available to templates in lib/hello_web/templates/page.

Displaying Lists

So far, we've only displayed singular values in our templates - strings here, and integers in other guides. How would we approach displaying all the elements of a list?

The answer is that we can use Elixir's list comprehensions.

Now that we have a function, visible to our template, that returns a list of keys in the conn struct, all we need to do is modify our lib/hello_web/templates/page/test.html.eex template a bit to display them.

We can add a header and a list comprehension like this.

<div class="phx-hero">
  <p><%= handler_info(@conn) %></p>

  <h3>Keys for the conn Struct</h3>

  <%= for key <- connection_keys(@conn) do %>
    <p><%= key %></p>
  <% end %>
</div>

We use the list of keys returned by the connection_keys function as the source list to iterate over. Note that we need the = in both <%= - one for the top line of the list comprehension and the other to display the key. Without them, nothing would actually be displayed.

When we visit localhost:4000/test again, we see all the keys displayed.

Render templates within templates

In our list comprehension example above, the part that actually displays the values is quite simple.

<p><%= key %></p>

We are probably fine with leaving this in place. Quite often, however, this display code is somewhat more complex, and putting it in the middle of a list comprehension makes our templates harder to read.

The simple solution is to use another template! Templates are just function calls, so like regular code, composing your greater template by small, purpose-built functions can lead to clearer design. This is simply a continuation of the rendering chain we have already seen. Layouts are templates into which regular templates are rendered. Regular templates may have other templates rendered into them.

Let's turn this display snippet into its own template. Let's create a new template file at lib/hello_web/templates/page/key.html.eex, like this.

<p><%= @key %></p>

We need to change key to @key here because this is a new template, not part of a list comprehension. The way we pass data into a template is by the assigns map, and the way we get the values out of the assigns map is by referencing the keys with a preceding @. @ is actually a macro that translates @key to Map.get(assigns, :key).

Now that we have a template, we simply render it within our list comprehension in the test.html.eex template.

<div class="phx-hero">
  <p><%= handler_info(@conn) %></p>

  <h3>Keys for the conn Struct</h3>

  <%= for key <- connection_keys(@conn) do %>
    <%= render("key.html", key: key) %>
  <% end %>
</div>

Let's take a look at localhost:4000/test again. The page should look exactly as it did before.

Shared Templates Across Views

Often, we find that small pieces of data need to be rendered the same way in different parts of the application. It's a good practice to move these templates into their own shared directory to indicate that they ought to be available anywhere in the app.

Let's move our template into a shared view.

key.html.eex is currently rendered by the HelloWeb.PageView module, but we use a render call which assumes that the current schema is what we want to render with. We could make that explicit, and re-write it like this:

<div class="phx-hero">
  ...

  <%= for key <- connection_keys(@conn) do %>
    <%= render(HelloWeb.PageView, "key.html", key: key) %>
  <% end %>
</div>

Since we want this to live in a new lib/hello_web/templates/shared directory, we need a new individual view to render templates in that directory, lib/hello_web/views/shared_view.ex.

defmodule HelloWeb.SharedView do
  use HelloWeb, :view
end

Now we can move key.html.eex from the lib/hello_web/templates/page directory into the lib/hello_web/templates/shared directory. Once that happens, we can change the render call in lib/hello_web/templates/page/test.html.eex to use the new HelloWeb.SharedView.

<%= for key <- connection_keys(@conn) do %>
  <%= render(HelloWeb.SharedView, "key.html", key: key) %>
<% end %>

Going back to localhost:4000/test again. The page should look exactly as it did before.