Counter
Reactive state with signals — SSR + hydration
Todo List
Add, complete, and remove tasks — client only
Reactive UI components compiled from MoonBit to JS, integrated seamlessly with Astro.
Reactive state with signals — SSR + hydration
Add, complete, and remove tasks — client only
///|
fn counter(props : @dom.Props) -> @a.Node {
let initial = props.get_int("initial")
let count = @signals.signal(initial)
@a.div(attrs={ "class": "counter-widget" }, [
@a.div(attrs={ "class": "counter-display" }, [
@a.span(
attrs={ "class": "counter-value" },
@a.dyn_text(fn() { count.get().to_string() }),
),
]),
@a.div(attrs={ "class": "counter-controls" }, [
@a.button(attrs={ "class": "counter-btn btn-dec" }, "−")
|> @a.on_click(fn(_) { count.update(fn(n) { n - 1 }) }),
@a.button(attrs={ "class": "counter-btn btn-reset" }, "Reset")
|> @a.on_click(fn(_) { count.set(initial) }),
@a.button(attrs={ "class": "counter-btn btn-inc" }, "+")
|> @a.on_click(fn(_) { count.update(fn(n) { n + 1 }) }),
]),
])
}
///|
pub fn mount(element : @dom.Element, props : @dom.Props) -> Unit {
@a.mount_dom(element, counter(props))
}
///|
pub fn render(props : @dom.Props) -> String {
@a.render_to_html(counter(props))
}
///|
pub fn hydrate(element : @dom.Element, props : @dom.Props) -> Unit {
@a.hydrate_dom(element, counter(props))
} ///|
pub extern "js" fn set_element_value(el : @dom.Element, value : String) -> Unit =
#|(el, v) => { el.value = v; }
priv struct Todo {
text : String
done : Bool
}
///|
fn todos(_props : @dom.Props) -> @a.Node {
let initial_todos : Array[Todo] = [
{ text: "Learn MoonBit", done: true },
{ text: "Build with Astrobit", done: false },
{ text: "Ship it!", done: false },
]
let todo_list = @signals.signal(initial_todos)
let input_text = @signals.signal("")
@a.div(attrs={ "class": "todo-widget" }, [
@a.dyn(fn() {
let list = todo_list.get()
if list.length() == 0 {
@a.p(attrs={ "class": "todo-empty" }, "No tasks yet. Add one below!")
} else {
@a.ul(
attrs={ "class": "todo-list" },
list.mapi(fn(i, todo) {
@a.li(
attrs={
"class": if todo.done { "todo-item todo-done" } else { "todo-item" },
},
[
@a.button(
attrs={
"class": if todo.done { "todo-toggle checked" } else { "todo-toggle" },
},
if todo.done { "✓" } else { "" },
)
|> @a.on_click(fn(_) {
let new_list = todo_list.get().mapi(fn(j, t) {
if j == i { { text: t.text, done: !t.done } } else { t }
})
todo_list.set(new_list)
}),
@a.span(attrs={ "class": "todo-text" }, todo.text),
@a.button(attrs={ "class": "todo-delete" }, "×")
|> @a.on_click(fn(_) {
let new_list = todo_list.get().copy()
let _ = new_list.remove(i)
todo_list.set(new_list)
}),
],
)
}),
)
}
}),
@a.div(attrs={ "class": "todo-footer" }, [
@a.dyn_text(fn() {
let list = todo_list.get()
let done_count = list.fold(
init=0,
fn(acc, t) { if t.done { acc + 1 } else { acc } },
)
done_count.to_string() + " / " + list.length().to_string() + " completed"
}),
]),
@a.form(attrs={ "class": "todo-form" }, [
@a.input(
attrs={
"type": "text",
"id": "todo-input",
"class": "todo-input",
"placeholder": "Add a new task...",
},
)
|> @a.on_input(fn(e) { input_text.set(@dom.target_value(e)) }),
@a.button(attrs={ "type": "submit", "class": "todo-add-btn" }, "Add"),
])
|> @a.on_submit(fn(e) {
@dom.prevent_default(e)
let text = input_text.get()
if text != "" {
let list = todo_list.get().copy()
list.push({ text: text, done: false })
todo_list.set(list)
input_text.set("")
match @dom.query_selector("#todo-input") {
Some(el) => set_element_value(el, "")
None => ()
}
}
}),
])
}
///|
pub fn mount(element : @dom.Element, props : @dom.Props) -> Unit {
@a.mount_dom(element, todos(props))
}
///|
pub fn render(props : @dom.Props) -> String {
@a.render_to_html(todos(props))
}
///|
pub fn hydrate(element : @dom.Element, props : @dom.Props) -> Unit {
@a.hydrate_dom(element, todos(props))
}