diff --git a/packages/fullstack/src/adapters/axum_adapter.rs b/packages/fullstack/src/adapters/axum_adapter.rs index 41a7c92be..09e6e1aa1 100644 --- a/packages/fullstack/src/adapters/axum_adapter.rs +++ b/packages/fullstack/src/adapters/axum_adapter.rs @@ -254,7 +254,6 @@ where fn register_server_fns(self, server_fn_route: &'static str) -> Self { self.register_server_fns_with_handler(server_fn_route, |func| { - use crate::layer::Service; move |req: Request| { let mut service = crate::server_fn_service(Default::default(), func); async move { diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index fcd72937c..8b44f4486 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -123,13 +123,14 @@ mod js { export function save_template(nodes, tmpl_id) { templates[tmpl_id] = nodes; } - export function hydrate() { + export function hydrate(ids) { + console.log("hydrating", ids); const hydrateNodes = document.querySelectorAll('[data-node-hydration]'); for (let i = 0; i < hydrateNodes.length; i++) { const hydrateNode = hydrateNodes[i]; const hydration = hydrateNode.getAttribute('data-node-hydration'); const split = hydration.split(','); - const id = parseInt(split[0]); + const id = ids[parseInt(split[0])]; nodes[id] = hydrateNode; console.log("hydrating node", hydrateNode, id); if (split.length > 1) { @@ -155,7 +156,7 @@ mod js { const split = id.split('node-id'); if (split.length > 1) { console.log("hydrating text", currentNode.nextSibling, id); - nodes[parseInt(split[1])] = currentNode.nextSibling; + nodes[ids[parseInt(split[1])]] = currentNode.nextSibling; } currentNode = treeWalker.nextNode(); } @@ -215,7 +216,7 @@ mod js { pub fn save_template(nodes: Vec, tmpl_id: u32); #[wasm_bindgen] - pub fn hydrate(); + pub fn hydrate(ids: Vec); #[wasm_bindgen] pub fn get_node(id: u32) -> Node; diff --git a/packages/ssr/src/cache.rs b/packages/ssr/src/cache.rs index 9adcf395b..11f90058a 100644 --- a/packages/ssr/src/cache.rs +++ b/packages/ssr/src/cache.rs @@ -28,9 +28,9 @@ pub enum Segment { /// A marker for where to insert a dynamic inner html InnerHtmlMarker, /// A marker for where to insert a node id for an attribute - AttributeNodeMarker(usize), + AttributeNodeMarker, /// A marker for where to insert a node id for a root node - RootNodeMarker(usize), + RootNodeMarker, } impl std::fmt::Write for StringChain { @@ -82,7 +82,7 @@ impl StringCache { // we need to collect the inner html and write it at the end let mut inner_html = None; // we need to keep track of if we have dynamic attrs to know if we need to insert a style and inner_html marker - let mut last_dyn_attr_id = None; + let mut has_dyn_attrs = false; for attr in *attrs { match attr { TemplateAttribute::Static { @@ -105,7 +105,7 @@ impl StringCache { TemplateAttribute::Dynamic { id: index } => { let index = *index; chain.segments.push(Segment::Attr(index)); - last_dyn_attr_id = Some(index); + has_dyn_attrs = true } } } @@ -120,21 +120,19 @@ impl StringCache { inside_style_tag: true, }); write!(chain, "\"")?; - } else if last_dyn_attr_id.is_some() { + } else if has_dyn_attrs { chain.segments.push(Segment::StyleMarker { inside_style_tag: false, }); } // write the id if we are prerendering and this is either a root node or a node with a dynamic attribute - if prerender { + if prerender && (has_dyn_attrs || is_root) { write!(chain, " data-node-hydration=\"")?; - if let Some(last_dyn_attr_id) = last_dyn_attr_id { - chain - .segments - .push(Segment::AttributeNodeMarker(last_dyn_attr_id)); + if has_dyn_attrs { + chain.segments.push(Segment::AttributeNodeMarker); } else if is_root { - chain.segments.push(Segment::RootNodeMarker(root_idx)); + chain.segments.push(Segment::RootNodeMarker); } write!(chain, "\"")?; } @@ -146,7 +144,7 @@ impl StringCache { // Write the static inner html, or insert a marker if dynamic inner html is possible if let Some(inner_html) = inner_html { chain.write_str(inner_html)?; - } else if last_dyn_attr_id.is_some() { + } else if has_dyn_attrs { chain.segments.push(Segment::InnerHtmlMarker); } diff --git a/packages/ssr/src/renderer.rs b/packages/ssr/src/renderer.rs index 87c882cde..9b0d9e7bb 100644 --- a/packages/ssr/src/renderer.rs +++ b/packages/ssr/src/renderer.rs @@ -28,6 +28,9 @@ pub struct Renderer { /// A cache of templates that have been rendered template_cache: HashMap<&'static str, Arc>, + + /// The current dynamic node id for hydration + dynamic_node_id: usize, } impl Renderer { @@ -54,6 +57,7 @@ impl Renderer { // We should never ever run into async or errored nodes in SSR // Error boundaries and suspense boundaries will convert these to sync if let RenderReturn::Ready(node) = dom.get_scope(scope).unwrap().root_node() { + self.dynamic_node_id = 0; self.render_template(buf, dom, node)? }; @@ -127,10 +131,8 @@ impl Renderer { DynamicNode::Text(text) => { // in SSR, we are concerned that we can't hunt down the right text node since they might get merged if self.pre_render { - let node_id = text - .mounted_element() - .expect("Text nodes must be mounted before rendering"); - write!(buf, "", node_id.0)?; + write!(buf, "", self.dynamic_node_id)?; + self.dynamic_node_id += 1; } write!( @@ -149,12 +151,14 @@ impl Renderer { } } - DynamicNode::Placeholder(el) => { + DynamicNode::Placeholder(_) => { if self.pre_render { - let id = el - .mounted_element() - .expect("Elements must be mounted before rendering"); - write!(buf, "
", id.0)?;
+                            write!(
+                                buf,
+                                "
",
+                                self.dynamic_node_id
+                            )?;
+                            self.dynamic_node_id += 1;
                         }
                     }
                 },
