rabbita

functional Web UI framework for MoonBit

functional
html
UI
web
TEA
moon add moonbit-community/rabbita@0.15.2
Download zip
Version
0.15.2
License
Apache-2.0
Last updated
18 hours ago
Downloads
50K

Dependencies

README

#Rabbita

A declarative, functional web UI framework inspired by Elm and Bonsai.

This project was previously named Rabbit-TEA and is now renamed to rabbita .

#Features

  • Predictable flow

    Each component handles state changes through typed messages. Commands keep side effects explicit.

  • Strict Types

    Rigorous types. No Any sprawl. No stringly-typed APIs.

  • Balanced bundle size

    ~15 KB min+gzip, includes streaming VDOM diff and the MoonBit standard library (DCE via moonc).

  • Modular & Incremental

    Organize reusable UI as ordinary functions that compose state, derived values, and child components. Updates reevaluate dependent callbacks, while equal results stop propagating.

#Quick Start

You can try it in the playground or set up a project in the terminal.

Make sure you have installed moon first:

moon install moonbit-community/warren warren new my-project cd my-project warren dev

See Warren for more information.

#Example

using @rabbita {type Html, type Val}
using @html {button, div, h1}

///|
enum Msg {
Inc
Dec
}

///|
fn counter() -> Val[Html] {
let (count, emit) = @rabbita.create_pure_state(0, update=fn(count, msg) {
match msg {
Inc => count + 1
Dec => count - 1
}
})
count.view(count => {
div([
h1(count.to_string()),
button(on_click=emit(Inc), "+"),
button(on_click=emit(Dec), "-"),
])
})
}

///|
fn main {
@rabbita.new(counter).mount("app")
}

#Used By

#
Cell

Deprecated compatibility alias for the former component type.

New components should return Val[Html] directly.

#
Cmd

A deferred command handled by the Rabbita runtime.

Cmd models side effects in the update loop. Commands are returned from update or embedded in Html event handlers, and are executed later by the runtime.

#
Emit

Message emitter type used by a state machine.

Calling emit(msg) returns a Cmd that enqueues msg into the state machine's update loop.

#
Enumerate

Provides stable branch identities for incremental selection.

Values that represent the same logical branch must return the same tag; distinct branches must return distinct tags.

#
Html

An HTML value produced by the constructors in this package.

#
Resource

type Resource[T] = Val[Status[T]]

An incremental asynchronous resource state.

#
App

type App

A running Rabbita application.

#
App::render

#internal(experimental, "This API is unstable and may change in the future.")
async fn App::render(self : App, url~ : String, head? : Array[
Html
], timeout? : Int) -> String

Renders the application to an HTML string for the given URL.

The nodes in head are appended to the application's existing <head> before the hydration transcript.

Throws @async.TimeoutError if rendering does not complete within timeout milliseconds.

#
Status

pub enum Status[T] {
Pending
Loaded(T)
Failed(Error)
}

The lifecycle state of an asynchronous resource.

For incremental equality, Pending equals Pending, loaded values compare by their payloads, and all Failed values compare equal regardless of the contained error.
impl Eq for Status[T]

#
Val

type Val[A]

A lazily evaluated value in Rabbita's incremental graph.

Derived values are recomputed on demand after one of their dependencies changes.

#
Val::assoc

fn[K : Hash + Eq, V : Eq, C : Eq] Val::assoc(a : Val[
Vector
[(K, V)]], f : (K, Val[V]) -> Val[C]) -> Val[
Vector
[C]]

Incrementally maps ordered keyed values into a vector.

Pass a named component to assoc; do not render inline. Keys must be unique; each key owns one branch whose Val tracks updates, and removing it disposes the branch. Output follows vector order, but keys are not attached to Html.

Example

fn todo_item(id : Int, title : Val[String]) -> Val[Html] {
title.view(title => @html.li("\{id}: \{title}"))
}

fn todo_list(todos : Val[Vector[(Int, String)]]) -> Val[Html] {
let rows = todos.assoc(todo_item)
rows.view(rows => @html.ul(rows))
}

