Split a Phoenix LiveView into smaller, composable modules.
Parent modules
A parent module declares its submodules with delegate/2:
defmodule MyAppWeb.DashboardLive do
use MyAppWeb, :live_view
use LiveDelegate
alias MyAppWeb.DashboardLive.Projects
alias MyAppWeb.DashboardLive.Users
# Mount order follows declaration order: Users, then Projects.
delegate(:users, Users)
delegate(:projects, Projects)
# A single call to delegate_mount/3 mounts the submodules,
# calling Users.on_mount/3 then Projects.on_mount/3.
def mount(params, session, socket) do
{:ok, socket |> delegate_mount(params, session)}
end
endMounting delegated submodules
delegate_mount/3 runs the on_mount/3 callback of each delegated submodule
whose mount: option is enabled:
# In parent module:
def mount(params, session, socket) do
{:ok, delegate_mount(socket, params, session)}
end
# In submodules:
def on_mount(params, session, socket) do
...
endDelegated submodules are mounted in declaration order. Each submodule receives
the same params and session, along with the socket returned by the preceding
submodule.
Unlike Phoenix.LiveView.mount/3, a delegated submodule's on_mount/3
callback must return the updated socket directly, not an {:ok, socket} tuple.
Set mount: false when a delegated submodule has no on_mount/3 callback,
or when you want to prevent its on_mount/3 callback from being called:
delegate(:notifications, Notifications, mount: false)If mounting is disabled for every delegated submodule, delegate_mount/3
returns the original socket unchanged.
Submodules
A submodule declares where it belongs with path::
defmodule MyAppWeb.DashboardLive.Projects do
use LiveDelegate, path: [:projects]
def on_mount(_params, _session, socket) do
socket |> delegate_assign(%{items: []})
end
def handle_event("add", _params, socket) do
{:noreply, socket}
end
endThe path namespaces its assigns, events, and DOM IDs:
delegate_assign(socket, value)
delegate_event("add") # "projects:add"
delegate_dom_id("form") # "projects-form"Nested submodules
A module can be both a submodule and a parent:
defmodule MyAppWeb.DashboardLive.Projects do
use LiveDelegate, path: [:projects]
alias MyAppWeb.DashboardLive.Projects.Filters
delegate(:filters, Filters)
def on_mount(params, session, socket) do
socket
|> delegate_assign(%{items: []})
|> delegate_mount(params, session)
end
endThe nested module declares its complete path:
defmodule MyAppWeb.DashboardLive.Projects.Filters do
use LiveDelegate, path: [:projects, :filters]
def on_mount(_params, _session, socket) do
socket |> delegate_assign(%{})
end
endIts helpers now use the nested namespace:
delegate_event("change") # "projects:filters:change"
delegate_dom_id("panel") # "projects-filters-panel"Messages and options
Use info: to route process messages by their first tuple element:
delegate(:notifications, Notifications,
events: false,
info: [:notification_received],
mount: false
)For example, {:notification_received, notification} calls:
Notifications.handle_info(notification, socket)Use mount: false, events: false, or info: false when a submodule does not
need that responsibility.
Summary
Functions
Assigns a value at the module's configured path.
Assigns a value below the module's configured path.
Builds a DOM ID from the module's configured path.
Builds a LiveView event name from the module's configured path.
Mounts the module's delegated submodules.
Functions
Assigns a value at the module's configured path.
Given:
use LiveDelegate, path: [:projects]This:
socket |> delegate_assign(%{items: []})is equivalent to:
socket |> assign(:projects, %{items: []})With a nested path such as [:projects, :filters], the value is assigned at
socket.assigns.projects.filters. The parent assign must already exist.
Assigns a value below the module's configured path.
Given:
use LiveDelegate, path: [:projects]This assigns the form under socket.assigns.projects.form:
socket |> delegate_assign(:form, form)A list can be used to update more deeply nested values:
socket |> delegate_assign([:form, :status], :ready)The configured assign and any intermediate maps must already exist.
Builds a DOM ID from the module's configured path.
Given:
use LiveDelegate, path: [:projects]This produces id="projects-form":
<.form id={delegate_dom_id("form")} for={@form}>
...
</.form>Nested paths are joined with hyphens. With
path: [:projects, :filters], delegate_dom_id("panel") returns
"projects-filters-panel".
Builds a LiveView event name from the module's configured path.
Given:
use LiveDelegate, path: [:projects]This button sends the "projects:add" event:
<button phx-click={delegate_event("add")}>
Add project
</button>Nested paths produce nested event names. With
path: [:projects, :filters], delegate_event("change") returns
"projects:filters:change".
Mounts the module's delegated submodules.
Call this from the parent module's mount/3 callback:
def mount(params, session, socket) do
{:ok, delegate_mount(socket, params, session)}
endEach delegated submodule whose mount: option is enabled receives the same
params and session. Submodules are called in declaration order, with each
receiving the socket returned by the preceding submodule. Their on_mount/3
callbacks must return the updated socket directly, not an {:ok, socket} tuple.
Set mount: false when a delegated submodule has no on_mount/3 callback,
or when you want to prevent its on_mount/3 callback from being called:
delegate(:notifications, Notifications, mount: false)If mounting is disabled for every delegated submodule, this returns the original socket unchanged.