束縛と文で足りないとき
たいていのページは束縛と文だけで書けます。ここで扱うのは、フォーカス、タイマー、変化への反応、ルーターの外へのリンク、ページを静的ファイルとして配信する方法です。
ノード参照
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 つ同じでもそのまま描画します。下のリセットボタンはストップウォッチを止め、フォーカスをスタートボタンに移します。
実行結果
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 のライブラリ で示すようにライブラリと値をやり取りするだけで、ライブラリが持つ状態はページのシグナルに入りません。
- オフラインで使うものなど、サーバーにつながらなくても動くアプリケーション。