PhoenixExRatatui.LiveComponent (PhoenixExRatatui v0.1.0)

Copy Markdown View Source

Macro that turns the calling module into a Phoenix.LiveComponent hosting an embedded TUI — the same module is both the component and the ExRatatui.App it drives.

Where PhoenixExRatatui.LiveView makes the page itself a TUI, this macro drops a TUI inside an existing LiveView alongside whatever else that LV is rendering — admin dashboards, dev consoles, half-page overlays.

Quick start

defmodule MyAppWeb.AdminCounterPanel do
  use PhoenixExRatatui.LiveComponent

  def tui_mount(_opts), do: {:ok, %{count: 0}}
  def tui_render(state, frame), do: [...]
  def tui_handle_event(_event, state), do: {:noreply, state}
end

# In a parent LiveView:
defmodule MyAppWeb.AdminLive do
  use Phoenix.LiveView

  def render(assigns) do
    ~H"""
    <h1>Admin Dashboard</h1>
    <.live_component module={MyAppWeb.AdminCounterPanel} id="admin-counter" />
    <p>Other admin content</p>
    """
  end
end

How it works

Same trick as PhoenixExRatatui.LiveView: the macro injects Phoenix.LiveComponent callbacks AND, via @after_compile, generates a sibling Module.Runtime proxy that conforms to ExRatatui.App by delegating to the tui_* callbacks. The handle_info/2 arity collision between LV and ExRatatui.App is the same in LiveComponents (well, LCs don't have handle_info, but the abstraction stays consistent).

TUI callbacks

Same as PhoenixExRatatui.LiveView:

  • tui_mount/1, tui_render/2, tui_handle_event/2, tui_handle_info/2, tui_terminate/2, tui_mount_opts/1

See PhoenixExRatatui.LiveView's moduledoc for full callback semantics.

Threading parent assigns into the App

The component's update/2 receives assigns from the parent LV. Override tui_mount_opts/1 (which gets the component socket) to thread them into tui_mount/1:

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

def tui_mount_opts(socket), do: [user: socket.assigns.user]

def tui_mount(opts), do: {:ok, %{user: opts[:user]}}

Wire model

LiveComponents share the parent LV's process — they don't have their own mailbox or handle_info/2. We hand the runtime Server a writer that calls Phoenix.LiveView.send_update/3 instead of send/2. Each rendered diff arrives in update(%{tui_diff: diff}, socket) and flows out to the client via push_event/3.

Mount-failure handling matches the LiveView macro: if tui_mount/1 returns {:error, _}, the runtime server's init returns {:stop, reason}, the linked parent LV gets the EXIT, and we set :tui_error so the next render shows a fallback. The parent LV must trap exits for this to work — otherwise the EXIT signal kills the whole LV. We set Process.flag(:trap_exit, true) lazily on the first resize event before calling Transport.start_link/1. This sets it on the parent LV process; if that LV traps exits for other reasons, the flag is shared.

Intents and the parent LV

When the embedded TUI emits intents (e.g. returning {:noreply, state, intents: [{:navigate, "/login"}]} from tui_handle_event/2), the intent must dispatch from the parent LV's process, not the LiveComponent — Phoenix LV forbids redirects from inside LiveComponent.update/2.

This module's intent_writer therefore sends a regular message to the parent LV: send(parent_pid, {:phoenix_ex_ratatui, :intent, intent}). The parent LV must have a handle_info/2 clause that picks it up:

def handle_info({:phoenix_ex_ratatui, :intent, intent}, socket) do
  {:noreply, PhoenixExRatatui.LiveView.dispatch_intent(socket, intent)}
end

PhoenixExRatatui.LiveView.dispatch_intent/2 handles the standard intent shapes ({:navigate, _}, {:patch, _}, {:redirect, _}).

When the LC is embedded inside another PhoenixExRatatui.LiveView (rare but supported), this clause is generated automatically. For the common case — embedding inside a plain Phoenix.LiveView — copy the snippet above.

Telemetry

Same events as PhoenixExRatatui.LiveView — see PhoenixExRatatui.Telemetry.

Summary

Functions

Generates the unified LiveComponent + App module. See the moduledoc.

Functions

__using__(opts)

(macro)

Generates the unified LiveComponent + App module. See the moduledoc.