///|
pub using @common {trait Enumerate}
///|
using @vector {type Vector}
///|
/// A lazily evaluated value in Rabbita's incremental graph.
///
/// Derived values are recomputed on demand after one of their dependencies
/// changes.
struct Val[A](@duplix.Node[A])
///|
/// Creates an incremental value by applying `f` to `a`.
///
/// The function is reevaluated when the value of `a` changes.
pub fn[A : Eq, B] Val::map(a : Val[A], f : (A) -> B) -> Val[B] {
a.0.map1(f)
}
///|
/// Creates an incremental value that always contains `a`.
pub fn[A] Val::constant(a : A) -> Val[A] {
@duplix.constant(a)
}
///|
/// Creates an incremental value derived from two inputs.
///
/// The function is reevaluated when either input value changes.
pub fn[A : Eq, B : Eq, C] Val::map2(
a : Val[A],
b : Val[B],
f : (A, B) -> C,
) -> Val[C] {
a.0.map2(b.0, f)
}
///|
/// Creates an incremental value derived from three inputs.
///
/// The function is reevaluated when any input value changes.
pub 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] {
a.0.map3(b.0, c.0, f)
}
///|
/// Creates an incremental value derived from four inputs.
///
/// The function is reevaluated when any input value changes.
pub 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] {
a.0.map4(b.0, c.0, d.0, f)
}
///|
/// Creates an incremental value derived from five inputs.
///
/// The function is reevaluated when any input value changes.
pub 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] {
a.0.map5(b.0, c.0, d.0, e.0, f)
}
///|
/// Creates an incremental value derived from six inputs.
///
/// The function is reevaluated when any input value changes.
pub 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] {
a.0.map6(b.0, c.0, d.0, e.0, f.0, g)
}
///|
/// Creates an incremental value derived from seven inputs.
///
/// The function is reevaluated when any input value changes.
pub 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] {
a.0.map7(b.0, c.0, d.0, e.0, f.0, g.0, h)
}
///|
/// Creates an incremental value derived from eight inputs.
///
/// The function is reevaluated when any input value changes.
pub 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] {
a.0.map8(b.0, c.0, d.0, e.0, f.0, g.0, h.0, i)
}
///|
/// Creates an incremental value derived from nine inputs.
///
/// The function is reevaluated when any input value changes.
pub 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] {
a.0.map9(b.0, c.0, d.0, e.0, f.0, g.0, h.0, i.0, j)
}
///|
/// Creates an HTML view derived from one incremental value.
///
/// The render function is reevaluated when `a` changes.
pub fn[A : Eq] Val::view(a : Val[A], render : (A) -> Html) -> Val[Html] {
a.0.map(render)
}
///|
/// Creates an HTML view derived from two incremental values.
///
/// The render function is reevaluated when either input changes.
pub fn[A : Eq, B : Eq] Val::view2(
a : Val[A],
b : Val[B],
render : (A, B) -> Html,
) -> Val[Html] {
a.0.map2(b.0, render)
}
///|
/// Creates an HTML view derived from three incremental values.
///
/// The render function is reevaluated when any input changes.
pub 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] {
a.0.map3(b.0, c.0, render)
}
///|
/// Creates an HTML view derived from four incremental values.
///
/// The render function is reevaluated when any input changes.
pub 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] {
a.0.map4(b.0, c.0, d.0, render)
}
///|
/// Creates an HTML view derived from five incremental values.
///
/// The render function is reevaluated when any input changes.
pub 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] {
a.0.map5(b.0, c.0, d.0, e.0, render)
}
///|
/// Creates an HTML view derived from six incremental values.
///
/// The render function is reevaluated when any input changes.
pub 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] {
a.0.map6(b.0, c.0, d.0, e.0, f.0, render)
}
///|
/// Creates an HTML view derived from seven incremental values.
///
/// The render function is reevaluated when any input changes.
pub 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] {
a.0.map7(b.0, c.0, d.0, e.0, f.0, g.0, render)
}
///|
/// Creates an HTML view derived from eight incremental values.
///
/// The render function is reevaluated when any input changes.
pub 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] {
a.0.map8(b.0, c.0, d.0, e.0, f.0, g.0, h.0, render)
}
///|
/// Creates an HTML view derived from nine incremental values.
///
/// The render function is reevaluated when any input changes.
pub 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] {
a.0.map9(b.0, c.0, d.0, e.0, f.0, g.0, h.0, i.0, render)
}
///|
/// 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
///
/// ```mbt nocheck
/// 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), "+"),
/// ])
/// })
/// }
/// ```
pub fn[Model : Eq, Msg] create_pure_state(
model : Model,
update~ : (Model, Msg) -> Model,
) -> (Val[Model], Emit[Msg]) {
let (node, emit) = @runtime.create_state_machine(_ => (model, none), (
model,
msg,
_,
) => (update(model, msg), none))
(node, emit)
}
///|
/// 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
///
/// ```mbt nocheck
/// 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())
/// })
/// }
/// ```
pub fn[Model : Eq, Msg] create_state(
model : Model,
update~ : (Model, Msg, Emit[Msg]) -> (Model, Cmd),
subscriptions? : (Model, Emit[Msg]) -> @sub.Sub,
) -> (Val[Model], Emit[Msg]) {
let (node, emit) = @runtime.create_state_machine(
_ => (model, none),
update,
subscriptions?,
)
(Val(node), emit)
}
///|
/// 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
///
/// ```mbt nocheck
/// 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))
/// }
/// ```
pub fn[K : Hash + Eq, V : Eq, C : Eq] Val::assoc(
a : Val[Vector[(K, V)]],
f : (K, Val[V]) -> Val[C],
) -> Val[Vector[C]] {
a.0.assoc((k, v) => f(k, v).0)
}
///|
/// Incrementally maps values using keys derived by `by`.
///
/// Derived keys must be unique and stable. Output follows source vector order.
pub 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]] {
a.0.assoc_by((k, v) => f(k, v).0, by~)
}
///|
/// 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
///
/// ```mbt nocheck
/// 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()
/// }
/// })
/// }
/// ```
pub fn[E : Enumerate + Eq, C : Eq] Val::enumerate(
a : Val[E],
f : (E) -> Val[C],
) -> Val[C] {
a.0.enumerate(e => f(e).0)
}
///|
/// Selects and caches branches using the tag returned by `by`.
///
/// Values with the same tag reuse the branch created for its first value.
pub fn[E : Eq, C : Eq] Val::enumerate_by(
a : Val[E],
f : (E) -> Val[C],
by~ : (E) -> String,
) -> Val[C] {
a.0.enumerate_by(e => f(e).0, by~)
}
///|
/// 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
///
/// ```mbt nocheck
/// 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()
/// }
/// })
/// }
/// ```
pub fn[E : Enumerate + Eq, C : Eq] Val::switch(
a : Val[E],
f : (E) -> Val[C],
) -> Val[C] {
a.0.switch(e => f(e).0)
}
///|
/// 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.
pub fn[E : Eq, C : Eq] Val::switch_by(
a : Val[E],
f : (E) -> Val[C],
by~ : (E) -> String,
) -> Val[C] {
a.0.switch_by(e => f(e).0, by~)
}
///|
/// 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
///
/// ```mbt nocheck
/// 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), "+"),
/// ])
/// })
/// }
/// ```
pub fn[Model : Eq, Msg] create_state_with_init(
init~ : (Emit[Msg]) -> (Model, Cmd),
update~ : (Model, Msg, Emit[Msg]) -> (Model, Cmd),
subscriptions? : (Model, Emit[Msg]) -> @sub.Sub,
) -> (Val[Model], Emit[Msg]) {
let (node, emit) = @runtime.create_state_machine(init, update, subscriptions?)
(node, emit)
}
///|
/// 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`.
///
/// ```mbt nocheck
/// 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), "+"),
/// ])
/// })
/// }
/// ```
pub 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.Sub,
input~ : Val[Input],
) -> (Val[Model], Emit[Msg]) {
let (node, emit) = @runtime.create_state_machine_with_input(
init,
update,
subscriptions?,
input.0,
)
(node, emit)
}
///|
/// 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
///
/// ```mbt nocheck
/// 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"
/// },
/// )
/// })
/// }
/// ```
pub fn[Model : Eq] create_variable(
init : Model,
) -> (Val[Model], Emit[(Model) -> Model]) {
fn update(model, f) {
f(model)
}
create_pure_state(init, update~)
}
///|
/// 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.
pub enum Status[T] {
Pending
Loaded(T)
Failed(Error)
}
///|
pub impl[T : Eq] Eq for Status[T] with fn equal(a, b) {
match (a, b) {
(Pending, Pending) | (Failed(_), Failed(_)) => true
(Loaded(a), Loaded(b)) => a == b
_ => false
}
}
///|
/// An incremental asynchronous resource state.
pub type Resource[T] = 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
///
/// ```mbt nocheck
/// 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")
/// }
/// })
/// }
/// ```
pub fn[T : Eq] create_resource(
f : (Emit[Result[T, Error]]) -> Cmd,
) -> Resource[T] {
let (status, _) : (Resource[T], _) = create_state_with_init(
init=fn(inject) { (Pending, f(inject)) },
update=fn(_, msg, _) {
match msg {
Ok(r) => (Loaded(r), none)
Err(e) => (Failed(e), none)
}
},
)
status
}