#
Val::assoc_by

fn[K : Hash + Eq, V : Eq, C : Eq] Val::assoc_by(a : Val[
Vector
[V]], f : (K, Val[V]) -> Val[C], by~ : (V) -> K) -> Val[
Vector
[C]]

Incrementally maps values using keys derived by by.

Derived keys must be unique and stable. Output follows source vector order.

#
Val::constant

fn[A] Val::constant(a : A) -> Val[A]

Creates an incremental value that always contains a.

#
Val::enumerate

fn[E :
Enumerate
+ Eq, C : Eq] Val::enumerate(a : Val[E], f : (E) -> Val[C]) -> Val[C]

Selects and caches an incremental branch for each enumeration tag.

Immediately match the tag in the enumerate callback and dispatch each case to its own component; do not render inline. Branches are cached with their state and subscriptions. Use Val::switch for disposable branches.

Example

priv enum Tab {
First
Second
} derive(Eq)

impl @rabbita.Enumerate for Tab with fn tag(self) {
match self {
First => "first"
Second => "second"
}
}

fn first_tab() -> Val[Html] {
Val::constant(@html.h1("First"))
}

fn second_tab() -> Val[Html] {
Val::constant(@html.h1("Second"))
}

fn tab_content(tab : Val[Tab]) -> Val[Html] {
tab.enumerate(tab => {
match tab {
First => first_tab()
Second => second_tab()
}
})
}

#
Val::enumerate_by

fn[E : Eq, C : Eq] Val::enumerate_by(a : Val[E], f : (E) -> Val[C], by~ : (E) -> String) -> Val[C]

Selects and caches branches using the tag returned by by.

Values with the same tag reuse the branch created for its first value.

#
Val::map

fn[A : Eq, B] Val::map(a : Val[A], f : (A) -> B) -> Val[B]

Creates an incremental value by applying f to a.

The function is reevaluated when the value of a changes.

#
Val::map2

fn[A : Eq, B : Eq, C] Val::map2(a : Val[A], b : Val[B], f : (A, B) -> C) -> Val[C]

Creates an incremental value derived from two inputs.

The function is reevaluated when either input value changes.

#
Val::map3

fn[A : Eq, B : Eq, C : Eq, D] Val::map3(a : Val[A], b : Val[B], c : Val[C], f : (A, B, C) -> D) -> Val[D]

Creates an incremental value derived from three inputs.

The function is reevaluated when any input value changes.

#
Val::map4

fn[A : Eq, B : Eq, C : Eq, D : Eq, E] Val::map4(a : Val[A], b : Val[B], c : Val[C], d : Val[D], f : (A, B, C, D) -> E) -> Val[E]

Creates an incremental value derived from four inputs.

The function is reevaluated when any input value changes.

#
Val::map5

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F] Val::map5(a : Val[A], b : Val[B], c : Val[C], d : Val[D], e : Val[E], f : (A, B, C, D, E) -> F) -> Val[F]

Creates an incremental value derived from five inputs.

The function is reevaluated when any input value changes.

#
Val::map6

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G] Val::map6(a : Val[A], b : Val[B], c : Val[C], d : Val[D], e : Val[E], f : Val[F], g : (A, B, C, D, E, F) -> G) -> Val[G]

Creates an incremental value derived from six inputs.

The function is reevaluated when any input value changes.

#
Val::map7

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H] Val::map7(a : Val[A], b : Val[B], c : Val[C], d : Val[D], e : Val[E], f : Val[F], g : Val[G], h : (A, B, C, D, E, F, G) -> H) -> Val[H]

Creates an incremental value derived from seven inputs.

The function is reevaluated when any input value changes.

#
Val::map8

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I] Val::map8(a : Val[A], b : Val[B], c : Val[C], d : Val[D], e : Val[E], f : Val[F], g : Val[G], h : Val[H], i : (A, B, C, D, E, F, G, H) -> I) -> Val[I]

Creates an incremental value derived from eight inputs.

The function is reevaluated when any input value changes.

