Skip to main content

Module runtime

Module runtime 

Source
Available on crate feature runtime only.
Expand description

Topcoat’s runtime makes server-rendered pages interactive without a wasm bundle, a client build step, or a separate frontend. Reactive state and expressions are written inline in view!, type-checked as ordinary Rust, and compiled to JavaScript that ships with the page.

The runtime is highly experimental and fairly limited today: expressions support only a small vocabulary of types and methods, and many patterns have no ergonomic answer yet. It will improve in future releases; expect both additions and breaking changes.

§Setup

Interactive pages need the runtime’s browser script. script() renders the script tag; include it in your document head:

view! {
    <html>
        <head>
            topcoat::runtime::script()
        </head>
        <body></body>
    </html>
}

The script is served as a Topcoat asset, so the asset bundle must be loaded on the router:

use topcoat::{
    asset::{AssetBundle, RouterBuilderAssetExt},
    router::{Router, RouterBuilderDiscoverExt},
};

pub fn router() -> Router {
    Router::builder()
        .discover()
        .assets(AssetBundle::load().unwrap())
        .build()
}

.discover() also registers the server endpoints behind procedures and shards, covered later in this guide.

§Runtime expressions

A $(...) block is a runtime expression and can stand wherever a view node can:

view! {
    <p>"The answer: " $(1.0 + 2.0)</p>
}

The expression is type-checked Rust, but it is compiled twice: the server evaluates it once for the initial HTML, and an equivalent JavaScript translation ships with the page, where it can run again without any help from the server.

Because a runtime expression must behave identically in both languages, only a subset of Rust is supported: a small vocabulary of types and methods. $(...) is syntactic sugar for the expr! macro, which documents that vocabulary, how captured variables behave, and the raw! escape hatch to hand-written JavaScript.

So far the browser has no reason to run 1.0 + 2.0 a second time; the answer stays 3. Expressions become useful when they read state that changes: signals.

§Signals

A signal is a piece of state that lives in the browser. Declare one with a signal statement inside a view! body, and read it in a runtime expression with .get():

view! {
    signal count = 0.0;

    <p>"Count: " $(count.get())</p>
}

The signal’s initial value is an ordinary Rust expression, evaluated once during the server render and serialized into the page; the browser picks it up as reactive state.

In the browser, a runtime expression re-runs whenever a signal it read changes – the text above updates the moment count does, with no server round-trip. Signals can only be read (using .get()) and modified (using .set(...)) inside expressions. Nothing changes count yet, though; that is what event handlers are for.

§Event handlers

An attribute starting with @ attaches an event handler: @click, @input, or any other DOM event name. Its value is a runtime expression evaluating to a closure, which runs in the browser each time the event fires. Handlers are where signals change:

view! {
    signal count = 0.0;

    <button @click=$(|_e| count.set(count.get() + 1.0))>"+1"</button>
    <p>"Count: " $(count.get())</p>
}

Clicking the button runs the closure, the closure updates the signal, and the $(count.get()) text re-renders. The entire loop happens in the browser.

The closure receives an Event mirroring the DOM event: fields like e.target.value, e.key, and e.client_x, and methods like e.prevent_default(). A typical input handler forwards the element’s value into a signal:

view! {
    signal query = String::new();

    <input @input=$(|e: Event| query.set(e.target.value))>
}

For the rare event logic the expression vocabulary cannot say, the value can also be a string literal of raw JavaScript: @click="alert('hi')".

§Bind attributes

An attribute starting with : is a bind attribute: its value is a runtime expression, and the attribute is kept in sync with it. The server renders the initial value like a normal attribute; the browser re-applies it whenever a signal the expression reads changes:

view! {
    signal open = false;

    <button @click=$(|_e| open.set(!open.get()))>"What is Topcoat?"</button>
    <p :hidden=$(!open.get())>"A fullstack Rust framework."</p>
}

Combining a bind attribute with an event handler syncs an element and a signal in both directions: :value keeps the input showing the signal, and @input writes every keystroke back into it:

