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 axum, for example, the pieces and the holes' content become one streamed body. 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,ignore
use futures_util::{StreamExt, stream};
enum Part {
Ready(String),
Later(Hole),
}
let Page { status, pieces, holes } = site.page(uri.path());
let mut parts = Vec::with_capacity(pieces.len() + holes.len());
let mut holes = holes.into_iter();
for piece in pieces {
parts.push(Part::Ready(piece));
parts.extend(holes.next().map(Part::Later));
}
async move {
Ok::<_, Infallible>(match part {
Part::Ready(html) => html,
Part::Later(hole) => site.fill(&hole).await,
})
}
});
axum::body::Body::from_stream(body)
```
A hole is written as `<fv-hole>`. Every interpolated value has its `<` escaped, and no template
writes an element of that name, so only a hole produces it. Do not write the marker in content of
your own, such as a slot closure, or the page will be cut there too.