Remove view layouts in favor of function components

Current situation

Currently, the structure of the HTML returned by phoenix is determined by the layouts (components/layouts/[root,app].html.heex) as well as the template you select and render in the controller or liveview.

Having an app and root layout seems to be required and the framework user is instructed to set those layouts somewhere within the plug pipeline by using put_layout/2 and put_root_layout/2, or switching the layout through render/3.

Furthermore, function components are now a thing and one can use slots to provide additional content and they also can be used to build complex layouts.

The issue

A layout as a concept seems to be mostly a concern only when generating HTML using a view. But currently it is necessary to add that view related logic in the router and controller. Even though it is only a few lines here and there, it doesn’t seem to add clarity of those router and controller modules and looks a bit out of place regarding separation of concerns.
Handling view layouts on that level makes the views/templates less self-contained. In order to know what the resulting HTML of a template will be, one also has to consider what has been set further up the chain.

There also seems to be some confusion around views and layouts in general from people mostly used to things like template inheritance (jinja2/django/twig style templating).
I also used that style of templating quite often and this proposal came in part from using templates as self-contained means for rendering whole HTML documents (while trying to minimize repetition and template logic spread into other parts of the system) in the past, as well as from the thoughts emerging from topics like Using function components for template inheritance for example.

Lastly, the newly introduced phoenix function components with slots seem to be the right tool for the job, but they aren’t used for that entry point part of the template rendering process.

The proposal

Make it possible to define the whole HTML output using a template which in itself contains all the information needed to render the whole page by means of using function components.

We could have the root and app layouts defined and used just like all other function components. A homepage template could contain:

<.root>
  <h1>Welcome to my applications homepage</h1>
</.root>

And a regular template (e.g. for a recipe) could look like this:

<.app>
  <:title><%= @recipe.name %></:title>
  <:aside><%= raw @recipe.ingredients %></:aside>
  <%= raw @recipe.instructions %>
</.app>

The app layout component could contain

<.root>
  <h1><%= render_slot(@title) %></h1>
  <main><%= render_slot(@inner_block) %></main>
  <aside><%= render_slot(@aside) %></main>
</.root>

And the root component could be something like the current default root layout (with @inner_block instead of @inner_content of course):

<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="csrf-token" content={get_csrf_token()} />
    <.live_title suffix=" · Phoenix Framework">
      <%= assigns[:page_title] || "TheDefault" %>
    </.live_title>
    <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
    <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
    </script>
  </head>
  <body class="bg-white antialiased">
    <%= render_slot(@inner_block) %>
  </body>
</html>

This could make the view/templating implementation easier to work with and more straightforward to explain to newcomers. The concept of layouts would move into the realm of the other view logic related concepts, and as a result could be removed from the routing and business logic calling parts (plug pipeline, routers, controllers…).

Implementing it

I could imagine that changing this could touch quite a few places in the code base. Also it would need some coordination between at least the phoenix_templates, phoenix_live_views, and phoenix libraries. On the other hand it also could lead to some cleanup of now no longer needed library api.

One thing that could be an impediment is that, as it stands, liveview doesn’t seem to use the current root layout directly for rendering but relies on it for providing the body wrapping part including the javascript needed for it to function properly. This could maybe be solved by conditional rendering in the <.root /> component, rendering the <html><head>...</head><body>...</body></html> structure at first for the initial request and only the @inner_block when liveview is taking over (if that makes sense).

Even though there are quite a few phoenix users that would be more capable of implementing the needed changes quickly and without breaking too much phoenix (live)view internals, I would be happy to help getting this implemented.

The impact on the existing eco-system

As this is quite a fundamental thing, I guess that there is some impact and that migration instructions would have to be written. Each entry point template would now have to be wrapped explicitly in a layout component.
I guess there could also be a deprecation period where current-style wrapping using layouts is still possible, and when the put_layout and put_root_layout calls are removed, the new style rendering is there to take over.

The impact on learning

As mentioned in the section about the issues with the current approach, I think that it would be easier to grasp the views and templating part of phoenix if done like this.

Could this easily be implemented or is it inline with the state of Phoenix as it is today?

Superficially, for me and not having that much in depth experience with the phoenix view layer internals yet myself, it would seem doable, though not easily as it touches at least phoenix_templates, phoenix_live_views, and phoenix itself.

Would the suggestion benefit Phoenix or the community as a whole?

Of course (as I am writing this), I can see how this would lead to a more clear separation of concerns and a more clear way of handling views and templates in phoenix, so yes I guess.

3 Likes

What you’re proposing, unless I’m missing a piece, is already possible. There is no requirement to specify layouts at all, so you can forgo them altogether and define everything as function components just as you described. I actually do something like this already in some projects. I leave root as is but use a layout component for each area (public and admin in my case). It’s funny because just last night I ditched this in a project and moved back to using the regular layouts I actually feel much better about it, so I’ve personally come to really like status quo.

Regarding some of your points, I’m not really concerned about separation of concerns within the router as it is already doing a few things. Plug pipelines also mix concerns in that they do business logic and http stuff. As for controllers, being that they are coordinate transforming a request into data and passing it to a view, I think it’s totally their concern to have control over which view gets rendered. Even though controllers often end up having business logic in them, they really shouldn’t, so when you remove that the concerns seem clearer. I also feel totally fine that controllers can inherit a default layout from the layer above, ie, the router.

All in a this is a really well written up and thought up proposal! Kudos on that. And these are just my opinions on it, of course. Was just funny to read after thinking a bunch about it last night :slight_smile:

2 Likes

Hi @rmoorman! This is a well written proposal.

I would just add that we need to keep the root layout isolated, because it draws a boundary over what is live updated or not.

