Datastar
Datastar updates HTML and reactive signals from server responses. Use data-* attributes to bind signals to elements and actions such as @get to send requests.
Hearth extracts signals from requests and creates Datastar responses. Return one update from a handler or send several over a server-sent event stream.
Everything below is re-exported from hearth::datastar and gated behind the datastar feature, which also enables the router's sse feature.
# Cargo.toml
[dependencies]
hearth = { version = "0.1.0", features = ["datastar"] }
Loading the Datastar script
Load the Datastar script before using its attributes. This example bundles the script as a Hearth asset:
use hearth::{
Result,
asset::asset,
router::{Slot, layout},
view::{View, view},
};
#[layout]
async fn root(slot: Slot<'_>) -> Result<impl View> {
Ok(view! {
<!DOCTYPE html>
<html>
<head>
<script
type="module"
src=(asset!("https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.2/bundles/datastar.js"))
></script>
</head>
<body>(slot)</body>
</html>
})
}
See the assets guide for loading the asset bundle on your router.
Reading signals
The Signals extractor deserializes request signals into your type. It reads JSON from the datastar query parameter for GET requests and from the body for other methods.
// src/app/increment.rs
use serde::{Deserialize, Serialize};
use hearth::{
Result,
datastar::{PatchSignals, Signals},
router::route,
};
#[derive(Deserialize, Serialize)]
struct Counter {
count: u64,
}
#[route(POST)]
async fn increment(Signals(counter): Signals<Counter>) -> Result<PatchSignals> {
PatchSignals::json(&Counter {
count: counter.count + 1,
})
}
Use Option<Signals<T>> to also accept requests without Datastar. It returns None unless Datastar-Request is true. A Datastar request with malformed signals still returns an error. Use datastar_request to check the header directly.
Patching elements
PatchElements updates page elements with HTML. By default, Datastar matches elements by id and morphs them to match the response. Use selector and mode to choose another target or update behavior. Returned from a handler, the patch sends one SSE event and closes the stream:
// src/app/entries.rs
use hearth::{
Result,
context::Cx,
datastar::{ElementPatchMode, PatchElements},
router::route,
view::{ViewExt, view},
};
#[route(POST)]
async fn create(cx: &Cx) -> Result<PatchElements> {
let entry = view! { cx => <li>"A new entry"</li> }.single().await?;
Ok(PatchElements::new(entry.render(cx))
.selector("#feed")
.mode(ElementPatchMode::Prepend))
}
PatchElements::remove deletes elements matching a selector.
Patching signals
PatchSignals merges a JSON object into the browser's signals. Set a value to null to remove it, or use only_if_missing to preserve existing values. PatchSignals::json serializes the object from a Serialize value.
Streaming events
For live updates, return an Sse stream and convert each patch into an Event with Into. See the server-sent events guide for keep-alives and handling reconnections.
// src/app/progress.rs
use futures_core::Stream;
use futures_util::stream;
use serde::Serialize;
use hearth::{
Result,
datastar::PatchSignals,
router::{
content::sse::{Event, KeepAlive, Sse},
route,
},
};
#[derive(Serialize)]
struct Progress {
percent: u8,
}
#[route(GET)]
async fn progress() -> Result<Sse<impl Stream<Item = Result<Event>> + use<>>> {
let events = stream::iter((0..=100u8).step_by(20).map(|percent| {
PatchSignals::json(&Progress { percent }).map(Into::into)
}));
Ok(Sse::new(events).keep_alive(KeepAlive::new()))
}
On the page, data-on:load="@get('/progress')" subscribes to the stream and applies each event as it arrives.
Executing scripts
ExecuteScript appends a <script> element to the page. By default, the element removes itself after running.
use hearth::datastar::ExecuteScript;
let script = ExecuteScript::new("console.log('saved')");
Plain responses
Datastar also accepts ordinary responses. It patches text/html into the page and merges application/json into the signals. Response types that implement IntoResponseParts set headers to control these updates. Place them before the body in a response tuple:
// src/app/save.rs
use hearth::{
Result,
context::Cx,
datastar::{DatastarMode, DatastarSelector, ElementPatchMode},
router::route,
view::{ViewExt, ViewHandle, view},
};
#[route(POST)]
async fn save(cx: &Cx) -> Result<(DatastarSelector, DatastarMode, ViewHandle)> {
let status = view! { cx => <p>"Saved!"</p> }.single().await?;
Ok((
DatastarSelector::from("#status"),
DatastarMode(ElementPatchMode::Inner),
status,
))
}
Header constants
The raw datastar-* header names are available as HeaderName constants in hearth::datastar::header, for when you want to read or write a header directly.