Dialup.Page behaviour (Dialup v0.1.3)

Copy Markdown View Source

The behaviour and macro for Dialup page modules.

A page module handles a single route. Place it at the appropriate path under your app_dir and it will be routed automatically:

# lib/app/page.ex         → /
# lib/app/blog/[slug]/page.ex → /blog/:slug

Usage

defmodule MyApp.App.Page do
  use Dialup.Page

  def mount(_params, assigns) do
    {:ok, Map.put(assigns, :count, 0)}
  end

  def handle_event("increment", _, assigns) do
    {:update, Map.update!(assigns, :count, &(&1 + 1))}
  end

  def render(assigns) do
    ~H"""
    <h1>Count: {@count}</h1>
    <button ws-event="increment">+1</button>
    """
  end
end

Callbacks

  • mount/2 — called on every page navigation. Receives URL params and the current assigns (which already include session data set by layouts). Return {:ok, new_assigns}. Defining mount/1 (assigns only) is also accepted as a convenience.
  • render/1 — renders the page HTML using HEEx.
  • handle_event/3 — called when a ws-event, ws-submit, or ws-change fires.
  • handle_info/2 — called for Erlang process messages (e.g. PubSub, timers).
  • page_title/1 — optional; return a string to set <title>. Return nil to use the application default.
  • agent_state/1 — returns the allowlisted state projection visible to an attached agent.
  • agent_message/1 — explains the page concept, operating flow, and safety constraints to an agent that has no source-code context.
  • agent_grant/1 — defines capabilities, projections, expiry, and version requirements for HTTP MCP session tokens.

Human and agent projections

Use <.dialup_action> instead of a plain event button when the same operation should be available to an attached agent. Use <.dialup_region> for stable domain areas that need a semantic name, structured data, or action relationships.

<.dialup_action> is the single boundary for what an agent can do: raw ws-event, ws-submit, ws-change, and ws-href elements are never auto-exposed as tools. To make a navigation link agent-operable, declare it with <.dialup_action navigate="/path">; the same declaration renders the human link and generates the navigation tool.

Ordinary visible DOM elements remain available for human spatial references, but their generated selectors are less stable than region IDs. See guides/agent-native-app-development.md for the complete implementation workflow.

Return values for handle_event/3 and handle_info/2

Return valueEffect
{:noreply, assigns}Update state, no re-render
{:update, assigns}Re-render the full page
{:patch, id, html, assigns}Replace only the element with the given id
{:redirect, path, assigns}Navigate to another page (session is preserved)
{:push_event, name, payload, assigns}Call a JS hook function and re-render

Module attributes

  • @layout false — disable all layout wrapping (useful for login/fullscreen pages).
  • @static true — serve the page without establishing a WebSocket connection.

Colocation CSS

Place a page.css file in the same directory as page.ex. It is automatically scoped to the page at compile time (no build tool required).

Helpers

use Dialup.Page imports overwrite/2, set_default/2, subscribe/2, declare_action/1, declare_region/1, dialup_action/1, and dialup_region/1.

Summary

Functions

Declares an action that is available to both the browser UI and an attached agent.

Declares a semantic region that an attached agent can reference.

Renders an action button and serializes extra component attributes as event parameters.

Wraps content in a semantic region that agents can read through read_scene.

Derives the canonical navigation action name for an app path.

Merges overwrite_map into assigns, replacing any existing keys.

Merges defaults into assigns, keeping existing values for keys that are already set.

Subscribes to a Phoenix.PubSub topic and registers it for automatic unsubscription on page navigation.

Callbacks

agent_grant(assigns)

(optional)
@callback agent_grant(assigns :: map()) :: map() | keyword()

agent_message(assigns)

(optional)
@callback agent_message(assigns :: map()) :: binary() | map()

agent_state(assigns)

(optional)
@callback agent_state(assigns :: map()) :: map()

handle_event(event, value, assigns)

@callback handle_event(event :: binary(), value :: any(), assigns :: map()) ::
  {:noreply, map()}
  | {:update, map()}
  | {:patch, target :: binary(), rendered :: any(), map()}
  | {:redirect, path :: binary(), map()}
  | {:push_event, event_name :: binary(), payload :: map(), map()}

handle_info(msg, assigns)

(optional)
@callback handle_info(msg :: any(), assigns :: map()) ::
  {:noreply, map()}
  | {:update, map()}
  | {:patch, target :: binary(), rendered :: any(), map()}
  | {:redirect, path :: binary(), map()}
  | {:push_event, event_name :: binary(), payload :: map(), map()}

mount(assigns)

(optional)
@callback mount(assigns :: map()) :: {:ok, map()}

mount(params, assigns)

@callback mount(params :: map(), assigns :: map()) :: {:ok, map()}

page_title(assigns)

(optional)
@callback page_title(assigns :: map()) :: binary() | nil

render(assigns)

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

Functions

declare_action(opts)

(macro)

Declares an action that is available to both the browser UI and an attached agent.

declare_action name: :increment,
               desc: "Increment the counter",
               params: %{amount: {:integer, default: 1}}

declare_region(opts)

(macro)

Declares a semantic region that an attached agent can reference.

dialup_action(assigns)

Renders an action button and serializes extra component attributes as event parameters.

<.dialup_action name={:add_item} available={@status == :draft} sku="SKU-1" qty="1">
  Add item
</.dialup_action>

Pass navigate to render a navigation link instead of an event button. The same declaration becomes a navigation tool for an attached agent, so the agent can move between pages exactly where a human can click:

<.dialup_action navigate="/docs/concepts">Concepts</.dialup_action>

Navigation actions take no parameters; the destination is fixed at the declaration site. When name is omitted it is derived from the path (e.g. /docs/concepts becomes :navigate_docs__concepts).

dialup_region(assigns)

Wraps content in a semantic region that agents can read through read_scene.

overwrite(assigns, overwrite)

Merges overwrite_map into assigns, replacing any existing keys.

assigns |> overwrite(%{user: user, loaded: true})

set_default(assigns, defaults)

Merges defaults into assigns, keeping existing values for keys that are already set.

assigns |> set_default(%{count: 0, page: 1})

subscribe(pubsub, topic)

Subscribes to a Phoenix.PubSub topic and registers it for automatic unsubscription on page navigation.

Call this inside mount/2 to ensure the subscription is cleaned up when the user navigates away.

def mount(_params, assigns) do
  subscribe(MyApp.PubSub, "room:lobby")
  {:ok, %{messages: []}}
end