From f3c650b073b57073d30b65fb4f7fe15a6c5b2be6 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 16 Jan 2021 10:24:30 -0500 Subject: [PATCH] Feat: include diffing and patching in Dioxus --- CHANGELOG.md | 5 +- packages/core/src/lib.rs | 443 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 445 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70e300ffb..e70f3a0d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Project: Live-View +# Project: Live-View 🤲 🍨 # Project: Sanitization (TBD) @@ -8,6 +8,9 @@ # Project: Examples > Get *all* the examples - [ ] (Examples) Tide example with templating +- [ ] (Examples) Tide example with templating +- [ ] (Examples) Tide example with templating +- [ ] (Examples) Tide example with templating # Project: State management > Get some global state management installed with the hooks API diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 2581733cd..1182d5bed 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -15,7 +15,7 @@ pub mod prelude { pub use nodes::iterables::IterableNodes; pub use nodes::*; - // hack "virtualnode" + // hack "VNode" pub type VirtualNode = VNode; // Re-export from the macro crate @@ -550,7 +550,446 @@ pub mod nodes { /// /// pub mod diff { - pub enum Patch {} + use super::*; + use crate::nodes::{VNode, VText}; + use std::cmp::min; + use std::collections::HashMap; + use std::mem; + + // pub use apply_patches::patch; + + /// A Patch encodes an operation that modifies a real DOM element. + /// + /// To update the real DOM that a user sees you'll want to first diff your + /// old virtual dom and new virtual dom. + /// + /// This diff operation will generate `Vec` with zero or more patches that, when + /// applied to your real DOM, will make your real DOM look like your new virtual dom. + /// + /// Each Patch has a u32 node index that helps us identify the real DOM node that it applies to. + /// + /// Our old virtual dom's nodes are indexed depth first, as shown in this illustration + /// (0 being the root node, 1 being it's first child, 2 being it's first child's first child). + /// + /// ```text + /// .─. + /// ( 0 ) + /// `┬' + /// ┌────┴──────┐ + /// │ │ + /// ▼ ▼ + /// .─. .─. + /// ( 1 ) ( 4 ) + /// `┬' `─' + /// ┌────┴───┐ │ + /// │ │ ├─────┬─────┐ + /// ▼ ▼ │ │ │ + /// .─. .─. ▼ ▼ ▼ + /// ( 2 ) ( 3 ) .─. .─. .─. + /// `─' `─' ( 5 ) ( 6 ) ( 7 ) + /// `─' `─' `─' + /// ``` + /// + /// The patching process is tested in a real browser in crates/virtual-dom-rs/tests/diff_patch.rs + #[derive(Debug, PartialEq)] + pub enum Patch<'a> { + /// Append a vector of child nodes to a parent node id. + AppendChildren(NodeIdx, Vec<&'a VNode>), + /// For a `node_i32`, remove all children besides the first `len` + TruncateChildren(NodeIdx, usize), + /// Replace a node with another node. This typically happens when a node's tag changes. + /// ex:
becomes + Replace(NodeIdx, &'a VNode), + /// Add attributes that the new node has that the old node does not + AddAttributes(NodeIdx, HashMap<&'a str, &'a str>), + /// Remove attributes that the old node had that the new node doesn't + RemoveAttributes(NodeIdx, Vec<&'a str>), + /// Change the text of a Text node. + ChangeText(NodeIdx, &'a VText), + } + + type NodeIdx = usize; + + impl<'a> Patch<'a> { + /// Every Patch is meant to be applied to a specific node within the DOM. Get the + /// index of the DOM node that this patch should apply to. DOM nodes are indexed + /// depth first with the root node in the tree having index 0. + pub fn node_idx(&self) -> usize { + match self { + Patch::AppendChildren(node_idx, _) => *node_idx, + Patch::TruncateChildren(node_idx, _) => *node_idx, + Patch::Replace(node_idx, _) => *node_idx, + Patch::AddAttributes(node_idx, _) => *node_idx, + Patch::RemoveAttributes(node_idx, _) => *node_idx, + Patch::ChangeText(node_idx, _) => *node_idx, + } + } + } + + /// Given two VNode's generate Patch's that would turn the old virtual node's + /// real DOM node equivalent into the new VNode's real DOM node equivalent. + pub fn diff<'a>(old: &'a VNode, new: &'a VNode) -> Vec> { + diff_recursive(&old, &new, &mut 0) + } + + fn diff_recursive<'a, 'b>( + old: &'a VNode, + new: &'a VNode, + cur_node_idx: &'b mut usize, + ) -> Vec> { + let mut patches = vec![]; + let mut replace = false; + + // Different enum variants, replace! + if mem::discriminant(old) != mem::discriminant(new) { + replace = true; + } + + if let (VNode::Element(old_element), VNode::Element(new_element)) = (old, new) { + // Replace if there are different element tags + if old_element.tag != new_element.tag { + replace = true; + } + + // Replace if two elements have different keys + // TODO: More robust key support. This is just an early stopgap to allow you to force replace + // an element... say if it's event changed. Just change the key name for now. + // In the future we want keys to be used to create a Patch::ReOrder to re-order siblings + if old_element.attrs.get("key").is_some() + && old_element.attrs.get("key") != new_element.attrs.get("key") + { + replace = true; + } + } + + // Handle replacing of a node + if replace { + patches.push(Patch::Replace(*cur_node_idx, &new)); + if let VNode::Element(old_element_node) = old { + for child in old_element_node.children.iter() { + increment_node_idx_for_children(child, cur_node_idx); + } + } + return patches; + } + + // The following comparison can only contain identical variants, other + // cases have already been handled above by comparing variant + // discriminants. + match (old, new) { + // We're comparing two text nodes + (VNode::Text(old_text), VNode::Text(new_text)) => { + if old_text != new_text { + patches.push(Patch::ChangeText(*cur_node_idx, &new_text)); + } + } + + // We're comparing two element nodes + (VNode::Element(old_element), VNode::Element(new_element)) => { + let mut add_attributes: HashMap<&str, &str> = HashMap::new(); + let mut remove_attributes: Vec<&str> = vec![]; + + // TODO: -> split out into func + for (new_attr_name, new_attr_val) in new_element.attrs.iter() { + match old_element.attrs.get(new_attr_name) { + Some(ref old_attr_val) => { + if old_attr_val != &new_attr_val { + add_attributes.insert(new_attr_name, new_attr_val); + } + } + None => { + add_attributes.insert(new_attr_name, new_attr_val); + } + }; + } + + // TODO: -> split out into func + for (old_attr_name, old_attr_val) in old_element.attrs.iter() { + if add_attributes.get(&old_attr_name[..]).is_some() { + continue; + }; + + match new_element.attrs.get(old_attr_name) { + Some(ref new_attr_val) => { + if new_attr_val != &old_attr_val { + remove_attributes.push(old_attr_name); + } + } + None => { + remove_attributes.push(old_attr_name); + } + }; + } + + if add_attributes.len() > 0 { + patches.push(Patch::AddAttributes(*cur_node_idx, add_attributes)); + } + if remove_attributes.len() > 0 { + patches.push(Patch::RemoveAttributes(*cur_node_idx, remove_attributes)); + } + + let old_child_count = old_element.children.len(); + let new_child_count = new_element.children.len(); + + if new_child_count > old_child_count { + let append_patch: Vec<&'a VNode> = + new_element.children[old_child_count..].iter().collect(); + patches.push(Patch::AppendChildren(*cur_node_idx, append_patch)) + } + + if new_child_count < old_child_count { + patches.push(Patch::TruncateChildren(*cur_node_idx, new_child_count)) + } + + let min_count = min(old_child_count, new_child_count); + for index in 0..min_count { + *cur_node_idx = *cur_node_idx + 1; + let old_child = &old_element.children[index]; + let new_child = &new_element.children[index]; + patches.append(&mut diff_recursive(&old_child, &new_child, cur_node_idx)) + } + if new_child_count < old_child_count { + for child in old_element.children[min_count..].iter() { + increment_node_idx_for_children(child, cur_node_idx); + } + } + } + (VNode::Text(_), VNode::Element(_)) | (VNode::Element(_), VNode::Text(_)) => { + unreachable!("Unequal variant discriminants should already have been handled"); + } + _ => todo!("Diffing Not yet implemented for all node types"), + }; + + // new_root.create_element() + patches + } + + fn increment_node_idx_for_children<'a, 'b>(old: &'a VNode, cur_node_idx: &'b mut usize) { + *cur_node_idx += 1; + if let VNode::Element(element_node) = old { + for child in element_node.children.iter() { + increment_node_idx_for_children(&child, cur_node_idx); + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + use crate::prelude::*; + type VirtualNode = VNode; + + /// Test that we generate the right Vec for some start and end virtual dom. + pub struct DiffTestCase<'a> { + // ex: "Patching root level nodes works" + pub description: &'static str, + // ex: html! {
} + pub old: VNode, + // ex: html! { } + pub new: VNode, + // ex: vec![Patch::Replace(0, &html! { })], + pub expected: Vec>, + } + + impl<'a> DiffTestCase<'a> { + pub fn test(&self) { + // ex: vec![Patch::Replace(0, &html! { })], + let patches = diff(&self.old, &self.new); + + assert_eq!(patches, self.expected, "{}", self.description); + } + } + use super::*; + use crate::nodes::{VNode, VText}; + use std::collections::HashMap; + + #[test] + fn replace_node() { + DiffTestCase { + description: "Replace the root if the tag changed", + old: html! {
}, + new: html! { }, + expected: vec![Patch::Replace(0, &html! { })], + } + .test(); + DiffTestCase { + description: "Replace a child node", + old: html! {
}, + new: html! {
}, + expected: vec![Patch::Replace(1, &html! { })], + } + .test(); + DiffTestCase { + description: "Replace node with a child", + old: html! {
1
}, + new: html! {
1
}, + expected: vec![ + Patch::Replace(1, &html! { 1 }), + Patch::Replace(3, &html! { }), + ], //required to check correct index + } + .test(); + } + + #[test] + fn add_children() { + DiffTestCase { + description: "Added a new node to the root node", + old: html! {
}, + new: html! {
}, + expected: vec![Patch::AppendChildren(0, vec![&html! { }])], + } + .test(); + } + + #[test] + fn remove_nodes() { + DiffTestCase { + description: "Remove all child nodes at and after child sibling index 1", + old: html! {
}, + new: html! {
}, + expected: vec![Patch::TruncateChildren(0, 0)], + } + .test(); + DiffTestCase { + description: "Remove a child and a grandchild node", + old: html! { +
+ + + // This `i` tag will get removed + + + // This `strong` tag will get removed + +
}, + new: html! { +
+ + + +
}, + expected: vec![Patch::TruncateChildren(0, 1), Patch::TruncateChildren(1, 1)], + } + .test(); + DiffTestCase { + description: "Removing child and change next node after parent", + old: html! {
}, + new: html! {
}, + expected: vec![ + Patch::TruncateChildren(1, 1), + Patch::Replace(4, &html! { }), + ], //required to check correct index + } + .test(); + } + + #[test] + fn add_attributes() { + let mut attributes = HashMap::new(); + attributes.insert("id", "hello"); + + DiffTestCase { + old: html! {
}, + new: html! {
}, + expected: vec![Patch::AddAttributes(0, attributes.clone())], + description: "Add attributes", + } + .test(); + + DiffTestCase { + old: html! {
}, + new: html! {
}, + expected: vec![Patch::AddAttributes(0, attributes)], + description: "Change attribute", + } + .test(); + } + + #[test] + fn remove_attributes() { + DiffTestCase { + old: html! {
}, + new: html! {
}, + expected: vec![Patch::RemoveAttributes(0, vec!["id"])], + description: "Add attributes", + } + .test(); + } + + #[test] + fn change_attribute() { + let mut attributes = HashMap::new(); + attributes.insert("id", "changed"); + + DiffTestCase { + description: "Add attributes", + old: html! {
}, + new: html! {
}, + expected: vec![Patch::AddAttributes(0, attributes)], + } + .test(); + } + + #[test] + fn replace_text_node() { + DiffTestCase { + description: "Replace text node", + old: html! { Old }, + new: html! { New }, + expected: vec![Patch::ChangeText(0, &VText::new("New"))], + } + .test(); + } + + // Initially motivated by having two elements where all that changed was an event listener + // because right now we don't patch event listeners. So.. until we have a solution + // for that we can just give them different keys to force a replace. + #[test] + fn replace_if_different_keys() { + DiffTestCase { + description: "If two nodes have different keys always generate a full replace.", + old: html! {
}, + new: html! {
}, + expected: vec![Patch::Replace(0, &html! {
})], + } + .test() + } + + // // TODO: Key support + // #[test] + // fn reorder_chldren() { + // let mut attributes = HashMap::new(); + // attributes.insert("class", "foo"); + // + // let old_children = vec![ + // // old node 0 + // html! {
}, + // // removed + // html! {
{ "This node gets removed"}
}, + // // old node 2 + // html! {
}, + // // removed + // html! {
{ "This node gets removed"}
}, + // ]; + // + // let new_children = vec![ + // html! {
}, + // html! {
}, + // html! {
}, + // ]; + // + // test(DiffTestCase { + // old: html! {
{ old_children }
}, + // new: html! {
{ new_children }
}, + // expected: vec![ + // // TODO: Come up with the patch structure for keyed nodes.. + // // keying should only work if all children have keys.. + // ], + // description: "Add attributes", + // }) + // } + } } ///