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
@type state() :: term()
Callbacks
@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.
@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}
endExample: 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
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).
@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.