# `StarView.Page`
[🔗](https://github.com/Neophen/star_view/blob/v0.7.0/lib/star_view/page.ex#L1)

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.

# `handle_connect`
*optional* 

```elixir
@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`
*optional* 

```elixir
@callback handle_disconnect(Plug.Conn.t()) :: term()
```

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

# `handle_event`
*optional* 

```elixir
@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`
*optional* 

```elixir
@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`
*optional* 

```elixir
@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`
*optional* 

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

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

# `render`

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

The full-page HEEx template. Required.

# `stream_key`
*optional* 

```elixir
@callback stream_key(Plug.Conn.t()) :: term()
```

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