Sending a page in the order its parts are ready.
A render is synchronous: it needs every value before it starts. A page whose parts arrive at
different times (the book now, its reviews after a slow lookup) can still be sent as soon as
possible, with holes.
[`hole()`](crate::hole) is a [`Slot`](crate::Slot) whose content you write later. Render the whole
page once, with a hole in each slow slot, and cut the output with
[`split_holes`](crate::split_holes): there is one more piece than there were holes. Send the first
piece at once, then each hole's content when it is ready, each followed by the next piece.
```rust
# mod book_page {
# // @generated by ferrovue from components/BookPage.vue. Do not edit: change the `.vue` file and run
# // `ferrovue`.
#
# use std::borrow::Cow;
#
# use ferrovue as fv;
#
# /// The component's name, as `data-island` carries it.
# pub const NAME: &str = "BookPage";
#
# /// The props `BookPage.vue` declares.
# #[derive(Debug, Clone, serde::Serialize)]
# #[cfg_attr(test, derive(serde::Deserialize))]
# pub struct Props<'a> {
# #[serde(rename = "title")]
# pub title: Cow<'a, str>,
# }
#
# impl<'a> Props<'a> {
# /// Props with its required fields.
# pub fn new(title: impl Into<Cow<'a, str>>) -> Self {
# Props { title: title.into() }
# }
#
# }
#
#
# /// What a parent puts in the slots `BookPage.vue` renders.
# #[derive(Clone, Copy, Default)]
# pub struct Slots<'s> {
# /// `<slot name="reviews">`
# pub reviews: Option<fv::Slot<'s>>,
# }
#
# /// Write the component's server render into `out`.
# pub fn render(out: &mut String, props: &Props<'_>, fv_slots: Slots<'_>) {
# out.reserve(54 + props.title.len());
# out.push_str("<article><h1>");
# fv::escape_into(out, &props.title);
# out.push_str("</h1><section>");
# fv::slot_into(out, fv_slots.reviews, Some(&mut |out: &mut String| {
# out.push_str("loading");
# }));
# out.push_str("</section></article>");
# }
#
# /// The component's markup, for a maud page that shows it.
# pub fn html<'p, 'a>(props: &'p Props<'a>, fv_slots: Slots<'p>) -> fv::Html<'p, Props<'a>, impl Fn(&mut String, &Props<'a>) + 'p> {
# fv::Html::markup(props, move |out: &mut String, props: &Props<'a>| render(out, props, fv_slots))
# }
# }
// BookPage.vue: <article><h1>{{ title }}</h1><section><slot name="reviews">loading</slot></section></article>
let props = book_page::Props { title: "Dune".into() };
let mut page = String::from("<!doctype html><body>");
book_page::render(&mut page, &props, book_page::Slots { reviews: Some(ferrovue::hole()) });
page.push_str("</body>");
let pieces = ferrovue::split_holes(&page);
assert_eq!(
pieces,
[
"<!doctype html><body><article><h1>Dune</h1><section><!--[-->",
"<!--]--></section></article></body>",
]
);
// Stream: the first piece now, then the reviews when they arrive, then the rest.
let mut sent = Vec::new();
sent.push(pieces[0].to_owned());
sent.push(String::from("<ul><li>A classic.</li></ul>")); // the slow part, rendered when ready
sent.push(pieces[1].to_owned());
assert_eq!(
sent.concat(),
"<!doctype html><body><article><h1>Dune</h1><section><!--[--><ul><li>A classic.</li></ul><!--]--></section></article></body>"
);
```
A hole is content to its slot, so the slot's fallback never shows, and the fragment markers Vue
writes around slot content are already in place. What goes in a hole must be what the client's
template would render in that slot, usually another component's render or island.
With the `stream` feature, which `axum` and `actix-web` turn on, `HtmlStream` does this for a response: give it the whole
render and a future for each hole's content, and it sends each piece and each hole's content as
soon as they are ready, the futures all running at once. This is the handler from the repository's
`examples/fullstack` (`src/main.rs`), where `site.page` renders a page with its holes and
`site.fill` renders one hole's content once its data is ready:
```rust
# #[cfg(feature = "axum")]
# fn main() {
# use std::sync::Arc;
# use axum::http::{StatusCode, Uri};
# use axum::response::{IntoResponse, Response};
# struct Page { status: u16, html: String, holes: Vec<String> }
# struct Site;
# impl Site {
# fn page(&self, path: &str) -> Page {
# let mut html = String::from("<main>");
# ferrovue::escape_into(&mut html, path);
# ferrovue::hole().render_to(&mut html);
# html.push_str("</main>");
# Page { status: 404, html, holes: vec!["Dune".to_owned()] }
# }
# async fn fill(&self, hole: String) -> String {
# format!("<p>{hole}</p>")
# }
# }
# async fn page(site: Arc<Site>, uri: Uri) -> Response {
use ferrovue::HtmlStream;
let Page { status, html, holes } = site.page(uri.path());
let body = HtmlStream::new(html).holes(holes.into_iter().map(|hole| {
let site = Arc::clone(&site);
async move { site.fill(hole).await }
}));
let status = StatusCode::from_u16(status).unwrap_or(StatusCode::OK);
(status, body).into_response()
# }
# let runtime = tokio::runtime::Builder::new_current_thread().build().unwrap();
# let response = runtime.block_on(page(Arc::new(Site), Uri::from_static("/books")));
# assert_eq!(response.status(), 404);
# let body = runtime.block_on(axum::body::to_bytes(response.into_body(), usize::MAX)).unwrap();
# assert_eq!(body, "<main>/books<p>Dune</p></main>");
# }
# #[cfg(not(feature = "axum"))]
# fn main() {}
```
[`web_frameworks`](crate::guide::web_frameworks) has the rest: a whole page, other statuses,
actix-web, and other servers.
A hole is written as a marker, `<fv-hole"</script</style</noscript>`, which no value ferrovue
writes can hold: every interpolated value has its `<` escaped, the page head's attribute values
have their `"` escaped, and its `<script>`, `<style>` and `<noscript>` content has its closing tag
rewritten, so only a hole produces it, wherever data from readers goes. Do not write the marker in
content of your own, such as a slot closure, or the page will be cut there too.