rabbita

    functional Web UI framework for MoonBit

    functional
    html
    UI
    web
    TEA
    Download zip
    Version
    0.11.9
    License
    Apache-2.0
    Last updated
    5 months ago
    Downloads
    79K

    #Rabbita

    A declarative, functional web UI framework inspired by The Elm Architecture.

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

    #Features

    • Predictable flow

      State changes follow a single, predictable update path, with explicit side‑effect management.

    • 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

      Use Cell to split logic and reuse stateful views. Skip diff and patching for non-dirty cells.

    #Quick Start

    Use the rabbita-template to get started quickly.

    #Examples

    #Counter

    ///|
    using @html {div, h1, button}

    ///|
    #cfg(target="js")
    test {
    struct Model {
    count : Int
    }
    enum Msg {
    Inc
    Dec
    }
    let app = @rabbita.simple_cell(
    model={ count: 0 },
    update=(msg, model) => {
    let { count } = model
    match msg {
    Inc => { count: count + 1 }
    Dec => { count: count - 1 }
    }
    },
    view=(dispatch, model) => {
    div([
    h1("\{model.count}"),
    button(on_click=dispatch(Inc), "+"),
    button(on_click=dispatch(Dec), "-"),
    ])
    },
    )
    new(app).mount("main")
    }

    #Multiple cells

    Each cell maintains its own model, view, and update logic, and only dirty cells need VDOM diffing and patching.

    ///|
    #cfg(target="js")
    using @html {fragment, input, nothing, ul, li, p}

    ///|
    #cfg(target="js")
    using @list {type List, empty}

    ///|
    /// The todo plan
    #cfg(target="js")
    fn plan(name : String) -> Cell {
    struct Model {
    value : String
    items : Map[String, Bool]
    }
    enum Msg {
    Add
    Change(String)
    Done(String)
    }
    @rabbita.simple_cell(
    model={ value: "", items: {} },
    update=(msg, model) => {
    let { value, items } = model
    match msg {
    Add => { value: "", items: items..set(value, false) }
    Done(key) => { ..model, items: items..set(key, true) }
    Change(value) => { ..model, value, }
    }
    },
    view=(dispatch, model) => {
    let { value, items } = model
    let items = items.map((todo, done) => {
    let text_style = if done { "text-decoration: line-through" } else { "" }
    li(style=[text_style], [
    p(todo),
    button(on_click=dispatch(Done(todo)), "done"),
    ])
    })
    div(style=["border: 1px solid black", "padding: 1em"], [
    h1(name),
    ul(items),
    input(
    input_type=Text,
    value~,
    on_change=s => dispatch(Change(s)),
    nothing,
    ),
    button(on_click=dispatch(Add), "add"),
    ])
    },
    )
    }

    ///|
    /// Main app
    #cfg(target="js")
    test {
    struct Model {
    plans : List[Cell]
    }
    enum Msg {
    NewPlan
    }
    let app = @rabbita.simple_cell(
    model={ plans: empty() },
    update=(msg, model) => {
    let id = model.plans.length()
    match msg {
    NewPlan => { plans: model.plans.add(plan("plan \{id}")) }
    }
    },
    view=(dispatch, model) => {
    fragment([
    div(model.plans.map(x => x.view())),
    button(on_click=dispatch(NewPlan), "new plan"),
    ])
    },
    )
    @rabbita.new(app).mount("app")
    }

    Cell is an opaque model: it is still managed by the outer model, but internal details are hidden. Cell::view() is a pure function that maps state to HTML.

    Unlike the hooks-style mental model, a cell's lifecycle is explicit: if its view is not present in the real DOM, the cell is inactive and messages to it are ignored. If the model is removed from the outer model, the cell is destroyed by the garbage collector.

    #Used By

    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.

    Dispatch

    Message dispatcher type used by a Cell.

    Calling dispatch(msg) returns a Cmd that enqueues msg into the cell's update loop.

    App

    type App

    A running Rabbita application.

    App::mount

    fn App::mount(self : App, element_id : String) -> Unit

    Mount the app into the DOM element identified by element_id.

    This initializes the runtime and schedules the first render flush. If routing is configured, the current URL is also dispatched on mount.

    App::with_init

    #internal(unstable, "Experimental API")
    fn App::with_init(self : App, cmd :
    Cmd
    ) -> Unit

    Registers a one-shot command to be queued when mount runs. This API is still evolving and may change in future releases.

    App::with_route

    #deprecated("use subscriptions and @sub.on_url_changed/@sub.on_url_request instead")
    fn App::with_route(self : App, url_changed? :
    Dispatch
    [
    Url
    ], url_request? :
    Dispatch
    [
    UrlRequest
    ]) -> Unit

    Configure routing callbacks for this app.

    • url_changed is triggered for browser history navigation and when push_url / replace_url commands are used.
    • url_request is triggered when captured links @html.a() are clicked.

    If the app is already mounted, a url_changed command for the current URL will be enqueued immediately.

    Cell

    type Cell

    A cell that encapsulates model, update, and view.

    Cell::view

    Render this cell as Html.

    attempt

    fn[A, E : Error] attempt(msg : (Result[A, E]) ->
    Cmd
    , f : async () -> A raise E) ->
    Cmd

    Create a command that runs an async function and handles errors.

    Similar to perform, but captures errors and passes Result[A, E] to msg.

    batch

    Combine multiple commands into one command.

    cell

    Create a stateful cell with full model/update/view functionality.

    Type parameters:

    • Model: a custom type you need to defined, represent the state of this cell.
    • Msg: a custom enum you need to defined, represent the events of this cell.

    Parameters:

    • model : Model: initial Model value of the cell.

    • update : (Dispatch[Msg], Msg,Model) -> (Cmd,Model): Describe how to compute new Model from Msg and old Model. This callback also receives a Dispatch[Msg], which converts a Msg into a Cmd.

    • view : (Dispatch[Msg], Model) -> Html: Describe how to compute Html from Model. This callback also receives a Dispatch[Msg], which converts a Msg into a Cmd.

    The update loop

    ┌─────────────────┐ │ ▼ │ ┌──────────┐ │ │ user │ │ └────┬─────┘ │ │ msg,model │ ▼ │ ┌────────────┐ msg,model │ │ update() │◄────────────┐ │ └──┬───────┬─┘ │ │ none,model│ │ │ │ ▼ │ │ │ ┌────────┐ │cmd,model │ │ │ view() │ │ │ │ └────┬───┘ │ │ │ │ │ │ │ html│ ▼ │ │ │ ┌─────────┐ │ └──────────────┘ │ runtime │─────────┘ └─────────┘

    This update loop is a bit more complex than simple_cell, but follows the same model -> update -> view flow.

    The difference is that update also returns a command representing a managed side effect. That side effect is executed by the runtime and may produce another message.

    cell_with_dispatch

    Create a cell and also return its Dispatch.

    This is useful when messages need to be sent from outside.

    Type parameters:

    • Model: a custom type you need to defined, represent the state of this cell.
    • Msg: a custom enum you need to defined, represent the events of this cell.

    Parameters:

    • model : Model: initial Model value of the cell.

    • update : (Dispatch[Msg], Msg,Model) -> (Cmd,Model): Describe how to compute new Model from Msg and old Model. This callback also receives a Dispatch[Msg], which converts a Msg into a Cmd.

    • view : (Dispatch[Msg], Model) -> Html: Describe how to compute Html from Model. This callback also receives a Dispatch[Msg], which converts a Msg into a Cmd.

    The update loop

    ┌─────────────────┐ │ ▼ │ ┌──────────┐ │ │ user │ │ └────┬─────┘ │ │ msg,model │ ▼ │ ┌────────────┐ msg,model │ │ update() │◄────────────┐ │ └──┬───────┬─┘ │ │ none,model│ │ │ │ ▼ │ │ │ ┌────────┐ │cmd,model │ │ │ view() │ │ │ │ └────┬───┘ │ │ │ │ │ │ │ html│ ▼ │ │ │ ┌─────────┐ │ └──────────────┘ │ runtime │─────────┘ └─────────┘

    This update loop is a bit more complex than simple_cell, but follows the same model -> update -> view flow.

    The difference is that update also returns a command representing a managed side effect. That side effect is executed by the runtime and may produce another message.

    delay

    Delay execution of a command by ms milliseconds.

    The delayed command is added back to the message queue after the timer fires.

    effect

    fn effect(f : async () -> Unit noraise) ->
    Cmd

    Create a command that runs an effectful async function

    new

    fn new(root : Cell) -> App

    Create an application from a root Cell.

    Use simple_cell(), static_cell(), cell(), cell_with_dispatch() to create a cell. Call mount to attach it to a DOM element. Optionally call with_route to install routing callbacks.

    For beginners, simple_cell is recommended. See its documentation for more details.

    Example

    test "minimal static page" {
    let app = @rabbita.static_cell(div("hello world"))
    ignore(app)
    // use `@rabbita.new(app).mount("id")` in client
    }

    none

    A command that does nothing.

    perform

    fn[A] perform(msg : (A) ->
    Cmd
    , f : async () -> A noraise) ->
    Cmd

    Create a command that runs an async function.

    The async function f is executed, then its result is converted into a new command by msg and scheduled back into the update loop.

    render_to_string

    #internal(unstable, "Experimental API")
    fn render_to_string(root : Cell) -> String

    Render a Cell tree into an HTML string.

    This is the server-side rendering entrypoint for Rabbita. It evaluates view 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

    #warnings("-alert_unstable")
    test "render html to string" {
    enum Msg {}
    struct Model {
    todos : Array[String]
    }
    let page = @rabbita.simple_cell(
    model={ todos: ["todo1", "todo2"] },
    update=(_ : Msg, model) => model,
    view=(_, model) => @html.ul(model.todos.map(x => @html.li(x))),
    )
    inspect(
    @rabbita.render_to_string(page),
    content=(
    #|<ul><li>todo1</li><li>todo2</li></ul>
    ),
    )
    }

    simple_cell

    fn[Model, Msg] simple_cell(model~ : Model, update~ : (Msg, Model) -> Model, view~ : (
    Dispatch
    [Msg], Model) ->
    Html
    ) -> Cell

    Create a cell with simplified model/update/view.

    Type parameters:

    • Model: a custom type you need to defined, represent the state of this cell.
    • Msg: a custom enum you need to defined, represent the events of this cell.

    Parameters:

    • model : Model: initial Model value of the cell.
    • update : (Msg, Model) -> Model: describe how to compute new Model from Msg and old Model.
    • view : (Dispatch[Msg], Model) -> Html: describes how to render Html from Model. This callback also receives a dispatch argument, which converts a Msg into a Cmd.

    The update loop

    ┌──────┐ │ user │◀─────────┐ └──────┘ │ │ msg, model │ ▼ │ ┌──────────┐ │ │ update() │ │ html └──────────┘ │ │ model │ ▼ │ ┌────────┐ │ │ view() │─────────┘ └────────┘

    At startup, the initial model is rendered as Html by view. The rendered HTML can include message-producing command, for example:

    button(on_click=dispatch(MyMsg), "click me")

    When the user clicks the button, MyMsg is sent to update with the current model. update then computes the next model from that message, and the new model is rendered again by view.

    Example

    test "counter" {
    struct Model {
    count : Int
    }
    enum Msg {
    Click
    }
    let app = @rabbita.simple_cell(
    model={ count: 0 },
    update=(msg, model) => {
    match msg {
    Click => { count: model.count + 1 }
    }
    },
    view=(dispatch, model) => {
    div([
    h1("You clicked \{model.count} times."),
    button(on_click=dispatch(Click), "+"),
    ])
    },
    )
    ignore(app) // use `new(app).mount("id")` in client
    }

    simple_cell_with_dispatch

    fn[Model, Msg] simple_cell_with_dispatch(model~ : Model, update~ : (Msg, Model) -> Model, view~ : (
    Dispatch
    [Msg], Model) ->
    Html
    , subscriptions? : (
    Dispatch
    [Msg], Model) ->
    Sub
    ) -> (
    Dispatch
    [Msg], Cell)

    static_cell

    Create a static cell that always renders the same Html.

    Rather than use static_cell, it's recommend to wrap a function that return html directly:

    fn button(text : String, on_click~ : Cmd) -> Html {
    @html.button(style=["..."], on_click~, text)
    }

    Source Files