This blog post is the third in a series on the creation of a small I18n application using Phoenix LiveView, which updates page content based on the language chosen from a dropdown menu:
- Internationalisation with Phoenix LiveView
- Internationalisation with Phoenix LiveComponents
- Internationalisation with Phoenix Live Layouts

LiveView version 0.5.0 introduced Live Layouts, a mechanism that allows LiveViews to move view-specific layout code into separate sub-layout files. This enables an individual LiveView’s template to nest itself within content that can dynamically update.
We will continue developing the application where we left off from the end of Internationalisation with Phoenix LiveComponents, which is its state on the 08-live-stateful-0-6 branch of the phx_i18n_example Github repository. That branch does not use Live Layouts, so we will see what the issues are with not using them, and then proceed to implement them.
If you do not have the repository already, just run these commands and it shall find its way to you:
git clone git@github.com:paulfioravanti/phx_i18n_example.git
cd phx_i18n_example
git checkout 08-live-stateful-0-6
The software versions used for this version of the application are the following:
- Elixir: 1.10.0
- Erlang: 22.2.1
- Phoenix: 1.4.12
- Gettext: 0.17.4
- LiveView: 0.6.0
- Node: 13.6.0
- Tachyons: 4.11.1
Current State of Play
There are three LiveViews in the application, each operating independently, containing their own LiveComponent, and communicating with each other by PubSub where needed:

