diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs
index f48168f9e..7cae98cec 100644
--- a/packages/yew/src/html/component/scope.rs
+++ b/packages/yew/src/html/component/scope.rs
@@ -204,8 +204,15 @@ mod feat_ssr {
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
};
+ use crate::virtual_dom::Collectable;
+
impl Scope {
- pub(crate) async fn render_to_string(self, w: &mut String, props: Rc) {
+ pub(crate) async fn render_to_string(
+ self,
+ w: &mut String,
+ props: Rc,
+ hydratable: bool,
+ ) {
let (tx, rx) = oneshot::channel();
let state = ComponentRenderState::Ssr { sender: Some(tx) };
@@ -222,10 +229,24 @@ mod feat_ssr {
);
scheduler::start();
+ #[cfg(debug_assertions)]
+ let collectable = Collectable::Component(std::any::type_name::());
+
+ #[cfg(not(debug_assertions))]
+ let collectable = Collectable::Component;
+
+ if hydratable {
+ collectable.write_open_tag(w);
+ }
+
let html = rx.await.unwrap();
let self_any_scope = AnyScope::from(self.clone());
- html.render_to_string(w, &self_any_scope).await;
+ html.render_to_string(w, &self_any_scope, hydratable).await;
+
+ if hydratable {
+ collectable.write_close_tag(w);
+ }
scheduler::push_component_destroy(Box::new(DestroyRunner {
state: self.state.clone(),
diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs
index b72773406..0a825da18 100644
--- a/packages/yew/src/server_renderer.rs
+++ b/packages/yew/src/server_renderer.rs
@@ -10,6 +10,7 @@ where
ICOMP: IntoComponent,
{
props: ICOMP::Properties,
+ hydratable: bool,
}
impl Default for ServerRenderer
@@ -39,7 +40,22 @@ where
{
/// Creates a [ServerRenderer] with custom properties.
pub fn with_props(props: ICOMP::Properties) -> Self {
- Self { props }
+ Self {
+ props,
+ hydratable: true,
+ }
+ }
+
+ /// Sets whether an the rendered result is hydratable.
+ ///
+ /// Defaults to `true`.
+ ///
+ /// When this is sets to `true`, the rendered artifact will include additional information
+ /// to assist with the hydration process.
+ pub fn hydratable(mut self, val: bool) -> Self {
+ self.hydratable = val;
+
+ self
}
/// Renders Yew Application.
@@ -54,6 +70,8 @@ where
/// Renders Yew Application to a String.
pub async fn render_to_string(self, w: &mut String) {
let scope = Scope::<::Component>::new(None);
- scope.render_to_string(w, self.props.into()).await;
+ scope
+ .render_to_string(w, self.props.into(), self.hydratable)
+ .await;
}
}
diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs
index 80e85ecaa..2970eae98 100644
--- a/packages/yew/src/virtual_dom/mod.rs
+++ b/packages/yew/src/virtual_dom/mod.rs
@@ -179,6 +179,84 @@ mod tests_attr_value {
}
}
+#[cfg(feature = "ssr")] // & feature = "hydration"
+mod feat_ssr_hydration {
+ /// A collectable.
+ ///
+ /// This indicates a kind that can be collected from fragment to be processed at a later time
+ pub(crate) enum Collectable {
+ #[cfg(debug_assertions)]
+ Component(&'static str),
+ #[cfg(not(debug_assertions))]
+ Component,
+ Suspense,
+ }
+
+ impl Collectable {
+ pub fn open_start_mark(&self) -> &'static str {
+ match self {
+ #[cfg(debug_assertions)]
+ Self::Component(_) => "<[",
+ #[cfg(not(debug_assertions))]
+ Self::Component => "<[",
+ Self::Suspense => "",
+ }
+ }
+ pub fn close_start_mark(&self) -> &'static str {
+ match self {
+ #[cfg(debug_assertions)]
+ Self::Component(_) => "[",
+ #[cfg(not(debug_assertions))]
+ Self::Component => "[",
+ Self::Suspense => "?",
+ }
+ }
+
+ pub fn end_mark(&self) -> &'static str {
+ match self {
+ #[cfg(debug_assertions)]
+ Self::Component(_) => "]>",
+ #[cfg(not(debug_assertions))]
+ Self::Component => "]>",
+ Self::Suspense => ">",
+ }
+ }
+
+ #[cfg(feature = "ssr")]
+ pub fn write_open_tag(&self, w: &mut String) {
+ w.push_str("");
+ }
+
+ #[cfg(feature = "ssr")]
+ pub fn write_close_tag(&self, w: &mut String) {
+ w.push_str("");
+ }
+ }
+}
+
+#[cfg(feature = "ssr")]
+pub(crate) use feat_ssr_hydration::*;
+
/// A collection of attributes for an element
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum Attributes {
diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs
index 7912238a0..e380ac506 100644
--- a/packages/yew/src/virtual_dom/vcomp.rs
+++ b/packages/yew/src/virtual_dom/vcomp.rs
@@ -70,6 +70,7 @@ pub(crate) trait Mountable {
&'a self,
w: &'a mut String,
parent_scope: &'a AnyScope,
+ hydratable: bool,
) -> LocalBoxFuture<'a, ()>;
}
@@ -117,10 +118,13 @@ impl Mountable for PropsWrapper {
&'a self,
w: &'a mut String,
parent_scope: &'a AnyScope,
+ hydratable: bool,
) -> LocalBoxFuture<'a, ()> {
async move {
let scope: Scope = Scope::new(Some(parent_scope.clone()));
- scope.render_to_string(w, self.props.clone()).await;
+ scope
+ .render_to_string(w, self.props.clone(), hydratable)
+ .await;
}
.boxed_local()
}
@@ -210,10 +214,15 @@ mod feat_ssr {
use crate::html::AnyScope;
impl VComp {
- pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
+ pub(crate) async fn render_to_string(
+ &self,
+ w: &mut String,
+ parent_scope: &AnyScope,
+ hydratable: bool,
+ ) {
self.mountable
.as_ref()
- .render_to_string(w, parent_scope)
+ .render_to_string(w, parent_scope, hydratable)
.await;
}
}
diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs
index 9304f00d2..3471636c7 100644
--- a/packages/yew/src/virtual_dom/vlist.rs
+++ b/packages/yew/src/virtual_dom/vlist.rs
@@ -90,12 +90,17 @@ mod feat_ssr {
use crate::html::AnyScope;
impl VList {
- pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
+ pub(crate) async fn render_to_string(
+ &self,
+ w: &mut String,
+ parent_scope: &AnyScope,
+ hydratable: bool,
+ ) {
// Concurrently render all children.
for fragment in futures::future::join_all(self.children.iter().map(|m| async move {
let mut w = String::new();
- m.render_to_string(&mut w, parent_scope).await;
+ m.render_to_string(&mut w, parent_scope, hydratable).await;
w
}))
@@ -123,9 +128,10 @@ mod ssr_tests {
html! { {"Hello "}{s}{"!"}
}
}
- let renderer = ServerRenderer::::new();
-
- let s = renderer.render().await;
+ let s = ServerRenderer::::new()
+ .hydratable(false)
+ .render()
+ .await;
assert_eq!(s, "Hello world!
");
}
@@ -153,9 +159,10 @@ mod ssr_tests {
}
}
- let renderer = ServerRenderer::::new();
-
- let s = renderer.render().await;
+ let s = ServerRenderer::::new()
+ .hydratable(false)
+ .render()
+ .await;
assert_eq!(
s,
diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs
index 975f3cb84..72ce6fafa 100644
--- a/packages/yew/src/virtual_dom/vnode.rs
+++ b/packages/yew/src/virtual_dom/vnode.rs
@@ -157,13 +157,20 @@ mod feat_ssr {
&'a self,
w: &'a mut String,
parent_scope: &'a AnyScope,
+ hydratable: bool,
) -> LocalBoxFuture<'a, ()> {
async move {
match self {
- VNode::VTag(vtag) => vtag.render_to_string(w, parent_scope).await,
- VNode::VText(vtext) => vtext.render_to_string(w).await,
- VNode::VComp(vcomp) => vcomp.render_to_string(w, parent_scope).await,
- VNode::VList(vlist) => vlist.render_to_string(w, parent_scope).await,
+ VNode::VTag(vtag) => vtag.render_to_string(w, parent_scope, hydratable).await,
+ VNode::VText(vtext) => {
+ vtext.render_to_string(w, parent_scope, hydratable).await
+ }
+ VNode::VComp(vcomp) => {
+ vcomp.render_to_string(w, parent_scope, hydratable).await
+ }
+ VNode::VList(vlist) => {
+ vlist.render_to_string(w, parent_scope, hydratable).await
+ }
// We are pretty safe here as it's not possible to get a web_sys::Node without DOM
// support in the first place.
//
@@ -175,7 +182,9 @@ mod feat_ssr {
// Portals are not rendered.
VNode::VPortal(_) => {}
VNode::VSuspense(vsuspense) => {
- vsuspense.render_to_string(w, parent_scope).await
+ vsuspense
+ .render_to_string(w, parent_scope, hydratable)
+ .await
}
}
}
diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs
index e9448c5aa..8d43ad360 100644
--- a/packages/yew/src/virtual_dom/vsuspense.rs
+++ b/packages/yew/src/virtual_dom/vsuspense.rs
@@ -28,11 +28,29 @@ impl VSuspense {
mod feat_ssr {
use super::*;
use crate::html::AnyScope;
+ use crate::virtual_dom::Collectable;
impl VSuspense {
- pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
+ pub(crate) async fn render_to_string(
+ &self,
+ w: &mut String,
+ parent_scope: &AnyScope,
+ hydratable: bool,
+ ) {
+ let collectable = Collectable::Suspense;
+
+ if hydratable {
+ collectable.write_open_tag(w);
+ }
+
// always render children on the server side.
- self.children.render_to_string(w, parent_scope).await;
+ self.children
+ .render_to_string(w, parent_scope, hydratable)
+ .await;
+
+ if hydratable {
+ collectable.write_close_tag(w);
+ }
}
}
}
@@ -120,9 +138,10 @@ mod ssr_tests {
let s = local
.run_until(async move {
- let renderer = ServerRenderer::::new();
-
- renderer.render().await
+ ServerRenderer::::new()
+ .hydratable(false)
+ .render()
+ .await
})
.await;
diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs
index ed9debd0d..eb064c5d1 100644
--- a/packages/yew/src/virtual_dom/vtag.rs
+++ b/packages/yew/src/virtual_dom/vtag.rs
@@ -418,8 +418,19 @@ mod feat_ssr {
use crate::{html::AnyScope, virtual_dom::VText};
use std::fmt::Write;
+ // Elements that cannot have any child elements.
+ static VOID_ELEMENTS: &[&str; 14] = &[
+ "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
+ "source", "track", "wbr",
+ ];
+
impl VTag {
- pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
+ pub(crate) async fn render_to_string(
+ &self,
+ w: &mut String,
+ parent_scope: &AnyScope,
+ hydratable: bool,
+ ) {
write!(w, "<{}", self.tag()).unwrap();
let write_attr = |w: &mut String, name: &str, val: Option<&str>| {
@@ -450,7 +461,9 @@ mod feat_ssr {
VTagInner::Input(_) => {}
VTagInner::Textarea { .. } => {
if let Some(m) = self.value() {
- VText::new(m.to_owned()).render_to_string(w).await;
+ VText::new(m.to_owned())
+ .render_to_string(w, parent_scope, hydratable)
+ .await;
}
w.push_str("");
@@ -460,9 +473,14 @@ mod feat_ssr {
ref children,
..
} => {
- children.render_to_string(w, parent_scope).await;
+ if !VOID_ELEMENTS.contains(&tag.as_ref()) {
+ children.render_to_string(w, parent_scope, hydratable).await;
- write!(w, "{}>", tag).unwrap();
+ write!(w, "{}>", tag).unwrap();
+ } else {
+ // We don't write children of void elements nor closing tags.
+ debug_assert!(children.is_empty(), "{} cannot have any children!", tag);
+ }
}
}
}
@@ -483,9 +501,10 @@ mod ssr_tests {
html! { }
}
- let renderer = ServerRenderer::::new();
-
- let s = renderer.render().await;
+ let s = ServerRenderer::::new()
+ .hydratable(false)
+ .render()
+ .await;
assert_eq!(s, "");
}
@@ -497,9 +516,10 @@ mod ssr_tests {
html! { }
}
- let renderer = ServerRenderer::::new();
-
- let s = renderer.render().await;
+ let s = ServerRenderer::::new()
+ .hydratable(false)
+ .render()
+ .await;
assert_eq!(s, r#""#);
}
@@ -511,9 +531,10 @@ mod ssr_tests {
html! { {"Hello!"}
}
}
- let renderer = ServerRenderer::::new();
-
- let s = renderer.render().await;
+ let s = ServerRenderer::::new()
+ .hydratable(false)
+ .render()
+ .await;
assert_eq!(s, r#"Hello!
"#);
}
@@ -525,9 +546,10 @@ mod ssr_tests {
html! { {"Hello!"}
}
}
- let renderer = ServerRenderer::::new();
-
- let s = renderer.render().await;
+ let s = ServerRenderer::::new()
+ .hydratable(false)
+ .render()
+ .await;
assert_eq!(s, r#"Hello!
"#);
}
@@ -539,9 +561,10 @@ mod ssr_tests {
html! { }
}
- let renderer = ServerRenderer::::new();
-
- let s = renderer.render().await;
+ let s = ServerRenderer::::new()
+ .hydratable(false)
+ .render()
+ .await;
assert_eq!(s, r#""#);
}
diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs
index 1756e0bbd..09b039282 100644
--- a/packages/yew/src/virtual_dom/vtext.rs
+++ b/packages/yew/src/virtual_dom/vtext.rs
@@ -34,9 +34,15 @@ impl PartialEq for VText {
#[cfg(feature = "ssr")]
mod feat_ssr {
use super::*;
+ use crate::html::AnyScope;
impl VText {
- pub(crate) async fn render_to_string(&self, w: &mut String) {
+ pub(crate) async fn render_to_string(
+ &self,
+ w: &mut String,
+ _parent_scope: &AnyScope,
+ _hydratable: bool,
+ ) {
html_escape::encode_text_to_string(&self.text, w);
}
}
@@ -46,16 +52,21 @@ mod feat_ssr {
mod ssr_tests {
use tokio::test;
- use super::*;
+ use crate::prelude::*;
+ use crate::ServerRenderer;
#[test]
async fn test_simple_str() {
- let vtext = VText::new("abc");
+ #[function_component]
+ fn Comp() -> Html {
+ html! { "abc" }
+ }
- let mut s = String::new();
+ let s = ServerRenderer::::new()
+ .hydratable(false)
+ .render()
+ .await;
- vtext.render_to_string(&mut s).await;
-
- assert_eq!("abc", s.as_str());
+ assert_eq!(s, r#"abc"#);
}
}