Component Systems in TermUI 1.0

View Source

TermUI exposes two component models. They are complementary APIs, not one automatically connected tree.

Elm root and embedded widgets

This is the normal application architecture. TermUI.Runtime starts one root module implementing TermUI.Elm and keeps one root state.

flowchart TB
  R[TermUI.Runtime] --> ROOT[Elm root state]
  ROOT --> A[embedded widget state A]
  ROOT --> B[embedded widget state B]
  ROOT --> V[one root view tree]

Stateless widgets are functions called from view/1. Stateful widgets are explicit state machines:

def init(_opts) do
  props = TermUI.Widgets.TextInput.new(placeholder: "Name")
  {:ok, input} = TermUI.Widgets.TextInput.init(props)
  %{input: input}
end

def event_to_msg(event, _state), do: {:msg, {:input_event, event}}

def update({:input_event, event}, state) do
  {:ok, input} = TermUI.Widgets.TextInput.handle_event(event, state.input)
  {%{state | input: input}, []}
end

def view(state) do
  TermUI.Widgets.TextInput.render(
    state.input,
    %{x: 0, y: 0, width: 40, height: 1}
  )
end

The root owns focus, event targeting, and widget state. The runtime's internal components map contains only :root, and :propagate has no parent target.

Component behaviours

TermUI.Component is the stateless render/2 behaviour.

TermUI.StatefulComponent adds lifecycle, event, process-message, and call callbacks. Built-in interactive widgets use this state-machine shape, but using the behaviour does not itself start a process. The standard ComponentServer invokes init/mount/props-update/event/unmount callbacks; it does not delegate arbitrary GenServer calls or messages to handle_call/3 or handle_info/2.

TermUI.Container adds child specifications, layout, routing decisions, and child-message callbacks. The Elm runtime does not interpret those callbacks; an application using Container must supply the component-tree integration.

Process-oriented component services

The repository also ships a standalone lifecycle subsystem:

flowchart TB
  S[ComponentSupervisor] --> CS1[ComponentServer]
  S --> CS2[ComponentServer]
  CS1 --> REG[ComponentRegistry]
  CS2 --> REG
  ER[EventRouter] --> REG
  FM[FocusManager] --> ER
  SI[SpatialIndex] --> ER
  CS1 --> SP[StatePersistence]
  CS2 --> SP

Start its services before use, normally in your own supervision tree. The APIs have default registered names, so a single local set is expected unless the module offers a naming option.

children = [
  TermUI.ComponentRegistry,
  TermUI.Component.StatePersistence,
  TermUI.ComponentSupervisor
]

Supervisor.start_link(children, strategy: :one_for_one)

Lifecycle

{:ok, pid} =
  TermUI.ComponentSupervisor.start_component(MyComponent, props,
    id: :editor,
    recovery: :last_state
  )

:ok = TermUI.ComponentServer.mount(pid)
:ok = TermUI.ComponentServer.update_props(pid, new_props)
:ok = TermUI.ComponentServer.send_event(pid, event)
component_state = TermUI.ComponentServer.get_state(pid)
:ok = TermUI.ComponentServer.unmount(pid)
:ok = TermUI.ComponentSupervisor.stop_component(pid)

ComponentServer performs the lifecycle/event callbacks and recognizes its legacy {:send, pid, message} and {:timer, milliseconds, message} command tuples. Because the server has no component-message delegation, its timer tuple will eventually be unhandled; use it only with a custom host that supplies that delegation. ComponentServer does not expose render/2; call MyComponent.render(component_state, area) in a custom rendering integration.

Registry and hierarchy metadata

TermUI.ComponentRegistry maps component IDs to PIDs and stores parent/child metadata. set_parent/2 changes metadata; it does not establish supervision or automatic visual composition.

Routing and focus

TermUI.EventRouter routes to its focused ID or explicit target and can broadcast to registered components. TermUI.FocusManager adds traversal, groups, a modal focus stack, and trapping. Applications must keep their desired focus/router relationship synchronized; neither service replaces the root-only dispatch in TermUI.Runtime.

TermUI.SpatialIndex.update/4 records bounds and z-index metadata. find_at/2 or find_all_at/2 can select targets for mouse events in a custom router. The default runtime does not query it.

Recovery

TermUI.Component.StatePersistence.persist/3 stores state and metadata, recover/2 reads it, and record_restart/1 tracks restart history. Recovery is used by ComponentServer; it is unrelated to Elm root state.

Choosing a model

Use the Elm root with embedded widgets for applications built with TermUI.Runtime. Choose the process services only when you intentionally need independently hosted component lifecycles and are prepared to connect event routing and rendering yourself.