#
Val::map9

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I : Eq, J] Val::map9(a : Val[A], b : Val[B], c : Val[C], d : Val[D], e : Val[E], f : Val[F], g : Val[G], h : Val[H], i : Val[I], j : (A, B, C, D, E, F, G, H, I) -> J) -> Val[J]

Creates an incremental value derived from nine inputs.

The function is reevaluated when any input value changes.

#
Val::switch

fn[E :
Enumerate
+ Eq, C : Eq] Val::switch(a : Val[E], f : (E) -> Val[C]) -> Val[C]

Selects one incremental branch using the input's enumeration tag.

Immediately match the tag in the switch callback and dispatch each case to its own component; do not render inline. Changing the tag disposes the active component, so returning to an old tag creates a fresh one.

Example

priv enum Page {
Home
Settings
} derive(Eq)

impl @rabbita.Enumerate for Page with fn tag(self) {
match self {
Home => "home"
Settings => "settings"
}
}

fn home_page() -> Val[Html] {
Val::constant(@html.h1("Home"))
}

fn settings_page() -> Val[Html] {
Val::constant(@html.h1("Settings"))
}

fn page_content(page : Val[Page]) -> Val[Html] {
page.switch(page => {
match page {
Home => home_page()
Settings => settings_page()
}
})
}

#
Val::switch_by

fn[E : Eq, C : Eq] Val::switch_by(a : Val[E], f : (E) -> Val[C], by~ : (E) -> String) -> Val[C]

Selects a disposable branch using the tag returned by by.

Values with the same tag keep the current branch; changing it disposes the branch before creating the next one.

#
Val::view

Creates an HTML view derived from one incremental value.

The render function is reevaluated when a changes.

#
Val::view2

fn[A : Eq, B : Eq] Val::view2(a : Val[A], b : Val[B], render : (A, B) ->
Html
) -> Val[
Html
]

Creates an HTML view derived from two incremental values.

The render function is reevaluated when either input changes.

#
Val::view3

fn[A : Eq, B : Eq, C : Eq] Val::view3(a : Val[A], b : Val[B], c : Val[C], render : (A, B, C) ->
Html
) -> Val[
Html
]

Creates an HTML view derived from three incremental values.

The render function is reevaluated when any input changes.

#
Val::view4

fn[A : Eq, B : Eq, C : Eq, D : Eq] Val::view4(a : Val[A], b : Val[B], c : Val[C], d : Val[D], render : (A, B, C, D) ->
Html
) -> Val[
Html
]

Creates an HTML view derived from four incremental values.

The render function is reevaluated when any input changes.

#
Val::view5

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq] Val::view5(a : Val[A], b : Val[B], c : Val[C], d : Val[D], e : Val[E], render : (A, B, C, D, E) ->
Html
) -> Val[
Html
]

Creates an HTML view derived from five incremental values.

The render function is reevaluated when any input changes.

#
Val::view6

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq] Val::view6(a : Val[A], b : Val[B], c : Val[C], d : Val[D], e : Val[E], f : Val[F], render : (A, B, C, D, E, F) ->
Html
) -> Val[
Html
]

Creates an HTML view derived from six incremental values.

The render function is reevaluated when any input changes.

#
Val::view7

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq] Val::view7(a : Val[A], b : Val[B], c : Val[C], d : Val[D], e : Val[E], f : Val[F], g : Val[G], render : (A, B, C, D, E, F, G) ->
Html
) -> Val[
Html
]

Creates an HTML view derived from seven incremental values.

The render function is reevaluated when any input changes.

#
Val::view8

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq] Val::view8(a : Val[A], b : Val[B], c : Val[C], d : Val[D], e : Val[E], f : Val[F], g : Val[G], h : Val[H], render : (A, B, C, D, E, F, G, H) ->
Html
) -> Val[
Html
]

Creates an HTML view derived from eight incremental values.

The render function is reevaluated when any input changes.

#
Val::view9

fn[A : Eq, B : Eq, C : Eq, D : Eq, E : Eq, F : Eq, G : Eq, H : Eq, I : Eq] Val::view9(a : Val[A], b : Val[B], c : Val[C], d : Val[D], e : Val[E], f : Val[F], g : Val[G], h : Val[H], i : Val[I], render : (A, B, C, D, E, F, G, H, I) ->
Html
) -> Val[
Html
]

