English

式と文

式はシグナルから値を計算し、文はシグナルを変更します。どちらも 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 に従い、桁を区切って前後に文字を付けたテキストにします。
Stringconcat、trim、to_lowercase、to_uppercase、contains、starts_with、is_empty、char_count
booland、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 つの文です。

実行結果

temperature.rs
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("リセット"),
                    ),
            ),
    )
}
ブラウザが実行できるのは hypersig が用意した操作だけです。日付の整形や JavaScript ライブラリの呼び出しのような、それ以外の処理はアクションでサーバーに任せることになり、結果はネットワークの往復 1 回分だけ遅れて返ります。