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
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, andon_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, andExpr::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.
Links the router does not serve
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.