Using function components for template inheritance

Hello, following this HN thread @josevalim asked me to open a thread regarding template inheritance.

I’d like to offer a basic example that would work in Twig or Django / Jinja2.

The root view, base.html.twig (which would be the layout BUT there is actually no concept of layout) is:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>{% block title %}Default title for any page extending this template{% endblock %}</title>
    </head>
    <body>
        {% block content %}{% endblock %}
    </body>
</html>

The view, page1.html.twig, which would be rendered by the action:

{% extends 'base.html.twig' %}

{% block title %}Better title for this page{% endblock %}

{% block content %}
    <h1>Phoenix is great</h1>
    <div>I've nothing more to say</div>
{% endblock %}

But wait there’s more! What if want another page that uses the same title and adds something to the body? I present to you page2.html.twig:

{% extends 'page1.html.twig' %}

{% block content %}
    {{ parent() }}
    <div>Oh wait, I also like LiveView ;)</div>
{% endblock %}

So I’m at lost how I would implement that with function components?

Thanks.

1 Like

Your example, uses inheritance. What jose mentioned uses composition (if i understood correctly what he meant to say), which is what modern frontend frameworks use.

Something like

defmodule ComponentA do
  use Phoenix.Component

  def show_a() do
    ~H"""
      Default title for any page extending this template
    """
  end
end

defmodule ComponentB do
  use Phoenix.Component

  def show_b() do
    ~H"""
      Phoenix is great
    """
  end
end


defmodule ComponentC do
  use Phoenix.Component

  import ComponentB, only: [show_b: 0] 

  def show_b() do
    ~H"""
      <.show_b />
      Oh wait, I also like LiveView ;)
    """
  end
end

Instead of extending templates, the idea behind components is to seperate them in components and use the components.

(instead of Phoenix.Component you can use LiveComponent if you need stateful compoents and the usage differs a bit but the idea is the same)

Hey! I’m also coming from the HN thread :slight_smile:

A library implementing this functionality in a functional language (clojure) is selmer : GitHub - yogthos/Selmer: A fast, Django inspired template system in Clojure.

Thanks for your reply @gpopides , I see your point.

I don’t think it solves every problems like the "page extends layout"solves over the “layout → content” does but I’ll try to think more in term of components I guess.

“Modern” frontend frameworks don’t exactly need the same thing as SSR because you’ll usually rely on a global state store (or some two way communication/event method) that will allow your UI to compose itself.

Hi @conradfr!

I will break the answer in two.

For customizing the head, which is pretty much what Phoenix calls the root layout, I would use assigns:

# layouts/root.html.heex
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title><%= assigns[:page_title] || "Default title" %></title>
    </head>
    <body>
        <%= @inner_content %>
    </body>
</html>

Why assigns? It works for both regular request/response and Phoenix LiveView. For example, if you want to add dynamic scripts, metadata, etc, you can store them in assigns and render them as part of the layout.

But the root layout is pretty much that: the root layout. We still need to address the actual layout as perceived by users: a sidebar, main content, a footer, etc. And maybe you want different pages to have the same sidebar layout but with different items. For those, use function components are great. For example, you could create a function component like this:

def layout(assigns) do
  ~H"""
  <div id="main">
    <ul id="sidebar">
      <%= render_slot(@sidebar) %>
    </ul>
    <%= render_slot(@inner_block) %>
  </div>
  """
end

Now, on each page, you can do this:

<.layout>
  <:sidebar>
    <li><a href="/foo">Foo</a></li>
    <li><a href="/bar">Bar</a></li>
  </:sidebar>
  This is the inner block.
</.layout>

This is using a functionality called slots, which in a way are equivalent to blocks above. However, they are quite more powerful than template inheritance because they are actually structured data (and not only strings).

For example, the sidebar above still has a couple issues:

  1. Each item has a lot of repetition by specifying li, a and perhaps even icons
  2. We are leaking the implementation detail that sidebars use ul internally

We can address this by also converting the sidebar to a component and specifying each sidebar item as a slot:

def layout(assigns) do
  ~H"""
  <div id="main">
    <%= render_slot(@inner_block) %>
  </div>
  """
end

