rsignal

rsignal - A reactive web programming library for Moonbit

moon add bikallem/rsignal@0.3.0
Download zip
Author
Version
0.3.0
License
MPL-2.0
Last updated
11 months ago
Downloads
21
README

#rsignal

rsignal is a reactive programming library for Moonbit. It is built with both web and terminal cli applications in mind. rsignal is short for Reactive Signal.

At the core of the library is a type called Rsignal. Any changes/updates to a Rsignal can be observed by observers. Observers register to observe changes to Rsignal. Once registered, the observers are notified whenever a Rsignal is updated or modified. Observers can then initiate a change operation called effect based on these changes. A Rsignal can have many observers.

A simple counter app in reactive:

///|
fnalias @rsignal.(div, button, style, onclick, h)

///|
typealias @web.(HTMLDivElement, ReactiveElement as RE, ReactiveAttr as RA)

///|
fn counter(initial_count : Int) -> HTMLDivElement {
let state = @reactive_core.new(initial_count) // root signal
div([
style("display: flex; flex-direction: column; align-items: center;"),
h("h2", ["The Greatest Counter Ever!"]),
div([
style("display: flex; flex-direction: row; column-gap: 1em;"),
button("Decrement", [
onclick(_ => state.update(state.val() - 1)),
RA::disabled(state.map(count => count <= 0)), // Disable button when count is 0
]),
RE::text(state, (txt, cur_count) => txt.set_text_content(cur_count)),
button("Increment", [onclick(_ => state.update(state.val() + 1))]),
]),
])
}

///|
fn main {
let el = counter(0)
@reactive_web.mount_to_body(el)
}

#rsignal vs react

For web apps, rsignal offers a fine-grained selection mechanisms to update UI views. This is in contrast to other JavaScript library like React where the whole virtual DOM is iterated on to find DOM updates/diffs. These fine-grained updates ensures that only the specific dom node and/or attribute that needs updating are accessed and updated.

#How to use

moon add bikallem/rsignal

#API Documentation

https://mooncakes.io/docs/bikallem/rsignal

#Status

Early and experimental. API may be changed/removed without deprecation/warning.

#NOTE

This library used to be known as reactive in the past. It has been renamed and restructured and henceforth will be known as rsignal.

#
Rsignal

type Rsignal[T]

A reactive signal that can be subscribed to and updated.

#
Rsignal::filter

fn[T] Rsignal::filter(self : Rsignal[T], f : (T) -> Bool, seed : T) -> Rsignal[T]

Creates a new signal that receives values from self where f(new_value) is true. If the initial value of f(self.get()) == false, the new signal will be initialized with the given seed value.

Parameters:
  • self: The original signal.
  • f: The filter function.
  • seed: The initial value for the new signal if the filter function returns false during creation.

Returns:
  • A new signal that emits only the values from self that pass the filter function f.

Example:
let filtered_values = []
let s = new(1)
let s2 = s.filter(fn(v) { v > 5 }, 0)
s2.subscribe_permanent_only(fn(v) { filtered_values.push(v) })
s.update(10) // This will trigger the subscriber with value 10.
s.update(7) // This will trigger the subscriber with value 7.
s.update(3) // This will not trigger the subscriber.
inspect(filtered_values, content="[10, 7]")

#
Rsignal::filter_map

fn[T, U] Rsignal::filter_map(self : Rsignal[T], f : (T) -> U?, seed : U) -> Rsignal[U]

Creates a new signal that maps the values of self iff f(self.get()) == Some(v). If f(self.get()) returns None, the new signal will be initialized with the given seed value

This combines the functionality of map and filter into a single operation.

Parameters:
  • self: The original signal.
  • f: The mapping function.
  • seed: The initial value for the new signal if the mapping function returns None during creation.

Returns:
  • A new signal that emits the values from self that pass the mapping function f.

Example:
let mapped_values = []
let s = new(1)
let s2 = s.filter_map(fn(v) { if v > 5 { Some(v+2) } else { None } }, 0)
s2.subscribe_permanent_only(fn(v) { mapped_values.push(v) })
s.update(10) // This will trigger the subscriber with value 12.
s.update(7) // This will trigger the subscriber with value 9.
s.update(3) // This will not trigger the subscriber.
inspect(mapped_values, content="[12, 9]")

#
Rsignal::id

fn[T] Rsignal::id(self : Rsignal[T]) -> String

Returns the unique ID of the signal

Parameters:
  • self: The signal to get the ID from.

