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 aview!body a runtime expression is written$(...), which lowers throughexpr!; the macro is rarely invoked directly. - impl_
surrogate - impl_
surrogate_ mut - impl_
surrogate_ ref
Structs§
- Bind
Attribute - Bool
Surrogate - Encoded
Signals - Erased
Procedure - Erased
Shard - Event
- Event
Handler - An event handler attribute. Emits a JavaScript closure expression into a
data-topcoat-on:<event>attribute on the element. The browser scanner wraps it innew Function('__cx', ...)to obtain a real handler. - Event
Target - Expr
- F64Surrogate
- Option
Surrogate - Procedure
- Procedure
Id - Procedure
Route - A
Routethat handles calls to one server procedure. - Procedure
Surrogate - Reactive
Scope - Reactive
Scope Id - Read
Signal - Result
Surrogate - Script
Props - Script
Props Builder - Typestate builder for
ScriptProps, created byScriptProps::builder().build()becomes available once every required property has been set. - ShardId
- Shard
Route - Signal
- Signal
Declaration - Signal
Id - Signal
Surrogate - StrSurrogate
- String
Surrogate - script
Constants§
Traits§
- Event
Handler Fn - Router
Builder Procedure Ext - Registers server procedures on a
RouterBuilder. - Router
Builder Shard Ext - Registers shards on a
RouterBuilder. - Signals
- Surrogate
- Surrogated
Type Aliases§
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.