Creates an HTML view derived from nine incremental values.

The render function is reevaluated when any input changes.

#
batch

Combine multiple commands into one command.

#
cell

#deprecated("Use `create_state` inside a `() -> Val[Html]` component; return `(Model, Cmd)` from `update`, then map the model to `Html`.")
fn[Model, Msg] cell(model~ : Model, update~ : (
Emit
[Msg], Msg, Model) -> (
Cmd
, Model), view~ : (
Emit
[Msg], Model) ->
Html
, subscriptions? : (
Emit
[Msg], Model) ->
Sub
) -> (() -> Val[
Html
])

Compatibility wrapper for the former effectful Cell constructor.

New code should call create_state inside a component and map the returned model to Html. The new update callback returns (Model, Cmd) instead of (Cmd, Model).

#
cell_with_emit

#deprecated("Use `create_state` inside a component and pass its returned `Emit` explicitly where it is needed.")
fn[Model, Msg] cell_with_emit(model~ : Model, update~ : (
Emit
[Msg], Msg, Model) -> (
Cmd
, Model), view~ : (
Emit
[Msg], Model) ->
Html
, subscriptions? : (
Emit
[Msg], Model) ->
Sub
) -> (
Emit
[Msg], () -> Val[
Html
])

Compatibility wrapper for the former effectful Cell constructor that also exposed its emitter.

#
create_pure_state

fn[Model : Eq, Msg] create_pure_state(model : Model, update~ : (Model, Msg) -> Model) -> (Val[Model],
Emit
[Msg])

Creates component-local state with a pure update function.

Call this while building a component. The returned emitter creates commands that apply messages with update, and the returned value tracks the current model.

Example

priv enum Msg {
Inc
Dec
}

fn counter() -> Val[Html] {
let (count, emit) = @rabbita.create_pure_state(0, update=fn(count, msg) {
match msg {
Inc => count + 1
Dec => count - 1
}
})
count.view(count => {
@html.div([
@html.button(on_click=emit(Dec), "-"),
@html.span(count.to_string()),
@html.button(on_click=emit(Inc), "+"),
])
})
}

#
create_resource

fn[T : Eq] create_resource(f : (
Emit
[Result[T, Error]]) ->
Cmd
) -> Val[Status[T]]

Starts an asynchronous resource command and tracks its result.

The resource starts as Pending, then becomes Loaded or Failed when the emitter passed to f receives a result. Call this while building a component.

Example

fn chapter() -> Val[Html] {
// Import "moonbit-community/rabbita/http" as @http in moon.pkg.
let chapter = @rabbita.create_resource(inject => {
@http.get("/chapter.md").expect_text(inject)
})
chapter.view(status => {
match status {
Pending => @html.p("Loading...")
Loaded(text) => @html.pre(text)
Failed(_) => @html.p("Failed to load chapter")
}
})
}

#
create_state

fn[Model : Eq, Msg] create_state(model : Model, update~ : (Model, Msg,
Emit
[Msg]) -> (Model,
Cmd
), subscriptions? : (Model,
Emit
[Msg]) ->
Sub
) -> (Val[Model],
Emit
[Msg])

Creates component-local state whose updates may schedule commands.

Call this while building a component. The returned emitter creates commands that deliver messages to update; subscriptions are refreshed after each processed message.

Example

priv enum CounterMsg {
Increment
IncrementLater
}

fn counter() -> Val[Html] {
let (count, emit) = @rabbita.create_state(0, update=fn(count, msg, emit) {
match msg {
Increment => (count + 1, @rabbita.none)
IncrementLater => (count, @rabbita.delay(emit(Increment), 1000))
}
})
count.view(count => {
@html.button(on_click=emit(IncrementLater), count.to_string())
})
}

#
create_state_with_init

Creates component-local state with an emitter-aware initializer.

init supplies the initial model and a command to schedule. Later messages are handled by update, as with create_state. Call this while building a component.

