topcoat 0.11.0

A modular, batteries-included Rust web framework for server-rendered apps.
Documentation
[Datastar](https://data-star.dev) updates HTML and reactive signals from server responses. Use `data-*` attributes to bind signals to elements and actions such as `@get` to send requests.

Topcoat extracts signals from requests and creates Datastar responses. Return one update from a handler or send several over a [server-sent event stream](crate::router::content::sse).

Everything below is re-exported from `topcoat::datastar` and gated behind the `datastar` feature, which also enables the router's `sse` feature.

```toml
# Cargo.toml
[dependencies]
topcoat = { version = "0.11.0", features = ["datastar"] }
```

# Loading the Datastar script

Load the Datastar script before using its attributes. This example bundles the script as a Topcoat asset:

```rust
use topcoat::{
    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](crate::asset) 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.

```rust
// src/app/increment.rs
use serde::{Deserialize, Serialize};
use topcoat::{
    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`](PatchElements::selector) and [`mode`](PatchElements::mode) to choose another target or update behavior. Returned from a handler, the patch sends one SSE event and closes the stream:

```rust
// src/app/entries.rs
use topcoat::{
    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`](PatchSignals::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](crate::router::content::sse) for keep-alives and handling reconnections.

```rust
// src/app/progress.rs
use futures_core::Stream;
use futures_util::stream;
use serde::Serialize;
use topcoat::{
    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.

```rust
use topcoat::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:

```rust
// src/app/save.rs
use topcoat::{
    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 [`topcoat::datastar::header`](crate::datastar::header), for when you want to read or write a header directly.

[`Sse`]: crate::router::content::sse::Sse
[`Event`]: crate::router::content::sse::Event
[`IntoResponseParts`]: crate::router::response::IntoResponseParts