def sidebar(assigns) do
  ~H"""
  <ul id="sidebar">
    <%= for item <- @items do %>
      <li><a href={item.href}><%= item.text %></a></li>
    <% end %>
  </ul>
  """
end

And now I can do:

<.layout>
  <.sidebar>
    <:item href="/foo" text="Foo" />
    <:item href="/bar" text="Bar" />
  </.sidebar>
  This is the inner block.
</.layout>

Now we have started to encapsulate and compose UI functionality using structured data! Phoenix LiveView v0.18 will even take a step further by allowing you to annotate function components with the exact type of data they expect:

slot :items do
  attr :href, :string, required: true
  attr :text, :string, required: true
end
  
def sidebar(assigns) do

And then if you forget any of the attributes, you get a compilation warning.

I think this is the lesson that Surface taught us and ultimately why HEEx is a big deal: it makes us think of templates as rich data, like the JS community has been doing over the last several years, instead of just a series of string interpolations like I did with PHP back in 2004. :slight_smile:


I will add two things:

  1. This approach plays really well with LiveView because if you render the same function component more than once, LiveView will send its body exactly once to the client, regardless of how many times you render it.

  2. This approach also fits nicely with Tailwind because, although your classes declarations tend to get verbose, they get encapsulated with the function components, which play nice with the fact LiveView renders them only once.

The only part that we are missing is also encapsulating the JavaScript bits within function components. I think Surface is already exploring something along these lines.

11 Likes

Thanks a lot for your reply, it certainly addresses many things. I’ll try to apply that on a project.

In my mind it’s still inferior to inheritance because of exactly this “Now, on each page, you can do this:”, which I actually would not have to do or think about (once setup) with inheritance.

Let’s say I have the regular pages with no sidebar and an admin section with a sidebar and I also want to add entries in the navbar when I’m in the admin section.