@@ -194,10 +198,10 @@ impl Renderer {
                     }
                 }
 
-                Segment::AttributeNodeMarker(idx) => {
-                    let id = template.dynamic_attrs[*idx].mounted_element();
+                Segment::AttributeNodeMarker => {
                     // first write the id
-                    write!(buf, "{}", id.0)?;
+                    write!(buf, "{}", self.dynamic_node_id)?;
+                    self.dynamic_node_id += 1;
                     // then write any listeners
                     for name in accumulated_listeners.drain(..) {
                         write!(buf, ",{}:", &name[2..])?;
@@ -205,9 +209,9 @@ impl Renderer {
                     }
                 }
 
-                Segment::RootNodeMarker(idx) => {
-                    let id = template.root_ids.borrow()[*idx];
-                    write!(buf, "{}", id.0)?;
+                Segment::RootNodeMarker => {
+                    write!(buf, "{}", self.dynamic_node_id)?;
+                    self.dynamic_node_id += 1
                 }
             }
         }
diff --git a/packages/ssr/tests/hydration.rs b/packages/ssr/tests/hydration.rs
index 736a1435b..360aa0c5f 100644
--- a/packages/ssr/tests/hydration.rs
+++ b/packages/ssr/tests/hydration.rs
@@ -11,7 +11,7 @@ fn root_ids() {
 
     assert_eq!(
         dioxus_ssr::pre_render(&dom),
-        r#"
"# + r#"
"# ); } @@ -29,7 +29,7 @@ fn dynamic_attributes() { assert_eq!( dioxus_ssr::pre_render(&dom), - r#"
"# + r#"
"# ); } @@ -46,7 +46,7 @@ fn listeners() { assert_eq!( dioxus_ssr::pre_render(&dom), - r#"
"# + r#"
"# ); fn app2(cx: Scope) -> Element { @@ -61,7 +61,7 @@ fn listeners() { assert_eq!( dioxus_ssr::pre_render(&dom), - r#"
"# + r#"
"# ); } @@ -79,7 +79,7 @@ fn text_nodes() { assert_eq!( dioxus_ssr::pre_render(&dom), - r#"
hello
"# + r#"
hello
"# ); fn app2(cx: Scope) -> Element { @@ -94,7 +94,85 @@ fn text_nodes() { assert_eq!( dioxus_ssr::pre_render(&dom), - r#"
1231234
"# + r#"
1231234
"# + ); +} + +#[test] +fn components_hydrate() { + fn app(cx: Scope) -> Element { + render! { Child {} } + } + + fn Child(cx: Scope) -> Element { + render! { div { "hello" } } + } + + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
hello
"# + ); + + fn app2(cx: Scope) -> Element { + render! { Child2 {} } + } + + fn Child2(cx: Scope) -> Element { + let dyn_text = "hello"; + render! { + div { dyn_text } + } + } + + let mut dom = VirtualDom::new(app2); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
hello
"# + ); + + fn app3(cx: Scope) -> Element { + render! { Child3 {} } + } + + fn Child3(cx: Scope) -> Element { + render! { div { width: "{1}" } } + } + + let mut dom = VirtualDom::new(app3); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"
"# + ); + + fn app4(cx: Scope) -> Element { + render! { Child4 {} } + } + + fn Child4(cx: Scope) -> Element { + render! { + for _ in 0..2 { + render! { + render! { + "{1}" + } + } + } + } + } + + let mut dom = VirtualDom::new(app4); + _ = dom.rebuild(); + + assert_eq!( + dioxus_ssr::pre_render(&dom), + r#"11"# ); } @@ -115,6 +193,6 @@ fn hello_world_hydrates() { assert_eq!( dioxus_ssr::pre_render(&dom), - r#"

High-Five counter: 0

"# + r#"

High-Five counter: 0

"# ); } diff --git a/packages/ssr/tests/simple.rs b/packages/ssr/tests/simple.rs index cdf186a80..a9034df9b 100644 --- a/packages/ssr/tests/simple.rs +++ b/packages/ssr/tests/simple.rs @@ -47,9 +47,7 @@ fn dynamic() { fn components() { #[component] fn MyComponent(cx: Scope, name: i32) -> Element { - render! { - div { "component {name}" } - } + render! { div { "component {name}" } } } assert_eq!( diff --git a/packages/web/src/dom.rs b/packages/web/src/dom.rs index b8e9255f7..bcf2065ec 100644 --- a/packages/web/src/dom.rs +++ b/packages/web/src/dom.rs @@ -1,11 +1,10 @@ //! Implementation of a renderer for Dioxus on the web. //! //! Oustanding todos: -//! - Removing event listeners (delegation) //! - Passive event listeners //! - no-op event listener patch for safari //! - tests to ensure dyn_into works for various event types. -//! - Partial delegation?> +//! - Partial delegation? use dioxus_core::{ BorrowedAttributeValue, ElementId, Mutation, Template, TemplateAttribute, TemplateNode, diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 9b80dc325..3d8ecf03f 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -215,21 +215,23 @@ pub async fn run_with_props(root: fn(Scope) -> Element, root_prop // todo: we need to split rebuild and initialize into two phases // it's a waste to produce edits just to get the vdom loaded - let mutations = dom.rebuild(); - web_sys::console::log_1(&format!("mutations: {:#?}", mutations).into()); - let templates = mutations.templates; - websys_dom.load_templates(&templates); - websys_dom.interpreter.flush(); - websys_dom.rehydrate(); - // if !true { - // tracing::error!("Rehydration failed. Rebuild DOM into element from scratch"); - // websys_dom.root.set_text_content(None); + { + let mutations = dom.rebuild(); + web_sys::console::log_1(&format!("mutations: {:#?}", mutations).into()); + let templates = mutations.templates; + websys_dom.load_templates(&templates); + websys_dom.interpreter.flush(); + } + if let Err(err) = websys_dom.rehydrate(&dom) { + tracing::error!("Rehydration failed. {:?}", err); + tracing::error!("Rebuild DOM into element from scratch"); + websys_dom.root.set_text_content(None); - // let edits = dom.rebuild(); + let edits = dom.rebuild(); - // websys_dom.load_templates(&edits.templates); - // websys_dom.apply_edits(edits.edits); - // } + websys_dom.load_templates(&edits.templates); + websys_dom.apply_edits(edits.edits); + } } } else { let edits = dom.rebuild(); diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index e151bb13f..2c67cd544 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -6,10 +6,122 @@ use dioxus_html::event_bubbles; use wasm_bindgen::JsCast; use web_sys::{Comment, Node}; +#[derive(Debug)] +pub enum RehydrationError { + VNodeNotInitialized, +} + +use RehydrationError::*; + impl WebsysDom { // we're streaming in patches, but the nodes already exist // so we're just going to write the correct IDs to the node and load them in - pub fn rehydrate(&mut self) { - dioxus_interpreter_js::hydrate() + pub fn rehydrate(&mut self, dom: &VirtualDom) -> Result<(), RehydrationError> { + let root_scope = dom.base_scope(); + let mut ids = Vec::new(); + + // Recursively rehydrate the dom from the VirtualDom + self.rehydrate_scope(root_scope, dom, &mut ids)?; + + dioxus_interpreter_js::hydrate(ids); + + Ok(()) + } + + fn rehydrate_scope( + &mut self, + scope: &ScopeState, + dom: &VirtualDom, + ids: &mut Vec, + ) -> Result<(), RehydrationError> { + let vnode = match scope.root_node() { + dioxus_core::RenderReturn::Ready(ready) => ready, + _ => return Err(VNodeNotInitialized), + }; + self.rehydrate_vnode(dom, vnode, ids) + } + + fn rehydrate_vnode( + &mut self, + dom: &VirtualDom, + vnode: &VNode, + ids: &mut Vec, + ) -> Result<(), RehydrationError> { + for (i, root) in vnode.template.get().roots.iter().enumerate() { + self.rehydrate_template_node( + dom, + vnode, + root, + ids, + Some(*vnode.root_ids.borrow().get(i).ok_or(VNodeNotInitialized)?), + )?; + } + Ok(()) + } + + fn rehydrate_template_node( + &mut self, + dom: &VirtualDom, + vnode: &VNode, + node: &TemplateNode, + ids: &mut Vec, + root_id: Option, + ) -> Result<(), RehydrationError> { + tracing::trace!("rehydrate template node: {:?}", node); + match node { + TemplateNode::Element { + children, attrs, .. + } => { + let mut mounted_id = root_id; + for attr in *attrs { + if let dioxus_core::TemplateAttribute::Dynamic { id } = attr { + let attribute = &vnode.dynamic_attrs[*id]; + let value = &attribute.value; + let id = attribute.mounted_element(); + mounted_id = Some(id); + } + } + if let Some(id) = mounted_id { + ids.push(id.0 as u32); + } + if !children.is_empty() { + for child in *children { + self.rehydrate_template_node(dom, vnode, child, ids, None)?; + } + } + } + TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => { + self.rehydrate_dynamic_node(dom, &vnode.dynamic_nodes[*id], ids)?; + } + _ => {} + } + Ok(()) + } + + fn rehydrate_dynamic_node( + &mut self, + dom: &VirtualDom, + dynamic: &DynamicNode, + ids: &mut Vec, + ) -> Result<(), RehydrationError> { + tracing::trace!("rehydrate dynamic node: {:?}", dynamic); + match dynamic { + dioxus_core::DynamicNode::Text(text) => { + ids.push(text.mounted_element().ok_or(VNodeNotInitialized)?.0 as u32); + } + dioxus_core::DynamicNode::Placeholder(placeholder) => { + ids.push(placeholder.mounted_element().ok_or(VNodeNotInitialized)?.0 as u32); + } + dioxus_core::DynamicNode::Component(comp) => { + let scope = comp.mounted_scope().ok_or(VNodeNotInitialized)?; + self.rehydrate_scope(dom.get_scope(scope).ok_or(VNodeNotInitialized)?, dom, ids)?; + } + dioxus_core::DynamicNode::Fragment(fragment) => { + for vnode in *fragment { + self.rehydrate_vnode(dom, vnode, ids)?; + } + } + } + Ok(()) } }