Example

priv enum Msg {
Inc
Dec
}

fn delayed_counter() -> Val[Html] {
let (count, emit) = @rabbita.create_state_with_init(
init=fn(emit) { (0, @rabbita.delay(emit(Inc), 1000)) },
update=fn(count, msg, _) {
match msg {
Inc => (count + 1, @rabbita.none)
Dec => (count - 1, @rabbita.none)
}
},
)
count.view(count => {
@html.div([
@html.button(on_click=emit(Dec), "-"),
@html.span(count.to_string()),
@html.button(on_click=emit(Inc), "+"),
])
})
}

#
create_state_with_input

fn[Model : Eq, Msg, Input : Eq] create_state_with_input(init~ : (
Emit
[Msg], Input) -> (Model,
Cmd
), update~ : (Model, Input, Msg,
Emit
[Msg]) -> (Model,
Cmd
), subscriptions? : (Model, Input,
Emit
[Msg]) ->
Sub
, input~ : Val[Input]) -> (Val[Model],
Emit
[Msg])

Creates component-local state whose callbacks receive an incremental input.

The current input is passed to init, update, and subscriptions when those callbacks run. Changing the input alone does not send a message, run update, or refresh subscriptions. Call this while building a component.

Example

Each click increments or decrements by the current value of step.

priv enum Msg {
Inc
Dec
}

fn stepped_counter(step : Val[Int]) -> Val[Html] {
let (count, emit) = @rabbita.create_state_with_input(
input=step,
init=fn(_, _) { (0, @rabbita.none) },
update=fn(count, step, msg, _) {
match msg {
Inc => (count + step, @rabbita.none)
Dec => (count - step, @rabbita.none)
}
},
)
count.view(count => {
@html.div([
@html.button(on_click=emit(Dec), "-"),
@html.span(count.to_string()),
@html.button(on_click=emit(Inc), "+"),
])
})
}

#
create_variable

fn[Model : Eq] create_variable(init : Model) -> (Val[Model],
Emit
[(Model) -> Model])

Creates component-local state updated by transformation functions.

The returned emitter turns a model transformation into a Cmd. When that command is scheduled, the transformation is applied to the current model and its result becomes the new model. Call this while building a component.

Example

fn toggle() -> Val[Html] {
let (open, set_open) = @rabbita.create_variable(false)
open.view(is_open => {
@html.button(
on_click=set_open(v => !v),
if is_open {
"Close"
} else {
"Open"
},
)
})
}

#
elmish

Creates an application using an Elm-style model, update, and view.

This is a convenience wrapper around create_state and new. Each emitted message updates the model, schedules the returned command, and refreshes the optional subscriptions.

#
new

Creates an application from a root component builder.

The builder runs when the app is mounted and must return its root incremental HTML value. Use Val::map, Val::switch, and Val::assoc to express subsequent changes, then call App::mount to start the app.

#
none

A command that does nothing.

#
simple_cell

#deprecated("Use `create_pure_state` inside a `() -> Val[Html]` component, then map the model to `Html`.")
fn[Model, Msg] simple_cell(model~ : Model, update~ : (Msg, Model) -> Model, view~ : (
Emit
[Msg], Model) ->
Html
) -> (() -> Val[
Html
])

Compatibility wrapper for the former pure Cell constructor.

#
simple_cell_with_emit

#deprecated("Use `create_pure_state` inside a component and pass its returned `Emit` explicitly where it is needed.")
fn[Model, Msg] simple_cell_with_emit(model~ : Model, update~ : (Msg, Model) -> Model, view~ : (
Emit
[Msg], Model) ->
Html
, subscriptions? : (
Emit
[Msg], Model) ->
Sub
) -> (
Emit
[Msg], () -> Val[
Html
])

Compatibility wrapper for the former pure Cell constructor that also exposed its emitter.

#
static_cell

#deprecated("Use a component returning `Val::constant(html)` instead.")
fn static_cell(html :
Html
) -> (() -> Val[
Html
])

Compatibility wrapper for the former static Cell constructor.