One module per page: render, event handlers, stream callbacks, and colocated function components.
defmodule MyAppWeb.CounterPage do
use StarView.Page
alias MyAppWeb.Components.StarView.Layout
def mount(conn, _params), do: assign(conn, count: 0)
def render(assigns) do
~H"""
<Layout.app conn={@conn}>
<div data-signals:count={@count}>
<button data-on:click={event("increment")}>+1</button>
</div>
</Layout.app>
"""
end
def handle_event("increment", %{"count" => count}, %{method: "POST"} = conn)
when is_integer(count) and count >= 0 and count < 1_000_000 do
patch_signals(conn, %{count: count + 1})
end
def handle_event(_event, _signals, conn), do: conn
endRoute it with StarView.Router.star_view/2:
import StarView.Router
star_view "/counter", MyAppWeb.CounterPageStarView.Page.Plug owns the HTTP and stream lifecycle; pages implement
application callbacks. Conn in, conn out.
Layout
StarView.Page.Plug disables the Phoenix root layout before mount/2,
because the generated Components.StarView.Layout owns the document
(Datastar script, CSRF signal, initial signals) — render through
Layout.app/1. To render inside your Phoenix root layout instead, put it
back in mount/2:
def mount(conn, _params) do
Phoenix.Controller.put_root_layout(conn, html: {MyAppWeb.Layouts, :root})
endOptions
:idle_check— ms between connection liveness checks in the stream loop (default30_000).:refresh_interval— minimum ms betweenhandle_refresh/1calls (default0).:checked_events— derive event contracts from actual handler clauses and check literal localevent/1,2references in~STAR(defaultfalse); seeStarView.Events.
Coalesced refresh
Define handle_refresh/1 to re-render after scoped invalidations. It runs
once after handle_connect/2 (so the stream starts from a current snapshot
after load or reconnect) and once per burst of StarView.invalidate/1 calls:
def handle_connect(conn, _params) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "orders:" <> conn.assigns.current_scope.org_id)
conn
end
def handle_info({:order_changed, _id}, conn), do: StarView.invalidate(conn)
def handle_refresh(conn) do
orders = MyApp.Orders.list_for(conn.assigns.current_scope)
patch(conn, &orders/1, [orders: orders], key: :orders, only_changed: true)
endRead and authorize business state inside the callback from trusted conn
assigns; it may return {:halt, conn}. See StarView.Stream.
StarView.invalidate/1 is called qualified rather than imported, so pages
that already define their own invalidate/1 keep compiling.
Summary
Callbacks
Stream open: subscribe to topics, assign loop state. Optional —
defining it enables POST /path streaming.
Runs once after stream claim release, including exceptions. Receives the latest completed callback conn.
Handles a Datastar event request. SSE is already started. Optional.
Handles one message from the library-owned receive loop. Optional.
Renders current state after handle_connect/2 and after coalesced
StarView.invalidate/1 calls. Optional — defining it enables refresh for the stream.
GET: load data and assign what render/1 needs. Optional.
The full-page HEEx template. Required.
If defined, the stream opens via StarView.Stream.open/2 keyed on the result. Optional.
Callbacks
@callback handle_connect(Plug.Conn.t(), params :: map()) :: Plug.Conn.t()
Stream open: subscribe to topics, assign loop state. Optional —
defining it enables POST /path streaming.
@callback handle_disconnect(Plug.Conn.t()) :: term()
Runs once after stream claim release, including exceptions. Receives the latest completed callback conn.
@callback handle_event(event :: String.t(), signals :: map(), Plug.Conn.t()) :: Plug.Conn.t()
Handles a Datastar event request. SSE is already started. Optional.
@callback handle_info(msg :: term(), Plug.Conn.t()) :: Plug.Conn.t() | {:halt, Plug.Conn.t()}
Handles one message from the library-owned receive loop. Optional.
@callback handle_refresh(Plug.Conn.t()) :: Plug.Conn.t() | {:halt, Plug.Conn.t()}
Renders current state after handle_connect/2 and after coalesced
StarView.invalidate/1 calls. Optional — defining it enables refresh for the stream.
@callback mount(Plug.Conn.t(), params :: map()) :: Plug.Conn.t()
GET: load data and assign what render/1 needs. Optional.
@callback render(assigns :: map()) :: Phoenix.LiveView.Rendered.t()
The full-page HEEx template. Required.
@callback stream_key(Plug.Conn.t()) :: term()
If defined, the stream opens via StarView.Stream.open/2 keyed on the result. Optional.