{# base.html.twig #}
<html>
  <head>
    {% block css_links %}{% endblock %}
  </head>
  <body>
    {% block navbar %}
      <div class="navbar">
        <ul>
          <li>navbar entry></li>
          {% block additional_navbar %}{% endblock %}
        </ul>
      </div>
    {% endblock %}

    {% block content %}
    {% endblock %}
  </body>
</html>
{# any_regular_page.html.twig #}
{% extends 'base.html.twig' %}

{% block content %}
  Great stuff {{ name }}
{% endblock %}
{# _admin_sidebar.html #}
<ul>
  {% for entry in entries %}
    <li>{{ entry }}</li>
  {% endfor %}
</ul>
{# admin_page_wrapper.html.twig #}
{% extends 'base.html.twig' %}

{% block additional_navbar %}<li>admin navbar entry</li>{% endblock %}

{% block content %}
  {% include 'admin_sidebar.html' %}
  {% block admin_content %}{% endblock %}
{% endblock %}
{# any_admin.page.html.twig #}
{% extends 'admin_page_wrapper.html.twig' %}

{% block admin_content %}This is my great admin content{% endblock %}

Obviously it’s not like you can’t ultimately get the same results with the conventional layout → page paradigm, just that it eliminates a lot of problems and workarounds.

Maybe it’s like Tailwind (or … the Beam :D), you have to experience it to get what the fuss is all about.

You can still compose them at arbitrary levels. Here is a pseudo-translation of your code to slots:

# any_regular_page.html.heex
<.base_layout>
  Great stuff <%= name %>
</.base_layout>
# admin_helpers.ex
def admin_sidebar(assigns) do
  ~H"""
  <ul>
    <%= for entry <- @entries %>
      <li><%= entry %></li>
    <% end %>
  </ul>
  """
end

def admin_layout(assigns) do
  ~H"""
  <.base_layout>
    <:additional_navbar>
      <li>admin navbar entry</li>
    </:additional_navbar>

    <.admin_sidebar />
    <%= render_slot(@inner_block) %>
  </.base_layout>
  """
end
# any_admin.page.html.heex
<.admin_layout>
  This is my great admin content
</.admin_layout>

You still need to pick one layout at the top, but that’s the same with template inheritance. Still, slots can be more structured, and they allow at the composition to happen at any moment, not only from the template you are inheriting.

[shameless plug]
Assigns can work well and Phoenix ships with a helper for the title. Some others use cases can benefit from using helper libs.

1 Like

By the way, there is also erlydtl, which is used at Zotonic (which also implements “extends” like Selmer).
Zotonic Inheritance
But if I understand correctly, this is not the right way, but “function components” and composition, right?

I’ve been thinking about “function components” since yesterday, but I’m still not quite sure how you could use it to make something like a plug-in system for an ecommerce system.

We are using Shopware, which uses PHP and Twig and provides a plugin system to customise Shopware.
For example, overwriting certain Twig blocks of Shopware in a super simple way
(without having to adapt the source of Shopware itself, which is important for Shopware updates).
BTW we are using Elixir now for some background parts, which is really super nice and easy :slight_smile:

An example is this one below, which overrides two block of the product description part of the product page.
It’s a twig-File in our Plugin:
custom/plugins/HflDecoration/src/Resources/views/storefront/page/product-detail/description.html.twig

{% sw_extends '@Storefront/storefront/page/product-detail/description.html.twig' %}

{% block page_product_detail_description_title %}

{% endblock %}

{% block page_product_detail_description_content_text %}
    {% if page.product.extensions.hflDecorationExtension.translated.descriptionUnique|default %}
        <div class="product-detail-description-text"
             itemprop="description">
            {{ page.product.extensions.hflDecorationExtension.translated.descriptionUnique|raw }}
        </div>
    {% elseif page.product.customFields.hfl_decoration_products_description_unique| default %}
        <div class="product-detail-description-text"
             itemprop="description">
            {{ page.product.customFields.hfl_decoration_products_description_unique|raw }}
        </div>
    {% else %}
        {{ parent() }}
    {% endif %}
{% endblock %}

It’s really simple to use and unfortunately, I don’t yet see how this could be done with function components and composition.

Maybe the point of confusion revolves around the implementation strategy. While Shopware is a complete software package that one has to apparently customize using overrides, phoenix is a framework that expects that the implementer wires up things on its own (something that is taken care of in Shopware and is then made customizable through plugins/template overrides).

I guess if something like Shopware would be conceived with phoenix as a base in mind, it would be more like a mix app that provides component modules (with default layouts maybe) for you to use and wire up yourself (among many other kind of modules). If a layout does not fit, it then can be replaced with your own version.
It could also maybe give you behaviours to implement or maybe more something like a DSL as well, that has special syntax for overrides (think spark DSL from ash). Those modules could then be passed through config or options to plugs for example to let the shopwarex machinery pick it up. But it seems to me that the composing things yourself in your own application is favourable, though, as doing it that way would hide quite a lot. Phoenix is a just a framework after all and explicitly connecting stuff may be arguably more flexible.

By the way, I can see somewhat where @Werner and @conradfr are coming from though, having used (and still using) template-inheritance based templates in several applications.

Personally I feel that there is some additional (superficially seemingly unneeded) complexity within the phoenix view implementation due to the concepts of layouts (root and app layout). With jinja2-style templates, there is just one template entry point/file. It is only within that template file that we usually determine which “layout” we use, and the structure of the whole HTML document is determined only based on that template.

Having to do a put_layout or setting the root layout somewhere in the plug pipeline feels a bit out of place when you are just concerned with rendering a the HTML document based on a template.
Suddenly there are three things that determine the document structure as a whole, and those are not really that co-located with the rest of the view/template/component stuff. It somehow feels a bit like mixing and spreading concerns. The router is for routing, the controller for calling your logic and returning html/json or whatever, and the view for the intricacies of building up the structure of an HTML document.

Luckily we now have components with slots as @josevalim mentioned though, and coming to think of it, is the root/app layout thing really still needed? I mean, couldn’t a layout just be wholly defined in a <.base_layout /> component (including the html, head and body wrapping things needed for e.g. liveview)?
Then, all the things about the html output could be determined through that component (e.g. slots for additional js/css things based on the template) and it’s children.
Additionally, the view implementation could be cleaned up a bit because put_layout/2, plug :put_root_layout, html: {MyWeb.Layouts, :root} and friends are not needed anymore.

Or would this be too far fetched or maybe even blatantly wrong thing to aspire @josevalim (or @chrismccord)?

1 Like

For reference, I made a related proposal over here: Remove view layouts in favor of function components