Intro: what and why
I’ve searched more or less high and low, but now one seems to have a good answer on how to properly implement a layout that implements things like:
-
live_patch
for links -
live_render
to embed various live components - works across all views and controllers
The reason for such a layout is simple: imagine a CMS system. You may want to have:
-
a sidebar with all the links to internal functionality: list all pages, edit a page, manage uploaded media, manage users. All these will be handled by different controllers. But you would still want a
live_patch
link to relevant pages -
in that same sidebar (or some other place like a sidebar to the right, or a footer, or a notification pop up) you want to place all manners of various things: your internal console, a list of things that are happening in the system (for example, you’re batch-processing a bunch of images and you want to see the status of the job even as you’re working on something else in the system) etc.
The problem
The problem is: how to achieve all that?
The setup
So, let’s assume you have the following in your router.ex
pipeline :admin do
plug :authenticate_user
plug :put_root_layout, {CMS.LayoutView, :admin}
end
scope "/admin", QuireWeb do
pipe_through [...all your other pipleines ..., :admin]
end
You have the following in your CMSWeb.AdminController
:
defmodule CMSWeb.AdminController do
use CMSWeb, :live_view
use CMSWeb, :controller
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end
def mount(_params, _session, socket) do
{:ok, Phoenix.LiveView.assign(socket, some_data: [])}
end
def render(assigns) do
~L"""... render your live data as you would..."""
end
You have the following in your CMSWeb.ProcessesView
:
defmodule CMSWeb.ProcessesView do
use CMSWeb, :live_view
def render(assigns) do
~L"""... render live data ..."""
end
def mount(_params, _, socket) do
## You would also subscribe to a PubSub queu here, and have
## multiple handle_info callbacks to update the view
{:ok, assign(socket, some_data: [])}
end
The layouts, the data flow and issues
The rendering goes through these layers
1. Root layout: can render live components, live_patch
is broken
The root layout will be template/lyout/admin.html.leex
(irrelevant code omitted for clarity):
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<%= live_patch "All pages", to: Routes.live_path(@conn, CMSWeb.AdminController) %>
<%= live_render(@conn, CMSWeb.ProcessesView) %>
<%= @inner_content %>
</body>
-
live_patch
in the root template is “broken” (aka works as expected). It works, it will do alive_redirect
, but the underlying<a>
tag will get aphx-click-loading
class that is never cleared. The reason is here: phx-click-loading class not removed · Issue #956 · phoenixframework/phoenix_live_view · GitHub -
live_render
works beautifully. It does create a separate LiveView instance, but other than that everything works as expected
2. Live layout: inclusion of live_render
ends up messing up live_patch
Let’s remove both live_patch
and live_render
from our root template, and add them to template/live.html.leex
:
<%= live_patch "All pages", to: Routes.live_path(@socket, CMSWeb.AdminController) %>
<%= live_render(@socket, CMSWeb.ProcessesView, id: "unique-id") %>
<%= @inner_content %>
First, the changes:
-
@conn
is no longer available, you use@socket
-
live_render
requires anid
because the component is now rendered within the context of an existing LiveView
Problems I encountered:
- if you include the
live_render
in this layout, alllive_patch
links get broken and do a full page reload instead of a live update. This even includeslive_patch
links rendered by LiveView inside@inner_content
- if you keep
live_render
in the root layout, no problem withlive_patch
, but then you can’t really have the layout you need
3. The controller/view
And the last step in the rendering is the actual view returned by your controller. The problem having a layout on this level is obvious:
- you need to copy-paste/include the layout in every separate view
- events sent to the live component will end up being sent to the currently active controller
Halp?
So, I’m stumped. And I can’t figure out a good way to do this. I looked at LiveView dashboard, but I doubt this is an approach that everyone will like or can use: phoenix_live_dashboard/page_builder.ex at master · phoenixframework/phoenix_live_dashboard · GitHub
Currently, sticking everything into the root layout kinda works, and is the least hassle, but doesn’t feel right.