mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Introduce additional information in SSR artifact to facilitate Hydration (#2540)
* Bring changes to this branch. * Child components always render after parents. * Add hydratable to render_to_string. * Revert portal example. * Fix vcomp. * Prefer debug_assert. * ServerRenderer now a Builder Pattern. * Collectable.
This commit is contained in:
parent
ee6a67e3ea
commit
2e098f4f6c
@ -204,8 +204,15 @@ mod feat_ssr {
|
|||||||
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
|
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::virtual_dom::Collectable;
|
||||||
|
|
||||||
impl<COMP: BaseComponent> Scope<COMP> {
|
impl<COMP: BaseComponent> Scope<COMP> {
|
||||||
pub(crate) async fn render_to_string(self, w: &mut String, props: Rc<COMP::Properties>) {
|
pub(crate) async fn render_to_string(
|
||||||
|
self,
|
||||||
|
w: &mut String,
|
||||||
|
props: Rc<COMP::Properties>,
|
||||||
|
hydratable: bool,
|
||||||
|
) {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
let state = ComponentRenderState::Ssr { sender: Some(tx) };
|
let state = ComponentRenderState::Ssr { sender: Some(tx) };
|
||||||
|
|
||||||
@ -222,10 +229,24 @@ mod feat_ssr {
|
|||||||
);
|
);
|
||||||
scheduler::start();
|
scheduler::start();
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
let collectable = Collectable::Component(std::any::type_name::<COMP>());
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let collectable = Collectable::Component;
|
||||||
|
|
||||||
|
if hydratable {
|
||||||
|
collectable.write_open_tag(w);
|
||||||
|
}
|
||||||
|
|
||||||
let html = rx.await.unwrap();
|
let html = rx.await.unwrap();
|
||||||
|
|
||||||
let self_any_scope = AnyScope::from(self.clone());
|
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 {
|
scheduler::push_component_destroy(Box::new(DestroyRunner {
|
||||||
state: self.state.clone(),
|
state: self.state.clone(),
|
||||||
|
|||||||
@ -10,6 +10,7 @@ where
|
|||||||
ICOMP: IntoComponent,
|
ICOMP: IntoComponent,
|
||||||
{
|
{
|
||||||
props: ICOMP::Properties,
|
props: ICOMP::Properties,
|
||||||
|
hydratable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<ICOMP> Default for ServerRenderer<ICOMP>
|
impl<ICOMP> Default for ServerRenderer<ICOMP>
|
||||||
@ -39,7 +40,22 @@ where
|
|||||||
{
|
{
|
||||||
/// Creates a [ServerRenderer] with custom properties.
|
/// Creates a [ServerRenderer] with custom properties.
|
||||||
pub fn with_props(props: ICOMP::Properties) -> Self {
|
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.
|
/// Renders Yew Application.
|
||||||
@ -54,6 +70,8 @@ where
|
|||||||
/// Renders Yew Application to a String.
|
/// Renders Yew Application to a String.
|
||||||
pub async fn render_to_string(self, w: &mut String) {
|
pub async fn render_to_string(self, w: &mut String) {
|
||||||
let scope = Scope::<<ICOMP as IntoComponent>::Component>::new(None);
|
let scope = Scope::<<ICOMP as IntoComponent>::Component>::new(None);
|
||||||
scope.render_to_string(w, self.props.into()).await;
|
scope
|
||||||
|
.render_to_string(w, self.props.into(), self.hydratable)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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("<!--");
|
||||||
|
w.push_str(self.open_start_mark());
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
match self {
|
||||||
|
Self::Component(type_name) => w.push_str(type_name),
|
||||||
|
Self::Suspense => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.push_str(self.end_mark());
|
||||||
|
w.push_str("-->");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub fn write_close_tag(&self, w: &mut String) {
|
||||||
|
w.push_str("<!--");
|
||||||
|
w.push_str(self.close_start_mark());
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
match self {
|
||||||
|
Self::Component(type_name) => w.push_str(type_name),
|
||||||
|
Self::Suspense => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.push_str(self.end_mark());
|
||||||
|
w.push_str("-->");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub(crate) use feat_ssr_hydration::*;
|
||||||
|
|
||||||
/// A collection of attributes for an element
|
/// A collection of attributes for an element
|
||||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
pub enum Attributes {
|
pub enum Attributes {
|
||||||
|
|||||||
@ -70,6 +70,7 @@ pub(crate) trait Mountable {
|
|||||||
&'a self,
|
&'a self,
|
||||||
w: &'a mut String,
|
w: &'a mut String,
|
||||||
parent_scope: &'a AnyScope,
|
parent_scope: &'a AnyScope,
|
||||||
|
hydratable: bool,
|
||||||
) -> LocalBoxFuture<'a, ()>;
|
) -> LocalBoxFuture<'a, ()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,10 +118,13 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
|||||||
&'a self,
|
&'a self,
|
||||||
w: &'a mut String,
|
w: &'a mut String,
|
||||||
parent_scope: &'a AnyScope,
|
parent_scope: &'a AnyScope,
|
||||||
|
hydratable: bool,
|
||||||
) -> LocalBoxFuture<'a, ()> {
|
) -> LocalBoxFuture<'a, ()> {
|
||||||
async move {
|
async move {
|
||||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
let scope: Scope<COMP> = 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()
|
.boxed_local()
|
||||||
}
|
}
|
||||||
@ -210,10 +214,15 @@ mod feat_ssr {
|
|||||||
use crate::html::AnyScope;
|
use crate::html::AnyScope;
|
||||||
|
|
||||||
impl VComp {
|
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
|
self.mountable
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.render_to_string(w, parent_scope)
|
.render_to_string(w, parent_scope, hydratable)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,12 +90,17 @@ mod feat_ssr {
|
|||||||
use crate::html::AnyScope;
|
use crate::html::AnyScope;
|
||||||
|
|
||||||
impl VList {
|
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.
|
// Concurrently render all children.
|
||||||
for fragment in futures::future::join_all(self.children.iter().map(|m| async move {
|
for fragment in futures::future::join_all(self.children.iter().map(|m| async move {
|
||||||
let mut w = String::new();
|
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
|
w
|
||||||
}))
|
}))
|
||||||
@ -123,9 +128,10 @@ mod ssr_tests {
|
|||||||
html! { <div>{"Hello "}{s}{"!"}</div> }
|
html! { <div>{"Hello "}{s}{"!"}</div> }
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderer = ServerRenderer::<Comp>::new();
|
let s = ServerRenderer::<Comp>::new()
|
||||||
|
.hydratable(false)
|
||||||
let s = renderer.render().await;
|
.render()
|
||||||
|
.await;
|
||||||
|
|
||||||
assert_eq!(s, "<div>Hello world!</div>");
|
assert_eq!(s, "<div>Hello world!</div>");
|
||||||
}
|
}
|
||||||
@ -153,9 +159,10 @@ mod ssr_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderer = ServerRenderer::<Comp>::new();
|
let s = ServerRenderer::<Comp>::new()
|
||||||
|
.hydratable(false)
|
||||||
let s = renderer.render().await;
|
.render()
|
||||||
|
.await;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
s,
|
s,
|
||||||
|
|||||||
@ -157,13 +157,20 @@ mod feat_ssr {
|
|||||||
&'a self,
|
&'a self,
|
||||||
w: &'a mut String,
|
w: &'a mut String,
|
||||||
parent_scope: &'a AnyScope,
|
parent_scope: &'a AnyScope,
|
||||||
|
hydratable: bool,
|
||||||
) -> LocalBoxFuture<'a, ()> {
|
) -> LocalBoxFuture<'a, ()> {
|
||||||
async move {
|
async move {
|
||||||
match self {
|
match self {
|
||||||
VNode::VTag(vtag) => vtag.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).await,
|
VNode::VText(vtext) => {
|
||||||
VNode::VComp(vcomp) => vcomp.render_to_string(w, parent_scope).await,
|
vtext.render_to_string(w, parent_scope, hydratable).await
|
||||||
VNode::VList(vlist) => vlist.render_to_string(w, parent_scope).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
|
// We are pretty safe here as it's not possible to get a web_sys::Node without DOM
|
||||||
// support in the first place.
|
// support in the first place.
|
||||||
//
|
//
|
||||||
@ -175,7 +182,9 @@ mod feat_ssr {
|
|||||||
// Portals are not rendered.
|
// Portals are not rendered.
|
||||||
VNode::VPortal(_) => {}
|
VNode::VPortal(_) => {}
|
||||||
VNode::VSuspense(vsuspense) => {
|
VNode::VSuspense(vsuspense) => {
|
||||||
vsuspense.render_to_string(w, parent_scope).await
|
vsuspense
|
||||||
|
.render_to_string(w, parent_scope, hydratable)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,11 +28,29 @@ impl VSuspense {
|
|||||||
mod feat_ssr {
|
mod feat_ssr {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::html::AnyScope;
|
use crate::html::AnyScope;
|
||||||
|
use crate::virtual_dom::Collectable;
|
||||||
|
|
||||||
impl VSuspense {
|
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.
|
// 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
|
let s = local
|
||||||
.run_until(async move {
|
.run_until(async move {
|
||||||
let renderer = ServerRenderer::<Comp>::new();
|
ServerRenderer::<Comp>::new()
|
||||||
|
.hydratable(false)
|
||||||
renderer.render().await
|
.render()
|
||||||
|
.await
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@ -418,8 +418,19 @@ mod feat_ssr {
|
|||||||
use crate::{html::AnyScope, virtual_dom::VText};
|
use crate::{html::AnyScope, virtual_dom::VText};
|
||||||
use std::fmt::Write;
|
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 {
|
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();
|
write!(w, "<{}", self.tag()).unwrap();
|
||||||
|
|
||||||
let write_attr = |w: &mut String, name: &str, val: Option<&str>| {
|
let write_attr = |w: &mut String, name: &str, val: Option<&str>| {
|
||||||
@ -450,7 +461,9 @@ mod feat_ssr {
|
|||||||
VTagInner::Input(_) => {}
|
VTagInner::Input(_) => {}
|
||||||
VTagInner::Textarea { .. } => {
|
VTagInner::Textarea { .. } => {
|
||||||
if let Some(m) = self.value() {
|
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("</textarea>");
|
w.push_str("</textarea>");
|
||||||
@ -460,9 +473,14 @@ mod feat_ssr {
|
|||||||
ref children,
|
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! { <div></div> }
|
html! { <div></div> }
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderer = ServerRenderer::<Comp>::new();
|
let s = ServerRenderer::<Comp>::new()
|
||||||
|
.hydratable(false)
|
||||||
let s = renderer.render().await;
|
.render()
|
||||||
|
.await;
|
||||||
|
|
||||||
assert_eq!(s, "<div></div>");
|
assert_eq!(s, "<div></div>");
|
||||||
}
|
}
|
||||||
@ -497,9 +516,10 @@ mod ssr_tests {
|
|||||||
html! { <div class="abc"></div> }
|
html! { <div class="abc"></div> }
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderer = ServerRenderer::<Comp>::new();
|
let s = ServerRenderer::<Comp>::new()
|
||||||
|
.hydratable(false)
|
||||||
let s = renderer.render().await;
|
.render()
|
||||||
|
.await;
|
||||||
|
|
||||||
assert_eq!(s, r#"<div class="abc"></div>"#);
|
assert_eq!(s, r#"<div class="abc"></div>"#);
|
||||||
}
|
}
|
||||||
@ -511,9 +531,10 @@ mod ssr_tests {
|
|||||||
html! { <div>{"Hello!"}</div> }
|
html! { <div>{"Hello!"}</div> }
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderer = ServerRenderer::<Comp>::new();
|
let s = ServerRenderer::<Comp>::new()
|
||||||
|
.hydratable(false)
|
||||||
let s = renderer.render().await;
|
.render()
|
||||||
|
.await;
|
||||||
|
|
||||||
assert_eq!(s, r#"<div>Hello!</div>"#);
|
assert_eq!(s, r#"<div>Hello!</div>"#);
|
||||||
}
|
}
|
||||||
@ -525,9 +546,10 @@ mod ssr_tests {
|
|||||||
html! { <div>{"Hello!"}<input value="abc" type="text" /></div> }
|
html! { <div>{"Hello!"}<input value="abc" type="text" /></div> }
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderer = ServerRenderer::<Comp>::new();
|
let s = ServerRenderer::<Comp>::new()
|
||||||
|
.hydratable(false)
|
||||||
let s = renderer.render().await;
|
.render()
|
||||||
|
.await;
|
||||||
|
|
||||||
assert_eq!(s, r#"<div>Hello!<input value="abc" type="text"></div>"#);
|
assert_eq!(s, r#"<div>Hello!<input value="abc" type="text"></div>"#);
|
||||||
}
|
}
|
||||||
@ -539,9 +561,10 @@ mod ssr_tests {
|
|||||||
html! { <textarea value="teststring" /> }
|
html! { <textarea value="teststring" /> }
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderer = ServerRenderer::<Comp>::new();
|
let s = ServerRenderer::<Comp>::new()
|
||||||
|
.hydratable(false)
|
||||||
let s = renderer.render().await;
|
.render()
|
||||||
|
.await;
|
||||||
|
|
||||||
assert_eq!(s, r#"<textarea>teststring</textarea>"#);
|
assert_eq!(s, r#"<textarea>teststring</textarea>"#);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,9 +34,15 @@ impl PartialEq for VText {
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
mod feat_ssr {
|
mod feat_ssr {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::html::AnyScope;
|
||||||
|
|
||||||
impl VText {
|
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);
|
html_escape::encode_text_to_string(&self.text, w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,16 +52,21 @@ mod feat_ssr {
|
|||||||
mod ssr_tests {
|
mod ssr_tests {
|
||||||
use tokio::test;
|
use tokio::test;
|
||||||
|
|
||||||
use super::*;
|
use crate::prelude::*;
|
||||||
|
use crate::ServerRenderer;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
async fn test_simple_str() {
|
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::<Comp>::new()
|
||||||
|
.hydratable(false)
|
||||||
|
.render()
|
||||||
|
.await;
|
||||||
|
|
||||||
vtext.render_to_string(&mut s).await;
|
assert_eq!(s, r#"abc"#);
|
||||||
|
|
||||||
assert_eq!("abc", s.as_str());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user