However, we don’t strictly need an app.html.heex and you could replace them by your proposal today. About changing Phoenix, I am not quite sure: I suspect a lot of people would find the requirement to wrap all of their templates inside a layout component quite repetitive. I assume a lot if it will boil down to the Rails vs Django background.

4 Likes

Before responding I tried moving the root layout to a function component, removing it from the pipeline (as well as all other layout calls) and using it in a liveview. It seemed to work fine. It had the injected wrapped div below <body> and everything. I didn’t thoroughly test this, of course, so would you see a problem with it? I’m just curious.

def root(assigns) do
  ~H"""
  <!DOCTYPE html>
  <html>
    <!-- etc, etc -->
    <body>
      <%= render_slot(@inner_block) %>
    </body>
  </html>
  """
end

def layout(assigns) do
  ~H"""
  <.root>
    <%= render_slot(@inner_block) %>
  </.root>
  """
end

Check the rendered HTML rather than what the browser shows you. The browser often tries to fix the content, which may not be reliable behavior across all browsers. I suspect you are wrapping your whole HTML inside a div.

Hey, I’m a 90s kid, I always View Source when looking at HTML :slight_smile: Buuuut you’re right. I botched my first test it seems and yes, the whole thing is wrapped in the phx-data-main div. My bad, sorry @rmoorman. Though still fine to use “app” layouts as function components, of course.

1 Like

Thank you for sharing your thoughts on this and the kind words. :slight_smile:

I can see how your approach would work, and even to some extend to achieve what I proposed, the only things being that the root layout is still set outside the template, the template does not give the whole picture of what is being done and there are still those tiny bits of code that I feel don’t need to be there.

When I try it like you sketched it in a fresh phoenix installation, I can make it work.
When I try to go for the whole document, I can also make it work for regular templates by defining a LayoutComponents module and adding the root and app components there, adjusting the top-level “Web” module so that the used controller code is instructed to not use layouts, as well using put_layout in the router to set the layouts to false (because otherwise, I get an ArgumentError from phoenix_template that no “app” template is defined. But things then fall apart when I add a liveview due to the way phoenix_live_view expects things to be (as you already encountered I suppose :wink:).

I too like to keep controllers and routing clean and one is well advised to push that business logic into a context for example. But I have the same feeling when it comes to view logic. Conceptually, a layout belongs to the code that makes up what is presented to the end-user. And so does the rest of the html. And those presentation related things should be grouped together as much as possible IMHO.

And the current approach leaves me with the feeling that just that could be improved. But maybe as you and @josevalim mentioned it may be also just a mindset thing. I personally don’t see a reason why it wouldn’t be possible to make it work and how explicitly stating in an entry-point template what html structure I would like to fill is an unreasonable thing.

By the way, it is indeed quite a coincidence that you just thought about the same issue last night :smiley:

1 Like

You should be able to delete put_layout altogether, no need to set to false.

I would say so. I personally don’t feel pain around tracing what layouts views are getting. I can’t remember exactly why I adopted the layout component thing for some projects (though I’m pretty I made a thread about it a couple of years ago) but I know it wasn’t because I wanted to see <.layout> in every template. Could it be better? Maybe, but I personally wouldn’t vote for specifying the layout every single time. While I am all about explicitness there is nothing magical happening here, the layouts are all spelled out in your application code so I find it easy to trace.

2 Likes

Hello @josevalim , thank you for saying that and sharing your thoughts :slight_smile:.

I indeed already noticed that liveview expects the page to be set up in a certain way for it to work properly.
Regarding isolation, I think there could be at least two ways to tackle that.
For one, an easy (but maybe not that fancy) way could be to use conditional rendering within the root layout component based on data the liveview would pass to the template.
Another way could be to add some kind of boundary, markers, or some other way to separate parts of generated template data and extract only the relevant part for liveview rendering.

As for not strictly needing an app layout, it seems that there is at least some code (in phoenix_template) that expects an app html template to be defined though (or at least phoenix gave me some errors when I tried to disable the layouts in the top level web module and the router).

And as for your last point, I can see how that could be a mindset issue.
In python land, having complete control over the html output in one place (including whitespace control) is a common thing as rendering a page template is oftentimes just gathering data for the template and calling the template with that data to get the document. Template inheritance can seem quite handy for those that are used to it.
I only have limited exposure to rails front-end development but from what I do know about ruby land, I can imagine that the mind set can be a bit different :slight_smile:

Personally I don’t mind declaring the base layout used on the entry point template explicitly for what it’s worth. The decision has to be made somewhere and I’d rather encode it closer to other things that determine how things are presented. A trade-off could be made for the “root layout” as is mostly plumbing/wrapping, but having a way to add some style or js to the head of the document from the template could also be a need for some.

I am really glad about the way those components turned out by the way and having slots seems to provide a lot of flexibility in building more complex page layouts. They look like a good alternative to “template inheritance” as well.

@josevalim , I thought about the conditional rendering part a bit and I am wondering if it would make sense (or even be feasible) to have components support “capturing” assigns from the view they are used in.

The idea would be to introduce a capture annotation for the component function (next to the attr and slot ones we already have). This can then be used to capture the knowledge of liveview being active for a given (layout) component.

capture :liveview
def root(assigns) do
  ~H"""
  <%= if not @liveview do %>
    <!-- all the HTML needed to get the basic js/css up and ready to go for live view to take over -->
      <%= render_slot(@inner_block) %>
    <!-- all the HTML needed to get the basic js/css up and ready to go for live view to take over -->
  <% else %>
      <%= render_slot(@inner_block) %>
  <% end %>
  """
end

Besides using it for this, capturing assigns could then also be used to minimize prop drilling into components in general.

What do you think?