日本語

Building the page

A page is a tree of El values built with Rust functions and rendered to HTML on the server.

Elements

The html module has one function per common tag, such as div(), p(), and button(), and el("tag") for any other. Each returns an El<S>, where S is the page's signal struct, and its methods return the element, so a tree is one expression.

use hypersig::Route;
use hypersig::html::{a, div, h1, p};

hypersig::routes! {
    fn pages(Router<()>);
    const ADA: Route = "/ada" => ada;
}

let card = div::<Profile>()
    .id("ada")
    .child(h1().text("Ada Lovelace"))
    .child(
        p().text("Wrote the first program. ")
            .child(a().href(ADA.link()).text("Read more")),
    );

assert_eq!(
    card.render(),
    r#"<div id="ada"><h1>Ada Lovelace</h1><p>Wrote the first program. <a href="/ada">Read more</a></p></div>"#,
);

text appends static text, and child appends an element or a string. href points a link at a page from a Route, which Pages and links covers. attr sets a plain attribute. id and class have their own methods, and attr panics on them and on names starting with data-hs-, which only the typed methods write.

The page

Page wraps the tree in a full document. Page::new takes the title and the initial values of the signals, child appends to <body>, and render returns the HTML a handler sends. The document loads the runtime from RUNTIME_PATH, a path under /_hypersig/ that names a hash of the file, and RouterExt::hypersig_runtime serves it there.

Values that follow the signals

An element can show a value that changes in the browser. Each binding takes an expression over the signals and is rendered with the initial values first, so the HTML shows what the runtime would compute.

use hypersig::html::{button, p, span};
use hypersig::{Signals, if_else};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
struct Upload {
    progress: i32,
    busy: bool,
}

let progress = Upload::progress();
let busy = Upload::busy();

// Text that follows a signal.
let label = span::<Upload>().bind_text(progress.to_text().concat(" %"));
// An attribute that follows a signal, and one that is present while a flag is true.
let cancel = button::<Upload>()
    .bind_attr("aria-label", if_else(busy, "Cancel the upload", "Nothing to cancel"))
    .attr_when("disabled", !busy)
    .text("Cancel");
// An element shown only while the upload runs.
let notice = p::<Upload>().show(busy).text("Uploading…");
  • bind_text replaces the element's text.
  • bind_attr keeps an attribute equal to a string expression, and attr_when adds a boolean attribute such as disabled while a condition holds.
  • show hides the element while a condition is false. when(cond, el) removes it instead, so its on_load runs again and its inputs start empty each time it returns.
  • class_when and css_when add a class or a style while a condition holds. Styling covers them.

The demo binds a text field to name and shows one of two paragraphs depending on whether it is empty.

Result

greeting.rs
use hypersig::html::{div, p, span};
use hypersig::style::Style;
use hypersig::style::pattern::stack;
use hypersig::style::value::rem;
use hypersig::{Page, Signals};
use hypersig_ui::TextField;
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
pub struct Greeting {
    name: String,
}

pub fn page() -> Page<Greeting> {
    let name = Greeting::name();
    let typed = !name.trim().is_empty();
    Page::new(
        "Greeting",
        Greeting {
            name: String::new(),
        },
    )
    .child(
        div()
            .css(stack(rem(1)).max_width(rem(20)))
            .child(TextField::new(
                "Your name",
                name,
            ))
            .child(p().show(!typed.clone()).text("Type your name above."))
            .child(
                p().show(typed)
                    .text("Hello, ")
                    .child(span().bind_text(name.trim()))
                    .text("!"),
            )
            .child(
                p().css(Style::new().font_size(rem(0.875)))
                    .show(name.char_count().gt(12))
                    .text("That is a long name."),
            ),
    )
}

Components

A component is a Rust function that returns an El<S>. Make it generic over S and take the signals it reads as arguments, and it works on any page.

use hypersig::html::{El, input, label, span};
use hypersig::{Sig, Signals};
use serde::{Deserialize, Serialize};

/// A labeled text field bound to `value`, on any page.
fn field<S>(text: &str, value: Sig<S, String>) -> El<S> {
    label()
        .child(span().text(text))
        .child(input().bind(value))
}

#[derive(Signals, Serialize, Deserialize)]
struct Signup {
    name: String,
    email: String,
}

let form = hypersig::html::form::<Signup>()
    .child(field("Name", Signup::name()))
    .child(field("Email", Signup::email()));

hypersig_ui is a set of such functions styled after shadcn/ui. The demos on this site use its buttons and text fields.