日本語

When bindings and statements are not enough

Most pages need only bindings and statements. These cover focus, timers, and reactions to a change, links outside the router, and serving a page as a static file.

Node refs

A NodeRef names one element for a DOM call that no binding covers. Declare it as a const, attach it with El::node_ref, and use its statements: focus, blur, select, scroll_into_view, show_popover, and hide_popover. Each item of a list may carry the same name, and any other two elements with one name make Page::render panic, so a test that renders the page finds the second. Ids set with El::id from a &'static str are checked by the same rule, and a fixed id inside a list template panics on its own, because every item repeats it. An id built as a String may come from stored data, so two of them alike render. The reset button below stops the stopwatch and moves focus to the start button.

Result

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",
        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,
                        "Stop",
                        "Start",
                    )),
            )
            .child(
                button(ButtonVariant::Outline, ButtonSize::Default)
                    .on_click(running.set(false).then(tenths.set(0)).then(START.focus()))
                    .text("Reset"),
            ),
    )
}

Timers and reactions

  • on_interval(period, stmt) runs a statement while the element is in the document, and on_timeout(delay, stmt) runs one once.
  • on_load(stmt) runs once when the element enters the document.
  • effect(stmt) runs when the element enters the document and again whenever a signal the statement reads changes.
  • on_signal_change(sig, stmt) runs whenever the signal, or any field inside it, changes, whatever changed it.
  • Expr::now() reads the browser's clock, and Expr::random_below(n) returns a random integer.
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"));

Keyboard patterns

roving_focus moves focus between the items inside an element with the arrow keys, Home, and End, as a tab list or a menu needs. active_descendant keeps focus on a text field while the up and down arrows move through the options of a listbox, as a combobox needs. hypersig_ui builds its tabs, menus, listbox, and combobox with them.

El::href takes only a link built from a Route. A link to another site, or to a #fragment on the same page, goes through attr("href", url), which writes the string as it is and checks nothing.

Static pages

A page that calls no action needs no server. Page::runtime_src loads the runtime from another URL, and RUNTIME_JS holds its source to write next to the page. This site is built that way: a Rust program renders each page with hypersig and writes the files that GitHub Pages serves.

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.

Where hypersig does not fit

The server holds the state, and the browser runs only the operations hypersig provides and the scripts a page loads. It does not suit these applications:

  • An application whose main work runs inside a JavaScript library, such as a drawing tool or a spreadsheet. hypersig only passes values in and out of the library, as JavaScript libraries shows, and the state the library keeps is not among the page's signals.
  • An application that works without a connection to the server, such as one used offline.