Chore: move old into packages

This commit is contained in:
Jonathan Kelley committed 2021-02-15 14:25:52 -05:00
1 parent f22ff83190
commit 4b4ea1a09c
20 files changed
+5

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
[package]
name = "dioxus-recoil"
version = "0.0.0"
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+7
View File
@@ -0,0 +1,7 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "dioxus-redux"
version = "0.0.0"
authors = ["Jonathan Kelley <jkelleyrtp@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+37
View File
@@ -0,0 +1,37 @@
pub struct Store<T> {
user_data: T,
}
/// Select just a small bit of the
fn use_selector() {}
/*
// borrow a closure so we can copy the reference
let dispatch = use_dispatch::<UserData>(ctx);
dispatch(|| UserData::Logout)
dispatch()
*/
fn use_dispatch() {}
mod tests {
struct UserData {}
// static SelectLoggedIn: FC<T> = |_| {};
/*
// Merge the various stores into a single context
// This auto generates the appropriate selectors, reducing the need to wrap the app in excess providers
let all = combine_stores(vec![store1, store2]);
<Redux store={all}>
</Redux>
*/
}
+58
View File
@@ -0,0 +1,58 @@
# virtual-dom-rs Changelog
Types of changes:
- `[added]` for new features.
- `[changed]` for changes in existing functionality.
- `[deprecated]` for once-stable features removed in upcoming releases.
- `[removed]` for deprecated features removed in this release.
- `[fixed]` for any bug fixes.
- `[security]` to invite users to upgrade in case of vulnerabilities.
## Not Yet Published
_Here we list notable things that have been merged into the master branch but have not been released yet._
- [added] SVG support [#104](https://github.com/chinedufn/percy/pull/104)
- ...
## 0.6.9 - May 23, 2019
- [added] `on_create_elem` [Docs](https://chinedufn.github.io/percy/html-macro/real-elements-and-nodes/on-create-elem/index.html)
- [added] `inner_html` [Docs](https://chinedufn.github.io/percy/html-macro/setting-inner-html/index.html)
## 0.6.7 - Mar 16, 2019
- [fixed] Spacing between elements is done by inserting space before and after existing text nodes instead of creating new ones.
## 0.6.6 - Mar 6, 2019
- [fixed] Proper spacing in between text nodes and elements in most use cases [PR](https://github.com/chinedufn/percy/pull/97)
- Still need to address [#98](https://github.com/chinedufn/percy/issues/98) and then we should have all cases handled.
## 0.6.5 - Mar 4, 2019
- [added] Start supporting braced text in the `html!` macro [#96](https://github.com/chinedufn/percy/pull/96)
- [removed] Removed the `text!` macro
```rust
let hello = "hello world";
html! { {hello} }
```
## 0.6.4 - Feb 24, 2019
- [fixed] Using the `html!` macro to create an event now uses the fully qualified path to `std::rc::Rc`
- [added] Started adding key support. If a VirtualNode's key attribute changes it will lead to a `Replace` patch.
```rust
// example
html! { <div key="5"></div> }`;
````
## 0.6.1 - Feb 22, 2019
- [fixed] Fix DomUpdater not storing closures for nodes that were created during `Patch::AppendChildren`
and `Patch::Replace`
- [Issue](https://github.com/chinedufn/percy/issues/70)
+47
View File
@@ -0,0 +1,47 @@
[package]
name = "virtual-dom-rs"
version = "0.6.14"
authors = ["Chinedu Francis Nwafili <frankie.nwafili@gmail.com>"]
description = "A standalone Virtual DOM creation, diffing and patching implementation"
keywords = ["virtual", "dom", "wasm", "assembly", "webassembly"]
license = "MIT/Apache-2.0"
repository = "https://github.com/chinedufn/percy"
documentation = "https://chinedufn.github.io/percy/api/virtual_dom_rs/"
edition = "2018"
[dependencies]
js-sys = "0.3"
wasm-bindgen = "0.2.33"
virtual-node = { path = "../virtual-node", version = "0.2.7" }
html-macro = { path = "../html-macro", version = "0.1.8"}
[dependencies.web-sys]
version = "0.3"
features = [
"Comment",
"Document",
"Element",
"HtmlElement",
"EventTarget",
"HtmlCollection",
"Node",
"NodeList",
"Text",
"CharacterData",
"Window",
]
[dev-dependencies]
wasm-bindgen-test = "0.2.33"
console_error_panic_hook = "0.1.5"
[dev-dependencies.web-sys]
version = "0.3"
features = [
"DomTokenList",
"HtmlInputElement",
"Event",
"MouseEvent",
"InputEvent",
"console",
]
+3
View File
@@ -0,0 +1,3 @@
# virtual-dom-rs
> A standalone Virtual DOM creation, diffing and patching implementation
@@ -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);
}
}
+351
View File
@@ -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()
}
}
+44
View File
@@ -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::dom_updater::ActiveClosures;
use crate::patch::Patch;
use std::cmp::min;
use std::collections::HashMap;
use std::collections::HashSet;
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,75 @@
//! 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,
}
}
}
@@ -0,0 +1,116 @@
//! Ensure that our DomUpdater maintains Rc's to closures so that they work even
//! after dropping virtual dom nodes.
//!
//! To run all tests in this file:
//!
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test closures
// #![feature(proc_macro_hygiene)]
use std::cell::RefCell;
use std::collections::hash_map::DefaultHasher;
use std::rc::Rc;
use virtual_dom_rs::prelude::*;
use virtual_dom_rs::DomUpdater;
use wasm_bindgen::JsCast;
use wasm_bindgen_test;
use wasm_bindgen_test::*;
use web_sys::*;
wasm_bindgen_test_configure!(run_in_browser);
// TODO: This test current fails in headless browsers but works in non headless browsers
// (tested in both geckodriver and chromedriver)
// Need to figure out why
#[wasm_bindgen_test]
fn closure_not_dropped() {
let text = Rc::new(RefCell::new("Start Text".to_string()));
let document = web_sys::window().unwrap().document().unwrap();
let mut dom_updater = None;
{
let mut input = make_input_component(Rc::clone(&text));
input
.as_velement_mut()
.expect("Not an element")
.attrs
.insert("id".into(), "old-input-elem".into());
let mount = document.create_element("div").unwrap();
mount.set_id("mount");
document.body().unwrap().append_child(&mount).unwrap();
dom_updater = Some(DomUpdater::new_replace_mount(input, mount));
let mut dom_updater = dom_updater.as_mut().unwrap();
// Input VirtualNode from above gets dropped at the end of this block,
// yet that element held Rc's to the Closure's that power the oninput event.
//
// We're patching the DOM with a new vdom, but since our new vdom doesn't contain any
// new elements, `.create_element` won't get called and so no new Closures will be
// created.
//
// So, we're testing that our old Closure's still work. The reason that they work is
// that dom_updater maintains Rc's to those Closures.
let mut new_node = make_input_component(Rc::clone(&text));
new_node
.as_velement_mut()
.expect("Not an element")
.attrs
.insert("id".into(), "new-input-elem".into());
dom_updater.update(new_node);
}
let dom_updater = dom_updater.as_ref().unwrap();
let input: HtmlInputElement = document
.get_element_by_id("new-input-elem")
.expect("Input element")
.dyn_into()
.unwrap();
let input_event = InputEvent::new("input").unwrap();
assert_eq!(&*text.borrow(), "Start Text");
// After dispatching the oninput event our `text` should have a value of the input elements value.
web_sys::EventTarget::from(input)
.dispatch_event(&input_event)
.unwrap();
assert_eq!(&*text.borrow(), "End Text");
assert_eq!(
dom_updater.active_closures.get(&1).as_ref().unwrap().len(),
1
);
}
// We're just making sure that things compile - other tests give us confidence that the closure
// will work just fine.
//
// https://github.com/chinedufn/percy/issues/81
//
//#[wasm_bindgen_test]
//fn closure_with_no_params_compiles() {
// let _making_sure_this_works = html! {
// <div onclick=|| {}></div>
// };
//}
fn make_input_component(text_clone: Rc<RefCell<String>>) -> VirtualNode {
html! {
<input
// On input we'll set our Rc<RefCell<String>> value to the input elements value
oninput=move |event: Event| {
let input_elem = event.target().unwrap();
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
*text_clone.borrow_mut() = input_elem.value();
}
value="End Text"
>
}
}
@@ -0,0 +1,118 @@
//! Tests that ensure that we create the right DOM element from a VirtualNode
//!
//! To run all tests in this file:
//!
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element
// #![feature(proc_macro_hygiene)]
extern crate wasm_bindgen_test;
extern crate web_sys;
use std::cell::Cell;
use std::rc::Rc;
use wasm_bindgen::JsCast;
use wasm_bindgen_test::*;
use web_sys::{Element, Event, EventTarget, MouseEvent};
use virtual_dom_rs::prelude::*;
wasm_bindgen_test_configure!(run_in_browser);
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element nested_divs
#[wasm_bindgen_test]
fn nested_divs() {
let vdiv = html! { <div> <div> <div></div> </div> </div> };
let div: Element = vdiv.create_dom_node().node.unchecked_into();
assert_eq!(&div.inner_html(), "<div><div></div></div>");
}
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element svg_element
/// TODO: Temporarily disabled until we figure out why it's failing in CI but not failing locally
// #[wasm_bindgen_test]
// fn svg_element() {
// let vdiv = html! { <div><svg xmlns="http://www.w3.org/2000/svg">
// <circle cx="50" cy="50" r="50"/>
// </svg></div> };
// let div: Element = vdiv.create_dom_node().node.unchecked_into();
// assert_eq!(
// &div.inner_html(),
// r#"<svg xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="50"></circle></svg>"#
// );
// }
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element div_with_attributes
#[wasm_bindgen_test]
fn div_with_attributes() {
let vdiv = html! { <div id="id-here" class="two classes"></div> };
let div: Element = vdiv.create_dom_node().node.unchecked_into();
assert_eq!(&div.id(), "id-here");
assert!(div.class_list().contains("two"));
assert!(div.class_list().contains("classes"));
assert_eq!(div.class_list().length(), 2);
}
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element click_event
#[wasm_bindgen_test]
fn click_event() {
let clicked = Rc::new(Cell::new(false));
let clicked_clone = Rc::clone(&clicked);
let div = html! {
<div
onclick=move |_ev: MouseEvent| {
clicked_clone.set(true);
}
>
</div>
};
let click_event = Event::new("click").unwrap();
let div = div.create_dom_node().node;
(EventTarget::from(div))
.dispatch_event(&click_event)
.unwrap();
assert_eq!(*clicked, Cell::new(true));
}
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element inner_html
/// @book start inner-html
#[wasm_bindgen_test]
fn inner_html() {
let div = html! {
<div
unsafe_inner_html="<span>hi</span>"
>
</div>
};
let div: Element = div.create_dom_node().node.unchecked_into();
assert_eq!(div.inner_html(), "<span>hi</span>");
}
// @book end inner-html
/// wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test create_element on_create_elem
/// @book start on-create-elem
#[wasm_bindgen_test]
fn on_create_elem() {
let div = html! {
<div
on_create_elem=|elem: web_sys::Element| {
elem.set_inner_html("Hello world");
}
>
<span>This span should get replaced</span>
</div>
};
let div: Element = div.create_dom_node().node.unchecked_into();
assert_eq!(div.inner_html(), "Hello world");
}
// @book end on-create-elem
@@ -0,0 +1,197 @@
//! Tests that ensure that diffing and patching work properly in a real browser.
//!
//! To run all tests in this file:
//!
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test diff_patch
// #![feature(proc_macro_hygiene)]
extern crate wasm_bindgen_test;
extern crate web_sys;
use wasm_bindgen_test::*;
use virtual_dom_rs::prelude::*;
wasm_bindgen_test_configure!(run_in_browser);
mod diff_patch_test_case;
use self::diff_patch_test_case::DiffPatchTest;
#[wasm_bindgen_test]
fn replace_child() {
DiffPatchTest {
desc: "Replace a root node attribute attribute and a child text node",
old: html! {
<div>
Original element
</div>
},
new: html! { <div> Patched element</div> },
override_expected: None,
}
.test();
}
#[wasm_bindgen_test]
fn truncate_children() {
DiffPatchTest {
desc: "Truncates extra children",
old: html! {
<div>
<div> <div> <b></b> <em></em> </div> </div>
</div>
},
new: html! {
<div>
<div> <div> <b></b> </div> </div>
</div>
},
override_expected: None,
}
.test();
DiffPatchTest {
desc: "https://github.com/chinedufn/percy/issues/48",
old: html! {
<div>
ab <p></p> c
</div>
},
new: html! {
<div>
ab <p></p>
</div>
},
override_expected: None,
}
.test();
}
#[wasm_bindgen_test]
fn remove_attributes() {
DiffPatchTest {
desc: "Removes attributes",
old: html! { <div style=""> </div>
},
new: html! { <div></div> },
override_expected: None,
}
.test();
}
#[wasm_bindgen_test]
fn append_children() {
DiffPatchTest {
desc: "Append a child node",
old: html! { <div> </div>
},
new: html! { <div> <span></span> </div> },
override_expected: None,
}
.test();
}
#[wasm_bindgen_test]
fn text_node_siblings() {
// NOTE: Since there are two text nodes next to eachother we expect a `<!--ptns-->` separator in
// between them.
// @see virtual_node/mod.rs -> create_dom_node() for more information
// TODO: A little more spacing than there should be in between the text nodes ... but doesn't
// impact the user experience so we can look into that later..
let override_expected = Some(
r#"<div id="after"><span> The button has been clicked: <!--ptns--> world </span></div>"#,
);
let old1 = VirtualNode::text("The button has been clicked: ");
let old2 = VirtualNode::text("hello");
let new1 = VirtualNode::text("The button has been clicked: ");
let new2 = VirtualNode::text("world");
DiffPatchTest {
desc: "Diff patch on text node siblings",
old: html! {
<div id="before">
<span> { {old1} {old2} } </span>
</div>
},
new: html! {
<div id="after">
<span> { {new1} {new2} } </span>
</div>
},
override_expected,
}
.test();
}
#[wasm_bindgen_test]
fn append_text_node() {
DiffPatchTest {
desc: "Append text node",
old: html! { <div> </div> },
new: html! { <div> Hello </div> },
override_expected: None,
}
.test();
}
#[wasm_bindgen_test]
fn append_sibling_text_nodes() {
let text1 = VirtualNode::text("Hello");
let text2 = VirtualNode::text("World");
DiffPatchTest {
desc: "Append sibling text nodes",
old: html! { <div> </div> },
new: html! { <div> {text1} {text2} </div> },
override_expected: None,
}
.test();
}
#[wasm_bindgen_test]
fn replace_with_children() {
DiffPatchTest {
desc: "Replace node that has children",
old: html! { <table><tr><th>0</th></tr><tr><td>1</td></tr></table> },
new: html! { <table><tr><td>2</td></tr><tr><th>3</th></tr></table> },
override_expected: None,
}
.test();
}
// https://github.com/chinedufn/percy/issues/62
#[wasm_bindgen_test]
fn replace_element_with_text_node() {
DiffPatchTest {
desc: "#62: Replace element with text node",
old: html! { <span> <br> </span> },
new: html! { <span> a </span> },
override_expected: None,
}
.test();
}
// https://github.com/chinedufn/percy/issues/68
#[wasm_bindgen_test]
fn text_root_node() {
DiffPatchTest {
desc: "Patching of text root node works",
old: html! { Old text },
new: html! { New text },
override_expected: None,
}
.test();
}
#[wasm_bindgen_test]
fn replace_text_with_element() {
DiffPatchTest {
desc: "Replacing a text node with an element works",
old: html! { <div>a</div> },
new: html! { <div><br></div> },
override_expected: None,
}
.test();
}
@@ -0,0 +1,53 @@
//! Kept in its own file to more easily import into the book
use console_error_panic_hook;
use virtual_dom_rs::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Element, Node};
/// A test case that both diffing and patching are working in a real browser
pub struct DiffPatchTest<'a> {
/// Description of the test case.
pub desc: &'static str,
/// The old virtual node.
pub old: VirtualNode,
/// The new virtual node.
pub new: VirtualNode,
/// By default we generate the expected based on `new.to_string()`. You can
/// use this field to override the expected HTML after patching.
pub override_expected: Option<&'a str>,
}
impl<'a> DiffPatchTest<'a> {
pub fn test(&mut self) {
console_error_panic_hook::set_once();
let document = web_sys::window().unwrap().document().unwrap();
// Create a DOM node of the virtual root node
let root_node: Node = self.old.create_dom_node().node;
// Clone since virtual_dom_rs::patch takes ownership of the root node.
let patched_root_node: Node = root_node.clone();
// Generate patches
let patches = virtual_dom_rs::diff(&self.old, &self.new);
// Patch our root node. It should now look like `self.new`
virtual_dom_rs::patch(root_node, &patches);
// Determine the expected outer HTML
let expected_outer_html = match self.override_expected {
Some(ref expected) => expected.to_string(),
None => self.new.to_string(),
};
let actual_outer_html = match patched_root_node.node_type() {
Node::ELEMENT_NODE => patched_root_node.unchecked_into::<Element>().outer_html(),
Node::TEXT_NODE => patched_root_node.text_content().unwrap_or("".into()),
_ => panic!("Unhandled node type"),
};
assert_eq!(&actual_outer_html, &expected_outer_html, "{}", self.desc);
}
}
@@ -0,0 +1,147 @@
//! Ensure that our DomUpdater maintains Rc's to closures so that they work even
//! after dropping virtual dom nodes.
//!
//! To run all tests in this file:
//!
//! wasm-pack test crates/virtual-dom-rs --chrome --headless -- --test dom_updater
// #![feature(proc_macro_hygiene)]
use console_error_panic_hook;
use std::cell::RefCell;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use virtual_dom_rs::prelude::*;
use virtual_dom_rs::DomUpdater;
use wasm_bindgen::JsCast;
use wasm_bindgen_test;
use wasm_bindgen_test::*;
use web_sys::*;
wasm_bindgen_test_configure!(run_in_browser);
// Verify that our DomUpdater's patch method works.
// We test a simple case here, since diff_patch.rs is responsible for testing more complex
// diffing and patching.
#[wasm_bindgen_test]
fn patches_dom() {
console_error_panic_hook::set_once();
let document = web_sys::window().unwrap().document().unwrap();
let vdom = html! { <div></div> };
let mut dom_updater = DomUpdater::new(vdom);
let new_vdom = html! { <div id="patched"></div> };
dom_updater.update(new_vdom);
document
.body()
.unwrap()
.append_child(&dom_updater.root_node());
assert_eq!(document.query_selector("#patched").unwrap().is_some(), true);
}
// When you replace a DOM node with another DOM node we need to make sure that the closures
// from the new DOM node are stored by the DomUpdater otherwise they'll get dropped and
// won't work.
#[wasm_bindgen_test]
fn updates_active_closure_on_replace() {
console_error_panic_hook::set_once();
let document = web_sys::window().unwrap().document().unwrap();
let body = document.body().unwrap();
let old = html! { <div> </div> };
let mut dom_updater = DomUpdater::new_append_to_mount(old, &body);
let text = Rc::new(RefCell::new("Start Text".to_string()));
let text_clone = Rc::clone(&text);
let id = "update-active-closures-on-replace";
{
let replace_node = html! {
<input
id=id
oninput=move |event: Event| {
let input_elem = event.target().unwrap();
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
*text_clone.borrow_mut() = input_elem.value();
}
value="End Text"
>
};
// New node replaces old node.
// We are testing that we've stored this new node's closures even though `new` will be dropped
// at the end of this block.
dom_updater.update(replace_node);
}
let input_event = InputEvent::new("input").unwrap();
assert_eq!(&*text.borrow(), "Start Text");
// After dispatching the oninput event our `text` should have a value of the input elements value.
let input = document.get_element_by_id(&id).unwrap();
web_sys::EventTarget::from(input)
.dispatch_event(&input_event)
.unwrap();
assert_eq!(&*text.borrow(), "End Text");
}
// When you replace a DOM node with another DOM node we need to make sure that the closures
// from the new DOM node are stored by the DomUpdater otherwise they'll get dropped and
// won't work.
#[wasm_bindgen_test]
fn updates_active_closures_on_append() {
console_error_panic_hook::set_once();
let document = web_sys::window().unwrap().document().unwrap();
let body = document.body().unwrap();
let old = html! { <div> </div> };
let mut dom_updater = DomUpdater::new_append_to_mount(old, &body);
let text = Rc::new(RefCell::new("Start Text".to_string()));
let text_clone = Rc::clone(&text);
let id = "update-active-closures-on-append";
{
let append_node = html! {
<div>
<input
id=id
oninput=move |event: Event| {
let input_elem = event.target().unwrap();
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
*text_clone.borrow_mut() = input_elem.value();
}
value="End Text"
>
</div>
};
// New node gets appended into the DOM.
// We are testing that we've stored this new node's closures even though `new` will be dropped
// at the end of this block.
dom_updater.update(append_node);
}
let input_event = InputEvent::new("input").unwrap();
assert_eq!(&*text.borrow(), "Start Text");
// After dispatching the oninput event our `text` should have a value of the input elements value.
let input = document.get_element_by_id(id).unwrap();
web_sys::EventTarget::from(input)
.dispatch_event(&input_event)
.unwrap();
assert_eq!(&*text.borrow(), "End Text");
}
@@ -0,0 +1,47 @@
// #![feature(proc_macro_hygiene)]
extern crate wasm_bindgen_test;
extern crate web_sys;
use std::cell::Cell;
use std::rc::Rc;
use wasm_bindgen_test::*;
use std::cell::RefCell;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::*;
use virtual_dom_rs::prelude::*;
wasm_bindgen_test_configure!(run_in_browser);
// Make sure that we successfully attach an event listener and see it work.
#[wasm_bindgen_test]
fn on_input() {
let text = Rc::new(RefCell::new("Start Text".to_string()));
let text_clone = Rc::clone(&text);
let input = html! {
<input
// On input we'll set our Rc<RefCell<String>> value to the input elements value
oninput=move |event: Event| {
let input_elem = event.target().unwrap();
let input_elem = input_elem.dyn_into::<HtmlInputElement>().unwrap();
*text_clone.borrow_mut() = input_elem.value();
}
value="End Text"
>
};
let input_event = InputEvent::new("input").unwrap();
let input = input.create_dom_node().node;
assert_eq!(&*text.borrow(), "Start Text");
// After dispatching the oninput event our `text` should have a value of the input elements value.
web_sys::EventTarget::from(input)
.dispatch_event(&input_event)
.unwrap();
assert_eq!(&*text.borrow(), "End Text");
}