Chai is TEA (The Elm Architecture) framework for MoonBit
Dependencies
| Package | Import | Purpose |
|---|---|---|
| bikallem/chai | @chai | Runtime — start, Cmd, Sub, Handle, component, routing, VNode, Attr types |
| bikallem/chai/h | @h | View helpers — div, text, el, attr, class, on_click, and all other HTML/attribute/event constructors |
import {
"bikallem/chai",
"bikallem/chai/h",
}| Operation | Chai JS | Chai WASM | Vanilla JS | React 18 | Preact |
|---|---|---|---|---|---|
| Create 1,000 rows | 45ms | 44ms | 61ms | 55ms | 51ms |
| Create 10,000 rows | 518ms | 483ms | 483ms | 620ms | 558ms |
| Append 1,000 rows | 58ms | 57ms | 47ms | 81ms | 74ms |
| Partial update (every 10th) | 23ms | 24ms | 21ms | 30ms | 12ms |
| Clear rows | 5.4ms | 5.4ms | 5.4ms | 12ms | 7.7ms |
| Swap rows | 8.4ms | 7.7ms | 11ms | 52ms | 13ms |
| Replace 1,000 rows | 44ms | 42ms | 44ms | 55ms | 66ms |
| Select row | 3.8ms | 3.3ms | 1.6ms | 4.9ms | 4.6ms |
| Remove row | 10ms | 11ms | 11ms | 14ms | 14ms |
make benchstruct Model { count : Int }
enum Msg { Increment; Decrement }
fn app_init() -> (Model, @chai.Cmd[Msg]) {
({ count: 0 }, @chai.Cmd::none())
}
fn update(model : Model, msg : Msg) -> (Model, @chai.Cmd[Msg]) {
match msg {
Increment => ({ count: model.count + 1 }, @chai.Cmd::none())
Decrement => ({ count: model.count - 1 }, @chai.Cmd::none())
}
}
fn view(model : Model) -> @chai.VNode[Msg] {
@h.div([], [
@h.button([@h.on_click(fn(_e) { Decrement })], [@h.text("-")]),
@h.span([], [@h.text(model.count.to_string())]),
@h.button([@h.on_click(fn(_e) { Increment })], [@h.text("+")]),
])
}
fn subscriptions(_model : Model) -> @chai.Sub[Msg] {
@chai.Sub::none()
}
fn main {
@chai.start(init=app_init, update~, view~, subscriptions~, selector="#app")
}@h.div([@h.class("container")], [
@h.h1([], [@h.text("Title")]),
@h.p([], [@h.text("Content")]),
])@h.ul([], @h.keyed_list(
items.map(fn(item) { (item.id.to_string(), view_item(item)) })
))// Conditional rendering
fn view(model : Model) -> @chai.VNode[Msg] {
@h.div([], [
if model.show_banner {
@h.div([@h.class("banner")], [@h.text("Welcome!")])
} else {
@h.null()
},
@h.p([], [@h.text("Content")]),
])
}
// Optional list items
@h.ul([], items.map(fn(item) {
if item.visible { @h.li([], [@h.text(item.name)]) } else { @h.null() }
}))@h.class("my-class") // HTML class
@h.class_list([("active", is_active), ("hidden", is_hidden)])
@h.id("my-id") // HTML id
@h.type_("checkbox") // HTML type
@h.value("hello") // input value (property)
@h.checked(true) // checkbox checked (property)
@h.placeholder("Type...") // placeholder
@h.disabled(true) // disabled (property)
@h.href("/page") // link href
@h.style("color", "red") // inline style
@h.attr("data-x", "value") // any attribute@h.on_click(fn(event) { MyMsg }) // click (receives Event)
@h.on_input(fn(value) { Input(value) }) // input (receives String value)
@h.on_change(fn(value) { Changed(value) }) // change (receives String value)
@h.on_check(fn(checked) { Toggle(checked) }) // checkbox (receives Bool)
@h.on_keydown(fn(key) { KeyPress(key) }) // keydown (receives String key name)
@h.on_submit(fn(event) { Submit }) // form submit (calls preventDefault)@h.on("mousemove", fn(event) { Move(event) })Cmd::none() // no side effects
Cmd::batch([cmd1, cmd2]) // combine commands
Cmd::task(fn(dispatch) { ... }) // custom async task
Cmd::after(500, DelayedMsg) // dispatch after delay (ms)
Cmd::http_get(url, fn(result) { GotResponse(result) }) // HTTP GET
Cmd::send(handle, child_msg) // send message to child component
Cmd::push_url("/path") // pushState navigation (see Routing)
Cmd::replace_url("/path") // replaceState navigation (see Routing)
Cmd::push_hash("/path") // hash navigation (see Routing)
cmd.map(fn(msg) { Wrapped(msg) }) // transform message typeSub::none()
Sub::batch([sub1, sub2])
Sub::every(1000, "tick", fn() { Tick }) // recurring timer (ms)
Sub::on_key_down("keys", fn(key) { KeyDown(key) }) // document keydown
Sub::on_window_resize("resize", fn(w, h) { Resized(w, h) }) // window resize
Sub::on_hash_change("url", fn(url) { UrlChanged(url) }) // hash routing (see Routing)
Sub::on_url_change("url", fn(url) { UrlChanged(url) }) // pushState routing (see Routing)
sub.map(fn(msg) { Wrapped(msg) }) // transform message typeSub::sub("my-sub", fn(dispatch) {
// set up listener, return cleanup function
fn() { /* cleanup */ }
})// Read the initial URL
fn app_init() -> (Model, Cmd[Msg]) {
({ route: to_route(hash_url()) }, Cmd::none())
}
// Subscribe to hash changes
fn subscriptions(_model : Model) -> Sub[Msg] {
Sub::on_hash_change("url", fn(url) { UrlChanged(url) })
}
// Navigate with hash links
@chai.hash_link("/about", [@h.class("nav-link")], [@h.text("About")])
// Or navigate programmatically
Cmd::push_hash("/about")fn app_init() -> (Model, Cmd[Msg]) {
({ route: to_route(url()) }, Cmd::none())
}
fn subscriptions(_model : Model) -> Sub[Msg] {
Sub::on_url_change("url", fn(url) { UrlChanged(url) })
}
@chai.link("/about", [@h.class("nav-link")], [@h.text("About")], on_nav=GoAbout)
Cmd::push_url("/about")
Cmd::replace_url("/about")pub struct Url {
path : Array[String] // "/foo/bar" → ["foo", "bar"]
query : String // "?x=1" (raw, including ?)
hash : String // "#section" (raw, including #)
}fn to_route(url : Url) -> Route {
match url.path {
[] => Home
["about"] => About
["posts", id] => Post(id)
_ => NotFound
}
}fn counter[ParentMsg]() -> VNode[ParentMsg] {
component(
init=fn() { ({ count: 0 }, Cmd::none()) },
update~,
view~,
)
}fn clock[ParentMsg]() -> VNode[ParentMsg] {
component(
init=fn() { ({ time: 0 }, Cmd::none()) },
update~,
view~,
subscriptions=fn(_model) { Sub::every(1000, "tick", fn() { Tick }) },
)
}let handle = Handle::new()
// In view: component(handle~, init~, update~, view~)
// In update: Cmd::send(handle, ChildMsg)make checkmake testtype Attr[Msg]type Cmd[Msg]type Handle[CMsg]type Sub[Msg]type VNode[Msg]Chai is TEA (The Elm Architecture) framework for MoonBit
Dependencies