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
22 hours ago
Downloads
51K

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.

#
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.

#
none

A command that does nothing.

#
render_to_string

#internal(unstable, "Experimental API")
fn render_to_string(builder : () -> Val[
Html
]) -> String

Render a component into an HTML string.

This is the server-side rendering entrypoint for Rabbita. It evaluates the component once and serializes the resulting virtual DOM into static HTML.

Hydration is currently not supported. If you later call mount on the client, Rabbita will do a fresh client-side render instead of attaching to the existing server-rendered DOM.

Example

test "render html to string" {
inspect(
@rabbita.render_to_string(() => {
@rabbita.Val::constant(@html.ul(["todo1", "todo2"].map(x => @html.li(x))))
}),
content=(
#|<ul><li>todo1</li><li>todo2</li></ul>
),
)
}

#
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.

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io