README

#@mizchi/signals/ui

Platform-independent Virtual Node (VNode) representation.

#Overview

This module provides:

  • VNode: Platform-agnostic virtual DOM node types
  • AsyncState: Generic async state representation

#Design

Type parameters E (event type) and A (attribute value type) enable the same VNode definitions across different platforms:

PlatformEA
BrowserDomEventString
SSRUnitString
TUITuiEventTuiAttrValue

#Usage

#Basic Elements

// Text node
let text_node : Node[Unit, String] = text("Hello World")

// Element node
let div : Node[Unit, String] = h(
"div",
[("class", attr_static("container"))],
[text("Content")]
)

// Nested elements
let nested : Node[Unit, String] = h("div", [], [
h("span", [], [text("Title")]),
h("p", [], [text("Paragraph")])
])

#Dynamic Content

// Dynamic text (value from getter function)
let count = @signals.signal(0)
let dynamic : Node[Unit, String] = text_dyn(fn() { count.get().to_string() })

// Dynamic attribute
let cls = @signals.signal("active")
let dynamic_attr : Node[Unit, String] = h(
"div",
[("class", attr_dynamic(fn() { cls.get() }))],
[]
)

#Conditional Rendering

// show: render only when condition is true
let visible = @signals.signal(true)
let conditional : Node[Unit, String] = show(
fn() { visible.get() },
fn() { text("Visible") }
)

// switch: multiple condition branches
let state = @signals.signal(1)
let switched : Node[Unit, String] = switch_(
cases=[
match_case(when=fn() { state.get() == 1 }, render=fn() { text("State 1") }),
match_case(when=fn() { state.get() == 2 }, render=fn() { text("State 2") }),
],
fallback=Some(fn() { text("Other") })
)

#List Rendering

let items = @signals.signal(["Apple", "Banana", "Cherry"])
let list : Node[Unit, String] = for_each(fn() {
items.get().map(fn(item) { h("li", [], [text(item)]) })
})

#Fragment

// Group multiple nodes without a wrapper element
let frag : Node[Unit, String] = fragment([
text("Hello"),
text(" "),
text("World")
])

#Component

fn button(label : String) -> Node[Unit, String] {
component(fn() {
h("button", [("class", attr_static("btn"))], [text(label)])
})
}

#Event Handler

let on_click : EventHandler[DomEvent] = handler(fn(e) {
// handle event
})

let btn : Node[DomEvent, String] = h(
"button",
[("onclick", attr_handler(on_click))],
[text("Click me")]
)

#AsyncState

Represents async operation state:

pub enum AsyncState[T] {
Pending
Success(T)
Failure(String)
}

let state : AsyncState[User] = Pending
state.is_pending() // true
state.value() // None

let loaded : AsyncState[User] = Success(user)
loaded.value() // Some(user)

#Node Types

TypeDescription
ElementHTML element with tag, attrs, children
TextStatic text content
DynamicTextText from getter function
FragmentGroup of nodes
ShowConditional rendering
ForList rendering
ComponentLazy-evaluated component
AsyncAsync content with fallback
ErrorBoundaryError catching wrapper
SwitchMulti-case conditional
RawHtmlUnescaped HTML string

#
AsyncState

pub(all) enum AsyncState[T] {
Pending
Success(T)
Failure(String)
}

Async state representation - environment independent

#
AsyncState::error

fn[T] AsyncState::error(self : AsyncState[T]) -> String?

Get error if failure, None otherwise

#
AsyncState::is_failure

fn[T] AsyncState::is_failure(self : AsyncState[T]) -> Bool

Check if state is failure

#
AsyncState::is_pending

fn[T] AsyncState::is_pending(self : AsyncState[T]) -> Bool

Check if state is pending

#
AsyncState::is_success

fn[T] AsyncState::is_success(self : AsyncState[T]) -> Bool

Check if state is success

#
AsyncState::value

fn[T] AsyncState::value(self : AsyncState[T]) -> T?

Get value if success, None otherwise

#
Attr

pub(all) enum Attr[E, A] {
VStatic(A)
VDynamic(() -> A)
VHandler(EventHandler[E])
}

Attribute value that can be static or dynamic (signal-based) Type parameter A represents the attribute value type:
  • Web: A = String (HTML attributes are strings)
  • TUI: A = TuiAttrValue (typed values like Dimension, Color)

#
EventHandler

pub struct EventHandler[E] {
callback : (E) -> Unit
}

Event handler type - newtype wrapper for callback function

#
EventHandler::get_callback

