Feat: event loop

This commit is contained in:
Jonathan Kelley committed 2021-02-12 16:11:33 -05:00
1 parent 8295ac4b3d
commit ea2aa4b0c9
39 files changed
+495 -287

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
{
"rust-analyzer.inlayHints.enable": true
"rust-analyzer.inlayHints.enable": false
}
+1 -1
View File
@@ -2,7 +2,7 @@
//! for components to be used within Nodes.
//!
use crate::inner::*;
use crate::innerlude::*;
/// The `Component` trait refers to any struct or funciton that can be used as a component
/// We automatically implement Component for FC<T>
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::prelude::*;
use crate::{inner::Scope, nodes::VNode};
use crate::{innerlude::Scope, nodes::VNode};
use bumpalo::Bump;
use hooks::Hook;
use std::{
+33 -12
View File
@@ -1,15 +1,4 @@
use crate::prelude::VNode;
// use crate::{
// cached_set::{CacheId, CachedSet},
// change_list::ChangeListBuilder,
// events::EventsRegistry,
// node::{Attribute, ElementNode, Listener, Node, NodeKind, TextNode},
// };
// use fxhash::{FxHashMap, FxHashSet};
// use std::cmp::Ordering;
// use std::u32;
// use wasm_bindgen::UnwrapThrowExt;
use crate::innerlude::{VNode, VText};
// Diff the `old` node with the `new` node. Emits instructions to modify a
// physical DOM node that reflects `old` into something that reflects `new`.
@@ -28,6 +17,38 @@ pub(crate) fn diff(
new: &VNode,
// cached_roots: &mut FxHashSet<CacheId>,
) {
match (old, new) {
// This case occurs when we generate two text nodes that are the sa
(VNode::Text(VText { text: old_text }), VNode::Text(VText { text: new_text })) => {
//
if old_text != new_text {}
}
// compare elements
// if different, schedule different types of update
(VNode::Element(_), VNode::Element(_)) => {}
(VNode::Element(_), VNode::Text(_)) => {}
(VNode::Element(_), VNode::Component(_)) => {}
(VNode::Text(_), VNode::Element(_)) => {}
(VNode::Text(_), VNode::Component(_)) => {}
(VNode::Component(_), VNode::Element(_)) => {}
(VNode::Component(_), VNode::Text(_)) => {}
// No immediate change to dom. If props changed though, queue a "props changed" update
// However, mark these for a
(VNode::Component(_), VNode::Component(_)) => {}
//
(VNode::Suspended, _) | (_, VNode::Suspended) => {
todo!("Suspended components not currently available")
} // (VNode::Element(_), VNode::Suspended) => {}
// (VNode::Text(_), VNode::Suspended) => {}
// (VNode::Component(_), VNode::Suspended) => {}
// (VNode::Suspended, VNode::Element(_)) => {}
// (VNode::Suspended, VNode::Text(_)) => {}
// (VNode::Suspended, VNode::Suspended) => {}
// (VNode::Suspended, VNode::Component(_)) => {}
}
todo!()
// match (&new.kind, &old.kind) {
// (
+23 -24
View File
@@ -7,38 +7,37 @@ pub enum Error {
#[error("No event to progress")]
NoEvent,
#[error("Out of compute credits")]
OutOfCredits,
// #[error("Out of compute credits")]
// OutOfCredits,
/// Used when errors need to propogate but are too unique to be typed
#[error("{0}")]
Unique(String),
// /// Used when errors need to propogate but are too unique to be typed
// #[error("{0}")]
// Unique(String),
#[error("GraphQL error: {0}")]
GraphQL(String),
// #[error("GraphQL error: {0}")]
// GraphQL(String),
// TODO(haze): Remove, or make a better error. This is pretty much useless
#[error("GraphQL response mismatch. Got {found} but expected {expected}")]
GraphQLMisMatch {
expected: &'static str,
found: String,
},
// // TODO(haze): Remove, or make a better error. This is pretty much useless
// #[error("GraphQL response mismatch. Got {found} but expected {expected}")]
// GraphQLMisMatch {
// expected: &'static str,
// found: String,
// },
#[error("Difference detected in SystemTime! {0}")]
SystemTime(#[from] std::time::SystemTimeError),
// #[error("Difference detected in SystemTime! {0}")]
// SystemTime(#[from] std::time::SystemTimeError),
#[error("Failed to parse Integer")]
ParseInt(#[from] std::num::ParseIntError),
// #[error("Failed to parse Integer")]
// ParseInt(#[from] std::num::ParseIntError),
#[error("")]
MissingAuthentication,
// #[error("")]
// MissingAuthentication,
#[error("Failed to create experiment run: {0}")]
FailedToCreateExperimentRun(String),
#[error("Could not find shared behavior with ID {0}")]
MissingSharedBehavior(String),
// #[error("Failed to create experiment run: {0}")]
// FailedToCreateExperimentRun(String),
// #[error("Could not find shared behavior with ID {0}")]
// MissingSharedBehavior(String),
#[error("I/O Error: {0}")]
IO(#[from] std::io::Error),
}
+13
View File
@@ -4,6 +4,19 @@
//! 3rd party renderers are responsible for forming this virtual events from events
//!
//! The goal here is to provide a consistent event interface across all renderer types
use generational_arena::Index;
pub struct EventTrigger {
pub component_id: Index,
pub listener_id: u32,
pub event: VirtualEvent,
}
impl EventTrigger {
pub fn new() -> Self {
todo!()
}
}
pub enum VirtualEvent {
ClipboardEvent,
+2 -2
View File
@@ -9,7 +9,7 @@ pub use use_ref_def::use_ref;
pub use use_state_def::use_state;
mod use_state_def {
use crate::inner::*;
use crate::innerlude::*;
use std::{cell::RefCell, ops::DerefMut, rc::Rc};
struct UseState<T: 'static> {
@@ -80,7 +80,7 @@ mod use_state_def {
}
mod use_ref_def {
use crate::inner::*;
use crate::innerlude::*;
use std::{cell::RefCell, ops::DerefMut};
pub struct UseRef<T: 'static> {
+2 -2
View File
@@ -83,7 +83,7 @@ pub mod builder {
}
// types used internally that are important
pub(crate) mod inner {
pub(crate) mod innerlude {
pub use crate::component::{Component, Properties};
use crate::context::hooks::Hook;
pub use crate::context::Context;
@@ -121,7 +121,7 @@ pub mod prelude {
// pub use nodes::iterables::IterableNodes;
/// This type alias is an internal way of abstracting over the static functions that represent components.
pub use crate::inner::FC;
pub use crate::innerlude::FC;
// TODO @Jon, fix this
// hack the VNode type until VirtualNode is fixed in the macro crate
+3 -5
View File
@@ -1071,11 +1071,9 @@ pub fn attr<'a>(name: &'a str, value: &'a str) -> Attribute<'a> {
/// ```
pub fn on<'a, 'b, F: 'static>(
bump: &'a Bump,
event: &'a str,
callback: impl Fn(()) + 'static,
) -> Listener<'a>
// F: Fn(()) + 'b,
{
event: &'static str,
callback: impl Fn(()) + 'a,
) -> Listener<'a> {
Listener {
event,
callback: bump.alloc(callback),
+6 -6
View File
@@ -174,11 +174,11 @@ mod velement {
}
/// An event listener.
pub struct Listener<'a> {
pub struct Listener<'bump> {
/// The type of event to listen for.
pub(crate) event: &'a str,
pub(crate) event: &'static str,
/// The callback to invoke when the event happens.
pub(crate) callback: &'a (dyn Fn(())),
pub(crate) callback: &'bump (dyn Fn(())),
}
/// The key for keyed children.
@@ -226,8 +226,8 @@ mod velement {
mod vtext {
#[derive(PartialEq)]
pub struct VText<'a> {
pub text: &'a str,
pub struct VText<'bump> {
pub text: &'bump str,
}
impl<'a> VText<'a> {
@@ -245,7 +245,7 @@ mod vtext {
/// Virtual Components for custom user-defined components
/// Only supports the functional syntax
mod vcomponent {
use crate::inner::{Properties, FC};
use crate::innerlude::{Properties, FC};
use std::{any::TypeId, fmt, future::Future, marker::PhantomData};
use super::VNode;
+17 -13
View File
@@ -1,4 +1,4 @@
use crate::inner::*;
use crate::innerlude::*;
use crate::nodes::VNode;
use crate::{context::hooks::Hook, diff::diff};
use bumpalo::Bump;
@@ -19,26 +19,26 @@ pub struct Scope {
// These hooks are actually references into the hook arena
// These two could be combined with "OwningRef" to remove unsafe usage
// could also use ourborous
hooks: RefCell<Vec<*mut Hook>>,
hook_arena: typed_arena::Arena<Hook>,
pub hooks: RefCell<Vec<*mut Hook>>,
pub hook_arena: typed_arena::Arena<Hook>,
// Map to the parent
parent: Option<Index>,
pub parent: Option<Index>,
// todo, do better with the active frame stuff
frames: [Bump; 2],
pub frames: [Bump; 2],
// somehow build this vnode with a lifetime tied to self
cur_node: *mut VNode<'static>,
pub cur_node: *mut VNode<'static>,
active_frame: u8,
pub active_frame: u8,
// IE Which listeners need to be woken up?
listeners: Vec<Box<dyn Fn()>>,
pub listeners: Vec<Box<dyn Fn()>>,
//
props_type: TypeId,
caller: *const i32,
pub props_type: TypeId,
pub caller: *const i32,
}
impl Scope {
@@ -76,7 +76,7 @@ impl Scope {
/// Create a new context and run the component with references from the Virtual Dom
/// This function downcasts the function pointer based on the stored props_type
///
/// Props is ?Sized because we borrow the props
/// Props is ?Sized because we borrow the props and don't need to know the size. P (sized) is used as a marker (unsized)
pub(crate) fn run<'a, P: Properties + ?Sized>(&self, props: &'a P) {
let bump = &self.frames[0];
@@ -96,7 +96,7 @@ impl Scope {
We hide the generic bound on the function item by casting it to raw pointer. When the function is actually called,
we transmute the function back using the props as reference.
we could do a better check to make sure that the TypeID is correct
we could do a better check to make sure that the TypeID is correct before casting
--
This is safe because we check that the generic type matches before casting.
*/
@@ -104,7 +104,11 @@ impl Scope {
let new_nodes = caller(ctx, props);
let old_nodes: &mut VNode<'static> = unsafe { &mut *self.cur_node };
// perform the diff, dumping into the change list
// TODO: Iterate through the new nodes
// move any listeners into ourself
// perform the diff, dumping into the mutable change list
// this doesnt perform any "diff compression" where an event and a re-render
crate::diff::diff(old_nodes, &new_nodes);
}
}
+3
View File
@@ -13,6 +13,9 @@ mod validation {
// look them up by their unique id.
// When the DomUpdater sees that the element no longer exists it will drop all of it's
// Rc'd Closures for those events.
// It doesn't quite make sense to keep this here, perhaps just in the html crate?
// Dioxus itself shouldn't be concerned with the attribute names
// a ftk!
static SELF_CLOSING_TAGS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
[
"area", "base", "br", "col", "hr", "img", "input", "link", "meta", "param", "command",
+63 -31
View File
@@ -1,29 +1,5 @@
/*
The Dioxus Virtual Dom integrates an event system and virtual nodes to create reactive user interfaces.
The Dioxus VDom uses the same underlying mechanics as Dodrio (double buffering, bump dom, etc).
Instead of making the allocator very obvious, we choose to parametrize over the DomTree trait. For our purposes,
the DomTree trait is simply an abstraction over a lazy dom builder, much like the iterator trait.
This means we can accept DomTree anywhere as well as return it. All components therefore look like this:
```ignore
function Component(ctx: Context<()>) -> VNode {
ctx.view(html! {<div> "hello world" </div>})
}
```
It's not quite as sexy as statics, but there's only so much you can do. The goal is to get statics working with the FC macro,
so types don't get in the way of you and your component writing. Fortunately, this is all generic enough to be split out
into its own lib (IE, lazy loading wasm chunks by function (exciting stuff!))
```ignore
#[fc] // gets translated into a function.
static Component: FC = |ctx| {
ctx.view(html! {<div> "hello world" </div>})
}
```
*/
use crate::inner::*;
use crate::nodes::VNode;
use crate::{events::EventTrigger, innerlude::*};
use any::Any;
use bumpalo::Bump;
use generational_arena::{Arena, Index};
@@ -46,13 +22,12 @@ pub struct VirtualDom<P: Properties> {
base_scope: Index,
/// Components generate lifecycle events
event_queue: Vec<LifecycleEvent>,
event_queue: Vec<LifecycleEvent>, // wrap in a lock or something so
// mark the root props with P, even though they're held by the root component
_p: PhantomData<P>,
}
/// Implement VirtualDom with no props for components that initialize their state internal to the VDom rather than externally.
impl VirtualDom<()> {
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
///
@@ -63,7 +38,6 @@ impl VirtualDom<()> {
}
}
/// Implement the VirtualDom for any Properties
impl<P: Properties + 'static> VirtualDom<P> {
/// Start a new VirtualDom instance with a dependent props.
/// Later, the props can be updated by calling "update" with a new set of props, causing a set of re-renders.
@@ -97,15 +71,69 @@ impl<P: Properties + 'static> VirtualDom<P> {
}
}
/// With access to the virtual dom, schedule an update to the Root component's props
pub fn update_props(&mut self, new_props: P) {
self.event_queue.push(LifecycleEvent {
event_type: LifecycleType::PropsChanged {
props: Box::new(new_props),
},
index: self.base_scope,
});
}
/// Pop an event off the event queue and process it
/// Update the root props, and progress
/// Takes a bump arena to allocate into, making the diff phase as fast as possible
///
pub fn progress(&mut self) -> Result<()> {
let event = self.event_queue.pop().ok_or(Error::NoEvent)?;
process_event(self, event)
}
/// Update the root props, causing a full event cycle
pub fn update_props(&mut self, new_props: P) {}
/// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered.
///
/// Given a synthetic event, the component that triggered the event, and the index of the callback, this runs the virtual
/// dom to completion, tagging components that need updates, compressing events together, and finally emitting a single
/// change list.
///
/// If implementing an external renderer, this is the perfect method to combine with an async event loop that waits on
/// listeners.
///
/// ```ignore
///
///
///
///
/// ```
pub async fn progress_with_event(&mut self, evt: EventTrigger) -> Result<()> {
let EventTrigger {
component_id,
listener_id,
event,
} = evt;
let component = self
.components
.get(component_id)
// todo: update this with a dedicated error type so implementors know what went wrong
.expect("Component should exist if an event was triggered");
let listener = component
.listeners
.get(listener_id as usize)
.expect("Listener should exist if it was triggered")
.as_ref();
// Run the callback
// Theoretically, this should
listener();
Ok(())
}
pub async fn progress_completely(&mut self) -> Result<()> {
Ok(())
}
}
/// Using mutable access to the Virtual Dom, progress a given lifecycle event
@@ -159,6 +187,9 @@ fn process_event<P: Properties>(
// Event from renderer was fired with a given listener ID
//
LifecycleType::Callback { listener_id } => {}
// Run any post-render callbacks on a component
LifecycleType::Rendered => {}
}
Ok(())
@@ -182,6 +213,7 @@ pub enum LifecycleType {
PropsChanged {
props: Box<dyn Properties>,
},
Rendered,
Mounted,
Removed,
Messaged,
-9
View File
@@ -1,9 +0,0 @@
[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
@@ -1,7 +0,0 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
-9
View File
@@ -1,9 +0,0 @@
[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
@@ -1,37 +0,0 @@
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
@@ -1,58 +0,0 @@
# 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
@@ -1,47 +0,0 @@
[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
@@ -1,3 +0,0 @@
# virtual-dom-rs
> A standalone Virtual DOM creation, diffing and patching implementation
@@ -1,26 +0,0 @@
//! 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
@@ -1,351 +0,0 @@
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",
// })
// }
}
-103
View File
@@ -1,103 +0,0 @@
//! 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
@@ -1,44 +0,0 @@
//! 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;
}
@@ -1,217 +0,0 @@
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(())
}
-76
View File
@@ -1,76 +0,0 @@
//! 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,
}
}
}
-116
View File
@@ -1,116 +0,0 @@
//! 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"
>
}
}
@@ -1,118 +0,0 @@
//! 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
-197
View File
@@ -1,197 +0,0 @@
//! 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();
}
@@ -1,53 +0,0 @@
//! 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);
}
}
@@ -1,147 +0,0 @@
//! 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");
}
-47
View File
@@ -1,47 +0,0 @@
// #![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");
}
+4 -2
View File
@@ -9,9 +9,11 @@ edition = "2018"
[dependencies]
dioxus-core = { path = "../core" }
js-sys = "0.3"
wasm-bindgen = "0.2.69"
html-validation = { path = "../html-validation", version = "0.1.1" }
wasm-bindgen = "0.2.70"
lazy_static = "1.4.0"
wasm-bindgen-futures = "0.4.20"
futures = "0.3.12"
# html-validation = { path = "../html-validation", version = "0.1.1" }
[dependencies.web-sys]
version = "0.3"
+21
View File
@@ -0,0 +1,21 @@
use dioxus_core::prelude::*;
use dioxus_web::WebsysRenderer;
fn main() {
// todo: set this up so the websys render can spawn itself rather than having to wrap it
// almost like bundling an executor with the wasm version
wasm_bindgen_futures::spawn_local(async {
WebsysRenderer::new(Example)
.run()
.await
.expect("Dioxus Failed! This should *not* happen!")
});
}
static Example: FC<()> = |ctx, props| {
ctx.view(html! {
<div>
"Hello world!"
</div>
})
};
+87 -185
View File
@@ -1,211 +1,113 @@
use std::{collections::HashMap, fmt, rc::Rc};
use web_sys::{self, Element, EventTarget, Node, Text};
//! Dioxus WebSys
//!
//! This crate implements a renderer of the Dioxus Virtual DOM for the web browser.
//!
//! While `VNode` supports "to_string" directly, it renders child components as the RSX! macro tokens. For custom components,
//! an external renderer is needed to progress the component lifecycles. The `WebsysRenderer` shows how to use the Virtual DOM
//! API to progress these lifecycle events to generate a fully-mounted Virtual DOM instance which can be renderer in the
//! `render` method.
//!
//! ```ignore
//! fn main() {
//! let renderer = WebsysRenderer::<()>::new(|_| html! {<div> "Hello world" </div>});
//! let output = renderer.render();
//! assert_eq!(output, "<div>Hello World</div>");
//! }
//! ```
//!
//! The `WebsysRenderer` is particularly useful when needing to cache a Virtual DOM in between requests
//!
use dioxus_core::prelude::{VElement, VNode, VText, VirtualNode};
use std::ops::Deref;
use std::sync::Mutex;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use dioxus_core::{
events::EventTrigger,
prelude::{Properties, VNode, VirtualDom, FC},
};
use futures::{channel::mpsc, future, SinkExt, StreamExt};
use mpsc::UnboundedSender;
pub struct DomRenderer {}
/// The `WebsysRenderer` provides a way of rendering a Dioxus Virtual DOM to the browser's DOM.
/// Under the hood, we leverage WebSys and interact directly with the DOM
// Used to uniquely identify elements that contain closures so that the DomUpdater can
// look them up by their unique id.
// When the DomUpdater sees that the element no longer exists it will drop all of it's
// Rc'd Closures for those events.
use lazy_static::lazy_static;
lazy_static! {
static ref ELEM_UNIQUE_ID: Mutex<u32> = Mutex::new(0);
///
pub struct WebsysRenderer<T: Properties> {
internal_dom: VirtualDom<T>,
}
fn create_unique_identifier() -> u32 {
let mut elem_unique_id = ELEM_UNIQUE_ID.lock().unwrap();
*elem_unique_id += 1;
*elem_unique_id
}
/// A node along with all of the closures that were created for that
/// node's events and all of it's child node's events.
pub struct CreatedNode<T> {
/// A `Node` or `Element` that was created from a `VirtualNode`
pub node: T,
/// A map of a node's unique identifier along with all of the Closures for that node.
/// Implement VirtualDom with no props for components that initialize their state internal to the VDom rather than externally.
impl WebsysRenderer<()> {
/// Create a new instance of the Dioxus Virtual Dom with no properties for the root component.
///
/// The DomUpdater uses this to look up nodes and see if they're still in the page. If not
/// the reference that we maintain to their closure will be dropped, thus freeing the Closure's
/// memory.
pub closures: HashMap<u32, Vec<DynClosure>>,
}
impl<T> CreatedNode<T> {
pub fn without_closures<N: Into<T>>(node: N) -> Self {
CreatedNode {
node: node.into(),
closures: HashMap::with_capacity(0),
}
/// This means that the root component must either consumes its own context, or statics are used to generate the page.
/// The root component can access things like routing in its context.
pub fn new(root: FC<()>) -> Self {
Self::new_with_props(root, ())
}
}
impl<T> Deref for CreatedNode<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.node
impl<T: Properties + 'static> WebsysRenderer<T> {
/// Create a new text-renderer instance from a functional component root.
/// Automatically progresses the creation of the VNode tree to completion.
///
/// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom`
pub fn new_with_props(root: FC<T>, root_props: T) -> Self {
Self::from_vdom(VirtualDom::new_with_props(root, root_props))
}
}
impl From<CreatedNode<Element>> for CreatedNode<Node> {
fn from(other: CreatedNode<Element>) -> CreatedNode<Node> {
CreatedNode {
node: other.node.into(),
closures: other.closures,
}
/// Create a new text renderer from an existing Virtual DOM.
/// This will progress the existing VDom's events to completion.
pub fn from_vdom(dom: VirtualDom<T>) -> Self {
Self { internal_dom: dom }
}
}
//----------------------------------
// Create nodes for the VNode types
// ---------------------------------
/// Run the renderer, progressing any events that crop up
/// Yield on event handlers
/// If the dom errors out, self is consumed and the dom is torn down
pub async fn run(self) -> dioxus_core::error::Result<()> {
let WebsysRenderer { mut internal_dom } = self;
/// Return a `Text` element from a `VirtualNode`, typically right before adding it
/// into the DOM.
pub fn create_text_node(text_node: &VText) -> Text {
let document = web_sys::window().unwrap().document().unwrap();
document.create_text_node(&text_node.text)
}
// Progress the mount of the root component
internal_dom
.progress()
.expect("Progressing the root failed :(");
/// Build a DOM element by recursively creating DOM nodes for this element and it's
/// children, it's children's children, etc.
pub fn create_element_node(velement: &VElement) -> CreatedNode<Element> {
let document = web_sys::window().unwrap().document().unwrap();
// set up the channels to connect listeners to the event loop
let (sender, mut receiver) = mpsc::unbounded::<EventTrigger>();
let element = if html_validation::is_svg_namespace(&velement.tag) {
document
.create_element_ns(Some("http://www.w3.org/2000/svg"), &velement.tag)
.unwrap()
} else {
document.create_element(&velement.tag).unwrap()
};
let mut closures = HashMap::new();
velement.attrs.iter().for_each(|(name, value)| {
if name == "unsafe_inner_html" {
element.set_inner_html(value);
return;
// Iterate through the nodes, attaching the closure and sender to the listener
{
let mut remote_sender = sender.clone();
let f = move || {
let event = EventTrigger::new();
wasm_bindgen_futures::spawn_local(async move {
remote_sender
.send(event)
.await
.expect("Updating receiver failed");
})
};
}
element
.set_attribute(name, value)
.expect("Set element attribute in create element");
});
// Event loop waits for the receiver to finish up
// TODO! Connect the sender to the virtual dom's suspense system
// Suspense is basically an external event that can force renders to specific nodes
while let Some(event) = receiver.next().await {
// event is triggered
// relevant listeners are ran
// internal state is modified, components are tagged for changes
todo!("Support events properly in web ");
// if velement.events.0.len() > 0 {
// let unique_id = create_unique_identifier();
// element
// .set_attribute("data-vdom-id".into(), &unique_id.to_string())
// .expect("Could not set attribute on element");
// closures.insert(unique_id, vec![]);
// velement.events.0.iter().for_each(|(onevent, callback)| {
// // onclick -> click
// let event = &onevent[2..];
// let current_elem: &EventTarget = element.dyn_ref().unwrap();
// current_elem
// .add_event_listener_with_callback(event, callback.as_ref().as_ref().unchecked_ref())
// .unwrap();
// closures
// .get_mut(&unique_id)
// .unwrap()
// .push(Rc::clone(callback));
// });
// }
let mut previous_node_was_text = false;
velement.children.iter().for_each(|child| {
match child {
VNode::Text(text_node) => {
let current_node = element.as_ref() as &web_sys::Node;
// We ensure that the text siblings are patched by preventing the browser from merging
// neighboring text nodes. Originally inspired by some of React's work from 2016.
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
// -> https://github.com/facebook/react/pull/5753
//
// `ptns` = Percy text node separator
if previous_node_was_text {
let separator = document.create_comment("ptns");
current_node
.append_child(separator.as_ref() as &web_sys::Node)
.unwrap();
}
current_node
.append_child(&create_text_node(&text_node))
// .append_child(&text_node.create_text_node())
.unwrap();
previous_node_was_text = true;
}
VNode::Element(element_node) => {
previous_node_was_text = false;
let child = create_element_node(&element_node);
// let child = element_node.create_element_node();
let child_elem: Element = child.node;
closures.extend(child.closures);
element.append_child(&child_elem).unwrap();
}
VNode::Component(component) => {
//
todo!("Support components in the web properly");
match internal_dom.progress_with_event(event).await {
Err(_) => {}
Ok(_) => render_diffs(),
}
// waiting for next event to arrive from the external triggers
}
});
todo!("Support events properly in web ");
// if let Some(on_create_elem) = velement.events.0.get("on_create_elem") {
// let on_create_elem: &js_sys::Function = on_create_elem.as_ref().as_ref().unchecked_ref();
// on_create_elem
// .call1(&wasm_bindgen::JsValue::NULL, &element)
// .unwrap();
// }
CreatedNode {
node: element,
closures,
Ok(())
}
}
/// Box<dyn AsRef<JsValue>>> is our js_sys::Closure. Stored this way to allow us to store
/// any Closure regardless of the arguments.
pub type DynClosure = Rc<dyn AsRef<JsValue>>;
/// For any listeners in the tree, attach the sender closure.
/// When a event is triggered, we convert it into the synthetic event type and dump it back in the Virtual Dom's queu
fn attach_listeners<P: Properties>(sender: &UnboundedSender<EventTrigger>, dom: &VirtualDom<P>) {}
/// We need a custom implementation of fmt::Debug since JsValue doesn't
/// implement debug.
pub struct Events(pub HashMap<String, DynClosure>);
impl PartialEq for Events {
// TODO: What should happen here..? And why?
fn eq(&self, _rhs: &Self) -> bool {
true
}
}
impl fmt::Debug for Events {
// Print out all of the event names for this VirtualNode
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let events: String = self.0.keys().map(|key| " ".to_string() + key).collect();
write!(f, "{}", events)
}
}
fn render_diffs() {}
+211
View File
@@ -0,0 +1,211 @@
use std::{collections::HashMap, fmt, rc::Rc};
use web_sys::{self, Element, EventTarget, Node, Text};
use dioxus_core::prelude::{VElement, VNode, VText, VirtualNode};
use std::ops::Deref;
use std::sync::Mutex;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
pub struct DomRenderer {}
// Used to uniquely identify elements that contain closures so that the DomUpdater can
// look them up by their unique id.
// When the DomUpdater sees that the element no longer exists it will drop all of it's
// Rc'd Closures for those events.
use lazy_static::lazy_static;
lazy_static! {
static ref ELEM_UNIQUE_ID: Mutex<u32> = Mutex::new(0);
}
fn create_unique_identifier() -> u32 {
let mut elem_unique_id = ELEM_UNIQUE_ID.lock().unwrap();
*elem_unique_id += 1;
*elem_unique_id
}
/// A node along with all of the closures that were created for that
/// node's events and all of it's child node's events.
pub struct CreatedNode<T> {
/// A `Node` or `Element` that was created from a `VirtualNode`
pub node: T,
/// A map of a node's unique identifier along with all of the Closures for that node.
///
/// The DomUpdater uses this to look up nodes and see if they're still in the page. If not
/// the reference that we maintain to their closure will be dropped, thus freeing the Closure's
/// memory.
pub closures: HashMap<u32, Vec<DynClosure>>,
}
impl<T> CreatedNode<T> {
pub fn without_closures<N: Into<T>>(node: N) -> Self {
CreatedNode {
node: node.into(),
closures: HashMap::with_capacity(0),
}
}
}
impl<T> Deref for CreatedNode<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.node
}
}
impl From<CreatedNode<Element>> for CreatedNode<Node> {
fn from(other: CreatedNode<Element>) -> CreatedNode<Node> {
CreatedNode {
node: other.node.into(),
closures: other.closures,
}
}
}
//----------------------------------
// Create nodes for the VNode types
// ---------------------------------
/// Return a `Text` element from a `VirtualNode`, typically right before adding it
/// into the DOM.
pub fn create_text_node(text_node: &VText) -> Text {
let document = web_sys::window().unwrap().document().unwrap();
document.create_text_node(&text_node.text)
}
/// Build a DOM element by recursively creating DOM nodes for this element and it's
/// children, it's children's children, etc.
pub fn create_element_node(velement: &VElement) -> CreatedNode<Element> {
let document = web_sys::window().unwrap().document().unwrap();
let element = if html_validation::is_svg_namespace(&velement.tag) {
document
.create_element_ns(Some("http://www.w3.org/2000/svg"), &velement.tag)
.unwrap()
} else {
document.create_element(&velement.tag).unwrap()
};
let mut closures = HashMap::new();
velement.attrs.iter().for_each(|(name, value)| {
if name == "unsafe_inner_html" {
element.set_inner_html(value);
return;
}
element
.set_attribute(name, value)
.expect("Set element attribute in create element");
});
todo!("Support events properly in web ");
// if velement.events.0.len() > 0 {
// let unique_id = create_unique_identifier();
// element
// .set_attribute("data-vdom-id".into(), &unique_id.to_string())
// .expect("Could not set attribute on element");
// closures.insert(unique_id, vec![]);
// velement.events.0.iter().for_each(|(onevent, callback)| {
// // onclick -> click
// let event = &onevent[2..];
// let current_elem: &EventTarget = element.dyn_ref().unwrap();
// current_elem
// .add_event_listener_with_callback(event, callback.as_ref().as_ref().unchecked_ref())
// .unwrap();
// closures
// .get_mut(&unique_id)
// .unwrap()
// .push(Rc::clone(callback));
// });
// }
let mut previous_node_was_text = false;
velement.children.iter().for_each(|child| {
match child {
VNode::Text(text_node) => {
let current_node = element.as_ref() as &web_sys::Node;
// We ensure that the text siblings are patched by preventing the browser from merging
// neighboring text nodes. Originally inspired by some of React's work from 2016.
// -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes
// -> https://github.com/facebook/react/pull/5753
//
// `ptns` = Percy text node separator
if previous_node_was_text {
let separator = document.create_comment("ptns");
current_node
.append_child(separator.as_ref() as &web_sys::Node)
.unwrap();
}
current_node
.append_child(&create_text_node(&text_node))
// .append_child(&text_node.create_text_node())
.unwrap();
previous_node_was_text = true;
}
VNode::Element(element_node) => {
previous_node_was_text = false;
let child = create_element_node(&element_node);
// let child = element_node.create_element_node();
let child_elem: Element = child.node;
closures.extend(child.closures);
element.append_child(&child_elem).unwrap();
}
VNode::Component(component) => {
//
todo!("Support components in the web properly");
}
}
});
todo!("Support events properly in web ");
// if let Some(on_create_elem) = velement.events.0.get("on_create_elem") {
// let on_create_elem: &js_sys::Function = on_create_elem.as_ref().as_ref().unchecked_ref();
// on_create_elem
// .call1(&wasm_bindgen::JsValue::NULL, &element)
// .unwrap();
// }
CreatedNode {
node: element,
closures,
}
}
/// Box<dyn AsRef<JsValue>>> is our js_sys::Closure. Stored this way to allow us to store
/// any Closure regardless of the arguments.
pub type DynClosure = Rc<dyn AsRef<JsValue>>;
/// We need a custom implementation of fmt::Debug since JsValue doesn't
/// implement debug.
pub struct Events(pub HashMap<String, DynClosure>);
impl PartialEq for Events {
// TODO: What should happen here..? And why?
fn eq(&self, _rhs: &Self) -> bool {
true
}
}
impl fmt::Debug for Events {
// Print out all of the event names for this VirtualNode
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let events: String = self.0.keys().map(|key| " ".to_string() + key).collect();
write!(f, "{}", events)
}
}
+1
View File
@@ -0,0 +1 @@
//! Utilities specific to websys