diff --git a/docs/.webui-press/config.json b/docs/.webui-press/config.json index ba615739..81cbb1ee 100644 --- a/docs/.webui-press/config.json +++ b/docs/.webui-press/config.json @@ -154,6 +154,10 @@ { "text": "WebAssembly", "link": "/guide/integrations/wasm" + }, + { + "text": "Embedded fragments", + "link": "/guide/integrations/fragments" } ] }, diff --git a/docs/guide/integrations/fragments.md b/docs/guide/integrations/fragments.md new file mode 100644 index 00000000..8bbfe2b7 --- /dev/null +++ b/docs/guide/integrations/fragments.md @@ -0,0 +1,109 @@ +# Embedded Fragment Rendering + +WebUI's integrations are designed around rendering a full page in one shot. There's a second, smaller story that the framework supports just as well but isn't yet called out in these docs: rendering a **single named fragment** from a host that is not itself a WebUI app. This page covers that. + +## When you'd want this + +- You have an existing app in another framework (Express + React, Tanstack Start, a Rust web server with a hand-written template engine, etc.) and want to adopt WebUI for one component - say, a citation group inside a chat stream - without rewriting the rest of the page. +- You're streaming HTML from a non-WebUI host and want to inject one rendered fragment into the stream as part of a larger response. + +Both of these are supported today via the `entry` option on the render call. The host stays in charge of the surrounding HTML; WebUI renders the named fragment and returns (or streams) just its bytes. + +## Recipe: Node host + +Use `renderStream` when you want the rendered fragment to interleave into an existing response stream, or `render` when you want the full string and you'll stitch it in yourself. + +```js +import { renderStream } from '@microsoft/webui'; +import { readFileSync } from 'node:fs'; + +const protocol = readFileSync('./dist/protocol.bin'); + +// Inside an existing request handler: +function streamCitationGroup(res, citations) { + res.write('
'); + renderStream( + protocol, + { citations }, + (chunk) => res.write(chunk), + { entry: 'citation-group.html' }, // <- the fragment to render + ); + res.write('
'); +} +``` + +`entry` is the fragment ID (the relative HTML filename inside `appDir` at build time). You can have many entries in one protocol and pick the one the host needs per call. Only the templates reachable from `entry` are walked; nothing else in the protocol is rendered. + +### How fragments are keyed + +The protocol stores two distinct maps and the embedded recipe only addresses the first: + +- `fragments: map` - keyed by the relative path of the source HTML file inside `appDir`. `appDir/index.html` becomes the `"index.html"` fragment; `appDir/widgets/citation-group.html` becomes the `"widgets/citation-group.html"` fragment. `entry` always selects from this map. +- `components: map` - keyed by tag name. Populated by the active parser plugin (for example, `fast-v3` registers each `` block under its `name` attribute). These are referenced *by* fragments via tag use; they cannot be the `entry` of a render call directly. + +In practice this means a FAST-3 host that wants to render exactly one custom element from a single embedded call needs a thin wrapper HTML file, not the `` block alone. For example, given `src/citation-group/citation-group.html` containing a `` block, also create a wrapper: + +```html + + +``` + +Build with `webui build src --plugin fast-v3 --out dist` and pass `entry: 'citation-group.html'` to `render` or `renderStream` (or `RenderOptions::new("citation-group.html", "/")` in Rust). The wrapper resolves the component reference, the plugin emits the hydration markers, and the host gets back the bytes for that one element. + +If you pass an `entry` that does not match a key in `fragments`, the call returns `HandlerError::MissingFragment(name)` in Rust and throws an analogous error from the Node API. You can list the keys in a built protocol with `webui inspect dist/protocol.bin`. + +If you'd rather get the rendered string back and inject it as a `${html}` substitution into your own template engine, swap `renderStream` for `render`: + +```js +import { render } from '@microsoft/webui'; + +const html = render(protocol, { citations }, { entry: 'citation-group.html' }); +// ... pass `html` to your existing template ... +``` + +## Recipe: Rust host + +The Rust integration exposes the same shape via `WebUIHandler::handle` plus a custom `ResponseWriter`. The writer is where you decide what to do with each chunk - write it to an `axum::body::Bytes` channel, push it onto a `Vec`, send it down a websocket frame, whatever the host expects. + +```rust +use std::{fs, sync::Arc}; +use webui::{HandlerResult, RenderOptions, ResponseWriter, WebUIHandler}; + +struct StringWriter(String); +impl ResponseWriter for StringWriter { + fn write(&mut self, content: &str) -> HandlerResult<()> { + self.0.push_str(content); + Ok(()) + } + fn end(&mut self) -> HandlerResult<()> { Ok(()) } +} + +fn render_citation_group( + handler: &WebUIHandler, + protocol: &[u8], + state: &serde_json::Value, +) -> String { + let mut writer = StringWriter(String::new()); + let options = RenderOptions::new("citation-group.html", "/"); + handler + .handle(protocol, state, options, &mut writer) + .expect("render failed"); + writer.0 +} +``` + +The handler is `Send + Sync` (see [Thread safety](./rust#thread-safety)), so the typical pattern is to construct it once at startup, wrap it in `Arc`, and call `handle` from any request task with a fresh writer. + +## Fragments and routing + +The `requestPath` argument is independent of `entry`. If your fragment contains a `` directive - for example, a "currently selected tab" pattern - pass the relevant path so the inner route matcher fires. If the fragment is route-free, pass `"/"`. Non-matching routes inside the fragment render hidden-and-empty exactly as they would in a full-page render. + +## What you don't get + +This recipe deliberately skips the things a full-page WebUI host gives you for free: + +- **The `webui-framework` client runtime.** If your fragment uses interactive components, you need to load that runtime in the host page yourself (`