Feat: docs, code frm percy
This commit is contained in:
commit
2b9c8d09d9
72 files changed
+5130
No files matched your search
@@ -0,0 +1,26 @@
|
||||
//! Kept in it's own file to more easily import into the Percy book.
|
||||
|
||||
use crate::diff::diff;
|
||||
use crate::patch::Patch;
|
||||
use virtual_node::VirtualNode;
|
||||
|
||||
/// 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: VirtualNode,
|
||||
// ex: html! { <strong> </strong> }
|
||||
pub new: VirtualNode,
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
use crate::Patch;
|
||||
use crate::VirtualNode;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
|
||||
/// Given two VirtualNode's generate Patch's that would turn the old virtual node's
|
||||
/// real DOM node equivalent into the new VirtualNode's real DOM node equivalent.
|
||||
pub fn diff<'a>(old: &'a VirtualNode, new: &'a VirtualNode) -> Vec<Patch<'a>> {
|
||||
diff_recursive(&old, &new, &mut 0)
|
||||
}
|
||||
|
||||
fn diff_recursive<'a, 'b>(
|
||||
old: &'a VirtualNode,
|
||||
new: &'a VirtualNode,
|
||||
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 (VirtualNode::Element(old_element), VirtualNode::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 VirtualNode::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
|
||||
(VirtualNode::Text(old_text), VirtualNode::Text(new_text)) => {
|
||||
if old_text != new_text {
|
||||
patches.push(Patch::ChangeText(*cur_node_idx, &new_text));
|
||||
}
|
||||
}
|
||||
|
||||
// We're comparing two element nodes
|
||||
(VirtualNode::Element(old_element), VirtualNode::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 VirtualNode> =
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
(VirtualNode::Text(_), VirtualNode::Element(_))
|
||||
| (VirtualNode::Element(_), VirtualNode::Text(_)) => {
|
||||
unreachable!("Unequal variant discriminants should already have been handled");
|
||||
}
|
||||
};
|
||||
|
||||
// new_root.create_element()
|
||||
patches
|
||||
}
|
||||
|
||||
fn increment_node_idx_for_children<'a, 'b>(old: &'a VirtualNode, cur_node_idx: &'b mut usize) {
|
||||
*cur_node_idx += 1;
|
||||
if let VirtualNode::Element(element_node) = old {
|
||||
for child in element_node.children.iter() {
|
||||
increment_node_idx_for_children(&child, cur_node_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod diff_test_case;
|
||||
#[cfg(test)]
|
||||
use self::diff_test_case::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{html, VText, VirtualNode};
|
||||
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",
|
||||
// })
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Diff virtual-doms and patch the real DOM
|
||||
|
||||
use crate::diff::diff;
|
||||
use crate::patch::patch;
|
||||
use std::collections::HashMap;
|
||||
use virtual_node::DynClosure;
|
||||
use virtual_node::VirtualNode;
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
/// Closures that we are holding on to to make sure that they don't get invalidated after a
|
||||
/// VirtualNode is dropped.
|
||||
///
|
||||
/// The u32 is a unique identifier that is associated with the DOM element that this closure is
|
||||
/// attached to.
|
||||
///
|
||||
/// TODO: Periodically check if the DOM element is still there, and if not drop the closure.
|
||||
/// Maybe whenever a DOM node is replaced or truncated we figure out all of it's
|
||||
/// descendants somehow and invalidate those closures..? Need to plan this out..
|
||||
/// At it stands now this hashmap will grow anytime a new element with closures is
|
||||
/// appended or replaced and we will never free those closures.
|
||||
pub type ActiveClosures = HashMap<u32, Vec<DynClosure>>;
|
||||
|
||||
/// Used for keeping a real DOM node up to date based on the current VirtualNode
|
||||
/// and a new incoming VirtualNode that represents our latest DOM state.
|
||||
pub struct DomUpdater {
|
||||
current_vdom: VirtualNode,
|
||||
/// The closures that are currently attached to elements in the page.
|
||||
///
|
||||
/// We keep these around so that they don't get dropped (and thus stop working);
|
||||
///
|
||||
/// FIXME: Drop them when the element is no longer in the page. Need to figure out
|
||||
/// a good strategy for when to do this.
|
||||
pub active_closures: ActiveClosures,
|
||||
root_node: Node,
|
||||
}
|
||||
|
||||
impl DomUpdater {
|
||||
/// Create a new `DomUpdater`.
|
||||
///
|
||||
/// A root `Node` will be created but not added to your DOM.
|
||||
pub fn new(current_vdom: VirtualNode) -> DomUpdater {
|
||||
let created_node = current_vdom.create_dom_node();
|
||||
DomUpdater {
|
||||
current_vdom,
|
||||
active_closures: created_node.closures,
|
||||
root_node: created_node.node,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `DomUpdater`.
|
||||
///
|
||||
/// A root `Node` will be created and appended (as a child) to your passed
|
||||
/// in mount element.
|
||||
pub fn new_append_to_mount(current_vdom: VirtualNode, mount: &Element) -> DomUpdater {
|
||||
let created_node = current_vdom.create_dom_node();
|
||||
mount
|
||||
.append_child(&created_node.node)
|
||||
.expect("Could not append child to mount");
|
||||
DomUpdater {
|
||||
current_vdom,
|
||||
active_closures: created_node.closures,
|
||||
root_node: created_node.node,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `DomUpdater`.
|
||||
///
|
||||
/// A root `Node` will be created and it will replace your passed in mount
|
||||
/// element.
|
||||
pub fn new_replace_mount(current_vdom: VirtualNode, mount: Element) -> DomUpdater {
|
||||
let created_node = current_vdom.create_dom_node();
|
||||
mount
|
||||
.replace_with_with_node_1(&created_node.node)
|
||||
.expect("Could not replace mount element");
|
||||
DomUpdater {
|
||||
current_vdom,
|
||||
active_closures: created_node.closures,
|
||||
root_node: created_node.node,
|
||||
}
|
||||
}
|
||||
|
||||
/// Diff the current virtual dom with the new virtual dom that is being passed in.
|
||||
///
|
||||
/// Then use that diff to patch the real DOM in the user's browser so that they are
|
||||
/// seeing the latest state of the application.
|
||||
pub fn update(&mut self, new_vdom: VirtualNode) {
|
||||
let patches = diff(&self.current_vdom, &new_vdom);
|
||||
|
||||
let active_closures = patch(self.root_node.clone(), &patches).unwrap();
|
||||
|
||||
self.active_closures.extend(active_closures);
|
||||
|
||||
self.current_vdom = new_vdom;
|
||||
}
|
||||
|
||||
/// Return the root node of your application, the highest ancestor of all other nodes in
|
||||
/// your real DOM tree.
|
||||
pub fn root_node(&self) -> Node {
|
||||
// Note that we're cloning the `web_sys::Node`, not the DOM element.
|
||||
// So we're effectively cloning a pointer here, which is fast.
|
||||
self.root_node.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//! virtual-dom-rs provides a virtual dom implementation as well as an `html!` macro
|
||||
//! that you can use to generate a virtual dom.
|
||||
//!
|
||||
//! The virtual dom works on both the client and server. On the client we'll render
|
||||
//! to an `HtmlElement`, and on the server we render to a `String`.
|
||||
|
||||
#![deny(missing_docs)]
|
||||
// #![cfg_attr(test, feature(proc_macro_hygiene))]
|
||||
|
||||
extern crate wasm_bindgen;
|
||||
|
||||
// Used so that `html!` calls work when people depend on this crate since `html!` needs
|
||||
// access to `Closure` when creating event handlers.
|
||||
pub use wasm_bindgen::prelude::Closure;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm_bindgen::JsCast;
|
||||
|
||||
pub extern crate web_sys;
|
||||
pub use web_sys::*;
|
||||
|
||||
pub use virtual_node::*;
|
||||
|
||||
mod diff;
|
||||
pub use crate::diff::*;
|
||||
|
||||
mod patch;
|
||||
pub use crate::patch::*;
|
||||
|
||||
pub use html_macro::html;
|
||||
|
||||
mod dom_updater;
|
||||
pub use self::dom_updater::DomUpdater;
|
||||
|
||||
/// Exports structs and macros that you'll almost always want access to in a virtual-dom
|
||||
/// powered application
|
||||
pub mod prelude {
|
||||
pub use crate::dom_updater::DomUpdater;
|
||||
pub use crate::VirtualNode;
|
||||
pub use html_macro::html;
|
||||
pub use std::vec::IntoIter;
|
||||
pub use virtual_node::IterableNodes;
|
||||
pub use virtual_node::View;
|
||||
pub use wasm_bindgen::prelude::Closure;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
use crate::patch::Patch;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::dom_updater::ActiveClosures;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::{Element, Node, Text};
|
||||
|
||||
/// Apply all of the patches to our old root node in order to create the new root node
|
||||
/// that we desire.
|
||||
/// This is usually used after diffing two virtual nodes.
|
||||
pub fn patch<N: Into<Node>>(root_node: N, patches: &Vec<Patch>) -> Result<ActiveClosures, JsValue> {
|
||||
let root_node: Node = root_node.into();
|
||||
|
||||
let mut cur_node_idx = 0;
|
||||
|
||||
let mut nodes_to_find = HashSet::new();
|
||||
|
||||
for patch in patches {
|
||||
nodes_to_find.insert(patch.node_idx());
|
||||
}
|
||||
|
||||
let mut element_nodes_to_patch = HashMap::new();
|
||||
let mut text_nodes_to_patch = HashMap::new();
|
||||
|
||||
// Closures that were added to the DOM during this patch operation.
|
||||
let mut active_closures = HashMap::new();
|
||||
|
||||
find_nodes(
|
||||
root_node,
|
||||
&mut cur_node_idx,
|
||||
&mut nodes_to_find,
|
||||
&mut element_nodes_to_patch,
|
||||
&mut text_nodes_to_patch,
|
||||
);
|
||||
|
||||
for patch in patches {
|
||||
let patch_node_idx = patch.node_idx();
|
||||
|
||||
if let Some(element) = element_nodes_to_patch.get(&patch_node_idx) {
|
||||
let new_closures = apply_element_patch(&element, &patch)?;
|
||||
active_closures.extend(new_closures);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(text_node) = text_nodes_to_patch.get(&patch_node_idx) {
|
||||
apply_text_patch(&text_node, &patch)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
unreachable!("Getting here means we didn't find the element or next node that we were supposed to patch.")
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
|
||||
fn find_nodes(
|
||||
root_node: Node,
|
||||
cur_node_idx: &mut usize,
|
||||
nodes_to_find: &mut HashSet<usize>,
|
||||
element_nodes_to_patch: &mut HashMap<usize, Element>,
|
||||
text_nodes_to_patch: &mut HashMap<usize, Text>,
|
||||
) {
|
||||
if nodes_to_find.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// We use child_nodes() instead of children() because children() ignores text nodes
|
||||
let children = root_node.child_nodes();
|
||||
let child_node_count = children.length();
|
||||
|
||||
// If the root node matches, mark it for patching
|
||||
if nodes_to_find.get(&cur_node_idx).is_some() {
|
||||
match root_node.node_type() {
|
||||
Node::ELEMENT_NODE => {
|
||||
element_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into());
|
||||
}
|
||||
Node::TEXT_NODE => {
|
||||
text_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into());
|
||||
}
|
||||
other => unimplemented!("Unsupported root node type: {}", other),
|
||||
}
|
||||
nodes_to_find.remove(&cur_node_idx);
|
||||
}
|
||||
|
||||
*cur_node_idx += 1;
|
||||
|
||||
for i in 0..child_node_count {
|
||||
let node = children.item(i).unwrap();
|
||||
|
||||
match node.node_type() {
|
||||
Node::ELEMENT_NODE => {
|
||||
find_nodes(
|
||||
node,
|
||||
cur_node_idx,
|
||||
nodes_to_find,
|
||||
element_nodes_to_patch,
|
||||
text_nodes_to_patch,
|
||||
);
|
||||
}
|
||||
Node::TEXT_NODE => {
|
||||
if nodes_to_find.get(&cur_node_idx).is_some() {
|
||||
text_nodes_to_patch.insert(*cur_node_idx, node.unchecked_into());
|
||||
}
|
||||
|
||||
*cur_node_idx += 1;
|
||||
}
|
||||
Node::COMMENT_NODE => {
|
||||
// At this time we do not support user entered comment nodes, so if we see a comment
|
||||
// then it was a delimiter created by virtual-dom-rs in order to ensure that two
|
||||
// neighboring text nodes did not get merged into one by the browser. So we skip
|
||||
// over this virtual-dom-rs generated comment node.
|
||||
}
|
||||
_other => {
|
||||
// Ignoring unsupported child node type
|
||||
// TODO: What do we do with this situation? Log a warning?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_element_patch(node: &Element, patch: &Patch) -> Result<ActiveClosures, JsValue> {
|
||||
let active_closures = HashMap::new();
|
||||
|
||||
match patch {
|
||||
Patch::AddAttributes(_node_idx, attributes) => {
|
||||
for (attrib_name, attrib_val) in attributes.iter() {
|
||||
node.set_attribute(attrib_name, attrib_val)?;
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::RemoveAttributes(_node_idx, attributes) => {
|
||||
for attrib_name in attributes.iter() {
|
||||
node.remove_attribute(attrib_name)?;
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::Replace(_node_idx, new_node) => {
|
||||
let created_node = new_node.create_dom_node();
|
||||
|
||||
node.replace_with_with_node_1(&created_node.node)?;
|
||||
|
||||
Ok(created_node.closures)
|
||||
}
|
||||
Patch::TruncateChildren(_node_idx, num_children_remaining) => {
|
||||
let children = node.child_nodes();
|
||||
let mut child_count = children.length();
|
||||
|
||||
// We skip over any separators that we placed between two text nodes
|
||||
// -> `<!--ptns-->`
|
||||
// and trim all children that come after our new desired `num_children_remaining`
|
||||
let mut non_separator_children_found = 0;
|
||||
|
||||
for index in 0 as u32..child_count {
|
||||
let child = children
|
||||
.get(min(index, child_count - 1))
|
||||
.expect("Potential child to truncate");
|
||||
|
||||
// If this is a comment node then we know that it is a `<!--ptns-->`
|
||||
// text node separator that was created in virtual_node/mod.rs.
|
||||
if child.node_type() == Node::COMMENT_NODE {
|
||||
continue;
|
||||
}
|
||||
|
||||
non_separator_children_found += 1;
|
||||
|
||||
if non_separator_children_found <= *num_children_remaining as u32 {
|
||||
continue;
|
||||
}
|
||||
|
||||
node.remove_child(&child).expect("Truncated children");
|
||||
child_count -= 1;
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::AppendChildren(_node_idx, new_nodes) => {
|
||||
let parent = &node;
|
||||
|
||||
let mut active_closures = HashMap::new();
|
||||
|
||||
for new_node in new_nodes {
|
||||
let created_node = new_node.create_dom_node();
|
||||
|
||||
parent.append_child(&created_node.node)?;
|
||||
|
||||
active_closures.extend(created_node.closures);
|
||||
}
|
||||
|
||||
Ok(active_closures)
|
||||
}
|
||||
Patch::ChangeText(_node_idx, _new_node) => {
|
||||
unreachable!("Elements should not receive ChangeText patches.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_text_patch(node: &Text, patch: &Patch) -> Result<(), JsValue> {
|
||||
match patch {
|
||||
Patch::ChangeText(_node_idx, new_node) => {
|
||||
node.set_node_value(Some(&new_node.text));
|
||||
}
|
||||
Patch::Replace(_node_idx, new_node) => {
|
||||
node.replace_with_with_node_1(&new_node.create_dom_node().node)?;
|
||||
}
|
||||
other => unreachable!(
|
||||
"Text nodes should only receive ChangeText or Replace patches, not {:?}.",
|
||||
other,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Our Patch enum is intentionally kept in it's own file for easy inclusion into
|
||||
//! The Percy Book.
|
||||
|
||||
use crate::{VText, VirtualNode};
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod apply_patches;
|
||||
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 VirtualNode>),
|
||||
/// 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 VirtualNode),
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user