Returns:
  • The unique ID of the signal.

Example:
let s = new(0)
assert_eq(s.id(), "signal#0")
let s2 = new(1, label="my_signal")
assert_eq(s2.id(), "my_signal#1")

#
Rsignal::map

fn[T, U] Rsignal::map(self : Rsignal[T], f : (T) -> U) -> Rsignal[U]

Creates a new signal that maps the value of self using the function f.

Parameters:
  • self: The original signal.
  • f: The mapping function that takes the value of self and returns a new value.

Returns:
  • A new signal that emits the result of applying f to the value of self.

Example:
let values = []
let s = new(0)
let s2 = s.map(fn(v) { v + 1 })
s2.subscribe_permanent(fn(v) { values.push(v) })
s.update(5) // This will trigger the subscriber with value 6.
s.update(10) // This will trigger the subscriber with value 11.
s.update(20) // This will trigger the subscriber with value 21.
inspect(values, content="[1, 6, 11, 21]")

#
Rsignal::subscribe

fn[T] Rsignal::subscribe(self : Rsignal[T], effect : (T) -> Unit) -> Subscriber[T]

Subscribes to a signal and immediately calls the effect function with the current value

#
Rsignal::subscribe_only

fn[T] Rsignal::subscribe_only(self : Rsignal[T], effect : (T) -> Unit) -> Subscriber[T]

Subscribes to a signal without immediately calling the effect function This is useful for cases where you want to set up a subscription but don't want to trigger the effect function until the signal changes.

#
Rsignal::subscribe_permanent

fn[T] Rsignal::subscribe_permanent(self : Rsignal[T], effect : (T) -> Unit) -> Unit

Subscribes to a signal permanently and immediately calls the effect function with the current value. This is useful for cases where you want to set up a subscription that will not be removed

#
Rsignal::subscribe_permanent_only

fn[T] Rsignal::subscribe_permanent_only(self : Rsignal[T], effect : (T) -> Unit) -> Unit

Subscribes to a signal permanently without immediately calling the effect function.

#
Rsignal::unsubscribe

fn[T] Rsignal::unsubscribe(self : Rsignal[T], subscription : Subscriber[T]) -> Unit

Unsubscribes a subscriber from a signal.

Parameters:
  • self: The signal to unsubscribe from.
  • subscription: The subscriber to unsubscribe.

Example:
let values = []
let s = new(0)
let sub = s.subscribe(fn(v) { values.push(v) })
assert_eq(s.val(), 0)
s.update(5) // This will notify subscribers with the new value.
assert_eq(s.val(), 5)
assert_eq(values, [0, 5])
s.unsubscribe(sub) // This will unsubscribe the subscriber.
assert_eq(s.val(), 5)
s.update(10) // This will not notify the unsubscribed subscriber.
assert_eq(s.val(), 10)
assert_eq(values, [0, 5])

#
Rsignal::update

fn[T] Rsignal::update(self : Rsignal[T], new_value : T, notify? : Bool) -> Unit

Updates the value of the signal. If notify is true, notifies all of its observers of the change.

Parameters:
  • self: The signal to update.
  • new_value: The new value to set.
  • notify : If true notifies all observers of this signal - default is true.

Example:
let values = []
let s = new(0)
s.subscribe_permanent(fn(v) { values.push(v) })
assert_eq(s.val(), 0)
s.update(5) // This will notify subscribers with the new value.
assert_eq(s.val(), 5)
assert_eq(values, [0, 5])
s.update(10) // This will notify subscribers with the new value.
assert_eq(s.val(), 10)
assert_eq(values, [0, 5, 10])

#
Rsignal::val

fn[T] Rsignal::val(s : Rsignal[T]) -> T

Returns the current value of the signal s

Parameters:
  • s: The signal to get the value from.

Returns:
  • The current value of the signal.

Example:
let s = new(0)
assert_eq(s.val(), 0)
s.update(5)
assert_eq(s.val(), 5)

#
Subscriber

type Subscriber[T]

#
combine_all

fn[T] combine_all(signals : Array[Rsignal[T]]) -> Rsignal[Array[T]]

Combines all signals in signals into a single signal s. If any of the signals change, s will emit an array of the latest values from all signals.

Parameters:
  • signals: An array of signals to combine.

Returns:
  • A new signal that emits an array of the latest values from all signals.

