Quick start
Build a counter that counts in the browser and resets on the server, in one Rust file.
Add the dependencies
Add hypersig to Cargo.toml with axum, serde, and tokio:
Cargo.toml
[dependencies]
hypersig = "0.2"
axum = "0.8"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }Write the server
Write src/main.rs. It holds the page, the handler that answers calls from the reset button, and the router that serves both.
src/main.rs
use axum::Router;
use hypersig::html::{button, div, span};
use hypersig::{Action, Page, Patches, Route, RouterExt, Signals};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
struct Counter {
count: i32,
}
hypersig::routes! {
fn pages(Router<()>);
const INDEX: Route = "/" => index;
}
const RESET: Action<Counter> = Action::post("/reset");
async fn index((): (), (): ()) -> Page<Counter> {
let count = Counter::count();
Page::new("Counter", Counter { count: 0 }).child(
div()
.child(button().on_click(count.set(count + 1)).text("+1"))
.child(span().bind_text(count.to_text()))
.child(button().on_click(RESET.call()).text("Reset on the server")),
)
}
async fn reset((): (), (): ()) -> Patches<Counter> {
Counter::count().patch(0).into()
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let app = pages(Router::new()).hypersig_runtime().action(RESET, reset);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
axum::serve(listener, app).await
}Run it
cargo runOpen http://127.0.0.1:3000. The +1 button counts in the browser without a request. The reset button calls /reset, and the handler's patch sets count back to 0. hypersig_runtime() serves the browser runtime from the crate, so no Node or JavaScript build is involved.
What each part does
#[derive(Signals)]onCounteraddsCounter::count(), aSig<Counter, i32>that names the field. A misspelled field is a compile error.count.set(count + 1)is a statement.on_clickwrites it into adata-hs-on-clickattribute as JSON, and the runtime runs it on each click.span().bind_text(count.to_text())keeps the text equal to the count.Page::renderevaluates it with the initial values, so the HTML already shows0.INDEXis aRoute, the page at/.routes!declares it together with its handlerindex, the functionpagesit writes serves it, and a link to it isINDEX.link(). Pages and links covers routes with parameters.RESETis anAction<Counter>. The page calls it withRESET.call()and the router serves it with.action(RESET, reset), so both sides share its path, its method, its argument type, and the page it belongs to.- The handler returns
Counter::count().patch(0). The server sends it as a Server-Sent Event, and the runtime writes the value into the signal, which rewrites the text.
Try it here
The counter below runs the browser side of the program on this page. It uses buttons from hypersig_ui, the component crate in the same repository.
Result
counter.rs
use hypersig::html::{div, span};
use hypersig::style::Style;
use hypersig::style::pattern::hstack;
use hypersig::style::value::{FontWeight, TextAlign, rem};
use hypersig::{Page, Signals};
use hypersig_ui::{ButtonSize, ButtonVariant, button};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
pub struct Counter {
count: i32,
}
pub fn page() -> Page<Counter> {
let count = Counter::count();
Page::new(
"Counter",
Counter { count: 0 },
)
.child(
div()
.css(hstack(rem(1)))
.child(
button(ButtonVariant::Outline, ButtonSize::Default)
.on_click(count.set(count - 1))
.text("-1"),
)
.child(
span()
.css(
Style::new()
.min_width(rem(3))
.text_align(TextAlign::Center)
.font_size(rem(1.5))
.font_weight(FontWeight::Number(600.0)),
)
.bind_text(count.to_text()),
)
.child(
button(ButtonVariant::Default, ButtonSize::Default)
.on_click(count.set(count + 1))
.text("+1"),
),
)
}This site is a set of static files on GitHub Pages, so its demos run on signals alone and none of them calls an action.
Next, Building the page shows how the HTML is built.