TermUI.StatefulComponent behaviour (TermUI v0.1.0)
View SourceBehaviour for stateful, interactive components.
StatefulComponent extends the base Component behaviour with state management and event handling. Use this for components that need to maintain internal state and respond to user input.
Basic Usage
defmodule MyApp.Counter do
use TermUI.StatefulComponent
@impl true
def init(props) do
{:ok, %{count: props[:initial] || 0}}
end
@impl true
def handle_event(%KeyEvent{key: :up}, state) do
{:ok, %{state | count: state.count + 1}}
end
def handle_event(%KeyEvent{key: :down}, state) do
{:ok, %{state | count: state.count - 1}}
end
def handle_event(_event, state) do
{:ok, state}
end
@impl true
def render(state, _area) do
text("Count: #{state.count}")
end
endLifecycle
init/1- Initialize state from propshandle_event/2- Process input eventsrender/2- Render current state
Commands
Event handlers can return commands for side effects:
def handle_event(%KeyEvent{key: :enter}, state) do
{:ok, state, [{:send, parent_pid, {:submitted, state.value}}]}
endOptional Callbacks
terminate/2- Cleanup when component stopshandle_info/2- Handle non-event messageshandle_call/3- Handle synchronous calls
Summary
Types
Commands for side effects
Event types from user input
Event handler return value
Component props
Available rendering area
Render tree output
Component state - any term
Callbacks
Handles synchronous calls.
Handles input events and updates state.
Handles non-event messages.
Initializes component state from props.
Called when the component is mounted to the active tree.
Renders the component's current state.
Handles component termination.
Called when the component is unmounted from the tree.
Called when the component's props change.
Types
@type command() :: {:send, pid(), term()} | {:timer, non_neg_integer(), term()} | {:focus, term()} | term()
Commands for side effects
@type event() :: term()
Event types from user input
@type event_result() :: {:ok, state()} | {:ok, state(), [command()]} | {:stop, reason :: term(), state()}
Event handler return value
@type props() :: map()
Component props
Available rendering area
@type render_tree() :: TermUI.Component.RenderNode.t() | [render_tree()] | String.t()
Render tree output
@type state() :: term()
Component state - any term
Callbacks
@callback handle_call(request :: term(), from :: term(), state()) :: {:reply, term(), state()} | {:reply, term(), state(), [command()]} | {:noreply, state()} | {:noreply, state(), [command()]}
Handles synchronous calls.
For request-response patterns where the caller needs a reply.
Parameters
request- The request termfrom- Caller identifier for replystate- Current component state
Returns
{:reply, response, new_state}- Reply and update state{:reply, response, new_state, commands}- Reply with commands{:noreply, new_state}- Don't reply yet
@callback handle_event(event(), state()) :: event_result()
Handles input events and updates state.
Called when the component receives a keyboard, mouse, or focus event. Returns updated state and optional commands.
Parameters
event- The input event (KeyEvent, MouseEvent, FocusEvent)state- Current component state
Returns
{:ok, new_state}- Updated state{:ok, new_state, commands}- Updated state with commands{:stop, reason, state}- Stop the component
Examples
@impl true
def handle_event(%KeyEvent{key: :enter}, state) do
{:ok, state, [{:send, state.parent, {:submit, state.value}}]}
end
def handle_event(%KeyEvent{char: char}, state) when char != nil do
{:ok, %{state | text: state.text <> char}}
end
def handle_event(_event, state) do
{:ok, state}
end
@callback handle_info(message :: term(), state()) :: event_result()
Handles non-event messages.
Called for messages that aren't input events, like timer callbacks or messages from other processes.
Parameters
message- The received messagestate- Current component state
Returns
Same as handle_event/2.
Initializes component state from props.
Called once when the component starts. Returns initial state.
Parameters
props- Initial properties passed to the component
Returns
{:ok, state}- Initial state{:ok, state, commands}- Initial state with startup commands{:stop, reason}- Fail to initialize
Examples
@impl true
def init(props) do
{:ok, %{
text: props[:text] || "",
cursor: 0
}}
end
Called when the component is mounted to the active tree.
Mount is the appropriate place for setup requiring the component to be "live": registering event handlers, starting timers, fetching data.
Parameters
state- Current component state after init
Returns
{:ok, new_state}- Mount successful{:ok, new_state, commands}- Mount with commands{:stop, reason}- Mount failed
@callback render(state(), rect()) :: render_tree()
Renders the component's current state.
Called after state changes to produce the visual output. Unlike stateless components, receives state instead of props.
Parameters
state- Current component statearea- Available rendering area
Returns
A render tree (RenderNode, list, or string).
Examples
@impl true
def render(state, _area) do
text(state.text)
end
Handles component termination.
Called when the component is stopping. Use for cleanup.
Parameters
reason- Why the component is stoppingstate- Final component state
@callback unmount(state()) :: :ok
Called when the component is unmounted from the tree.
This is the appropriate place for cleanup: canceling timers, closing files, unregistering handlers.
Parameters
state- Current component state
Called when the component's props change.
The parent passes new props, triggering this callback. Update may modify state based on new props.
Parameters
new_props- The new props from parentstate- Current component state
Returns
{:ok, new_state}- Update successful{:ok, new_state, commands}- Update with commands