TermUI.Container behaviour (TermUI v0.2.0)

View Source

Behaviour for container components that manage children.

Container extends StatefulComponent with child management capabilities. Use this for components that contain and organize other components, like panels, forms, tabs, or split views.

Basic Usage

defmodule MyApp.Panel do
  use TermUI.Container

  @impl true
  def init(props) do
    {:ok, %{title: props[:title] || "Panel"}}
  end

  @impl true
  def children(_state) do
    [
      {MyApp.Label, %{text: "Header"}, :header},
      {MyApp.Content, %{}, :content}
    ]
  end

  @impl true
  def layout(children, state, area) do
    # Arrange children within available area
    header_area = %{area | height: 1}
    content_area = %{area | y: area.y + 1, height: area.height - 1}

    [
      {Enum.at(children, 0), header_area},
      {Enum.at(children, 1), content_area}
    ]
  end

  @impl true
  def render(state, _area) do
    # Container render is called after children
    # Return empty if children handle all rendering
    empty()
  end

  @impl true
  def handle_event(_event, state) do
    {:ok, state}
  end
end

Child Specifications

Children are specified as tuples:

  • {Module, props} - Child with auto-generated ID
  • {Module, props, id} - Child with explicit ID

IDs are used for event routing and child lookup.

Layout

The layout/3 callback positions children within the container's area. It receives the list of child specs and must return tuples of {child_spec, area} assigning each child its rendering bounds.

Event Routing

Containers can route events to specific children or handle them directly. Override route_event/2 to customize event routing.

Summary

Types

Child with assigned area

Child specification

Command for side effects

Event from user input

Available rendering area

Render tree output

Event routing target

Component state

Callbacks

Returns the list of child components.

Called when a child emits a message.

Handles input events.

Initializes container state from props.

Lays out children within the available area.

Renders the container.

Routes an event to the appropriate handler.

Types

child_layout()

@type child_layout() :: {child_spec(), rect()}

Child with assigned area

child_spec()

@type child_spec() ::
  {module(), props :: map()} | {module(), props :: map(), id :: term()}

Child specification

command()

@type command() :: term()

Command for side effects

event()

@type event() :: term()

Event from user input

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

route_target()

@type route_target() :: :self | {:child, id :: term()} | :broadcast

Event routing target

state()

@type state() :: term()

Component state

Callbacks

children(state)

@callback children(state()) :: [child_spec()]

Returns the list of child components.

Called to determine which children the container should manage. Children are specified as tuples with module, props, and optional ID.

Parameters

  • state - Current container state

Returns

List of child specifications.

Examples

@impl true
def children(state) do
  [
    {Label, %{text: state.title}, :title},
    {Button, %{label: "OK"}, :ok_button},
    {Button, %{label: "Cancel"}, :cancel_button}
  ]
end

handle_child_message(child_id, message, state)

(optional)
@callback handle_child_message(child_id :: term(), message :: term(), state()) ::
  {:ok, state()} | {:ok, state(), [command()]}

Called when a child emits a message.

Use to handle messages bubbling up from child components.

Parameters

  • child_id - ID of the child that sent the message
  • message - The message from the child
  • state - Current container state

handle_event(event, state)

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

Handles input events.

Same as StatefulComponent.handle_event/2.

init(props)

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

Initializes container state from props.

Same as StatefulComponent.init/1.

layout(list, state, rect)

@callback layout([child_spec()], state(), rect()) :: [child_layout()]

Lays out children within the available area.

Determines the position and size of each child component. The default implementation stacks children vertically.

Parameters

  • children - List of child specifications from children/1
  • state - Current container state
  • area - Available area for the container

Returns

List of {child_spec, area} tuples.

Examples

@impl true
def layout(children, _state, area) do
  # Horizontal layout with equal widths
  child_width = div(area.width, length(children))

  children
  |> Enum.with_index()
  |> Enum.map(fn {child, i} ->
    child_area = %{
      x: area.x + i * child_width,
      y: area.y,
      width: child_width,
      height: area.height
    }
    {child, child_area}
  end)
end

render(state, rect)

@callback render(state(), rect()) :: render_tree()

Renders the container.

Called after children are rendered. Can render container chrome (borders, titles) or return empty if children handle everything.

Same signature as StatefulComponent.render/2.

route_event(event, state)

(optional)
@callback route_event(event(), state()) :: route_target()

Routes an event to the appropriate handler.

Override to customize how events are distributed to children. Default routes all events to self.

Parameters

  • event - The input event
  • state - Current container state

Returns

  • :self - Handle event in this container
  • {:child, id} - Route to specific child
  • :broadcast - Send to all children