Fine-grained reactive UI library for Moonbit/JS
Dependencies
// moon.mod.json
{ "deps": { "mizchi/luna": "0.19.2" } }moon check --target js # workspace check (luna + sol + astra)
moon test --target js # workspace test
pnpm test:browser # vitest browser runner
pnpm test:e2e # luna's Playwright suite| Submodule | Responsibility |
|---|---|
| signal/ | Reactive primitives (Signal, Effect, Computed) |
| render/ | VNode → HTML string rendering |
| routes/ | Type-safe routing |
| serialize/ | State serialization/deserialization |
| vnode.mbt | VNode type definitions |
pub enum Node[E] {
Element(VElement[E]) // HTML element
Text(String) // Static text
DynamicText(() -> String) // Dynamic text
Fragment(Array[Node[E]]) // Fragment
Show(...) // Conditional rendering
For(...) // List rendering
Island(VIsland[E]) // Hydration boundary
WcIsland(VWcIsland[E]) // Web Components Island
Async(VAsync[E]) // Async node
// ...
}let count = @signal.signal(0)
count.get() // 0
count.set(1) // Set value
count.update(fn(n) { n + 1 }) // Update function
// Derived value
let doubled = @signal.computed(fn() { count.get() * 2 })
// Side effects
@signal.effect(fn() {
println(count.get())
})pub enum Attr[E] {
VStatic(String) // Static value
VDynamic(() -> String) // Signal-linked
VHandler(EventHandler[E]) // Event handler
VAction(String) // Declarative action
}pub enum TriggerType {
Load // On page load
Idle // On requestIdleCallback
Visible // On IntersectionObserver detection
Media(String) // On media query match
None // Manual trigger
}pub enum MyAction { Increment; Decrement } derive(Show)
h("button", [("onclick", action(Increment))], [...])test {
let mut count = 0
let attr : Attr[Unit, String] = attr_dynamic(fn() {
"count-" + count.to_string()
})
guard attr is VDynamic(getter) else { fail("expected VDynamic") }
inspect(getter(), content="count-0")
count = 5
inspect(getter(), content="count-5")
}test {
let attr : Attr[Unit, String] = attr_static("my-class")
guard attr is VStatic(v) else { fail("expected VStatic") }
inspect(v, content="my-class")
}fn[T] batch(f : () -> T) -> Ttest {
let node : Node[Unit, String] = component(fn() {
h("div", [], [text("Component")])
})
guard node is Component(render~) else { fail("expected Component") }
guard render() is Element(el) else { fail("expected Element") }
inspect(el.tag, content="div")
}fn[T] computed(compute : () -> T) -> (() -> T)fn[T] create_root(f : (() -> Unit) -> T) -> Tfn[T] create_root_with_dispose(f : () -> T) -> (T, () -> Unit)fn effect(fn_ : () -> Unit) -> (() -> Unit)fn effect_once(fn_ : () -> Unit) -> Unitfn effect_when(condition : () -> Bool, fn_ : () -> Unit) -> (() -> Unit)error_boundary(
children=fn() { risky_component() },
fallback=fn(err, reset) {
h("div", [], [
text("Error: " + err.to_string()),
h("button", [("onclick", handler(fn(_) { reset() }))], [text("Retry")])
])
}
)test {
let items = ["a", "b", "c"]
let node : Node[Unit, String] = for_each(fn() { items.map(fn(s) { text(s) }) })
guard node is For(render~) else { fail("expected For") }
inspect(render().length(), content="3")
}test {
let node : Node[Unit, String] = fragment([
text("Hello"),
text(" "),
text("World"),
])
guard node is Fragment(children) else { fail("expected Fragment") }
inspect(children.length(), content="3")
}test {
let node : Node[Unit, String] = h(
"div",
[("class", attr_static("container"))],
[text("Hello")],
)
guard node is Element(el) else { fail("expected Element") }
inspect(el.tag, content="div")
}test {
let h : EventHandler[Int] = handler(fn(x) { let _ = x * 2 })
inspect(h.get_callback()(5), content="()")
}fn[E, A] internal_ref(url : String, state : String, trigger? : TriggerType, styles? : String, children? : Array[Node[E, A]]) -> Node[E, A]fn[T] memo(compute : () -> T) -> (() -> T)fn on_cleanup(cleanup : () -> Unit) -> Unitfn on_mount(fn_ : () -> Unit) -> Unittest {
let node : Node[Unit, String] = raw_html("<strong>Bold</strong>")
guard node is RawHtml(html) else { fail("expected RawHtml") }
inspect(html, content="<strong>Bold</strong>")
}fn register_disposer(disposer : () -> Unit) -> Unitfn register_owner_cleanup(cleanup : () -> Unit) -> Unitfn render_effect(fn_ : () -> Unit) -> (() -> Unit)test {
let visible = true
let node : Node[Unit, String] = show(fn() { visible }, fn() {
text("Visible!")
})
guard node is Show(condition~, ..) else { fail("expected Show") }
inspect(condition(), content="true")
}switch_(
cases=[
match_case(when=fn() { state.get() == 1 }, render=fn() { text("One") }),
match_case(when=fn() { state.get() == 2 }, render=fn() { text("Two") }),
],
fallback=Some(fn() { text("Other") })
)test {
let node : Node[Unit, String] = text("Hello World")
guard node is Text(s) else { fail("expected Text") }
inspect(s, content="Hello World")
}test {
let mut count = 5
let node : Node[Unit, String] = text_dyn(fn() { count.to_string() })
guard node is DynamicText(getter) else { fail("expected DynamicText") }
inspect(getter(), content="5")
count = 10
inspect(getter(), content="10")
}fn[T] untracked(f : () -> T) -> Tfn[E, A] wc_island(name : String, url : String, styles : String, state : String, children : Array[Node[E, A]], trigger? : TriggerType) -> Node[E, A]Fine-grained reactive UI library for Moonbit/JS
Dependencies