Can you set assigns within a heex template to be used within a parent layout?

Is there a way for an HTML module to render some content that can be hoisted up into one of the parent layouts?

Our goal is to mimic the behavior of “Portals” that are present in some of the JS frameworks. This feature can be helpful for things like dynamically adding script tags at the bottom of the layout on a per-route basis or for adding DOM elements that might be displayed completely outside of the hierarchy for where they would otherwise be rendered. (Modals, for example.)

* root.html.heex
  * app.html.heex

* records_controller.ex => Demo.UsersController
* records_web.ex => Demo.UsersHTML

In app.html.heex

<div>
    <%= @inner_content %>
</div>

<!-- Render some content here that an HTML module might have created. -->

In Demo.UsersHTML

defmodule Demo.UsersHTML do
  use DemoWeb, :html
  embed_templates "templates_html/*"

  def index(assigns) do
    // Not sure if we could set/render something here,
    // update assigns but still render index.html.heex
  end
end

In index.html.heex:

<div>My Index Page</div>

<!-- Alternatively, render something right in the template that is captured? -->
<_some_tag_or_slot_>This should go inside app layout.</_some_tag_or_slot_>

Not really. Slots allow a parent to place parts of something larger into distinct parts within the actual markup, but it‘s always going to be top-down.

You can send a Phoenix Component in the controller assigns and use it in the root layout.

In your controller:

defmodule NewPhoenixWeb.PageController do
  use NewPhoenixWeb, :controller

  def home(conn, _params) do
    render(conn, :home, my_root_component_content: apply(NewPhoenixWeb.Layouts, :custom_content_in_layout, [[]]))
  end

  def test(conn, _params) do
    render(conn, :home, my_root_component_content: apply(NewPhoenixWeb.Layouts, :another_custom_content_in_layout, [[]]))
  end
end

In your layouts.ex:

defmodule NewPhoenixWeb.Layouts do
  use NewPhoenixWeb, :html

  embed_templates "layouts/*"

  def custom_content_in_layout(assigns) do
    ~H"""
    <p>content one</p>
    """
  end

  def another_custom_content_in_layout(assigns) do
    ~H"""
    <p>content two</p>
    """
  end
end

Finally, in your root.html.heex (or any other layout file you want to use):

<div class="custom-content">
  <%= @my_root_component_content %>
</div>

Also, you can use slots:

defmodule NewPhoenixWeb.Layouts do
  slot :inner_block

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

In root.html.heex:

<.my_root_component>
  <%= @my_root_component_content %>
</.my_root_component>

There’s a similar question here: Phoenix 1.7 and inversion of control to render a sidebar

Interesting, thanks for sharing that. We did know that we could set an assigns from within the Controller, but not sure if we could set it from inside the View module.

The benefit of the View module is that we have one for HTML and one for JSON content, so any injection can be isolated to just the HTML Module. As well, the actual content we want to “send” to the root layout is resolved inside the HTML View’s template, not the Controller.

We’re basically wondering if we can recreate something like Portals, as provided by frameworks like React and SolidJS:

References:

1 Like