feat: support diffing inside component children in hot-reload (#690)

This commit is contained in:
Greg Johnston 2023-03-17 13:53:53 -04:00 committed by GitHub
parent 3bd52fcc9d
commit f2ac412253
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 84 additions and 17 deletions

View file

@ -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<LNode, Vec<usize>>);
@ -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![],
}
}

View file

@ -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<LNode>,
},
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}>--><pre>&lt;{name}/&gt; will load once Rust code \
has been compiled.</pre><!--</{name}>-->"
),

View file

@ -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) {

View file

@ -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)
})
}
};