StarView.Page behaviour (star_view v0.7.0)

Copy Markdown View Source

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
end

Route it with StarView.Router.star_view/2:

import StarView.Router
star_view "/counter", MyAppWeb.CounterPage

StarView.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})
end

Options

  • :idle_check — ms between connection liveness checks in the stream loop (default 30_000).
  • :refresh_interval — minimum ms between handle_refresh/1 calls (default 0).
  • :checked_events — derive event contracts from actual handler clauses and check literal local event/1,2 references in ~STAR (default false); see StarView.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)
end

Read 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

handle_connect(t, params)

(optional)
@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.

handle_disconnect(t)

(optional)
@callback handle_disconnect(Plug.Conn.t()) :: term()

Runs once after stream claim release, including exceptions. Receives the latest completed callback conn.

handle_event(event, signals, t)

(optional)
@callback handle_event(event :: String.t(), signals :: map(), Plug.Conn.t()) ::
  Plug.Conn.t()

Handles a Datastar event request. SSE is already started. Optional.

handle_info(msg, t)

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

handle_refresh(t)

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

mount(t, params)

(optional)
@callback mount(Plug.Conn.t(), params :: map()) :: Plug.Conn.t()

GET: load data and assign what render/1 needs. Optional.

render(assigns)

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

The full-page HEEx template. Required.

stream_key(t)

(optional)
@callback stream_key(Plug.Conn.t()) :: term()

If defined, the stream opens via StarView.Stream.open/2 keyed on the result. Optional.