Example:
let values = []
let s1 = new(1)
let s2 = new(2)
let s3 = new(3)
let s4 = new(4)
let combined_signal = combine_all([s1, s2, s3, s4])
combined_signal.subscribe_permanent(fn(v) { values.push(v) })
s1.update(5) // This will trigger the subscriber with value [5, 2, 3, 4].
s2.update(6) // This will trigger the subscriber with value [5, 6, 3, 4].
s3.update(7) // This will trigger the subscriber with value [5, 6, 7, 4].
s4.update(8) // This will trigger the subscriber with value [5, 6, 7, 8].
inspect(values, content="[[1, 2, 3, 4], [5, 2, 3, 4], [5, 6, 3, 4], [5, 6, 7, 4], [5, 6, 7, 8]]")

#
combine_pair

fn[T, U] combine_pair(s1 : Rsignal[T], s2 : Rsignal[U]) -> Rsignal[(T, U)]

Creates a new signal that emits a tuple of the latest values from both signals.

Parameters:
  • s1: The first signal.
  • s2: The second signal.

Returns:
  • A new signal that emits a tuple of the latest values from s1 and s2.

Example:
let values = []
let s1 = new(1)
let s2 = new(2)
let s3 = combine_pair(s1, s2)
s3.subscribe_permanent(fn(v) { values.push(v) })
s1.update(3) // This will trigger the subscriber with value (3, 2).
s2.update(4) // This will trigger the subscriber with value (3, 4).
s1.update(5) // This will trigger the subscriber with value (5, 4).
inspect(values, content="[(1, 2), (3, 2), (3, 4), (5, 4)]")

#
combine_triple

fn[T, U, V] combine_triple(s1 : Rsignal[T], s2 : Rsignal[U], s3 : Rsignal[V]) -> Rsignal[(T, U, V)]

Creates a new signal that emits a tuple of the latest values from three signals.

Parameters:
  • s1: The first signal.
  • s2: The second signal.
  • s3: The third signal.

Returns:
  • A new signal that emits a tuple of the latest values from s1, s2, and s3.

Example:
let values = []
let s1 = new(1)
let s2 = new(2)
let s3 = new(3)
let s4 = combine_triple(s1, s2, s3)
s4.subscribe_permanent(fn(v) { values.push(v) })
s1.update(4) // This will trigger the subscriber with value (4, 2, 3).
s2.update(5) // This will trigger the subscriber with value (4, 5, 3).
s3.update(6) // This will trigger the subscriber with value (4, 5, 6).
s1.update(7) // This will trigger the subscriber with value (7, 5, 6).
inspect(values, content="[(1, 2, 3), (4, 2, 3), (4, 5, 3), (4, 5, 6), (7, 5, 6)]")

#
map2

fn[T, U, V] map2(s1 : Rsignal[T], s2 : Rsignal[U], f : (T, U) -> V) -> Rsignal[V]

Creates a new signal that maps the values of two signals using the function f.

Parameters:
  • s1: The first signal.
  • s2: The second signal.
  • f: The mapping function that takes the values of both signals and returns a new value.

Returns:
  • A new signal that emits the result of applying f to the values of s1 and s2.

Example:
let values = []
let s1 = new(1)
let s2 = new(2)
let s3 = map2(s1, s2, fn(a, b) { a + b })
s3.subscribe_permanent(fn(v) { values.push(v) })
s1.update(3) // This will trigger the subscriber with value 5.
s2.update(4) // This will trigger the subscriber with value 7.
s1.update(5) // This will trigger the subscriber with value 9.
inspect(values, content="[3, 5, 7, 9]")

#
new

fn[T] new(value : T, label? : String) -> Rsignal[T]

Creates a new signal with the given initial value.

#
select_one

fn[T : Default] select_one(signals : Array[Rsignal[T]]) -> Rsignal[T]

Creates a new signal which re-emits the latest signal from one of the signals in signals array.

This is useful for combining multiple signals into a single signal.

The initial value of the new signal will be the default value of T.

Parameters:
  • signals: An array of signals to combine.

Returns:
  • A new signal that emits the values from all signals in the array.

Example:
let values = []
let s1 = new(1)
let s2 = new(2)
let s3 = new(3)
let combined_signal = select_one([s1, s2, s3])
combined_signal.subscribe_permanent_only(fn(v) { values.push(v) })
s1.update(4) // This will trigger the subscriber with value 4.
s2.update(5) // This will trigger the subscriber with value 5.
s3.update(6) // This will trigger the subscriber with value 6.
inspect(values, content="[4, 5, 6]")

Source Files

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io