LiveDelegate (live_delegate v0.1.0)

Copy Markdown View Source

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

  delegate(:projects, Projects)

  def mount(params, session, socket) do
    {:ok, socket |> delegate_mount(params, session)}
  end
end

Submodules are mounted in declaration order. Each submodule's on_mount/3 returns the updated socket directly. When every submodule uses mount: false, delegate_mount/3 returns the 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
end

The 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
end

The 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
end

Its 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.

Functions

delegate(name, module_ast, opts \\ [])

(macro)

delegate_assign(socket, value)

(macro)

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.

delegate_assign(socket, relative_path, value)

(macro)

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.

delegate_dom_id(value)

(macro)

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".

delegate_event(name)

(macro)

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".