view! {
    signal name = String::new();

    <input
        :value=$(name.get())
        @input=$(|e: Event| name.set(e.target.value))
    >

    <p>"Hello, " $(name.get()) "!"</p>
}

§Procedures

Runtime expressions run in the browser, so they cannot query the database or use Rust beyond the shared vocabulary. When an event handler needs the server, it calls a procedure: an async server function invoked from a runtime expression like any other async function:

#[procedure]
async fn double(value: f64) -> Result<f64> {
    Ok(value * 2.0)
}

view! {
    signal count = 1.0;

    <button @click=$(async |_e| {
        let doubled = double(count.get()).await;
        count.set(doubled);
    })>
        "double it"
    </button>
}

The call is an HTTP request under the hood: the arguments travel to the server, the function runs there, and the .await resolves to its result. That also means every procedure is exposed as an API endpoint from your server; anyone can call it with any arguments, so inputs can be spoofed and must not be trusted. See #[procedure] for the details: argument and return types, the cx parameter, error handling, and registration.

§Shards

When it is the markup itself that needs the server – fresh search results as the user types – use a shard: a component that re-renders on the server whenever one of its arguments changes. Arguments are runtime expressions; the browser sends their current values to the server and swaps the returned HTML in place:

#[shard]
async fn search_results(cx: &Cx, query: String) -> Result {
    let products = search_products(cx, &query).await?;
    view! {
        for product in products {
            <div>(product)</div>
        }
    }
}

view! {
    signal query = String::new();

    <input @input=$(|e: Event| query.set(e.target.value))>

    search_results(query: $(query.get()))
}

A shard body is ordinary server code, like any component. The re-renders are served by an API endpoint exposed from your server, so a shard’s arguments can be spoofed just like a procedure’s and must not be trusted. See #[shard] for the details: how re-renders behave, shard state, and registration.

Macros§

expr
The expr! macro compiles a single Rust expression twice: into ordinary Rust that runs on the server during the initial render, and into equivalent JavaScript that ships with the page and re-runs in the browser. Inside a view! body a runtime expression is written $(...), which lowers through expr!; the macro is rarely invoked directly.
impl_surrogate
impl_surrogate_mut
impl_surrogate_ref

Structs§

BindAttribute
BoolSurrogate
EncodedSignals
ErasedProcedure
ErasedShard
Event
EventHandler
An event handler attribute. Emits a JavaScript closure expression into a data-topcoat-on:<event> attribute on the element. The browser scanner wraps it in new Function('__cx', ...) to obtain a real handler.
EventTarget
Expr
F64Surrogate
OptionSurrogate
Procedure
ProcedureId
ProcedureRoute
A Route that handles calls to one server procedure.
ProcedureSurrogate
ReactiveScope
ReactiveScopeId
ReadSignal
ResultSurrogate
ScriptProps
ScriptPropsBuilder
Typestate builder for ScriptProps, created by ScriptProps::builder(). build() becomes available once every required property has been set.
ShardId
ShardRoute
Signal
SignalDeclaration
SignalId
SignalSurrogate
StrSurrogate
StringSurrogate
script

Constants§

SCRIPT

Traits§

EventHandlerFn
RouterBuilderProcedureExt
Registers server procedures on a RouterBuilder.
RouterBuilderShardExt
Registers shards on a RouterBuilder.
Signals
Surrogate
Surrogated

Type Aliases§

ProcedureHandlerFn
ShardRenderFn

Attribute Macros§

procedure
A procedure is an async server function that the browser can call from inside a runtime expression. Use procedures to run more complex Rust codes that are not supported by runtime expressions, or to use server-only resources like the database. Procedures are exposed as HTTP API endpoints from your server; parameters can be spoofed and must not be trusted.
shard
A shard is a special type of component that can re-run whenever its arguments change in the browser. Arguments are runtime expressions: the browser tracks the signals they read, and when one changes it requests a fresh render from the server and swaps the result into the DOM. Shards are exposed as API endpoints from your server; arguments must not be trusted.