Render Item Content With LiveViews

Copy Markdown View Source

Item content is what appears inside a canvas item frame.

The item owns runtime behavior: identity, placement, visibility, resize rules, selection, ports, persistence, and shared coordination. The content owns the UI inside the frame.

For many apps, item content is another LiveView.

Define A Content LiveView

Use LivePanels.Item.Content.LiveView for a LiveView that will run inside an item:

defmodule MyAppWeb.NoteLive do
  use MyAppWeb, :live_view

  use LivePanels.Item.Content.LiveView,
    pubsub: MyApp.PubSub

  @impl true
  def mount_item(socket, props) do
    {:ok,
     socket
     |> assign(:body, LivePanels.Item.prop(props, :body, ""))
     |> assign(:saved?, true)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <form phx-submit="save">
      <textarea name="body" phx-change="edit">{@body}</textarea>
      <button type="submit">Save</button>
    </form>
    """
  end

  @impl true
  def handle_event("edit", %{"body" => body}, socket) do
    {:noreply, assign(socket, body: body, saved?: false)}
  end
end

mount_item/2 receives the item props from the canvas declaration or add command. Use it to initialize content assigns.

Use The LiveView Content Adapter

Source-owned items declare the content LiveView directly on the item:

<:item
  id="note-1"
  type="note"
  at={{120, 80}}
  size={{520, 320}}
  live_view={MyAppWeb.NoteLive}
  props={%{body: "Start writing..."}}
>
</:item>

Runtime-created items use the same content adapter in the command payload. The application renders their frame and places the supervised content with context.content:

<.canvas_runtime {canvas_runtime_attrs(assigns)}>
  <:item :let={context}>
    <article class="note-frame">
      <header {drag_handle_attrs()}>{context.item.label}</header>
      <section>{context.content}</section>
      <.resize_handles :if={context.view.resizable?} />
    </article>
  </:item>
</.canvas_runtime>

The content LiveView should not render the outer draggable frame. Keep the frame in the canvas item renderer so every item uses the same canvas behavior.

Send Commands From Content

Item content can ask the canvas to do item-scoped work through LivePanels.Item.Content.LiveView.Commands.

For example, a note can remove itself:

def handle_event("remove", _params, socket) do
  {:noreply, LivePanels.Item.Content.LiveView.Commands.remove(socket)}
end

Or it can update its application item props:

def handle_event("save", %{"body" => body}, socket) do
  socket =
    socket
    |> LivePanels.Item.Content.LiveView.Commands.merge_props(%{body: body})
    |> assign(body: body, saved?: true)

  {:noreply, socket}
end

Use item-content helpers when the content wants to affect its own canvas item. Use canvas application commands when the application is coordinating multiple items or app state.

Message Between App And Content

Item content is a LiveView process. That matters for lifecycle and messaging.

The runtime tracks the child process while the item exists. When an item is removed, the runtime can notify and terminate the content. When a content process exits, the item can remain in the canvas while the content is vector itemed unavailable.

Applications can subscribe to item or canvas runtime topics when they need external coordination:

:ok = LivePanels.subscribe(socket, {:item, "note-1"})
:ok = LivePanels.subscribe(socket, :events)

Prefer explicit commands for mutations. Use messages for domain events, status updates, or integration with the rest of your Phoenix app.

Use Non-LiveView Content When It Fits

Not every item needs a child LiveView.

LivePanels item declarations can also represent static rendered content, remote embeds, nested canvases, or custom renderer modules. Use a LiveView when the inside of the item needs LiveView lifecycle, events, assigns, subscriptions, or stateful UI.

Use simpler content when the item is mostly presentation or when the application already owns the state.

Keep The Boundary Clear

Good item content:

  • renders the UI inside the item;
  • handles its own form events and local state;
  • uses item-content helpers for its own item commands;
  • leaves placement, drag, resize, focus, selection, and z-order to the canvas.

Good application item frames:

  • render the card/window/chrome around content;
  • attach drag_handle_attrs/1, resize handles, remove controls, and graph or drawing attrs;
  • read context.view instead of reimplementing capability checks;
  • keep app-specific style and product language outside LivePanels internals.

That boundary is what lets the same item content work in local canvases, shared canvases, graph nodes, nested canvases, and persisted layouts.