日本語

Expressions and statements

An expression computes a value from the signals, and a statement changes them. Both are Rust values that the browser receives as JSON.

Expressions

An Expr<S, T> evaluates to a T in the browser. A Sig is an expression, and the operators and methods build larger ones. The server evaluates the same expressions for the first HTML.

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

#[derive(Signals, Serialize, Deserialize)]
struct Order {
    price: f64,
    quantity: i32,
    coupon: String,
}

let price = Order::price();
let quantity = Order::quantity();
let coupon = Order::coupon();

let subtotal: Expr<Order, f64> = price * quantity.to_f64();
let discounted = coupon.trim().to_uppercase().equals("TEA10");
let total = if_else(discounted.clone(), subtotal.clone() * 0.9, subtotal);

let summary = p::<Order>()
    .bind_text(total.to_fixed(2))
    .show(quantity.gt(0).and(price.ge(0.0)));
OnOperations
i32 and f64+, -, *, %, min, max, and to_fixed. f64 adds /, floor, ceil, round, log10, and pow. to_f64 turns an i32 into an f64, and format writes a number in a NumberFormat, with grouped digits and affixes.
Stringconcat, trim, to_lowercase, to_uppercase, contains, starts_with, is_empty, char_count
booland, or, !
Ordered valuesgt, ge, lt, le on i32, f64, and String
Any valueequals, not_equals, to_text for display, if_else(cond, then, otherwise)
Structsat for a field, with for a copy with one field replaced. let_in names any value once for the expression built from it.

Expr::lit(&value) turns a Rust value into a constant expression, and most methods accept a plain &value where they take an expression. An expression of the wrong type is a compile error, with a message that names the type the place expects:

use hypersig::Signals;
use serde::{Deserialize, Serialize};

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

// error[E0277]: `&str` is not an expression of type `i32` on the page whose signals are `Counter`
let reset = Counter::count().set("zero");

Statements

A Stmt<S> runs in the browser on an event. sig.set(value) assigns, flag.toggle() flips a bool, and then runs one statement after another. Stmt::when and Stmt::if_else branch on a condition.

use hypersig::html::button;
use hypersig::{Signals, Stmt};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
struct Player {
    score: i32,
    lives: i32,
    over: bool,
}

let score = Player::score();
let lives = Player::lives();
let over = Player::over();

let hit = lives
    .set(lives - 1)
    .then(Stmt::when(lives.le(1), over.set(true)));
let bonus = Stmt::if_else(over, Stmt::none(), score.set(score + 100));

let play = button::<Player>().on_click(bonus.then(hit)).text("Play");

Events

on_click, on_input, on_submit, on_change, and on_blur run a statement on their event, and on(Event::Focus, ...) covers the rest of Event. on_key filters keydown by a Key or a Shortcut such as Key::Char('k').primary(), and leaves the characters a text field inside the element types to the field. Page::on_key does the same for the whole page, even while nothing has focus. on_visible runs each time the element scrolls into view, which loads the next page of a list. on_debounced waits until the event has stopped firing for a given time.

bind ties a text field, a checkbox, or a number field to a signal in both directions. bind_radio and bind_select do the same for radio buttons and a <select>.

use std::time::Duration;

use hypersig::html::{Event, Key, form, input};
use hypersig::Signals;
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
struct Chat {
    draft: String,
    sent: i32,
    typing: bool,
}

let draft = Chat::draft();
let sent = Chat::sent();

let composer = form::<Chat>()
    // Runs after `preventDefault`, so the page does not reload.
    .on_submit(sent.set(sent + 1).then(draft.set("")))
    .child(
        input()
            // Two-way: typing writes `draft`, and setting `draft` rewrites the field.
            .bind(draft)
            .on_key(Key::Escape, draft.set(""))
            .on(Event::Focus, Chat::typing().set(true))
            .on_debounced(Event::Input, Duration::from_millis(500), Chat::typing().set(false)),
    );

The converter below computes Fahrenheit and a description from one f64, and each button is one statement.

Result

temperature.rs
use hypersig::html::{div, input, label, p, span};
use hypersig::style::Style;
use hypersig::style::pattern::{hstack, stack};
use hypersig::style::value::{BorderStyle, px, rem};
use hypersig::{Page, Signals, if_else};
use hypersig_ui::{ButtonSize, ButtonVariant, button, token};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
pub struct Weather {
    celsius: f64,
}

pub fn page() -> Page<Weather> {
    let celsius = Weather::celsius();
    let fahrenheit = celsius * 1.8 + 32.0;
    Page::new(
        "Temperature",
        Weather { celsius: 21.0 },
    )
    .child(
        div()
            .css(stack(rem(0.75)))
            .child(
                label()
                    .css(hstack(rem(0.5)))
                    .text("Celsius")
                    .child(
                        input().attr("type", "number").bind(celsius).css(
                            Style::new()
                                .width(rem(6))
                                .padding_inline(rem(0.5))
                                .border_width(px(1))
                                .border_style(BorderStyle::Solid)
                                .border_color(token::input().var())
                                .border_radius(px(6)),
                        ),
                    ),
            )
            .child(
                p().text("That is ")
                .child(span().bind_text(fahrenheit.to_fixed(1)))
                .text(" °F, ")
                .child(span().bind_text(if_else(
                    celsius.le(0.0),
                    "freezing.",
                    "above freezing.",
                ))),
            )
            .child(
                div()
                    .css(hstack(rem(0.5)))
                    .child(
                        button(ButtonVariant::Outline, ButtonSize::Sm)
                            .on_click(celsius.set(celsius - 10.0))
                            .text("-10"),
                    )
                    .child(
                        button(ButtonVariant::Outline, ButtonSize::Sm)
                            .on_click(celsius.set(celsius + 10.0))
                            .text("+10"),
                    )
                    .child(
                        button(ButtonVariant::Ghost, ButtonSize::Sm)
                            .on_click(celsius.set(0.0))
                            .text("Reset"),
                    ),
            ),
    )
}
The browser runs only the operations hypersig provides. Anything else, such as formatting a date or calling a JavaScript library, goes through an action to the server, and its result comes back one network round trip later.