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)));| On | Operations |
|---|---|
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. |
String | concat, trim, to_lowercase, to_uppercase, contains, starts_with, is_empty, char_count |
bool | and, or, ! |
| Ordered values | gt, ge, lt, le on i32, f64, and String |
| Any value | equals, not_equals, to_text for display, if_else(cond, then, otherwise) |
| Structs | at 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
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"),
),
),
)
}