Feat: event loop
This commit is contained in:
1 parent
8295ac4b3d
commit
ea2aa4b0c9
39 files changed
+495
-287
No files matched your search
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"rust-analyzer.inlayHints.enable": true
|
||||
"rust-analyzer.inlayHints.enable": false
|
||||
}
|
||||
@@ -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,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
@@ -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
@@ -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),
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
@@ -1,7 +0,0 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
@@ -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>
|
||||
*/
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
// })
|
||||
// }
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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() {}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1 @@
|
||||
//! Utilities specific to websys
|
||||
Reference in new issue
Block a user