rabbita

    functional Web UI framework for MoonBit

    functional
    html
    UI
    web
    TEA
    Download zip
    Version
    0.15.6
    License
    Apache-2.0
    Last updated
    1 hour ago
    Downloads
    61K

    #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 --browser-entry main

    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 Enumerate for Status[T]
    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.