Forráskód Böngészése

Feat: include diffing and patching in Dioxus

Jonathan Kelley 4 éve
szülő
commit
f3c650b
2 módosított fájl, 445 hozzáadás és 3 törlés
  1. 4 1
      CHANGELOG.md
  2. 441 2
      packages/core/src/lib.rs

+ 4 - 1
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

+ 441 - 2
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<Patch>` 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: <div> becomes <span>
+        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<Patch<'a>> {
+        diff_recursive(&old, &new, &mut 0)
+    }
+
+    fn diff_recursive<'a, 'b>(
+        old: &'a VNode,
+        new: &'a VNode,
+        cur_node_idx: &'b mut usize,
+    ) -> Vec<Patch<'a>> {
+        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<Patch> for some start and end virtual dom.
+        pub struct DiffTestCase<'a> {
+            // ex: "Patching root level nodes works"
+            pub description: &'static str,
+            // ex: html! { <div> </div> }
+            pub old: VNode,
+            // ex: html! { <strong> </strong> }
+            pub new: VNode,
+            // ex: vec![Patch::Replace(0, &html! { <strong></strong> })],
+            pub expected: Vec<Patch<'a>>,
+        }
+
+        impl<'a> DiffTestCase<'a> {
+            pub fn test(&self) {
+                // ex: vec![Patch::Replace(0, &html! { <strong></strong> })],
+                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! { <div> </div> },
+                new: html! { <span> </span> },
+                expected: vec![Patch::Replace(0, &html! { <span></span> })],
+            }
+            .test();
+            DiffTestCase {
+                description: "Replace a child node",
+                old: html! { <div> <b></b> </div> },
+                new: html! { <div> <strong></strong> </div> },
+                expected: vec![Patch::Replace(1, &html! { <strong></strong> })],
+            }
+            .test();
+            DiffTestCase {
+                description: "Replace node with a child",
+                old: html! { <div> <b>1</b> <b></b> </div> },
+                new: html! { <div> <i>1</i> <i></i> </div>},
+                expected: vec![
+                    Patch::Replace(1, &html! { <i>1</i> }),
+                    Patch::Replace(3, &html! { <i></i> }),
+                ], //required to check correct index
+            }
+            .test();
+        }
+
+        #[test]
+        fn add_children() {
+            DiffTestCase {
+                description: "Added a new node to the root node",
+                old: html! { <div> <b></b> </div> },
+                new: html! { <div> <b></b> <span></span> </div> },
+                expected: vec![Patch::AppendChildren(0, vec![&html! { <span></span> }])],
+            }
+            .test();
+        }
+
+        #[test]
+        fn remove_nodes() {
+            DiffTestCase {
+                description: "Remove all child nodes at and after child sibling index 1",
+                old: html! { <div> <b></b> <span></span> </div> },
+                new: html! { <div> </div> },
+                expected: vec![Patch::TruncateChildren(0, 0)],
+            }
+            .test();
+            DiffTestCase {
+                description: "Remove a child and a grandchild node",
+                old: html! {
+                <div>
+                 <span>
+                   <b></b>
+                   // This `i` tag will get removed
+                   <i></i>
+                 </span>
+                 // This `strong` tag will get removed
+                 <strong></strong>
+                </div> },
+                new: html! {
+                <div>
+                 <span>
+                  <b></b>
+                 </span>
+                </div> },
+                expected: vec![Patch::TruncateChildren(0, 1), Patch::TruncateChildren(1, 1)],
+            }
+            .test();
+            DiffTestCase {
+                description: "Removing child and change next node after parent",
+                old: html! { <div> <b> <i></i> <i></i> </b> <b></b> </div> },
+                new: html! { <div> <b> <i></i> </b> <i></i> </div>},
+                expected: vec![
+                    Patch::TruncateChildren(1, 1),
+                    Patch::Replace(4, &html! { <i></i> }),
+                ], //required to check correct index
+            }
+            .test();
+        }
+
+        #[test]
+        fn add_attributes() {
+            let mut attributes = HashMap::new();
+            attributes.insert("id", "hello");
+
+            DiffTestCase {
+                old: html! { <div> </div> },
+                new: html! { <div id="hello"> </div> },
+                expected: vec![Patch::AddAttributes(0, attributes.clone())],
+                description: "Add attributes",
+            }
+            .test();
+
+            DiffTestCase {
+                old: html! { <div id="foobar"> </div> },
+                new: html! { <div id="hello"> </div> },
+                expected: vec![Patch::AddAttributes(0, attributes)],
+                description: "Change attribute",
+            }
+            .test();
+        }
+
+        #[test]
+        fn remove_attributes() {
+            DiffTestCase {
+                old: html! { <div id="hey-there"></div> },
+                new: html! { <div> </div> },
+                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! { <div id="hey-there"></div> },
+                new: html! { <div id="changed"> </div> },
+                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! { <div key="1"> </div> },
+                new: html! { <div key="2"> </div> },
+                expected: vec![Patch::Replace(0, &html! {<div key="2"> </div>})],
+            }
+            .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! { <div key="hello", id="same-id", style="",></div> },
+        //            // removed
+        //            html! { <div key="gets-removed",> { "This node gets removed"} </div>},
+        //            // old node 2
+        //            html! { <div key="world", class="changed-class",></div>},
+        //            // removed
+        //            html! { <div key="this-got-removed",> { "This node gets removed"} </div>},
+        //        ];
+        //
+        //        let new_children = vec![
+        //            html! { <div key="world", class="foo",></div> },
+        //            html! { <div key="new",> </div>},
+        //            html! { <div key="hello", id="same-id",></div>},
+        //        ];
+        //
+        //        test(DiffTestCase {
+        //            old: html! { <div> { old_children } </div> },
+        //            new: html! { <div> { new_children } </div> },
+        //            expected: vec![
+        //                // TODO: Come up with the patch structure for keyed nodes..
+        //                // keying should only work if all children have keys..
+        //            ],
+        //            description: "Add attributes",
+        //        })
+        //    }
+    }
 }
 
 ///