LocalLiveView behaviour (LocalLiveView v0.1.0)

Copy Markdown View Source

LocalLiveView implements functionality of Phoenix.LiveView inside the browser in Popcorn runtime.

Instead of running the code on the server and sending updates via WebSocket, the local live view's code is sent to the browser on page load. Whenever you render a local live view on the page, it is run on the client.

LocalLiveView API similar to Phoenix.LiveView:

Thus, you can implement a simple local live view just like a regular live view:

defmodule DemoLive do
  use LocalLiveView

  def render(assigns) do
    ~H"""
    Hello world!
    """
  end
end

and add it to your page using the local_live_view/1 component:

<.local_live_view view="DemoLive" />

The main difference between LocalLiveView and Phoenix.LiveView is that the former can also accept assigns and has an update callback, like Phoenix.LiveComponent:

defmodule Cart do
  use LocalLiveView

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, open: false)}
  end

  @impl true
  def update(%{items: items}, socket) do
    {:ok, assign(socket, :items, items)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>The cart has {length(@items)} items</div>
    """
  end
end

You can reder it the following way:

<.local_live_view view="Cart" items={@items} />

Sending events to the server

When a local live view is rendered inside a Phoenix.LiveView, it can send events to that server-side live view by calling push_server_event/3 from Phoenix.LiveView.handle_event/3 or handle_info/2:

def handle_event("archive", %{"id" => id}, socket) do
  {:noreply,
   socket
   |> assign(:items, archive_item(socket.assigns.items, id))
   |> push_server_event("archive", %{"id" => id})}
end

This pattern gives optimistic updates: the event is handled locally first (the optimistic edit), then delivered to the host LiveView's handle_event/3, whose authoritative state can later override it.

Handling failed server pushes

A push_server_event/3 can fail: the page may have no host LiveView, the websocket may be disconnected, or the host may reply with an error or time out. When that happens, the view's handle_push_error/4 callback is invoked with the event, its params and server_assigns — the last value of each assign received from the host server. The default implementation feeds server_assigns through update/2, rolling optimistic local edits back to the latest authoritative state.

Another way of communicating the server is by using mirror_sync/2.

Summary

Callbacks

Handles an event triggered from the template, such as phx-click.

Handles a message sent to the view's process.

Invoked with the query params of the page the view is rendered on.

Invoked when a push_server_event/3 fails: there is no host LiveView on the page, the websocket is disconnected, or the host replies with an error or times out.

Invoked once when the view is initialized, before the first render/1.

Returns the HEEx template for the view's current state.

Receives the assigns the host LiveView passes down.

Functions

Mimics Phoenix.LiveView.connected?/1. Always returns true.

Syncs the declared mirror assigns to the server-side mirror channel. Must be called from within a LocalLiveView callback (handle_event, handle_info) after assigns have been updated.

Navigates to the given path with a browser history push, then calls handle_params/3 with the new URL query params. No server round-trip.

Sends an event to the host (server) LiveView that mounts this local live view.

Types

unsigned_params()

@type unsigned_params() :: map()

Callbacks

handle_event(event, unsigned_params, socket)

(optional)
@callback handle_event(
  event :: binary(),
  unsigned_params(),
  socket :: Phoenix.LiveView.Socket.t()
) ::
  {:noreply, Phoenix.LiveView.Socket.t()}
  | {:reply, map(), Phoenix.LiveView.Socket.t()}

Handles an event triggered from the template, such as phx-click.

Bindings work exactly as in Phoenix.LiveView, but the event is dispatched to the local process instead of travelling to the server, so the following render/1 happens without a round-trip.

def handle_event("increment", _params, socket) do
  {:noreply, update(socket, :count, &(&1 + 1))}
end

To notify the host LiveView as well, push the event on with push_server_event/3.

handle_info(message, socket)

(optional)
@callback handle_info(message :: term(), socket :: Phoenix.LiveView.Socket.t()) ::
  {:noreply, Phoenix.LiveView.Socket.t()}

Handles a message sent to the view's process.

A local live view runs as its own Elixir process inside the browser, so anything that can reach that process arrives here — for example, a timer set with Process.send_after/3:

def mount(_params, _session, socket) do
  Process.send_after(self(), :tick, 1000)
  {:ok, assign(socket, :time, Time.utc_now())}
end

def handle_info(:tick, socket) do
  Process.send_after(self(), :tick, 1000)
  {:noreply, assign(socket, :time, Time.utc_now())}
end

The resulting render is pushed to the DOM as a diff, exactly as after handle_event/3. The default implementation ignores the message.

handle_params(params, uri, socket)

(optional)
@callback handle_params(
  params :: unsigned_params(),
  uri :: String.t(),
  socket :: Phoenix.LiveView.Socket.t()
) :: {:noreply, Phoenix.LiveView.Socket.t()}

Invoked with the query params of the page the view is rendered on.

Called after mount/3 and again after every push_patch/2. params holds the query string decoded into a map with string keys and uri is the full URL.

def handle_params(%{"tab" => tab}, _uri, socket) do
  {:noreply, assign(socket, :tab, tab)}
end

handle_push_error(event, params, server_assigns, socket)

(optional)
@callback handle_push_error(
  event :: binary(),
  params :: unsigned_params(),
  server_assigns :: map(),
  socket :: Phoenix.LiveView.Socket.t()
) :: {:noreply, Phoenix.LiveView.Socket.t()}

Invoked when a push_server_event/3 fails: there is no host LiveView on the page, the websocket is disconnected, or the host replies with an error or times out.

event and params are the event name and payload the failed push carried (params has string keys, after a JSON round-trip, like handle_event/3 params). server_assigns holds the last value of each assign received from the host server (through mount and update/2) — assigns only ever set locally are absent.

The default implementation feeds server_assigns through update/2, as if the host had re-sent them — restoring the latest authoritative state through the view's usual derivation path. A view whose update/2 skips unchanged data (e.g. guarded by a revision counter) should override this callback and force its rollback explicitly.

mount(params, session, socket)

(optional)
@callback mount(
  params :: unsigned_params() | :not_mounted_at_router,
  session :: map(),
  socket :: Phoenix.LiveView.Socket.t()
) ::
  {:ok, Phoenix.LiveView.Socket.t()}
  | {:ok, Phoenix.LiveView.Socket.t(), keyword()}

Invoked once when the view is initialized, before the first render/1.

Use it to set up the initial assigns. Assigns coming from the host LiveView are not delivered here — update/2 runs with them right after this callback, before the first render.

def mount(_params, _session, socket) do
  {:ok, assign(socket, count: 0, label: "Counter")}
end

render(assigns)

@callback render(assigns :: Phoenix.LiveView.Socket.assigns()) ::
  Phoenix.LiveView.Rendered.t()

Returns the HEEx template for the view's current state.

Called on mount and again after every state change, exactly like Phoenix.LiveView.render/1 — except the render happens in the browser, so no diff travels over the network.

def render(assigns) do
  ~H"""
  <p>{@label}: {@count}</p>
  """
end

update(assigns, socket)

(optional)
@callback update(assigns :: map(), socket :: Phoenix.LiveView.Socket.t()) ::
  {:ok, Phoenix.LiveView.Socket.t()}

Receives the assigns the host LiveView passes down.

The mechanism is the same as in Phoenix.LiveComponent and its Phoenix.LiveComponent.update/2 callback.

Every attribute other than view given to local_live_view/1 is forwarded here, the same way Phoenix.LiveComponent receives its assigns:

<.local_live_view view="Cart" items={@items} currency="EUR" />

It runs right after mount/3 with the initial values, and then on every re-render of the host LiveView. The host always sends the full set of forwarded assigns, not a diff, so a view that needs to react only to actual changes has to compare against its own assigns.

def update(%{items: items}, socket) do
  {:ok, assign(socket, :items, items)}
end

The default implementation assigns everything it receives.

This callback is also called by the default implementation of handle_push_error/4.

Functions

connected?(socket)

Mimics Phoenix.LiveView.connected?/1. Always returns true.

Helps code reusability between server and local LiveViews.

mirror_sync(socket, mirror_keys)

Syncs the declared mirror assigns to the server-side mirror channel. Must be called from within a LocalLiveView callback (handle_event, handle_info) after assigns have been updated.

push_patch(socket, opts)

Navigates to the given path with a browser history push, then calls handle_params/3 with the new URL query params. No server round-trip.

Mirrors Phoenix.LiveView.push_patch/2 semantics.

Options

  • :to — the path to navigate to (required)
  • :replace — when true, replaces the current history entry instead of pushing a new one

push_server_event(socket, event, payload \\ %{})

Sends an event to the host (server) LiveView that mounts this local live view.

Callable from handle_event/3 or handle_info/2, it delivers event/payload to the host LiveView's handle_event(event, payload, socket) over the regular Phoenix websocket.

If the push fails (no host LiveView, disconnected socket, error reply or timeout), the view's handle_push_error/4 callback is invoked.