TermUI.StatefulComponent behaviour (TermUI v0.1.0)

View Source

Behaviour 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
end

Lifecycle

  1. init/1 - Initialize state from props
  2. handle_event/2 - Process input events
  3. render/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}}]}
end

Optional Callbacks

  • terminate/2 - Cleanup when component stops
  • handle_info/2 - Handle non-event messages
  • handle_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

command()

@type command() ::
  {:send, pid(), term()}
  | {:timer, non_neg_integer(), term()}
  | {:focus, term()}
  | term()

Commands for side effects

event()

@type event() :: term()

Event types from user input

event_result()

@type event_result() ::
  {:ok, state()}
  | {:ok, state(), [command()]}
  | {:stop, reason :: term(), state()}

Event handler return value

props()

@type props() :: map()

Component props

rect()

@type rect() :: %{x: integer(), y: integer(), width: integer(), height: integer()}

Available rendering area

render_tree()

@type render_tree() :: TermUI.Component.RenderNode.t() | [render_tree()] | String.t()

Render tree output

state()

@type state() :: term()

Component state - any term

Callbacks

handle_call(request, from, state)

(optional)
@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 term
  • from - Caller identifier for reply
  • state - 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

handle_event(event, state)

@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

handle_info(message, state)

(optional)
@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 message
  • state - Current component state

Returns

Same as handle_event/2.

init(props)

@callback init(props()) :: {:ok, state()} | {:ok, state(), [command()]} | {:stop, term()}

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

mount(state)

(optional)
@callback mount(state()) :: {:ok, state()} | {:ok, state(), [command()]} | {:stop, term()}

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

render(state, rect)

@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 state
  • area - Available rendering area

Returns

A render tree (RenderNode, list, or string).

Examples

@impl true
def render(state, _area) do
  text(state.text)
end

terminate(reason, state)

(optional)
@callback terminate(reason :: term(), state()) :: term()

Handles component termination.

Called when the component is stopping. Use for cleanup.

Parameters

  • reason - Why the component is stopping
  • state - Final component state

unmount(state)

(optional)
@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

update(new_props, state)

(optional)
@callback update(new_props :: props(), state()) ::
  {:ok, state()} | {:ok, state(), [command()]}

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 parent
  • state - Current component state

Returns

  • {:ok, new_state} - Update successful
  • {:ok, new_state, commands} - Update with commands