Signals
The page's state is one Rust struct, and a signal is a typed reference to one of its fields.
The signal struct
Derive Signals next to serde's Serialize and Deserialize. The derive adds one function per field, named after the field, that returns a Sig. Sig<Counter, i32> reads: an i32 inside the signal struct Counter.
use hypersig::{Sig, Signals};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
struct Counter {
count: i32,
label: String,
}
// The derive adds one function per field, named after the field.
let count: Sig<Counter, i32> = Counter::count();
let label: Sig<Counter, String> = Counter::label();
assert_eq!(count.path(), "count");
assert_eq!(label.path(), "label");The browser keeps the struct as one JSON object, and every expression, statement, and patch names its fields through a Sig. A misspelled field has no function to call, so it fails to compile:
use hypersig::Signals;
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
struct Counter {
count: i32,
}
// error[E0599]: no associated function or constant named `cuont` found for struct `Counter`
let count = Counter::cuont();The S in Sig<S, T> also stops a signal of one page from being used on another. An element of a Counter page is an El<Counter>, and it accepts only expressions over Counter.
Fields inside fields
A field may itself be a Signals struct. at reaches into it, and the result is a Sig like any other. A path is at most 8 fields deep.
use hypersig::html::span;
use hypersig::Signals;
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
struct Address {
city: String,
}
#[derive(Signals, Serialize, Deserialize)]
struct Account {
name: String,
address: Address,
}
// `at` reaches a field of the struct a signal holds.
let city = Account::address().at(Address::city());
assert_eq!(city.path(), "address.city");
let line = span::<Account>().bind_text(city);A Vec of values or of structs is a list, which Lists covers. An Option field has is_some, is_none, and unwrap_or.
The first render
Page::new takes the initial values, and Page::render evaluates every binding with them. Text, visibility, classes, attributes, form values, and list items are in the HTML before the runtime loads, and the runtime uses those elements as they are rather than building them again. The browser fetches nothing to show the page, because the data is the signal struct's JSON on <body>.
Computed signals
Page::computed(signal, value) keeps a field equal to an expression over other fields. Bindings read it like any field and run again only when its value changes, rather than each evaluating the expression. Computed signals are evaluated in the order they are declared, so one may read an earlier one.
Result
use hypersig::html::{div, p, span};
use hypersig::style::pattern::{hstack, stack};
use hypersig::style::value::rem;
use hypersig::{Page, Signals};
use hypersig_ui::{ButtonSize, ButtonVariant, button};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
pub struct Item {
name: String,
price: f64,
}
#[derive(Signals, Serialize, Deserialize)]
pub struct Cart {
item: Item,
quantity: i32,
total: f64,
}
pub fn page() -> Page<Cart> {
let item = Cart::item();
let quantity = Cart::quantity();
let total = Cart::total();
let initial = Cart {
item: Item {
name: "Green tea"
.into(),
price: 4.5,
},
quantity: 1,
total: 0.0,
};
Page::new(
"Cart",
initial,
)
.computed(total, item.at(Item::price()) * quantity.to_f64())
.child(
div()
.css(stack(rem(0.75)))
.child(
p().child(span().bind_text(item.at(Item::name())))
.text(" at $")
.child(span().bind_text(item.at(Item::price()).to_fixed(2))),
)
.child(
div()
.css(hstack(rem(0.75)))
.child(
button(ButtonVariant::Outline, ButtonSize::Sm)
.attr_when("disabled", quantity.le(1))
.on_click(quantity.set(quantity - 1))
.text("-"),
)
.child(span().bind_text(quantity.to_text()))
.child(
button(ButtonVariant::Outline, ButtonSize::Sm)
.on_click(quantity.set(quantity + 1))
.text("+"),
),
)
.child(
p().text("Total: $")
.child(span().bind_text(total.to_fixed(2))),
),
)
}