Tutorial

Nested Templates and Layouts in Phoenix Framework

templates layouts phoenix

Eventually when your site reaches a certain maturity, you look into ways to refactor the code. One is to add parts of the templates in reusable layout templates. The reason is that HTML templates can be pretty verbose and you want certain elements to look the same across the site.

This approach is an alternative to using partials and even though partials are fine, it might be a little cumbersome if you want to pass in a lot of content. Like a form or a large block of HTML.

STEP 1 - Create a shared components module

All the shared components will live in a module called SharedComponents. I usually add the view-related helpers in the views folder.

# lib/sample_web/views/shared_components.ex

defmodule SampleWeb.SharedComponents do
  def card(assigns \\ %{}, do: block), do: render_template("card.html", assigns, block)

  defp render_template(template, assigns, block) do
    assigns =
      assigns
      |> Map.new()
      |> Map.put(:inner_content, block)

    SampleWeb.SharedView.render(template, assigns)
  end
end

But I also want to add an actual view file for rendering the templates:

# lib/sample_web/views/shared_view.ex

defmodule SampleWeb.SharedView do
  use SampleWeb, :view
end

STEP 2 - Add HTML templates

I will start by adding the card component. Note that since I am using it as a block, everything inside the block will be outputted with the <%= @inner_content %>

<!-- /templates/shared/card.html.eex -->

<div class="max-w-sm rounded overflow-hidden shadow-lg">
  <div class="px-6 py-4">
     <%= @inner_content %>
  </div>
</div>

Moving on to the card header. Note that I also made it so I can pass in arguments when I call the <%= card_header do %> function. In this case, I might want some extra HTML class attributes.

<!-- /templates/shared/card_header.html.eex -->

<div class="bg-gray-50 py-4 px-6 border-b border-gray-200 rounded-t-lg <%= assigns[:class] %>">
  <%= @inner_content %>
</div>

And the same thing goes for the card body component.

<!-- /templates/shared/card_body.html.eex -->

<div class="px-8 py-8 <%= assigns[:class] %>">
  <%= @inner_content %>
</div>

STEP 3 - Start using the card component

With the templates set up, I should now be able to start using the cards in different templates. I can use only the outer card like this:

<%= card do %>
  <div class="font-bold text-xl mb-2">The Coldest Sunset</div>
  <p class="text-gray-700 text-base">
    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia, nulla! Maiores et perferendis eaque, exercitationem praesentium nihil.
  </p>
<% end %>

Or I can nest the components with cardheader and cardbody inside the card:

<%= card do %>
  <%= card_header do %>
    The Coldest Sunset
  <% end %>
  <%= card_body do %>
    <p class="text-gray-700 text-base">
      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia,
      nulla! Maiores et perferendis eaque, exercitationem praesentium nihil.
    </p>
  <% end %>
<% end %>

Related Tutorials

Published 04 May - 2021
Updated 05 May - 2022

How to combine Phoenix LiveView with Alpine.js

No matter how great Phoenix LiveView is, there is still some use case for sprinking some JS in your app to improve UX. For example, tabs, dropdowns,..

Published 23 Dec - 2020

Getting Started with Phoenix and LiveView