A2UI.SurfaceProvider behaviour (A2UI v0.3.0)

Copy Markdown View Source

Behaviour for defining A2UI surfaces and handling user actions.

Implement this behaviour to serve interactive UI surfaces over WebSocket. The server calls your callbacks when a client connects and when users interact with your surface.

Example

defmodule MyApp.DashboardProvider do
  @behaviour A2UI.SurfaceProvider

  alias A2UI.Builder, as: UI

  @impl true
  def init(_opts), do: {:ok, %{counter: 0}}

  @impl true
  def surface(state) do
    UI.surface("dashboard")
    |> UI.text("count", "Count: #{state.counter}")
    |> UI.button("inc", "Increment", action: "increment")
    |> UI.card("main", children: ["count", "inc"])
    |> UI.root("main")
  end

  @impl true
  def handle_action(%A2UI.Action{name: "increment"}, state) do
    new_state = %{state | counter: state.counter + 1}
    {:reply, surface(new_state), new_state}
  end

  def handle_action(_action, state), do: {:noreply, state}
end

Summary

Callbacks

Handle a user action received from the client.

Handle an arbitrary message sent to the socket process.

Called when a new WebSocket connection is established.

Build the current surface from state.

Types

state()

@type state() :: term()

Callbacks

handle_action(t, state)

@callback handle_action(A2UI.Action.t(), state()) ::
  {:noreply, state()} | {:reply, A2UI.Surface.t(), state()}

Handle a user action received from the client.

Return {:noreply, state} to acknowledge without sending a response, or {:reply, surface, state} to send an updated surface to the client.

handle_info(msg, state)

(optional)
@callback handle_info(msg :: term(), state()) ::
  {:noreply, state()}
  | {:push_data, String.t(), map(), state()}
  | {:push_surface, A2UI.Surface.t(), state()}

Handle an arbitrary message sent to the socket process.

This is optional. Implement it to react to timers, PubSub messages, GenServer casts, or any external event and push updates to the client.

Return values:

  • {:noreply, state} — update state, send nothing to client
  • {:push_data, surface_id, data, state} — send a data model update
  • {:push_surface, surface, state} — send a full surface update

Example: Timer-based push

def init(_opts) do
  Process.send_after(self(), :tick, 1000)
  {:ok, %{uptime: 0}}
end

def handle_info(:tick, state) do
  Process.send_after(self(), :tick, 1000)
  new_state = %{state | uptime: state.uptime + 1}
  {:push_surface, surface(new_state), new_state}
end

Example: Phoenix.PubSub

# Add {:phoenix_pubsub, "~> 2.1"} to your app's deps
def init(_opts) do
  Phoenix.PubSub.subscribe(MyApp.PubSub, "updates")
  {:ok, %{}}
end

def handle_info({:data_changed, data}, state) do
  {:push_data, "dashboard", data, state}
end

init(opts)

@callback init(opts :: map()) :: {:ok, state()} | {:error, term()}

Called when a new WebSocket connection is established.

Receives connection options (currently an empty map; future versions may include query params and headers).

Return {:ok, state} to accept the connection, or {:error, reason} to reject it (the WebSocket will be closed).

surface(state)

@callback surface(state()) :: A2UI.Surface.t()

Build the current surface from state.

Called after init/1 to produce the initial surface sent to the client. Also called by the application to produce a fresh surface from current state.