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.