LanguageDropdownLiveView and TitleLiveView are live rendered from the (static) layout:
lib/phx_i18n_example_web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
<%= live_render @conn,
TitleLiveView,
session: %{"locale" => @locale, "user_id" => @user_id} %>
<!-- ... -->
</head>
<body class="<%= body() %>">
<%= live_render @conn,
LanguageDropdownLiveView,
session: %{"locale" => @locale, "user_id" => @user_id} %>
<main role="main">
<%= render @view_module, @view_template, assigns %>
</main>
</body>
</html>
The Problem
For the application in its current form, this presents no issue. However, what if we wanted to add another routed LiveView (a LiveView used from router.ex) to the application to complement PageLiveView? What if we wanted to have this new LiveView set its own page title, or what if its content did not need to be internationalised, and hence we would not need the language dropdown menu to display?
The app.html.eex file is the main layout template within which all other template content is embedded, which means that any new LiveView would not have any control over the content that surrounds its template code:
- its title would be set according to whatever
TitleLiveViewrenders, rather than being able to provide its own page title logic -
LanguageDropdownLiveViewwill always be rendered (meaning also that the new LiveView would have to unnecessarily implement handlers for the"change-locale"events that the dropdown menu emits)
So, let’s set about giving our routed LiveView, in this case PageLiveView, more control over its surrounding content, starting with the page title.
LiveView-Controlled Page Title Updates
With LiveView 0.5.0, updating the HTML document title of a page becomes possible through specific use of an assigns variable called @page_title. Normally, the content of app.html.eex, or any non-.leex template, cannot be dynamically changed. But, Phoenix LiveView special-cases the page title, enabling a LiveView module to set the page title in Phoenix.LiveView.mount/3, and update it in any event handling callback functions.
So, in the layout, let’s switch out TitleLiveView for @page_title:
lib/phx_i18n_example_web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... -->
<title><%= @page_title %></title>
<!-- ... -->
</head>
<!-- ... -->
</html>
<!DOCTYPE html>
Now, in PageLiveView, let’s initialise the page_title assigns in mount/3, and update its value when the locale changes (ie we receive an external "change-locale" PubSub message):
lib/phx_i18n_example_web/live/views/page_live_view.ex
defmodule PhxI18nExampleWeb.PageLiveView do
# ...
require Gettext
require PhxI18nExampleWeb.Gettext
@title "Multilingualisation in Phoenix"
@locale_changes "locale-changes:"
def mount(
%{} = _params,
%{"locale" => locale, "user_id" => user_id},
socket
) do
Endpoint.subscribe(@locale_changes <> user_id)
socket =
assign(
socket,
locale: locale,
user_id: user_id,
page_title: page_title(locale)
)
{:ok, socket}
end
# ...
def handle_info(
%{event: "change-locale", payload: %{locale: locale}},
socket
) do
send_update(PageLiveComponent, id: :page, locale: locale)
socket = assign(socket, locale: locale, page_title: page_title(locale))
{:noreply, socket}
end
defp page_title(locale), do: Gettext.with_locale(locale, &title/0)
defp title, do: PhxI18nExampleWeb.Gettext.gettext(@title)
end
You should now see that the application continues to work as expected.
Just by virtue of setting and updating the page_title assigns value, Phoenix does all the heavy lifting of dynamically updating the @page_title module attribute. PageLiveView now has complete control over the page title when its template is rendered, which means the TitleLiveView and TitleLiveComponent modules have become completely obsolete. So, we can reduce our maintenance burden by removing them entirely. Hurray!
Live Layouts
Let’s now move our focus over to enabling LiveViews to choose whether they want to display a language selection dropdown menu or not, by extracting code for it into a separate layout.
We will start by removing the call to live render the LanguageDropdownLiveView from the main layout’s <body> tag:
lib/phx_i18n_example_web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en">
<!-- ... -->
<body class="<%= body() %>">
<%= render @view_module, @view_template, assigns %>
</body>
</html>
That code will now go directly inside a new page Live Layout file (note the .leex filename):
lib/phx_i18n_example_web/templates/layout/page.html.leex
<%= live_render @socket,
LanguageDropdownLiveView,
id: :language_dropdown,
session: %{"locale" => @locale, "user_id" => @user_id} %>
<main role="main">
<%= @live_view_module.render(assigns) %>
</main>
In this case, the @live_view_module attribute refers to the PageLiveView module.
Now, we need to specify that PageLiveView will be using this layout to wrap its template content:
lib/phx_i18n_example_web/live/views/page_live_view.ex
defmodule PhxI18nExampleWeb.PageLiveView do
alias PhxI18nExampleWeb.{Endpoint, LayoutView, PageLiveComponent}
use Phoenix.LiveView, layout: {LayoutView, "page.html"}
# ...
end
Note that the LayoutView is doing double-duty here as the view file for both app.html.eex and page.html.leex. If we were to extract the template code inline with the view code, it would look like this (also note the differences in sigils used; ~E for standard embedded Elixir templates vs ~L for LiveView templates):
lib/phx_i18n_example_web/views/layout_view.ex
defmodule PhxI18nExampleWeb.LayoutView do
use PhxI18nExampleWeb, :view
alias PhxI18nExampleWeb.{LanguageDropdownLiveView, LayoutStyle}
defdelegate body, to: LayoutStyle
def render("app.html", assigns) do
~E"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<%= csrf_meta_tag() %>
<title><%= @page_title %></title>
<link rel="stylesheet"
href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
<script type="text/javascript"
src="<%= Routes.static_path(@conn, "/js/app.js") %>">
</script>
</head>
<body class="<%= body() %>">
<%= render @view_module, @view_template, assigns %>
</body>
</html>
"""
end
def render("page.html", assigns) do
~L"""
<%= live_render @socket,
LanguageDropdownLiveView,
id: :language_dropdown,
session: %{"locale" => @locale, "user_id" => @user_id} %>
<main role="main">
<%= @live_view_module.render(assigns) %>
</main>
"""
end
end
The layout code extraction is now complete, and the application works as expected! Or, at least I thought it did, until I tried out the specific use case of opening the language dropdown menu, and then clicking the text on the page, which closes it:

Looks like there is a glitch in the LiveView Matrix… Why is the open language dropdown menu disappearing momentarily before re-appearing closed? Ultimately, all we did was cut code from one file, and paste it in another…right?
Although I’m not entirely sure of the specifics, it looks like perhaps the communication processes via PubSub between PageLiveView and DropdownLanguageLiveView are clobbering each other, and thus a re-think of how these two LiveViews and their LiveComponents talk to each other is in order, as well as deciding whether all these LiveView modules are even needed at all.
Too Many LiveViews?
Each of the LiveViews we had in the application at the beginning of this blog post, PageLiveView, LanguageDropdownLiveView, and TitleLiveView, were like isolated islands, functionality-wise.

There was no coupling between any of them; naturally, there was coupling between parent LiveViews and their child LiveComponents, but not between the LiveViews themselves.
With the introduction of Live Layouts, this has changed: now, PageLiveView, as well as being the parent of its own PageLiveComponent, is also, via the Live Layout, the parent of LanguageDropdownLiveView, which renders LanguageDropdownLiveComponent.

With TitleLiveView gone, the only place that LanguageDropdownLiveComponent needs to notify about locale changes is the PageLiveView, its “grandparent”. Similarly, LanguageDropdownLiveView would only seem to exist to let its child, LanguageDropdownLiveComponent, know about any "hide-dropdown" messages that it receives.
Given that the "hide-dropdown" messages come from PageLiveComponent, wouldn’t it be easier, and maybe less message-clobbery, to:
- get rid of the
LanguageDropdownLiveViewmiddleman - let
LanguageDropdownLiveComponentbePageLiveView’s child, rather than grandchild - have
PageLiveComponentandLanguageDropdownLiveComponenttalk to each other as siblings throughPageLiveView?
Let’s find out!
Family Tree Engineering
Okay, first thing’s first, LanguageDropdownLiveView is now gone. What do we need to do to get this working again? Let’s start with PageLiveView’s Live Layout, which now needs to directly render LanguageDropdownLiveComponent:
lib/phx_i18n_example_web/views/layout_view.ex
defmodule PhxI18nExampleWeb.LayoutView do
use PhxI18nExampleWeb, :view
alias PhxI18nExampleWeb.{LanguageDropdownLiveComponent, LayoutStyle}
# ...
end
lib/phx_i18n_example_web/templates/layout/page.html.leex
<%= live_component @socket,
LanguageDropdownLiveComponent,
id: :language_dropdown,
locale: @locale,
user_id: @user_id %>
<main role="main">
<%= @live_view_module.render(assigns) %>
</main>
Now, in the LanguageDropdownLiveComponent, whenever we get a local "locale-changed" event, rather than blast out a PubSub message, we instead want to send that message to the now-direct parent, PageLiveView:
lib/phx_i18n_example_web/live/components/language_dropdown_live_component.ex
defmodule PhxI18nExampleWeb.LanguageDropdownLiveComponent do
use Phoenix.LiveComponent
alias PhxI18nExampleWeb.LanguageDropdownView
@locales Gettext.known_locales(PhxI18nExampleWeb.Gettext)
# ...
def handle_event("locale-changed", %{"locale" => locale}, socket) do
send(self(), {:change_locale, locale})
state = update_locale_changed_state(socket.assigns, locale)
socket = assign(socket, state)
{:noreply, socket}
end
# ...
end
Note here that because the LiveComponent and the LiveView run in the same process, sending a message to self() sends the message from the component to the parent LiveView.
While we are purging PubSub message passing, let’s go next to PageLiveView’s other child, PageLiveComponent, and perform a similar refactor for when it receives "hide-dropdown" messages:
lib/phx_i18n_example_web/live/components/page_live_component.ex
defmodule PhxI18nExampleWeb.PageLiveComponent do
use Phoenix.LiveComponent
alias PhxI18nExampleWeb.PageView
# ...
def handle_event("hide-dropdown", _value, socket) do
send(self(), :hide_dropdown)
{:noreply, socket}
end
end
All message-passing from child LiveComponents to parent LiveViews is now being done without PubSub. Finally, we need to make changes in the parent PageLiveView to swap any PubSub-related subscription and message-handling code for taking messages directly from children:
lib/phx_i18n_example_web/live/views/page_live_view.ex
defmodule PhxI18nExampleWeb.PageLiveView do
alias PhxI18nExampleWeb.{
LanguageDropdownLiveComponent,
LayoutView,
PageLiveComponent
}
# ...
def mount(
%{} = _params,
%{"locale" => locale, "user_id" => user_id},
socket
) do
socket =
assign(
socket,
locale: locale,
user_id: user_id,
page_title: page_title(locale)
)
{:ok, socket}
end
# ...
def handle_info(:hide_dropdown, socket) do
send_update(
LanguageDropdownLiveComponent,
id: :language_dropdown,
show_available_locales: false
)
{:noreply, socket}
end
def handle_info({:change_locale, locale}, socket) do
send_update(PageLiveComponent, id: :page, locale: locale)
socket = assign(socket, locale: locale, page_title: page_title(locale))
{:noreply, socket}
end
# ...
end
Notice that in the handle_info/2 functions, the parameters are now atoms or tuples, and not the maps (eg %{event: "change-locale", payload: %{locale: locale}}) that we had before.
The application should now be flicker-less when hiding an open dropdown by clicking the page.
You can find the code for this iteration of the application in this post’s companion Github repo on the 09-live-layout branch.
Wrapping Up
The use of Live Layouts, even in this small application, has affected its architecture greatly. It has been interesting, at least for myself as the author, to have seen the codebase expand initially with lots of LiveViews and LiveComponents, and now contract back as we purge half of them away.
Deleted code is the easiest kind to maintain, though, so I do not mourn for it. Rather, I think it’s great that Live Layouts have enabled more flexibility in architecting LiveView functionality, and I look forward to using them more moving forward!
Leave a comment