Phoenix 1.7 and inversion of control to render a sidebar

Hi!
I’m trying to have a layout in Phoenix 1.7, where besides @inner_content I also have a dynamic sidebar.

The content of the sidebar should depend on the view. How should I approach this? The last place where I can affect the sidebar is a controller I guess - i can set the assigns[:sidebar_content] and then read this in sidebar component.

However, it seems more elegant to set the sidebar content in the view – the controller assigns data, and the view decides how it is displayed in @inner_content and in the sidebar.
Is such inversion of control flow possible?

You can send a Phoenix Component in the controller assigns instead of sending the sidebar content.

In your controller:

defmodule NewPhoenixWeb.PageController do
  use NewPhoenixWeb, :controller

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

  def test(conn, _params) do
    render(conn, :test, sidebar_content: apply(NewPhoenixWeb.Layouts, :admin_sidebar, [[]]))
  end
end

In your layouts.ex:

defmodule NewPhoenixWeb.Layouts do
  use NewPhoenixWeb, :html

  embed_templates "layouts/*"

  def default_sidebar(assigns) do
    ~H"""
    <p>default sidebar</p>
    """
  end

  def admin_sidebar(assigns) do
    ~H"""
    <p>admin sidebar</p>
    """
  end
end

(There’s probably a better place for the above code, but layouts.ex exists in all Phoenix 1.7 apps.)

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

<aside class="sidebar">
  <%= @sidebar_content %>
</aside>

If your sidebar is a component, you can use slots instead.

defmodule NewPhoenixWeb.Layouts do
  slot :inner_block

  def sidebar(assigns) do
    ~H"""
    <aside>
      <%= render_slot(@inner_block) %>
    </aside>
    """
  end
end
<.sidebar>
  <%= @sidebar_content %>
</.sidebar>



Things can get complicated depending on the number of sidebar variations, but it can be a good start.

2 Likes

Another approach is to have two app layouts. One for regular users and another for admin. Any logic or markup shared between the layouts can be moved to the root layout or into function components. So the layouts are very similar, except for the sidebar.

4 Likes

Thanks! The sidebar here will have more variants then just 2 (user/admin) – it could show a bit different contextual data depending on the view.

I think I will couple the decision about sidebar content with the controller.

@mssantosdev - thanks! Appreciate you took time to give me examples in your reply! <3
I have one question though: why do use apply here instead of just writing NewPhoenixWeb.Layouts.default_sidebar()?

As far as I know, it’s just a matter of preference. I don’t have a better answer for that, sorry. :frowning:

Tested with the following and it works as well:

defmodule NewPhoenixWeb.PageController do
  use NewPhoenixWeb, :controller

  def home(conn, _params) do
    # The home page is often custom made,
    # so skip the default app layout.
    render(conn, :home, layout: false, sidebar_content: NewPhoenixWeb.Layouts.default_sidebar([]))
  end
end