Discovering the extra container element for Phoenix LiveViews

May 31, 2022

tl;dr

live.html.heex has an element that is injected by Phoenix LiveView which causes a difference in DOM structure between live.html.heex and app.html.heex. This difference in structure might impact sharing layouts between app.html.heex and live.html.heex due to slight differences in your application DOM structure.

One approach to fixing the problem is through using CSS. You can target the additional element with the following:

[data-phx-main] {
  /* your additional styling here */
}

The better approach is to configure your LiveView container value in your application's my_app_web.ex module. Specifically, set the :container value to the following:

# ...
  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {MyAppWeb.LayoutView, "live.html"},
        # Add the following
        container: {:div, class: "liveview-container"}

      unquote(view_helpers())
    end
  end
#...
/* Some css */
.liveview-container {
  /* your styling here */
}

Problem

In order to keep the experience the consistent between web page views that are LiveViews and static views I wanted to create a shared layout. However, when applying the same styling to the two the shared layout in LiveViews and non-LiveViews, the LiveViews did not render the same. This is because the structure of the DOM for LiveView views was slightly different, causing some of the CSS to apply differently.

Assumptions

App name

For the rest of the post, assume that the app name is my_app. So if you started a new phoenix project, you would pass the following parameters: mix phx.new my_app.

First principles

Before we get down to the one line change, let's tease back a little bit of the magic in Phoenix to make sure we understand how everything works.

Phoenix layout templates

Let's define the different layout templates in a Phoenix LiveView project. When you generate a Phoenix LiveView project there are three files in the /lib/my_app/templates/layout directorylive-layouts.

  • /lib/my_app/templates/layout/root.html.heex: An html template that wraps all child views in your app. Both LiveView and non-LiveView views.
  • /lib/my_app/templates/layout/app.html.heex: An html template that wraps non-LiveView views.
  • /lib/my_app/templates/layout/live.html.heex: An html template that wraps LiveView views that call use Phoenix.LiveView passing the layout option of :layout

These views call <%= @inner_content %> to transclude the child views into the layout templates.

How is the layout option passed for child views?

lib/my_app_web.ex

If you read the above description and check your current project's LiveViews, you might be wondering how the layout option is being set for LiveViews. If you read through your LiveView module, you'll most likely see something like the following.

defmodule MyAppWeb.ExampleLive.Index do
  use MyAppWeb, :live_view
  # ...
end

There aren't any references to passing the :layout param to LiveView anywhere. There is one key line here: use MyAppWeb, :live_view. If you open up lib/my_app_web.ex you'll see something similar to the following:

# ...
  def live_view do
    quote do
      use Phoenix.LiveView,
        # Here's the line of interest!
        layout: {MyAppWeb.LayoutView, "live.html"}

      unquote(view_helpers())
    end
  end
# ...

In this module the live_view macro is defined which passes the needed :layout argument to Phoenix.LiveView. This is how LiveViews are using live.html.heex as their layout.

Where does the magical data-phx-main div come from?

I actually had a heck of a time finding out where the data-phx-main element came from. In the long run, I ended up finding the first call and the last call where the element of interest is rendered without finding the exact code that's responsible for rendering data-phx-main upon first pageload.

LiveViews have an associated container element that is set when rendering. The container element can be configured for the LiveView by passing a :container parameter when useing Phoenix.LiveView in your app.

Solution

Passing container: {:div, class: "liveview-container"} allows you to define any element as well as sets of attributes, styles and classes you want to define on the containing element. For sake of simplicity I've defined a div with a class of liveview-container. You can of course use more semantic html elements if you so choose.

# ...
  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {MyAppWeb.LayoutView, "live.html"},
        # Add the following
        container: {:div, class: "liveview-container"}

      unquote(view_helpers())
    end
  end
#...

References

  1. Transclude
  2. Live layouts

© 2023, Built with ❤️ by Blake Dietz