Browse documentation

Declarative elements

Build validated UI trees from data and apply event-aware diffs.

Zaniah::Describe rebuilds element trees from plain data. The application owns the vocabulary, so it decides which node types and properties untrusted or remote producers may use.

vocabulary = Zaniah::Describe::Vocabulary.build do
  node :column, props: {gap: :integer} do |props, children|
    Zaniah::Div.new.flex_col.gap(props.fetch(:gap)).children(children)
  end

  node :button,
    props: {label: :string, on_click: :handler}, children: :none do |props|
    Zaniah::UI::Button.new(props.fetch(:label)).on_click(&props.fetch(:on_click))
  end
end

tree = {
  "type" => "column",
  "props" => {"gap" => 8},
  "children" => [{
    "type" => "button",
    "props" => {"label" => "Save", "on_click" => ["save", 1]},
    "children" => [],
    "key" => "save"
  }]
}

surface = Zaniah::Describe::Surface.new(
  vocabulary: vocabulary,
  on_event: ->(id, payload) { send_event(id, payload) }
)
surface.replace(tree)
window.draw { surface.element }

Property validators include :string, :integer, :number, :boolean, :array, :hash, :symbol, :handler, :any, classes, callables, and arrays of allowed values. Children may be :none, :one, :many, an exact count, or a range. JSON string keys are normalized to symbols, and string values are normalized for symbol enums.

Handler properties contain an opaque string or array ID. The factory receives a callable in its place; invoking it sends the ID and the first payload argument to on_event.

Zaniah::Describe.diff(previous, current) returns Patch records with :replace, :insert, :remove, or :update operations. Paths are arrays of child indexes from the root, and :update contains the complete new property map. Surface#apply validates and applies a patch set atomically. Its element reader performs no building or I/O, and unchanged keyed subtrees retain their element instances across reordering.