I’m migrating a small/medium sized application to use the new phx 1.7 view-less component paradigm and I’ve run into a few issues.
Our layout is non-trivial in that each page can potentially have a sub-navigation menu, header and search bar.
We previously accomplished this by allowing each controller action to pass a specific view as part of assigns, then the templates would check if that view was set and render it. Something like this:
in the controller:
render(conn, "show.html",
class: class,
content_header_view: {AppWeb.ClassView, "_header.html"}
)
And in our “admin” layout:
<%= if content_header_view = assigns[:content_header_view] do %>
<% {module, template} = content_header_view %>
<section>
<%= render(module, template, assigns) %>
</section>
This worked well-enough and allowed for the flexibility we desired at a per-action level.
With the move to components, I thought this would be a great use case for slots but I’m now a bit baffled at what the “right” way to do this is.
Here’s what I tried first with the move to phx 1.7:
I moved the old “admin.html.heex” layout to /components/layouts/
and in my layouts file called embed_templates
. Then I created a bodyless function as described by the docs in order to define my slots, like so:
defmodule AppWeb.Layouts do
@moduledoc """
The module for handling layouts
"""
use AppWeb, :html
embed_templates "layouts/*"
slot :search_form_content,
doc: "A special slot for a search form that lives at the top of the page",
required: false
slot :header_content,
doc: "A slot for content that is fixed at the top of the page",
required: false
slot :secondary_nav_content, doc: "A slot for a secondary nav", required: false
def admin(assigns)
end
I set the controller to use the admin
layout and then, in my index.html.heex
file, I added this at the top:
<:secondary_nav_content>
Test Nav Content!
</:secondary_nav_content>
Existing content here was here
When I refreshed the page, I got this error: invalid slot entry <:secondary_nav_content>. A slot entry must be a direct child of a component
… which, after the fact, made sense to me since I didn’t wrap that slot in a component. But my naive expectation was that I could call into the admin
layout’s slot from within the template that’s being rendered.
I then tried:
<.admin>
<:secondary_nav_content>
Test Nav Content!
</:secondary_nav_content>
Existing content here was here
<.admin/>
But that gave me an error about not being able to find the admin
component. And even if it had worked, I believe it would have rendered the admin layout twice, once inside the other since we’re using put_layout
to set the admin layout.
Here’s where I ended up:
I ended up making a layout file for each major “resource” in my site and then I called into the admin
component directly in that file.
E.g.: /components/layouts/student.html.heex
now has this content in it:
<.admin {assigns}>
<:search_form_content>
<AppWeb.StudentHTML.search_form {assigns} />
</:search_form_content>
<:header_content :if={assigns[:student]}>
<AppWeb.StudentHTML.header {assigns} />
</:header_content>
<:secondary_nav_content :if={assigns[:student]}>
<AppWeb.StudentHTML.subnav {assigns} />
</:secondary_nav_content>
</.admin>
Which is slightly different from another section like /components/layouts/class.html.heex
which has only one slot filled out:
<.admin {assigns}>
<:header_content :if={assigns[:class]}>
<AppWeb.ClassHTML.header {assigns} />
</:header_content>
</.admin>
And the controllers now specifically put_layout on each action:
def show(conn, %{"id" => id}) do
course = LegacyData.get_course_with_requisites!(id)
conn
|> put_layout({AppWeb.Layouts, :course})
|> render(:show,
page_title: course.course_name,
course: course
)
end
In a sense, this is what I hoped for but for some reason it feels a bit icky and I’m not sure why.
My questions are: is this sane? Is it the intended approach? Is there a better way to do this?
Can anyone point to another project that handles non-trivial layouts like this?
I suppose my issue with this approach is: It’s odd that you can define slots on the admin view that cannot be used unless you compose a new view on top of it. This solution “works” in that I get the ability to define slots on a case by case basis but it requires that I define a new layout that uses the admin
layout as a component internally.
This is not a pattern I’ve seen elsewhere so I’m curious what others think.