How to override the default app layout for a specific LiveView?

How can I set a specific layout for a specific LiveView?

I have the following route:

  scope "/:locale", MyAppWeb do
    pipe_through :browser

    live "/", LandingPageLive, :home
    # some other routes
  end

And this in MyAppWeb:

 def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {MyAppWeb.Layouts, :app}

      on_mount MyAppWeb.SetLocale
      unquote(html_helpers())
    end
  end

This means that by default all LiveViews have the app layout.
How can I make it so that the LandingPageLive does not have the app layout (which contains a sidebar)?

Looking at the documentation, I see here that there are two ways to override the default layout:

  • live_session which I do not have
  • :layout option when mounting the LiveView.

Since I don’t have a live_session in my router, I’m going for the second option using {MyAppWeb.Layouts, :landing_page}:

defmodule MyAppWeb.LandingPageLive do
  use MyAppWeb, :live_view
  use Gettext, backend: MyAppWeb.Gettext

  @impl LiveView
  def mount(params, _session, socket) do
    socket = assign(socket, layout: {MyAppWeb.Layouts, :landing_page})
    {:ok, socket}
  end

  @impl LiveView
  def render(assigns) do
    ~H"""
    Welcome!
    """
  end
end

And I have a the following directory structure:

lib/my_app_web/
β”œβ”€β”€ components
β”‚   β”œβ”€β”€ core_components.ex
β”‚   β”œβ”€β”€ layouts
β”‚   β”‚   β”œβ”€β”€ app.html.heex
β”‚   β”‚   β”œβ”€β”€ landing_page.html.heex
β”‚   β”‚   └── root.html.heex
β”‚   β”œβ”€β”€ layouts.ex
β”‚   └── sidebar.ex
β”œβ”€β”€ live
β”‚   β”œβ”€β”€ landing_page_live.ex

However, I get this error:

invalid value for reserved key :layout in Phoenix.Template.render/4 assigns.
:layout accepts a tuple of the form {LayoutModule, "template.extension"},
got: {ClipboardWeb.Layouts, :landing_page}

Looks like I need to pass a string:

defmodule MyAppWeb.LandingPageLive do
  use MyAppWeb, :live_view
  use Gettext, backend: MyAppWeb.Gettext

  @impl LiveView
  def mount(params, _session, socket) do
    socket = assign(socket, layout: {MyAppWeb.Layouts, "landing_page.html"})
    {:ok, socket}
  end

  @impl LiveView
  def render(assigns) do
    ~H"""
    Welcome!
    """
  end
end

Not enough:

no "landing_page.html" html template defined for ClipboardWeb.Layouts  (the module exists but does not define landing_page.html/1 nor render/2

My MyAppWeb.Layouts looks like so:

defmodule MyAppWeb.Layouts do
  @moduledoc """
  This module holds different layouts used by your application.

  See the `layouts` directory for all templates available.
  The "root" layout is a skeleton rendered as part of the
  application router. The "app" layout is set as the default
  layout on both `use MyAppWeb, :controller` and
  `use MyAppWeb, :live_view`.
  """
  use MyAppWeb, :html

  alias MyAppWeb.Sidebar

  embed_templates "layouts/*"
end

Thus I assume that it embeds all templates under the lib/my_app_web/components/layouts/.

What’s the expectation here?

Decided to hack around by conditionally rendering the sidebar in the app layout based on the @sidebar_enabled variable that I set on a server hook:

defmodule MyAppWeb.Hooks.ConfigureLayout do
  @moduledoc """
  Module responsible for configuring the layout.
  """
  import Phoenix.Component, only: [assign: 2]

  alias MyAppWeb.LandingPageLive

  @impl true
  def on_mount(:default, _params, _session, socket) when socket.view == LandingPageLive do
    socket = assign(socket, sidebar_enabled: false)
    {:cont, socket}
  end

  def on_mount(:default, _params, _session, socket) do
    socket = assign(socket, sidebar_enabled: true)
    {:cont, socket}
  end
end

Which I then call in MyAppWeb:

 def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {MyAppWeb.Layouts, :app}

      on_mount MyAppWeb.Hooks.SetLocale
      on_mount MyAppWeb.Hooks.ConfigureLayout
      unquote(html_helpers())
    end
  end

Though feels more like a hack than a solution.

The return value from mount can also set the layout:

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:mount/3

It must return either {:ok, socket} or {:ok, socket, options}, where options is one of:

  • :temporary_assigns - a keyword list of assigns that are temporary and must be reset to their value after every render. Note that once the value is reset, it won’t be re-rendered again until it is explicitly assigned
  • :layout - the optional layout to be used by the LiveView. Setting this option will override any layout previously set via Phoenix.LiveView.Router.live_session/2 or on use Phoenix.LiveView
1 Like

This is wrong. You don’t assign the layout as an assign on the socket, but it’s an option on the return tuple of mount.

def mount(params, _session, socket) do
    {:ok, socket, layout: {MyAppWeb.Layouts, :landing_page}}
  end
2 Likes

Ah yeah completely on my, I now see clearly how it’s written in the documentation:

The :layout option returned on mount, via {:ok, socket, layout: ...} will override any previously set layout option

Many thanks both of you @rhcarvalho @LostKobrakai !