From f2ac41225361acaac6e2bb0df59e5471cd439128 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Fri, 17 Mar 2023 13:53:53 -0400 Subject: [PATCH] feat: support diffing inside component children in hot-reload (#690) --- leptos_hot_reload/src/diff.rs | 41 ++++++++++++++++++++++++++++------ leptos_hot_reload/src/node.rs | 22 +++++++++++++----- leptos_hot_reload/src/patch.js | 33 ++++++++++++++++++++++++--- leptos_macro/src/view.rs | 5 ++++- 4 files changed, 84 insertions(+), 17 deletions(-) diff --git a/leptos_hot_reload/src/diff.rs b/leptos_hot_reload/src/diff.rs index 8a67218..4089956 100644 --- a/leptos_hot_reload/src/diff.rs +++ b/leptos_hot_reload/src/diff.rs @@ -2,10 +2,6 @@ use crate::node::{LAttributeValue, LNode}; use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -// TODO: insertion and removal code are still somewhat broken -// namely, it will tend to remove and move or mutate nodes, -// which causes a bit of a problem for DynChild etc. - #[derive(Debug, Default)] struct OldChildren(IndexMap>); @@ -58,7 +54,7 @@ impl LNode { .collect(), }, LNode::Text(_) - | LNode::Component(_, _) + | LNode::Component { .. } | LNode::DynChild(_) => ReplacementNode::Html(self.to_html()), }, } @@ -80,10 +76,19 @@ impl LNode { child.add_old_children(new_path, positions); } } - // only need to insert dynamic content, as these might change - LNode::Component(_, _) | LNode::DynChild(_) => { + // need to insert dynamic content and children, as these might change + LNode::DynChild(_) => { positions.0.insert(self.clone(), path); } + LNode::Component { children, .. } => { + positions.0.insert(self.clone(), path.clone()); + + for (idx, child) in children.iter().enumerate() { + let mut new_path = path.clone(); + new_path.push(idx); + child.add_old_children(new_path, positions); + } + } // can just create text nodes, whatever LNode::Text(_) => {} } @@ -148,6 +153,28 @@ impl LNode { .collect() } // components + dynamic context: no patches + ( + LNode::Component { + name: old_name, + children: old_children, + .. + }, + LNode::Component { + name: new_name, + children: new_children, + .. + }, + ) if old_name == new_name => { + let mut path = path.to_vec(); + path.push(0); + path.push(0); + LNode::diff_children( + &path, + old_children, + new_children, + orig_children, + ) + } _ => vec![], } } diff --git a/leptos_hot_reload/src/node.rs b/leptos_hot_reload/src/node.rs index f26ce81..217c5e4 100644 --- a/leptos_hot_reload/src/node.rs +++ b/leptos_hot_reload/src/node.rs @@ -20,7 +20,11 @@ pub enum LNode { }, // don't need anything; skipped during patching because it should // contain its own view macros - Component(String, Vec<(String, String)>), + Component { + name: String, + props: Vec<(String, String)>, + children: Vec, + }, DynChild(String), } @@ -71,9 +75,14 @@ impl LNode { } Node::Element(el) => { if is_component_node(&el) { - views.push(LNode::Component( - el.name.to_string(), - el.attributes + let mut children = Vec::new(); + for child in el.children { + LNode::parse_node(child, &mut children)?; + } + views.push(LNode::Component { + name: el.name.to_string(), + props: el + .attributes .into_iter() .filter_map(|attr| match attr { Node::Attribute(attr) => Some(( @@ -83,7 +92,8 @@ impl LNode { _ => None, }) .collect(), - )); + children, + }); } else { let name = el.name.to_string(); let mut attrs = Vec::new(); @@ -125,7 +135,7 @@ impl LNode { match self { LNode::Fragment(frag) => frag.iter().map(LNode::to_html).collect(), LNode::Text(text) => text.to_owned(), - LNode::Component(name, _) => format!( + LNode::Component { name, .. } => format!( "
<{name}/> will load once Rust code \
                  has been compiled.
" ), diff --git a/leptos_hot_reload/src/patch.js b/leptos_hot_reload/src/patch.js index 6cfa02b..606eb82 100644 --- a/leptos_hot_reload/src/patch.js +++ b/leptos_hot_reload/src/patch.js @@ -2,7 +2,8 @@ console.log("[HOT RELOADING] Connected to server."); function patch(json) { try { const views = JSON.parse(json); - for (const [id, patches] of views) { + for (const [id, patches] of views) { + console.log("[HOT RELOAD]", patches); const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT), open = `leptos-view|${id}|open`, close = `leptos-view|${id}|close`; @@ -250,7 +251,33 @@ function patch(json) { node: walker.currentNode }); } else if (walker.currentNode.nodeType == Node.COMMENT_NODE) { - if (walker.currentNode.textContent.trim().startsWith("leptos-view")) { + if (walker.currentNode.textContent.trim().startsWith("leptos-view")) { + if (walker.currentNode.textContent.trim().endsWith("-children|open")) { + const startingName = walker.currentNode.textContent.trim(); + const componentName = startingName.replace("-children|open").replace("leptos-view|"); + const endingName = `leptos-view|${componentName}-children|close`; + let start = walker.currentNode; + let depth = 1; + + while (walker.nextNode()) { + if (walker.currentNode.textContent.trim() == endingName) { + depth--; + } else if (walker.currentNode.textContent.trim() == startingName) { + depth++; + } + + if(depth == 0) { + break; + } + } + let end = walker.currentNode; + actualChildren.push({ + type: "fragment", + start: start.nextSibling, + end: end.previousSibling, + children: childrenFromRange(start.parentElement, start.nextSibling, end.previousSibling) + }); + } } else if (walker.currentNode.textContent.trim() == "<() />") { actualChildren.push({ type: "unit", @@ -326,7 +353,7 @@ function patch(json) { return actualChildren; } - function childAtPath(element, path) { + function childAtPath(element, path) { if (path.length == 0) { return element; } else if (element.children) { diff --git a/leptos_macro/src/view.rs b/leptos_macro/src/view.rs index b3280d3..5a5912e 100644 --- a/leptos_macro/src/view.rs +++ b/leptos_macro/src/view.rs @@ -1102,6 +1102,9 @@ pub(crate) fn component_to_tokens( let children = if node.children.is_empty() { quote! {} } else { + let marker = format!("<{component_name}/>-children"); + let view_marker = quote! { .with_view_marker(#marker) }; + let children = fragment_to_tokens( cx, span, @@ -1120,7 +1123,7 @@ pub(crate) fn component_to_tokens( .children({ #(#clonables)* - Box::new(move |#cx| #children) + Box::new(move |#cx| #children #view_marker) }) } };