シグナル
ページの状態は Rust の構造体 1 つで表し、シグナルはそのフィールド 1 つを指す型付きの参照です。
シグナル構造体
serde の Serialize と Deserialize と並べて Signals を derive します。この derive はフィールドごとにそのフィールドと同じ名前で Sig を返す関数を追加します。Sig<Counter, i32> は「シグナル構造体 Counter の中の i32」と読みます。
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");ブラウザは構造体を 1 つの JSON オブジェクトとして持ち、式も文も差分も Sig を通してそのフィールドを指します。打ち間違えたフィールド名には呼び出せる関数がないので、次のようにコンパイルが通りません。
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();Sig<S, T> の S はあるページのシグナルを別のページで使うことも防ぎます。Counter のページの要素は El<Counter> で、Counter のシグナルを使った式しか受け取りません。
フィールドの中のフィールド
フィールド自体を Signals の構造体にもできます。その中へは at でたどり、結果はほかと同じ Sig になります。パスの深さはフィールド 8 つまでです。
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);値や構造体の Vec はリストで、リスト で説明します。Option のフィールドには is_some、is_none、unwrap_or があります。
最初の描画
Page::new が初期値を受け取り、Page::render がその値ですべての束縛を評価します。テキスト、表示と非表示、クラス、属性、フォームの値、リストの項目は、ランタイムを読み込む前から HTML に入っています。ランタイムはそれらの要素を作り直さず、そのまま使います。データは <body> に置いたシグナル構造体の JSON なので、ブラウザはページを表示するために何も取得しません。
計算されるシグナル
Page::computed(signal, value) は、フィールドをほかのフィールドを使った式の値に合わせ続けます。束縛はそれぞれ式を評価する代わりに、ほかのフィールドと同じようにこの値を読み、値が変わったときだけ評価し直します。計算されるシグナルは宣言した順に評価されるので、先に宣言したものを読めます。
実行結果
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: "緑茶"
.into(),
price: 4.5,
},
quantity: 1,
total: 0.0,
};
Page::new(
"カート",
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("、単価 $")
.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("合計 $")
.child(span().bind_text(total.to_fixed(2))),
),
)
}