式と文
式はシグナルから値を計算し、文はシグナルを変更します。どちらも Rust の値で、ブラウザは JSON として受け取ります。
式
Expr<S, T> は、ブラウザで評価すると T になります。Sig も式で、演算子やメソッドで大きな式を組み立てます。最初の HTML を作るときは、サーバーが同じ式を評価します。
use hypersig::html::p;
use hypersig::{Expr, Signals, if_else};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
struct Order {
price: f64,
quantity: i32,
coupon: String,
}
let price = Order::price();
let quantity = Order::quantity();
let coupon = Order::coupon();
let subtotal: Expr<Order, f64> = price * quantity.to_f64();
let discounted = coupon.trim().to_uppercase().equals("TEA10");
let total = if_else(discounted.clone(), subtotal.clone() * 0.9, subtotal);
let summary = p::<Order>()
.bind_text(total.to_fixed(2))
.show(quantity.gt(0).and(price.ge(0.0)));| 対象 | 操作 |
|---|---|
i32 と f64 | +、-、*、%、min、max、to_fixed。f64 では /、floor、ceil、round、log10、pow も使えます。to_f64 は i32 を f64 に変えます。format は数を NumberFormat に従い、桁を区切って前後に文字を付けたテキストにします。 |
String | concat、trim、to_lowercase、to_uppercase、contains、starts_with、is_empty、char_count |
bool | and、or、! |
| 順序のある値 | i32、f64、String の gt、ge、lt、le |
| 任意の値 | equals、not_equals、表示用の to_text、if_else(cond, then, otherwise) |
| 構造体 | フィールドを読む at、1 つのフィールドを置き換えた複製を作る with。let_in はどの値にも使え、値に一度だけ名前を付けて、それを使う式を作ります。 |
Expr::lit(&value) は Rust の値を定数の式に変えます。式を受け取るメソッドの多くは、&value をそのまま受け取れます。型の違う式はコンパイルエラーになり、メッセージにはその場所が求める型が出ます。
use hypersig::Signals;
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
struct Counter {
count: i32,
}
// error[E0277]: `&str` is not an expression of type `i32` on the page whose signals are `Counter`
let reset = Counter::count().set("zero");文
Stmt<S> は、イベントが起きたときにブラウザで実行されます。sig.set(value) は値を代入し、flag.toggle() は bool を反転し、then は文を順に実行します。Stmt::when と Stmt::if_else は条件で分岐します。
use hypersig::html::button;
use hypersig::{Signals, Stmt};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
struct Player {
score: i32,
lives: i32,
over: bool,
}
let score = Player::score();
let lives = Player::lives();
let over = Player::over();
let hit = lives
.set(lives - 1)
.then(Stmt::when(lives.le(1), over.set(true)));
let bonus = Stmt::if_else(over, Stmt::none(), score.set(score + 100));
let play = button::<Player>().on_click(bonus.then(hit)).text("Play");イベント
on_click、on_input、on_submit、on_change、on_blur は、それぞれのイベントで文を実行します。Event のほかのイベントには on(Event::Focus, ...) を使います。on_key は keydown を Key か、Key::Char('k').primary() のような Shortcut で絞り込みます。要素の中のテキスト欄で打つ文字は、その欄に任せます。Page::on_key は同じことをページ全体で行い、どこにもフォーカスがないときにも動きます。on_visible は要素がスクロールして見えるたびに実行し、リストの続きを読み込むのに使えます。on_debounced はイベントが指定した時間続けて起きなくなるまで待ちます。
bind は、テキスト欄、チェックボックス、数値の欄をシグナルと双方向に結び付けます。ラジオボタンと <select> には bind_radio と bind_select で同じことをします。
use std::time::Duration;
use hypersig::html::{Event, Key, form, input};
use hypersig::Signals;
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
struct Chat {
draft: String,
sent: i32,
typing: bool,
}
let draft = Chat::draft();
let sent = Chat::sent();
let composer = form::<Chat>()
// Runs after `preventDefault`, so the page does not reload.
.on_submit(sent.set(sent + 1).then(draft.set("")))
.child(
input()
// Two-way: typing writes `draft`, and setting `draft` rewrites the field.
.bind(draft)
.on_key(Key::Escape, draft.set(""))
.on(Event::Focus, Chat::typing().set(true))
.on_debounced(Event::Input, Duration::from_millis(500), Chat::typing().set(false)),
);下の変換器は 1 つの f64 から華氏の値と説明文を計算します。ボタンはそれぞれ 1 つの文です。
実行結果
use hypersig::html::{div, input, label, p, span};
use hypersig::style::Style;
use hypersig::style::pattern::{hstack, stack};
use hypersig::style::value::{BorderStyle, px, rem};
use hypersig::{Page, Signals, if_else};
use hypersig_ui::{ButtonSize, ButtonVariant, button, token};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
pub struct Weather {
celsius: f64,
}
pub fn page() -> Page<Weather> {
let celsius = Weather::celsius();
let fahrenheit = celsius * 1.8 + 32.0;
Page::new(
"気温",
Weather { celsius: 21.0 },
)
.child(
div()
.css(stack(rem(0.75)))
.child(
label()
.css(hstack(rem(0.5)))
.text("摂氏")
.child(
input().attr("type", "number").bind(celsius).css(
Style::new()
.width(rem(6))
.padding_inline(rem(0.5))
.border_width(px(1))
.border_style(BorderStyle::Solid)
.border_color(token::input().var())
.border_radius(px(6)),
),
),
)
.child(
p().text("華氏では ")
.child(span().bind_text(fahrenheit.to_fixed(1)))
.text(" °F で、")
.child(span().bind_text(if_else(
celsius.le(0.0),
"水が凍る温度です。",
"水が凍らない温度です。",
))),
)
.child(
div()
.css(hstack(rem(0.5)))
.child(
button(ButtonVariant::Outline, ButtonSize::Sm)
.on_click(celsius.set(celsius - 10.0))
.text("-10"),
)
.child(
button(ButtonVariant::Outline, ButtonSize::Sm)
.on_click(celsius.set(celsius + 10.0))
.text("+10"),
)
.child(
button(ButtonVariant::Ghost, ButtonSize::Sm)
.on_click(celsius.set(0.0))
.text("リセット"),
),
),
)
}