English

束縛と文で足りないとき

たいていのページは束縛と文だけで書けます。ここで扱うのは、フォーカス、タイマー、変化への反応、ルーターの外へのリンク、ページを静的ファイルとして配信する方法です。

ノード参照

NodeRef は、束縛では扱えない DOM の呼び出しのために要素を 1 つ指す名前です。const として宣言し、El::node_ref で要素に付け、focus、blur、select、scroll_into_view、show_popover、hide_popover の文を使います。リストの項目ごとに同じ名前を付けるのはかまいませんが、それ以外で 2 つの要素に同じ名前を付けると Page::render が panic するので、ページを描画するテストで見つかります。El::id に &'static str で付けた id も同じ規則で検査し、リストのテンプレートの中の固定の id は、項目ごとに同じ id が並ぶので、それだけで panic します。String で作った id は保存したデータから作られることがあるので、2 つ同じでもそのまま描画します。下のリセットボタンはストップウォッチを止め、フォーカスをスタートボタンに移します。

実行結果

stopwatch.rs
use std::time::Duration;

use hypersig::html::{div, span};
use hypersig::style::Style;
use hypersig::style::pattern::hstack;
use hypersig::style::value::{FontWeight, rem};
use hypersig::{NodeRef, Page, Signals, Stmt, if_else};
use hypersig_ui::{ButtonSize, ButtonVariant, button};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
pub struct Stopwatch {
    running: bool,
    tenths: i32,
}

const START: NodeRef<Stopwatch> = NodeRef::new("start");

pub fn page() -> Page<Stopwatch> {
    let running = Stopwatch::running();
    let tenths = Stopwatch::tenths();
    let seconds = tenths.to_f64() / 10.0;
    Page::new(
        "ストップウォッチ",
        Stopwatch {
            running: false,
            tenths: 0,
        },
    )
    .child(
        div()
            .css(hstack(rem(1)))
            .on_interval(
                Duration::from_millis(100),
                Stmt::when(running, tenths.set(tenths + 1)),
            )
            .child(
                span()
                    .css(
                        Style::new()
                            .min_width(rem(4))
                            .font_size(rem(1.5))
                            .font_weight(FontWeight::Number(600.0)),
                    )
                    .bind_text(seconds.to_fixed(1)),
            )
            .child(
                button(ButtonVariant::Default, ButtonSize::Default)
                    .node_ref(START)
                    .on_click(running.toggle())
                    .bind_text(if_else(
                        running,
                        "ストップ",
                        "スタート",
                    )),
            )
            .child(
                button(ButtonVariant::Outline, ButtonSize::Default)
                    .on_click(running.set(false).then(tenths.set(0)).then(START.focus()))
                    .text("リセット"),
            ),
    )
}

タイマーと変化への反応

  • on_interval(period, stmt) は要素が文書にある間、文を繰り返し実行し、on_timeout(delay, stmt) は文を 1 回だけ実行します。
  • on_load(stmt) は、要素が文書に入ったときに 1 回実行します。
  • effect(stmt) は、要素が文書に入ったときと、文が読むシグナルが変わるたびに実行します。
  • on_signal_change(sig, stmt) は、シグナルかその中のフィールドが変わるたびに、何が変えたかにかかわらず実行します。
  • Expr::now() はブラウザの時計を読み、Expr::random_below(n) はランダムな整数を返します。
use std::time::Duration;

use hypersig::html::{div, input, p};
use hypersig::{Expr, NodeRef, Signals};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
struct Editor {
    text: String,
    saved_at: f64,
    edits: i32,
    dirty: bool,
}

const FIELD: NodeRef<Editor> = NodeRef::new("editor-text");

let editor = div::<Editor>()
    // Focus the field when the page loads.
    .on_load(FIELD.focus())
    // Count every change to `text`, however it was made.
    .on_signal_change(
        Editor::text(),
        Editor::edits().set(Editor::edits() + 1).then(Editor::dirty().set(true)),
    )
    // Every 30 seconds, note the time of the save.
    .on_interval(
        Duration::from_secs(30),
        Editor::saved_at().set(Expr::now()).then(Editor::dirty().set(false)),
    )
    .child(input().node_ref(FIELD).bind(Editor::text()))
    .child(p().show(Editor::dirty()).text("Unsaved changes"));

キーボード操作のパターン

roving_focus は、タブの一覧やメニューに要るように、矢印キーと Home、End で要素の中の項目間にフォーカスを動かします。active_descendant は、コンボボックスに要るように、フォーカスをテキスト欄に置いたまま上下の矢印キーでリストボックスの選択肢を移動させます。hypersig_ui のタブ、メニュー、リストボックス、コンボボックスは、この 2 つを使って作っています。

El::href が受け取るのは Route から作ったリンクだけです。ほかのサイトや、同じページの #fragment へのリンクは attr("href", url) で書きます。attr は文字列をそのまま書き、何も検査しません。

静的なページ

アクションを呼び出さないページにはサーバーが要りません。Page::runtime_src は別の URL からランタイムを読み込み、RUNTIME_JS はページの隣に書き出すランタイムのソースを持ちます。このサイトもその方法で作っています。Rust のプログラムが hypersig で各ページを描画し、GitHub Pages が配信するファイルを書き出します。

use hypersig::html::{button, span};
use hypersig::{Page, RUNTIME_JS, Signals};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
struct Counter {
    count: i32,
}

let count = Counter::count();
let html = Page::new("Counter", Counter { count: 0 })
    // Load the runtime from a file next to the page.
    .runtime_src("./hypersig.js")
    .child(button().on_click(count.set(count + 1)).text("+1"))
    .child(span().bind_text(count.to_text()))
    .render();

// Write `html` to index.html and RUNTIME_JS to hypersig.js, then serve both.

hypersig が向かない場面

状態はサーバーが持ち、ブラウザは hypersig が用意した操作と、ページが読み込んだスクリプトしか実行しません。そのため、次のようなアプリケーションには向きません。

  • お絵かきツールや表計算ソフトのように、主な処理が JavaScript のライブラリの中で動くアプリケーション。hypersig は JavaScript のライブラリ で示すようにライブラリと値をやり取りするだけで、ライブラリが持つ状態はページのシグナルに入りません。
  • オフラインで使うものなど、サーバーにつながらなくても動くアプリケーション。