fn[E] EventHandler::get_callback(self : EventHandler[E]) -> ((E) -> Unit)

Get the callback function

#
MatchCase

pub struct MatchCase[E, A] {
when : () -> Bool
render : () -> Node[E, A]
}

Match case for Switch - pairs a condition with content

#
Node

pub(all) enum Node[E, A] {
Element(VElement[E, A])
Text(String)
DynamicText(() -> String)
Fragment(Array[Node[E, A]])
Show(condition~ : () -> Bool, child~ : () -> Node[E, A])
For(render~ : () -> Array[Node[E, A]])
Component(render~ : () -> Node[E, A])
Async(VAsync[E, A])
ErrorBoundary(VErrorBoundary[E, A])
Switch(VSwitch[E, A])
RawHtml(String)
}

Virtual DOM node types Type parameter A represents the attribute value type (see Attr[E, A])

#
VAsync

pub struct VAsync[E, A] {
render : async () -> Node[E, A]
fallback : () -> Node[E, A]
on_error : (Error) -> Node[E, A]?
}

Virtual Async node for async rendering with error handling

#
VElement

pub struct VElement[E, A] {
tag : String
attrs : Array[(String, Attr[E, A])]
children : Array[Node[E, A]]
}

Virtual element node

#
VErrorBoundary

pub struct VErrorBoundary[E, A] {
children : () -> Node[E, A] raise
fallback : (Error, () -> Unit) -> Node[E, A] raise
}

Virtual ErrorBoundary node for catching rendering errors

#
VSwitch

pub struct VSwitch[E, A] {
cases : Array[MatchCase[E, A]]
fallback : () -> Node[E, A]?
}

Virtual Switch node - renders first matching case

#
async_

fn[E, A] async_(render~ : async () -> Node[E, A], fallback~ : () -> Node[E, A], on_error? : (Error) -> Node[E, A]?) -> Node[E, A]

Create an async VNode with fallback

#
attr_dynamic

fn[E, A] attr_dynamic(getter : () -> A) -> Attr[E, A]

Create a dynamic attribute value.

#
attr_dynamic_style

fn[E] attr_dynamic_style(getter : () -> String) -> Attr[E, String]

Create a dynamic style attribute value

#
attr_handler

fn[E, A] attr_handler(handler : EventHandler[E]) -> Attr[E, A]

Create a handler attribute value

#
attr_static

fn[E, A] attr_static(value : A) -> Attr[E, A]

Create a static attribute value.

#
attr_style

fn[E] attr_style(style : String) -> Attr[E, String]

Create a style attribute value (string form)

#
component

fn[E, A] component(render : () -> Node[E, A]) -> Node[E, A]

Create a component VNode.

#
error_boundary

fn[E, A] error_boundary(children~ : () -> Node[E, A] raise, fallback~ : (Error, () -> Unit) -> Node[E, A] raise) -> Node[E, A]

Create an error boundary VNode

#
for_each

fn[E, A] for_each(items : () -> Array[Node[E, A]]) -> Node[E, A]

Create a list VNode.

#
fragment

fn[E, A] fragment(children : Array[Node[E, A]]) -> Node[E, A]

Create a fragment VNode.
fn[E, A] h(tag : String, attrs : Array[(String, Attr[E, A])], children : Array[Node[E, A]]) -> Node[E, A]

Create a VNode element.

#
handler

fn[E] handler(f : (E) -> Unit) -> EventHandler[E]

Create an event handler from a callback.

#
has_dynamic_content

fn[E, A] has_dynamic_content(attrs : Array[(String, Attr[E, A])]) -> Bool

Check if element has dynamic content that needs hydration

#
match_case

fn[E, A] match_case(when~ : () -> Bool, render~ : () -> Node[E, A]) -> MatchCase[E, A]

Create a match case for Switch

#
raw_html

fn[E, A] raw_html(content : String) -> Node[E, A]

Create a raw HTML VNode (content is not escaped).

#
show

fn[E, A] show(when : () -> Bool, child : () -> Node[E, A]) -> Node[E, A]

Create a conditional VNode.

#
switch_

fn[E, A] switch_(cases~ : Array[MatchCase[E, A]], fallback? : () -> Node[E, A]?) -> Node[E, A]

Create a switch VNode - renders first matching case

#
text

fn[E, A] text(content : String) -> Node[E, A]

Create a text VNode.

#
text_dyn

fn[E, A] text_dyn(content : () -> String) -> Node[E, A]

Create a dynamic text VNode.

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io