From 5f6e8ed3aaa6c8596b930ef302b51cf9d64afe71 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 29 Dec 2021 14:28:30 +0100 Subject: [PATCH 01/26] detach destructures now --- packages/yew/src/dom_bundle/mod.rs | 59 +++++++++++++++++++ packages/yew/src/html/component/lifecycle.rs | 3 +- packages/yew/src/lib.rs | 1 + packages/yew/src/tests/layout_tests.rs | 3 +- packages/yew/src/virtual_dom/mod.rs | 60 ++------------------ packages/yew/src/virtual_dom/vcomp.rs | 5 +- packages/yew/src/virtual_dom/vlist.rs | 11 ++-- packages/yew/src/virtual_dom/vnode.rs | 21 +++---- packages/yew/src/virtual_dom/vportal.rs | 9 +-- packages/yew/src/virtual_dom/vsuspense.rs | 13 +++-- packages/yew/src/virtual_dom/vtag.rs | 13 ++--- packages/yew/src/virtual_dom/vtext.rs | 7 ++- 12 files changed, 112 insertions(+), 93 deletions(-) create mode 100644 packages/yew/src/dom_bundle/mod.rs diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs new file mode 100644 index 00000000000..262125456c8 --- /dev/null +++ b/packages/yew/src/dom_bundle/mod.rs @@ -0,0 +1,59 @@ +//! Realizing a virtual dom on the actual DOM +//! +//! A bundle, borrowed from the mathematical meaning, is any structure over some base space. +//! In our case, the base space is the virtual dom we're trying to render. +//! In order to efficiently implement updates, and diffing, additional information has to be +//! kept around. + +use web_sys::Element; + +use crate::{html::AnyScope, virtual_dom::VNode, NodeRef}; + +// TODO(#938): What about implementing `VDiff` for `Element`? +// It would make it possible to include ANY element into the tree. +// `Ace` editor embedding for example? + +/// This trait provides features to update a tree by calculating a difference against another tree. +pub(crate) trait VDiff { + /// Remove self from parent. + fn detach(self, parent: &Element); + + /// Move elements from one parent to another parent. + /// This is currently only used by `VSuspense` to preserve component state without detaching + /// (which destroys component state). + /// Prefer `detach` then apply if possible. + fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef); + + /// Scoped diff apply to other tree. + /// + /// Virtual rendering for the node. It uses parent node and existing + /// children (virtual and DOM) to check the difference and apply patches to + /// the actual DOM representation. + /// + /// Parameters: + /// - `parent_scope`: the parent `Scope` used for passing messages to the + /// parent `Component`. + /// - `parent`: the parent node in the DOM. + /// - `next_sibling`: the next sibling, used to efficiently find where to + /// put the node. + /// - `ancestor`: the node that this node will be replacing in the DOM. This + /// method will _always_ remove the `ancestor` from the `parent`. + /// + /// Returns a reference to the newly inserted element. + /// + /// ### Internal Behavior Notice: + /// + /// Note that these modify the DOM by modifying the reference that _already_ + /// exists on the `ancestor`. If `self.reference` exists (which it + /// _shouldn't_) this method will panic. + /// + /// The exception to this is obviously `VRef` which simply uses the inner + /// `Node` directly (always removes the `Node` that exists). + fn apply( + &mut self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ancestor: Option, + ) -> NodeRef; +} diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index e1b7a5e4108..73865457d78 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -1,10 +1,11 @@ //! Component lifecycle module use super::{AnyScope, BaseComponent, Scope}; +use crate::dom_bundle::VDiff; use crate::html::RenderError; use crate::scheduler::{self, Runnable, Shared}; use crate::suspense::{Suspense, Suspension}; -use crate::virtual_dom::{VDiff, VNode}; +use crate::virtual_dom::VNode; use crate::Callback; use crate::{Context, NodeRef}; use std::rc::Rc; diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index cf238e08062..467d9be7381 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -258,6 +258,7 @@ pub mod macros { mod app_handle; pub mod callback; pub mod context; +pub mod dom_bundle; pub mod functional; pub mod html; pub mod scheduler; diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/tests/layout_tests.rs index 283d78566ab..41f58ebee56 100644 --- a/packages/yew/src/tests/layout_tests.rs +++ b/packages/yew/src/tests/layout_tests.rs @@ -1,5 +1,6 @@ +use crate::dom_bundle::VDiff; use crate::html::{AnyScope, Scope}; -use crate::virtual_dom::{VDiff, VNode, VText}; +use crate::virtual_dom::{VNode, VText}; use crate::{Component, Context, Html}; use gloo::console::log; use web_sys::Node; diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index b09ce562b17..d1c7ced8934 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -19,12 +19,6 @@ pub mod vtag; #[doc(hidden)] pub mod vtext; -use crate::html::{AnyScope, NodeRef}; -use indexmap::IndexMap; -use std::borrow::Cow; -use std::{collections::HashMap, fmt, hint::unreachable_unchecked, iter}; -use web_sys::{Element, Node}; - #[doc(inline)] pub use self::key::Key; #[doc(inline)] @@ -43,9 +37,14 @@ pub use self::vsuspense::VSuspense; pub use self::vtag::VTag; #[doc(inline)] pub use self::vtext::VText; + +use indexmap::IndexMap; +use std::borrow::Cow; use std::fmt::Formatter; use std::ops::Deref; use std::rc::Rc; +use std::{collections::HashMap, fmt, hint::unreachable_unchecked, iter}; +use web_sys::{Element, Node}; /// Attribute value #[derive(Debug)] @@ -490,55 +489,6 @@ impl Default for Attributes { } } -// TODO(#938): What about implementing `VDiff` for `Element`? -// It would make it possible to include ANY element into the tree. -// `Ace` editor embedding for example? - -/// This trait provides features to update a tree by calculating a difference against another tree. -pub(crate) trait VDiff { - /// Remove self from parent. - fn detach(&mut self, parent: &Element); - - /// Move elements from one parent to another parent. - /// This is currently only used by `VSuspense` to preserve component state without detaching - /// (which destroys component state). - /// Prefer `detach` then apply if possible. - fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef); - - /// Scoped diff apply to other tree. - /// - /// Virtual rendering for the node. It uses parent node and existing - /// children (virtual and DOM) to check the difference and apply patches to - /// the actual DOM representation. - /// - /// Parameters: - /// - `parent_scope`: the parent `Scope` used for passing messages to the - /// parent `Component`. - /// - `parent`: the parent node in the DOM. - /// - `next_sibling`: the next sibling, used to efficiently find where to - /// put the node. - /// - `ancestor`: the node that this node will be replacing in the DOM. This - /// method will _always_ remove the `ancestor` from the `parent`. - /// - /// Returns a reference to the newly inserted element. - /// - /// ### Internal Behavior Notice: - /// - /// Note that these modify the DOM by modifying the reference that _already_ - /// exists on the `ancestor`. If `self.reference` exists (which it - /// _shouldn't_) this method will panic. - /// - /// The exception to this is obviously `VRef` which simply uses the inner - /// `Node` directly (always removes the `Node` that exists). - fn apply( - &mut self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ancestor: Option, - ) -> NodeRef; -} - pub(crate) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) { match next_sibling { Some(next_sibling) => parent diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index a0055b20697..009b89839ac 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -1,6 +1,7 @@ //! This module contains the implementation of a virtual component (`VComp`). -use super::{Key, VDiff, VNode}; +use super::{Key, VNode}; +use crate::dom_bundle::VDiff; use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped}; use std::any::TypeId; use std::borrow::Borrow; @@ -221,7 +222,7 @@ impl Mountable for PropsWrapper { } impl VDiff for VComp { - fn detach(&mut self, _parent: &Element) { + fn detach(mut self, _parent: &Element) { self.take_scope().destroy(); } diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 420a12f900f..580a5a3cd89 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -1,5 +1,6 @@ //! This module contains fragments implementation. -use super::{Key, VDiff, VNode, VText}; +use super::{Key, VNode, VText}; +use crate::dom_bundle::VDiff; use crate::html::{AnyScope, NodeRef}; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; @@ -152,7 +153,7 @@ impl VList { } // Remove extra nodes while diff < 0 { - let mut r = rights_it.next().unwrap(); + let r = rights_it.next().unwrap(); test_log!("removing: {:?}", r); r.detach(parent); diff += 1; @@ -266,7 +267,7 @@ impl VList { } // Remove any extra rights - for (_, (mut r, _)) in rights_diff.drain() { + for (_, (r, _)) in rights_diff.drain() { test_log!("removing: {:?}", r); r.detach(parent); } @@ -285,8 +286,8 @@ impl VList { } impl VDiff for VList { - fn detach(&mut self, parent: &Element) { - for mut child in self.children.drain(..) { + fn detach(mut self, parent: &Element) { + for child in self.children.drain(..) { child.detach(parent); } } diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index e2af46a89cf..e801484985c 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -1,6 +1,7 @@ //! This module contains the implementation of abstract virtual node. -use super::{Key, VChild, VComp, VDiff, VList, VPortal, VSuspense, VTag, VText}; +use super::{Key, VChild, VComp, VList, VPortal, VSuspense, VTag, VText}; +use crate::dom_bundle::VDiff; use crate::html::{AnyScope, BaseComponent, NodeRef}; use gloo::console; use std::cmp::PartialEq; @@ -126,19 +127,19 @@ impl VNode { impl VDiff for VNode { /// Remove VNode from parent. - fn detach(&mut self, parent: &Element) { - match *self { - VNode::VTag(ref mut vtag) => vtag.detach(parent), - VNode::VText(ref mut vtext) => vtext.detach(parent), - VNode::VComp(ref mut vcomp) => vcomp.detach(parent), - VNode::VList(ref mut vlist) => vlist.detach(parent), + fn detach(self, parent: &Element) { + match self { + VNode::VTag(vtag) => vtag.detach(parent), + VNode::VText(vtext) => vtext.detach(parent), + VNode::VComp(vcomp) => vcomp.detach(parent), + VNode::VList(vlist) => vlist.detach(parent), VNode::VRef(ref node) => { if parent.remove_child(node).is_err() { console::warn!("Node not found to remove VRef"); } } - VNode::VPortal(ref mut vportal) => vportal.detach(parent), - VNode::VSuspense(ref mut vsuspense) => vsuspense.detach(parent), + VNode::VPortal(vportal) => vportal.detach(parent), + VNode::VSuspense(vsuspense) => vsuspense.detach(parent), } } @@ -182,7 +183,7 @@ impl VDiff for VNode { vlist.apply(parent_scope, parent, next_sibling, ancestor) } VNode::VRef(ref mut node) => { - if let Some(mut ancestor) = ancestor { + if let Some(ancestor) = ancestor { if let VNode::VRef(n) = &ancestor { if node == n { return NodeRef::new(node.clone()); diff --git a/packages/yew/src/virtual_dom/vportal.rs b/packages/yew/src/virtual_dom/vportal.rs index 87243991599..308dff96e48 100644 --- a/packages/yew/src/virtual_dom/vportal.rs +++ b/packages/yew/src/virtual_dom/vportal.rs @@ -1,6 +1,7 @@ //! This module contains the implementation of a portal `VPortal`. -use super::{VDiff, VNode}; +use super::VNode; +use crate::dom_bundle::VDiff; use crate::html::{AnyScope, NodeRef}; use web_sys::{Element, Node}; @@ -17,7 +18,7 @@ pub struct VPortal { } impl VDiff for VPortal { - fn detach(&mut self, _: &Element) { + fn detach(self, _: &Element) { self.node.detach(&self.host); self.sibling_ref.set(None); } @@ -38,7 +39,7 @@ impl VDiff for VPortal { let VPortal { host: old_host, next_sibling: old_sibling, - mut node, + node, .. } = old_portal; if old_host != self.host { @@ -53,7 +54,7 @@ impl VDiff for VPortal { Some(*node) } } - Some(mut node) => { + Some(node) => { node.detach(parent); None } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 5cf6d177510..8c377b78549 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -1,5 +1,8 @@ -use super::{Key, VDiff, VNode}; -use crate::html::{AnyScope, NodeRef}; +use super::{Key, VNode}; +use crate::{ + dom_bundle::VDiff, + html::{AnyScope, NodeRef}, +}; use web_sys::{Element, Node}; /// This struct represents a suspendable DOM fragment. @@ -48,7 +51,7 @@ impl VSuspense { } impl VDiff for VSuspense { - fn detach(&mut self, parent: &Element) { + fn detach(self, parent: &Element) { if self.suspended { self.fallback.detach(parent); self.children.detach(&self.detached_parent); @@ -75,7 +78,7 @@ impl VDiff for VSuspense { ancestor: Option, ) -> NodeRef { let (already_suspended, children_ancestor, fallback_ancestor) = match ancestor { - Some(VNode::VSuspense(mut m)) => { + Some(VNode::VSuspense(m)) => { // We only preserve the child state if they are the same suspense. if m.key != self.key || self.detached_parent != m.detached_parent { m.detach(parent); @@ -85,7 +88,7 @@ impl VDiff for VSuspense { (m.suspended, Some(*m.children), Some(*m.fallback)) } } - Some(mut m) => { + Some(m) => { m.detach(parent); (false, None, None) } diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index c69260ab8f7..3f1b5bff6c2 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1,6 +1,7 @@ //! This module contains the implementation of a virtual element node [VTag]. -use super::{Apply, AttrValue, Attributes, Key, Listener, Listeners, VDiff, VList, VNode}; +use super::{Apply, AttrValue, Attributes, Key, Listener, Listeners, VList, VNode}; +use crate::dom_bundle::VDiff; use crate::html::{AnyScope, IntoPropValue, NodeRef}; use gloo::console; use gloo_utils::document; @@ -471,7 +472,7 @@ impl VTag { impl VDiff for VTag { /// Remove VTag from parent. - fn detach(&mut self, parent: &Element) { + fn detach(mut self, parent: &Element) { let node = self .reference .take() @@ -480,7 +481,7 @@ impl VDiff for VTag { self.listeners.unregister(); // recursively remove its children - if let VTagInner::Other { children, .. } = &mut self.inner { + if let VTagInner::Other { children, .. } = self.inner { children.detach(&node); } if parent.remove_child(&node).is_err() { @@ -518,7 +519,7 @@ impl VDiff for VTag { // unpack the enums (including `Option`s) all the time, resulting in a more streamlined // patching flow let (ancestor_tag, el) = match ancestor { - Some(mut ancestor) => { + Some(ancestor) => { // If the ancestor is a tag of the same type, don't recreate, keep the // old tag and update its attributes and children. if match &ancestor { @@ -593,9 +594,7 @@ impl VDiff for VTag { } ( VTagInner::Other { children: new, .. }, - VTagInner::Other { - children: mut old, .. - }, + VTagInner::Other { children: old, .. }, ) => { if !new.is_empty() { new.apply(parent_scope, &el, NodeRef::default(), Some(old.into())); diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index 9b67aa665bb..a3a3d6a77d3 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -1,6 +1,7 @@ //! This module contains the implementation of a virtual text node `VText`. -use super::{AttrValue, VDiff, VNode}; +use super::{AttrValue, VNode}; +use crate::dom_bundle::VDiff; use crate::html::{AnyScope, NodeRef}; use gloo::console; use gloo_utils::document; @@ -44,7 +45,7 @@ impl std::fmt::Debug for VText { impl VDiff for VText { /// Remove VText from parent. - fn detach(&mut self, parent: &Element) { + fn detach(mut self, parent: &Element) { let node = self .reference .take() @@ -74,7 +75,7 @@ impl VDiff for VText { next_sibling: NodeRef, ancestor: Option, ) -> NodeRef { - if let Some(mut ancestor) = ancestor { + if let Some(ancestor) = ancestor { if let VNode::VText(mut vtext) = ancestor { self.reference = vtext.reference.take(); let text_node = self From b302a8886aea84a59dc8dab7cfd29fcd733535ad Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Tue, 4 Jan 2022 22:05:12 +0100 Subject: [PATCH 02/26] add failing keyed-list issue --- packages/yew/src/virtual_dom/vlist.rs | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 580a5a3cd89..7abe208c247 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -988,6 +988,35 @@ mod layout_tests_keys { }, ]); + layouts.extend(vec![ + TestLayout { + name: "Swap 1,2 <-> 3,4 - before", + node: html! { + <> + + +

+ + + + }, + expected: "

", + }, + TestLayout { + name: "Swap 1,2 <-> 3,4 - after", + node: html! { + <> +

+ + + + + + }, + expected: "

", + }, + ]); + layouts.extend(vec![ TestLayout { name: "Swap lists - before", From 994a6db4cdd8ad420feb06b89a0d8d2c232805f8 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 5 Jan 2022 02:05:02 +0100 Subject: [PATCH 03/26] crude port to the new bundle infrastructure --- packages/yew/src/dom_bundle/mod.rs | 24 +- packages/yew/src/html/component/lifecycle.rs | 16 +- packages/yew/src/tests/layout_tests.rs | 30 +- packages/yew/src/virtual_dom/listeners.rs | 77 ++--- packages/yew/src/virtual_dom/mod.rs | 7 +- packages/yew/src/virtual_dom/vcomp.rs | 78 +++-- packages/yew/src/virtual_dom/vlist.rs | 270 +++++++++------- packages/yew/src/virtual_dom/vnode.rs | 173 ++++++---- packages/yew/src/virtual_dom/vportal.rs | 90 +++--- packages/yew/src/virtual_dom/vsuspense.rs | 112 ++++--- packages/yew/src/virtual_dom/vtag.rs | 323 +++++++++---------- packages/yew/src/virtual_dom/vtext.rs | 61 ++-- 12 files changed, 697 insertions(+), 564 deletions(-) diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 262125456c8..e23980af65b 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -13,16 +13,26 @@ use crate::{html::AnyScope, virtual_dom::VNode, NodeRef}; // It would make it possible to include ANY element into the tree. // `Ace` editor embedding for example? -/// This trait provides features to update a tree by calculating a difference against another tree. -pub(crate) trait VDiff { +pub(crate) trait DomBundle { /// Remove self from parent. fn detach(self, parent: &Element); /// Move elements from one parent to another parent. - /// This is currently only used by `VSuspense` to preserve component state without detaching + /// This is for example used by `VSuspense` to preserve component state without detaching /// (which destroys component state). - /// Prefer `detach` then apply if possible. - fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef); + fn shift(&self, next_parent: &Element, next_sibling: NodeRef); +} + +/// This trait provides features to update a tree by calculating a difference against another tree. +pub(crate) trait VDiff { + type Bundle: DomBundle; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle); /// Scoped diff apply to other tree. /// @@ -50,10 +60,10 @@ pub(crate) trait VDiff { /// The exception to this is obviously `VRef` which simply uses the inner /// `Node` directly (always removes the `Node` that exists). fn apply( - &mut self, + self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: Option, + ancestor: &mut VNode, ) -> NodeRef; } diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 73865457d78..eba82d5532c 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -1,7 +1,7 @@ //! Component lifecycle module use super::{AnyScope, BaseComponent, Scope}; -use crate::dom_bundle::VDiff; +use crate::dom_bundle::{DomBundle, VDiff}; use crate::html::RenderError; use crate::scheduler::{self, Runnable, Shared}; use crate::suspense::{Suspense, Suspension}; @@ -130,9 +130,7 @@ impl Runnable for UpdateRunner { } } UpdateEvent::Shift(parent, next_sibling) => { - state - .root_node - .shift(&state.parent, &parent, next_sibling.clone()); + state.root_node.shift(&parent, next_sibling.clone()); state.parent = parent; state.next_sibling = next_sibling; @@ -191,12 +189,9 @@ impl Runnable for RenderRunner { crate::virtual_dom::vcomp::log_event(state.vcomp_id, "render"); match state.component.view(&state.context) { - Ok(m) => { + Ok(root) => { // Currently not suspended, we remove any previous suspension and update // normally. - let mut root = m; - std::mem::swap(&mut root, &mut state.root_node); - if let Some(ref m) = state.suspension { let comp_scope = AnyScope::from(state.context.scope.clone()); @@ -206,12 +201,11 @@ impl Runnable for RenderRunner { suspense.resume(m.clone()); } - let ancestor = Some(root); - let new_root = &mut state.root_node; let scope = state.context.scope.clone().into(); let next_sibling = state.next_sibling.clone(); - let node = new_root.apply(&scope, &state.parent, next_sibling, ancestor); + let node = + root.apply(&scope, &state.parent, next_sibling, &mut state.root_node); state.node_ref.link(node); } diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/tests/layout_tests.rs index 41f58ebee56..e2ec170eeb3 100644 --- a/packages/yew/src/tests/layout_tests.rs +++ b/packages/yew/src/tests/layout_tests.rs @@ -42,16 +42,16 @@ pub fn diff_layouts(layouts: Vec>) { let parent_node: Node = parent_element.clone().into(); let end_node = document.create_text_node("END"); parent_node.append_child(&end_node).unwrap(); - let mut empty_node: VNode = VText::new("").into(); + let empty_node: VNode = VText::new("").into(); // Tests each layout independently let next_sibling = NodeRef::new(end_node.into()); for layout in layouts.iter() { // Apply the layout - let mut node = layout.node.clone(); + let vnode = layout.node.clone(); log!("Independently apply layout '{}'", layout.name); - node.apply(&parent_scope, &parent_element, next_sibling.clone(), None); + let (_, mut node) = vnode.attach(&parent_scope, &parent_element, next_sibling.clone()); assert_eq!( parent_element.inner_html(), format!("{}END", layout.expected), @@ -60,15 +60,15 @@ pub fn diff_layouts(layouts: Vec>) { ); // Diff with no changes - let mut node_clone = layout.node.clone(); + let vnode = layout.node.clone(); log!("Independently reapply layout '{}'", layout.name); - node_clone.apply( + vnode.apply( &parent_scope, &parent_element, next_sibling.clone(), - Some(node), + &mut node, ); assert_eq!( parent_element.inner_html(), @@ -82,7 +82,7 @@ pub fn diff_layouts(layouts: Vec>) { &parent_scope, &parent_element, next_sibling.clone(), - Some(node_clone), + &mut node, ); assert_eq!( parent_element.inner_html(), @@ -95,14 +95,14 @@ pub fn diff_layouts(layouts: Vec>) { // Sequentially apply each layout let mut ancestor: Option = None; for layout in layouts.iter() { - let mut next_node = layout.node.clone(); + let next_vnode = layout.node.clone(); log!("Sequentially apply layout '{}'", layout.name); - next_node.apply( + next_vnode.apply_sequentially( &parent_scope, &parent_element, next_sibling.clone(), - ancestor, + &mut ancestor, ); assert_eq!( parent_element.inner_html(), @@ -110,19 +110,18 @@ pub fn diff_layouts(layouts: Vec>) { "Sequential apply failed for layout '{}'", layout.name, ); - ancestor = Some(next_node); } // Sequentially detach each layout for layout in layouts.into_iter().rev() { - let mut next_node = layout.node.clone(); + let next_vnode = layout.node.clone(); log!("Sequentially detach layout '{}'", layout.name); - next_node.apply( + next_vnode.apply_sequentially( &parent_scope, &parent_element, next_sibling.clone(), - ancestor, + &mut ancestor, ); assert_eq!( parent_element.inner_html(), @@ -130,11 +129,10 @@ pub fn diff_layouts(layouts: Vec>) { "Sequential detach failed for layout '{}'", layout.name, ); - ancestor = Some(next_node); } // Detach last layout - empty_node.apply(&parent_scope, &parent_element, next_sibling, ancestor); + empty_node.apply_sequentially(&parent_scope, &parent_element, next_sibling, &mut ancestor); assert_eq!( parent_element.inner_html(), "END", diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs index 7a9c0c0f546..dce8db00602 100644 --- a/packages/yew/src/virtual_dom/listeners.rs +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -8,6 +8,15 @@ use std::{ use wasm_bindgen::{prelude::*, JsCast}; use web_sys::{Element, Event}; +/// Log an operation during tests for debugging purposes +/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. +macro_rules! test_log { + ($fmt:literal, $($arg:expr),* $(,)?) => { + #[cfg(all(test, feature = "wasm_test", verbose_tests))] + ::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*) + }; +} + thread_local! { /// Global event listener registry static REGISTRY: RefCell = Default::default(); @@ -223,22 +232,38 @@ impl super::Apply for Listeners { } } - fn apply_diff(&mut self, el: &Self::Element, ancestor: Self) { + fn apply_diff(self, el: &Self::Element, bundle: &mut Self) { use Listeners::*; - match (std::mem::take(self), ancestor) { - (Pending(pending), Registered(id)) => { + match (self, bundle) { + (Pending(pending), Registered(ref id)) => { // Reuse the ID - Registry::with(|reg| reg.patch(&id, &*pending)); - *self = Registered(id); + test_log!("reusing listeners for {}", id); + Registry::with(|reg| reg.patch(id, &*pending)); } - (Pending(pending), None) => { - *self = Self::register(el, &pending); + (Pending(pending), bundle @ None) => { + *bundle = Self::register(el, &pending); + test_log!( + "registering listeners for {}", + match bundle { + Self::Registered(id) => id, + _ => unreachable!(), + } + ); } - (None, Registered(id)) => { - Registry::with(|reg| reg.unregister(&id)); + (None, bundle @ Registered(_)) => { + let id = match bundle { + Self::Registered(ref id) => id, + _ => unreachable!(), + }; + test_log!("unregistering listeners for {}", id); + Registry::with(|reg| reg.unregister(id)); + *bundle = None; } - _ => (), + (None, None) => { + test_log!("{}", &"unchanged empty listeners"); + } + (self_, bundle) => unreachable!("{:?} -> {:?}", bundle, &self_), }; } } @@ -525,7 +550,6 @@ mod tests { use crate::{html, html::TargetCast, AppHandle, Component, Context, Html}; use gloo_utils::document; use wasm_bindgen::JsCast; - use wasm_bindgen_futures::JsFuture; #[derive(Clone)] enum Message { @@ -655,37 +679,6 @@ mod tests { assert_count(&el, 2); } - async fn await_animation_frame() { - JsFuture::from(js_sys::Promise::new(&mut |resolve, _| { - gloo_utils::window() - .request_animation_frame(&resolve) - .unwrap(); - })) - .await - .unwrap(); - } - - async fn assert_async() { - let (link, el) = init::("a"); - - macro_rules! assert_after_click { - ($c:expr) => { - el.click(); - await_animation_frame().await; - assert_count(&el, $c); - }; - } - - assert_count(&el, 0); - - assert_after_click!(1); - - assert_after_click!(2); - - link.send_message(Message::StopListening); - assert_after_click!(2); - } - #[test] async fn non_bubbling_event() { struct NonBubbling; diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index d1c7ced8934..e7bf896dc7c 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -206,7 +206,7 @@ trait Apply { fn apply(&mut self, el: &Self::Element); /// Apply diff between [self] and `ancestor` to [Element]. - fn apply_diff(&mut self, el: &Self::Element, ancestor: Self); + fn apply_diff(self, el: &Self::Element, ancestor: &mut Self); } /// A collection of attributes for an element @@ -414,13 +414,14 @@ impl Apply for Attributes { } } - fn apply_diff(&mut self, el: &Element, ancestor: Self) { + fn apply_diff(self, el: &Element, bundle: &mut Self) { #[inline] fn ptr_eq(a: &[T], b: &[T]) -> bool { a.as_ptr() == b.as_ptr() } - match (self, ancestor) { + let ancestor = std::mem::replace(bundle, self); + match (bundle, ancestor) { // Hot path (Self::Static(new), Self::Static(old)) if ptr_eq(new, old) => (), // Hot path diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 009b89839ac..a94fe5d736d 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -1,7 +1,7 @@ //! This module contains the implementation of a virtual component (`VComp`). use super::{Key, VNode}; -use crate::dom_bundle::VDiff; +use crate::dom_bundle::{DomBundle, VDiff}; use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped}; use std::any::TypeId; use std::borrow::Borrow; @@ -221,40 +221,28 @@ impl Mountable for PropsWrapper { } } -impl VDiff for VComp { +impl DomBundle for VComp { fn detach(mut self, _parent: &Element) { self.take_scope().destroy(); } - fn shift(&self, _previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { let scope = self.scope.as_ref().unwrap(); scope.shift_node(next_parent.clone(), next_sibling); } +} - fn apply( - &mut self, +impl VDiff for VComp { + type Bundle = VComp; + + fn attach( + mut self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: Option, - ) -> NodeRef { + ) -> (NodeRef, Self::Bundle) { let mountable = self.props.take().expect("VComp has already been mounted"); - if let Some(mut ancestor) = ancestor { - if let VNode::VComp(ref mut vcomp) = &mut ancestor { - // If the ancestor is the same type, reuse it and update its properties - if self.type_id == vcomp.type_id && self.key == vcomp.key { - self.node_ref.reuse(vcomp.node_ref.clone()); - let scope = vcomp.take_scope(); - mountable.reuse(self.node_ref.clone(), scope.borrow(), next_sibling); - self.scope = Some(scope); - return vcomp.node_ref.clone(); - } - } - - ancestor.detach(parent); - } - self.scope = Some(mountable.mount( self.node_ref.clone(), parent_scope, @@ -262,7 +250,32 @@ impl VDiff for VComp { next_sibling, )); - self.node_ref.clone() + (self.node_ref.clone(), self) + } + + fn apply( + mut self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ancestor: &mut VNode, + ) -> NodeRef { + if let VNode::VComp(ref mut vcomp) = ancestor { + // If the ancestor is the same type, reuse it and update its properties + if self.type_id == vcomp.type_id && self.key == vcomp.key { + let mountable = self.props.take().expect("VComp has already been mounted"); + let mut ancestor = std::mem::replace(vcomp, self); + + vcomp.node_ref.reuse(ancestor.node_ref.clone()); + let scope = ancestor.take_scope(); + mountable.reuse(vcomp.node_ref.clone(), scope.borrow(), next_sibling); + vcomp.scope = Some(scope); + return vcomp.node_ref.clone(); + } + } + let (node_ref, self_) = self.attach(parent_scope, parent, next_sibling); + ancestor.replace(parent, self_.into()); + node_ref } } @@ -330,18 +343,17 @@ mod tests { let parent_scope: AnyScope = crate::html::Scope::::new(None).into(); let parent_element = document.create_element("div").unwrap(); - let mut ancestor = html! { }; - ancestor.apply(&parent_scope, &parent_element, NodeRef::default(), None); + let ancestor = html! { }; + let (_, mut comp) = ancestor.attach(&parent_scope, &parent_element, NodeRef::default()); for _ in 0..10000 { - let mut node = html! { }; + let node = html! { }; node.apply( &parent_scope, &parent_element, NodeRef::default(), - Some(ancestor), + &mut comp, ); - ancestor = node; } } @@ -377,7 +389,7 @@ mod tests { fn set_component_key() { let test_key: Key = "test".to_string().into(); let check_key = |vnode: VNode| { - assert_eq!(vnode.key().as_ref(), Some(&test_key)); + assert_eq!(vnode.key(), Some(&test_key)); }; let props = Props { @@ -489,11 +501,11 @@ mod tests { (scope, parent) } - fn get_html(mut node: Html, scope: &AnyScope, parent: &Element) -> String { + fn get_html(node: Html, scope: &AnyScope, parent: &Element) -> String { // clear parent parent.set_inner_html(""); - node.apply(scope, parent, NodeRef::default(), None); + node.attach(scope, parent, NodeRef::default()); parent.inner_html() } @@ -551,8 +563,8 @@ mod tests { document().body().unwrap().append_child(&parent).unwrap(); let node_ref = NodeRef::default(); - let mut elem: VNode = html! { }; - elem.apply(&scope, &parent, NodeRef::default(), None); + let elem: VNode = html! { }; + let (_, elem) = elem.attach(&scope, &parent, NodeRef::default()); let parent_node = parent.deref(); assert_eq!(node_ref.get(), parent_node.first_child()); elem.detach(&parent); diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 7abe208c247..30f74a21c75 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -1,8 +1,10 @@ //! This module contains fragments implementation. use super::{Key, VNode, VText}; -use crate::dom_bundle::VDiff; +use crate::dom_bundle::{DomBundle, VDiff}; use crate::html::{AnyScope, NodeRef}; -use std::collections::HashMap; +use std::borrow::Borrow; +use std::collections::HashSet; +use std::hash::Hash; use std::ops::{Deref, DerefMut}; use web_sys::Element; @@ -58,22 +60,32 @@ struct ElementWriter<'s> { } impl<'s> ElementWriter<'s> { - fn add(self, node: &mut VNode) -> Self { + fn add(self, node: VNode) -> (Self, VNode) { test_log!("adding: {:?}", node); - self.write(node, None) + test_log!("parent={:?}", self.parent.outer_html()); + // Advance the next sibling reference (from right to left) and log it for testing purposes + // Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. + #[cfg(all(test, feature = "wasm_test", verbose_tests))] + let current = format!("{:?}", self.next_sibling); + let (next, bundle) = node.attach(self.parent_scope, self.parent, self.next_sibling); + test_log!("advance next_sibling: {:?} -> {:?}", current, next); + ( + Self { + next_sibling: next, + ..self + }, + bundle, + ) } - fn patch(self, node: &mut VNode, ancestor: VNode) -> Self { + fn patch(self, node: VNode, ancestor: &mut VNode) -> Self { test_log!("patching: {:?} -> {:?}", ancestor, node); - self.write(node, Some(ancestor)) - } - - fn write(self, node: &mut VNode, ancestor: Option) -> Self { test_log!("parent={:?}", self.parent.outer_html()); // Advance the next sibling reference (from right to left) and log it for testing purposes // Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. #[cfg(all(test, feature = "wasm_test", verbose_tests))] let current = format!("{:?}", self.next_sibling); + ancestor.move_before(self.parent, &self.next_sibling.get()); let next = node.apply(self.parent_scope, self.parent, self.next_sibling, ancestor); test_log!("advance next_sibling: {:?} -> {:?}", current, next); Self { @@ -83,6 +95,24 @@ impl<'s> ElementWriter<'s> { } } +struct NodeEntry(VNode); +impl Borrow for NodeEntry { + fn borrow(&self) -> &Key { + self.0.key().expect("unkeyed child in fully keyed list") + } +} +impl Hash for NodeEntry { + fn hash(&self, state: &mut H) { + >::borrow(self).hash(state) + } +} +impl PartialEq for NodeEntry { + fn eq(&self, other: &Self) -> bool { + >::borrow(self) == >::borrow(other) + } +} +impl Eq for NodeEntry {} + impl VList { /// Creates a new empty [VList] instance. pub const fn new() -> Self { @@ -133,36 +163,34 @@ impl VList { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - lefts: &mut [VNode], - rights: Vec, + lefts: Vec, + rights: &mut Vec, ) -> NodeRef { - let mut diff = lefts.len() as isize - rights.len() as isize; - let mut lefts_it = lefts.iter_mut().rev(); - let mut rights_it = rights.into_iter().rev(); let mut writer = ElementWriter { parent_scope, parent, next_sibling, }; - // Add missing nodes - while diff > 0 { - let l = lefts_it.next().unwrap(); - writer = writer.add(l); - diff -= 1; - } // Remove extra nodes - while diff < 0 { - let r = rights_it.next().unwrap(); - test_log!("removing: {:?}", r); - r.detach(parent); - diff += 1; + if lefts.len() < rights.len() { + for r in rights.drain(lefts.len()..) { + test_log!("removing: {:?}", r); + r.detach(parent); + } } - for (l, r) in lefts_it.zip(rights_it) { + let mut lefts_it = lefts.into_iter().rev(); + for (r, l) in rights.iter_mut().zip(&mut lefts_it) { writer = writer.patch(l, r); } + // Add missing nodes + for l in lefts_it { + let (next_writer, el) = writer.add(l); + rights.push(el); + writer = next_writer; + } writer.next_sibling } @@ -174,19 +202,9 @@ impl VList { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - lefts: &mut [VNode], - rights: Vec, + lefts: Vec, + rights: &mut Vec, ) -> NodeRef { - macro_rules! map_keys { - ($src:expr) => { - $src.iter() - .map(|v| v.key().expect("unkeyed child in fully keyed list")) - .collect::>() - }; - } - let lefts_keys = map_keys!(lefts); - let rights_keys = map_keys!(rights); - /// Find the first differing key in 2 iterators fn matching_len<'a, 'b>( a: impl Iterator, @@ -195,88 +213,91 @@ impl VList { a.zip(b).take_while(|(a, b)| a == b).count() } - // Find first key mismatch from the front - let from_start = matching_len(lefts_keys.iter(), rights_keys.iter()); + // Find first key mismatch from the back + let matching_len_end = matching_len( + lefts + .iter() + .map(|v| v.key().expect("unkeyed child in fully keyed list")) + .rev(), + rights + .iter() + .map(|v| v.key().expect("unkeyed child in fully keyed list")), + ); - if from_start == std::cmp::min(lefts.len(), rights.len()) { + if matching_len_end == std::cmp::min(lefts.len(), rights.len()) { // No key changes return Self::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights); } - + // We partially deconstruct the new vector in several steps. + let mut lefts = lefts; let mut writer = ElementWriter { parent_scope, parent, next_sibling, }; - // Find first key mismatch from the back - let from_end = matching_len( - lefts_keys[from_start..].iter().rev(), - rights_keys[from_start..].iter().rev(), - ); - // We partially deconstruct the rights vector in several steps. - let mut rights = rights; - // Diff matching children at the end - let lefts_to = lefts_keys.len() - from_end; - let rights_to = rights_keys.len() - from_end; - for (l, r) in lefts[lefts_to..] - .iter_mut() - .zip(rights.drain(rights_to..)) + let lefts_to = lefts.len() - matching_len_end; + for (l, r) in lefts + .drain(lefts_to..) .rev() + .zip(rights[..matching_len_end].iter_mut()) { writer = writer.patch(l, r); } + // Find first key mismatch from the front + let matching_len_start = matching_len( + lefts + .iter() + .map(|v| v.key().expect("unkeyed child in fully keyed list")), + rights + .iter() + .map(|v| v.key().expect("unkeyed child in fully keyed list")) + .rev(), + ); // Diff mismatched children in the middle - let mut next_right_key: Option<&Key> = None; - let mut rights_diff: HashMap<&Key, (VNode, Option<&Key>)> = - HashMap::with_capacity(rights_to - from_start); - for (k, v) in rights_keys[from_start..rights_to] - .iter() - .zip(rights.drain(from_start..)) // rights_to.. has been drained already - .rev() - { - let next_r_key = std::mem::replace(&mut next_right_key, Some(k)); - rights_diff.insert(k, (v, next_r_key)); + let rights_to = rights.len() - matching_len_start; + let mut spliced_middle = rights.splice(matching_len_end..rights_to, std::iter::empty()); + let mut rights_diff: HashSet = + HashSet::with_capacity((matching_len_end..rights_to).len()); + for r in &mut spliced_middle { + rights_diff.insert(NodeEntry(r)); } - let mut next_left_key: Option<&Key> = None; - for (l_key, l) in lefts_keys[from_start..lefts_to] - .iter() - .zip(lefts[from_start..lefts_to].iter_mut()) + let mut replacements: Vec = Vec::with_capacity((matching_len_start..lefts_to).len()); + for l in lefts + .drain(matching_len_start..) // lefts_to.. has been drained .rev() { - match rights_diff.remove(l_key) { - // Reorder and diff any existing children - Some((r, next_r_key)) => { - match (next_r_key, next_left_key) { - // If the next sibling was already the same, we don't need to move the node - (Some(r_next), Some(l_next)) if r_next == l_next => (), - _ => { - test_log!("moving as next: {:?}", r); - r.move_before(parent, &writer.next_sibling.get()); - } - } - writer = writer.patch(l, r); + let l_key = l.key().expect("unkeyed child in fully keyed list"); + let bundle = match rights_diff.take(l_key) { + Some(NodeEntry(mut r_bundle)) => { + writer = writer.patch(l, &mut r_bundle); + r_bundle } - // Add new children None => { - writer = writer.add(l); + let (next_writer, bundle) = writer.add(l); + writer = next_writer; + bundle } - } - next_left_key = Some(l_key); + }; + replacements.push(bundle); } + // now drop the splice iterator + std::mem::drop(spliced_middle); + rights.splice(matching_len_end..matching_len_end, replacements); // Remove any extra rights - for (_, (r, _)) in rights_diff.drain() { + for NodeEntry(r) in rights_diff.drain() { test_log!("removing: {:?}", r); r.detach(parent); } // Diff matching children at the start - for (l, r) in lefts[..from_start] - .iter_mut() - .zip(rights.into_iter()) // from_start.. has been drained already + let rights_to = rights.len() - matching_len_start; + for (l, r) in lefts + .drain(..) // matching_len_start.. has been drained already .rev() + .zip(rights[rights_to..].iter_mut()) { writer = writer.patch(l, r); } @@ -285,29 +306,44 @@ impl VList { } } -impl VDiff for VList { - fn detach(mut self, parent: &Element) { - for child in self.children.drain(..) { +impl DomBundle for VList { + fn detach(self, parent: &Element) { + for child in self.children.into_iter() { child.detach(parent); } } - fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { - let mut last_node_ref = next_sibling; - - for node in self.children.iter().rev() { - node.shift(previous_parent, next_parent, last_node_ref); - last_node_ref = NodeRef::default(); - last_node_ref.set(node.first_node()); + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + for node in self.children.iter() { + node.shift(next_parent, next_sibling.clone()); } } +} + +impl VDiff for VList { + type Bundle = VList; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let mut self_ = VNode::VList(VList::new()); + let node_ref = self.apply(parent_scope, parent, next_sibling, &mut self_); + let self_ = match self_ { + VNode::VList(self_) => self_, + _ => unreachable!("applying list should leave a VList in bundle ref"), + }; + (node_ref, self_) + } fn apply( - &mut self, + mut self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: Option, + ancestor: &mut VNode, ) -> NodeRef { // Here, we will try to diff the previous list elements with the new // ones we want to insert. For that, we will use two lists: @@ -325,31 +361,41 @@ impl VDiff for VList { self.add_child(VText::new("").into()); } - let lefts = &mut self.children; + let lefts = self.children; let (rights, rights_fully_keyed) = match ancestor { // If the ancestor is also a VList, then the "right" list is the previously // rendered items. - Some(VNode::VList(v)) => (v.children, v.fully_keyed), - + VNode::VList(ref mut v) => { + v.key = self.key; + (&mut v.children, &mut v.fully_keyed) + } // If the ancestor was not a VList, then the "right" list is a single node - Some(v) => { - let has_key = v.has_key(); - (vec![v], has_key) + _ => { + let v = std::mem::replace(ancestor, VNode::VList(VList::new())); + match ancestor { + VNode::VList(ref mut vlist) => { + vlist.key = v.key().cloned(); + vlist.add_child(v); + (&mut vlist.children, &mut vlist.fully_keyed) + } + _ => unreachable!(""), + } } - - // No unkeyed nodes in an empty VList - _ => (vec![], true), }; test_log!("lefts: {:?}", lefts); test_log!("rights: {:?}", rights); + if let Some(additional) = rights.len().checked_sub(lefts.len()) { + rights.reserve_exact(additional); + } #[allow(clippy::let_and_return)] - let first = if self.fully_keyed && rights_fully_keyed { + let first = if self.fully_keyed && *rights_fully_keyed { Self::apply_keyed(parent_scope, parent, next_sibling, lefts, rights) } else { Self::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights) }; - test_log!("result: {:?}", lefts); + *rights_fully_keyed = self.fully_keyed; + test_log!("result: {:?}", rights); first } } diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index e801484985c..3fee2ed2141 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -1,13 +1,12 @@ //! This module contains the implementation of abstract virtual node. use super::{Key, VChild, VComp, VList, VPortal, VSuspense, VTag, VText}; -use crate::dom_bundle::VDiff; +use crate::dom_bundle::{DomBundle, VDiff}; use crate::html::{AnyScope, BaseComponent, NodeRef}; use gloo::console; use std::cmp::PartialEq; use std::fmt; use std::iter::FromIterator; -use wasm_bindgen::JsCast; use web_sys::{Element, Node}; @@ -31,15 +30,15 @@ pub enum VNode { } impl VNode { - pub fn key(&self) -> Option { + pub fn key(&self) -> Option<&Key> { match self { - VNode::VComp(vcomp) => vcomp.key.clone(), - VNode::VList(vlist) => vlist.key.clone(), + VNode::VComp(vcomp) => vcomp.key.as_ref(), + VNode::VList(vlist) => vlist.key.as_ref(), VNode::VRef(_) => None, - VNode::VTag(vtag) => vtag.key.clone(), + VNode::VTag(vtag) => vtag.key.as_ref(), VNode::VText(_) => None, VNode::VPortal(vportal) => vportal.node.key(), - VNode::VSuspense(vsuspense) => vsuspense.key.clone(), + VNode::VSuspense(vsuspense) => vsuspense.key.as_ref(), } } @@ -55,23 +54,6 @@ impl VNode { } } - /// Returns the first DOM node if available - pub(crate) fn first_node(&self) -> Option { - match self { - VNode::VTag(vtag) => vtag.reference().cloned().map(JsCast::unchecked_into), - VNode::VText(vtext) => vtext - .reference - .as_ref() - .cloned() - .map(JsCast::unchecked_into), - VNode::VComp(vcomp) => vcomp.node_ref.get(), - VNode::VList(vlist) => vlist.get(0).and_then(VNode::first_node), - VNode::VRef(node) => Some(node.clone()), - VNode::VPortal(vportal) => vportal.next_sibling(), - VNode::VSuspense(vsuspense) => vsuspense.first_node(), - } - } - /// Returns the first DOM node that is used to designate the position of the virtual DOM node. pub(crate) fn unchecked_first_node(&self) -> Node { match self { @@ -100,16 +82,14 @@ impl VNode { .unchecked_first_node(), VNode::VRef(node) => node.clone(), VNode::VPortal(_) => panic!("portals have no first node, they are empty inside"), - VNode::VSuspense(vsuspense) => { - vsuspense.first_node().expect("VSuspense is not mounted") - } + VNode::VSuspense(_) => unreachable!("no need to get the first node of a suspense"), } } pub(crate) fn move_before(&self, parent: &Element, next_sibling: &Option) { match self { VNode::VList(vlist) => { - for node in vlist.iter() { + for node in vlist.iter().rev() { node.move_before(parent, next_sibling); } } @@ -119,13 +99,16 @@ impl VNode { .expect("VComp has no root vnode") .move_before(parent, next_sibling); } + VNode::VSuspense(vsuspense) => { + vsuspense.active_node().move_before(parent, next_sibling) + } VNode::VPortal(_) => {} // no need to move portals _ => super::insert_node(&self.unchecked_first_node(), parent, next_sibling.as_ref()), }; } } -impl VDiff for VNode { +impl DomBundle for VNode { /// Remove VNode from parent. fn detach(self, parent: &Element) { match self { @@ -143,63 +126,91 @@ impl VDiff for VNode { } } - fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { match *self { - VNode::VTag(ref vtag) => vtag.shift(previous_parent, next_parent, next_sibling), - VNode::VText(ref vtext) => vtext.shift(previous_parent, next_parent, next_sibling), - VNode::VComp(ref vcomp) => vcomp.shift(previous_parent, next_parent, next_sibling), - VNode::VList(ref vlist) => vlist.shift(previous_parent, next_parent, next_sibling), + VNode::VTag(ref vtag) => vtag.shift(next_parent, next_sibling), + VNode::VText(ref vtext) => vtext.shift(next_parent, next_sibling), + VNode::VComp(ref vcomp) => vcomp.shift(next_parent, next_sibling), + VNode::VList(ref vlist) => vlist.shift(next_parent, next_sibling), VNode::VRef(ref node) => { - previous_parent.remove_child(node).unwrap(); next_parent .insert_before(node, next_sibling.get().as_ref()) .unwrap(); } - VNode::VPortal(ref vportal) => { - vportal.shift(previous_parent, next_parent, next_sibling) + VNode::VPortal(ref vportal) => vportal.shift(next_parent, next_sibling), + VNode::VSuspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling), + } + } +} + +impl VDiff for VNode { + type Bundle = VNode; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + match self { + VNode::VTag(vtag) => { + let (node_ref, tag) = vtag.attach(parent_scope, parent, next_sibling); + (node_ref, tag.into()) + } + VNode::VText(vtext) => { + let (node_ref, text) = vtext.attach(parent_scope, parent, next_sibling); + (node_ref, text.into()) + } + VNode::VComp(vcomp) => { + let (node_ref, comp) = vcomp.attach(parent_scope, parent, next_sibling); + (node_ref, comp.into()) + } + VNode::VList(vlist) => { + let (node_ref, list) = vlist.attach(parent_scope, parent, next_sibling); + (node_ref, list.into()) + } + VNode::VRef(node) => { + super::insert_node(&node, parent, next_sibling.get().as_ref()); + (NodeRef::new(node.clone()), VNode::VRef(node)) } - VNode::VSuspense(ref vsuspense) => { - vsuspense.shift(previous_parent, next_parent, next_sibling) + VNode::VPortal(vportal) => { + let (node_ref, portal) = vportal.attach(parent_scope, parent, next_sibling); + (node_ref, portal.into()) + } + VNode::VSuspense(vsuspense) => { + let (node_ref, suspense) = vsuspense.attach(parent_scope, parent, next_sibling); + (node_ref, suspense.into()) } } } fn apply( - &mut self, + self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: Option, + ancestor: &mut VNode, ) -> NodeRef { - match *self { - VNode::VTag(ref mut vtag) => vtag.apply(parent_scope, parent, next_sibling, ancestor), - VNode::VText(ref mut vtext) => { - vtext.apply(parent_scope, parent, next_sibling, ancestor) - } - VNode::VComp(ref mut vcomp) => { - vcomp.apply(parent_scope, parent, next_sibling, ancestor) - } - VNode::VList(ref mut vlist) => { - vlist.apply(parent_scope, parent, next_sibling, ancestor) - } - VNode::VRef(ref mut node) => { - if let Some(ancestor) = ancestor { - if let VNode::VRef(n) = &ancestor { - if node == n { - return NodeRef::new(node.clone()); - } + match self { + VNode::VTag(vtag) => vtag.apply(parent_scope, parent, next_sibling, ancestor), + VNode::VText(vtext) => vtext.apply(parent_scope, parent, next_sibling, ancestor), + VNode::VComp(vcomp) => vcomp.apply(parent_scope, parent, next_sibling, ancestor), + VNode::VList(vlist) => vlist.apply(parent_scope, parent, next_sibling, ancestor), + VNode::VRef(node) => { + if let VNode::VRef(ref n) = ancestor { + if &node == n { + return NodeRef::new(node); } - ancestor.detach(parent); } - super::insert_node(node, parent, next_sibling.get().as_ref()); - NodeRef::new(node.clone()) + let (node_ref, self_) = + VNode::VRef(node).attach(parent_scope, parent, next_sibling); + ancestor.replace(parent, self_); + node_ref } - VNode::VPortal(ref mut vportal) => { - vportal.apply(parent_scope, parent, next_sibling, ancestor) - } - VNode::VSuspense(ref mut vsuspense) => { + VNode::VSuspense(vsuspense) => { vsuspense.apply(parent_scope, parent, next_sibling, ancestor) } + VNode::VPortal(vportal) => vportal.apply(parent_scope, parent, next_sibling, ancestor), } } } @@ -245,6 +256,13 @@ impl From for VNode { } } +impl From for VNode { + #[inline] + fn from(vportal: VPortal) -> Self { + VNode::VPortal(vportal) + } +} + impl From> for VNode where COMP: BaseComponent, @@ -297,6 +315,31 @@ impl PartialEq for VNode { } } +impl VNode { + pub(crate) fn replace(&mut self, parent: &Element, next_node: VNode) { + let ancestor = std::mem::replace(self, next_node); + ancestor.detach(parent); + } + + #[cfg(test)] + pub(crate) fn apply_sequentially( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ancestor: &mut Option, + ) -> NodeRef { + match ancestor { + None => { + let (node_ref, node) = self.attach(parent_scope, parent, next_sibling); + *ancestor = Some(node); + node_ref + } + Some(ref mut ancestor) => self.apply(parent_scope, parent, next_sibling, ancestor), + } + } +} + #[cfg(test)] mod layout_tests { use super::*; diff --git a/packages/yew/src/virtual_dom/vportal.rs b/packages/yew/src/virtual_dom/vportal.rs index 308dff96e48..023fabdac4e 100644 --- a/packages/yew/src/virtual_dom/vportal.rs +++ b/packages/yew/src/virtual_dom/vportal.rs @@ -1,10 +1,21 @@ //! This module contains the implementation of a portal `VPortal`. +use std::borrow::BorrowMut; + use super::VNode; -use crate::dom_bundle::VDiff; +use crate::dom_bundle::{DomBundle, VDiff}; use crate::html::{AnyScope, NodeRef}; use web_sys::{Element, Node}; +/// Log an operation during tests for debugging purposes +/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. +macro_rules! test_log { + ($fmt:literal, $($arg:expr),* $(,)?) => { + #[cfg(all(test, feature = "wasm_test", verbose_tests))] + ::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*); + }; +} + #[derive(Debug, Clone)] pub struct VPortal { /// The element under which the content is inserted. @@ -17,57 +28,60 @@ pub struct VPortal { sibling_ref: NodeRef, } -impl VDiff for VPortal { +impl DomBundle for VPortal { fn detach(self, _: &Element) { + test_log!("Detaching portal from host{:?}", self.host.outer_html()); self.node.detach(&self.host); + test_log!("Detached portal from host{:?}", self.host.outer_html()); self.sibling_ref.set(None); } - fn shift(&self, _previous_parent: &Element, _next_parent: &Element, _next_sibling: NodeRef) { + fn shift(&self, _next_parent: &Element, _next_sibling: NodeRef) { // portals have nothing in it's original place of DOM, we also do nothing. } +} + +impl VDiff for VPortal { + type Bundle = VPortal; + + fn attach( + mut self, + parent_scope: &AnyScope, + _: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let (_, inner) = self + .node + .attach(parent_scope, &self.host, self.next_sibling.clone()); + self.node = Box::new(inner); + self.sibling_ref = next_sibling.clone(); + (next_sibling, self) + } fn apply( - &mut self, + self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: Option, + ancestor: &mut VNode, ) -> NodeRef { - let inner_ancestor = match ancestor { - Some(VNode::VPortal(old_portal)) => { - let VPortal { - host: old_host, - next_sibling: old_sibling, - node, - .. - } = old_portal; - if old_host != self.host { - // Remount the inner node somewhere else instead of diffing - node.detach(&old_host); - None - } else if old_sibling != self.next_sibling { - // Move the node, but keep the state - node.move_before(&self.host, &self.next_sibling.get()); - Some(*node) - } else { - Some(*node) - } + if let VNode::VPortal(portal) = ancestor { + let old_host = std::mem::replace(&mut portal.host, self.host); + let old_sibling = std::mem::replace(&mut portal.next_sibling, self.next_sibling); + let node = &mut portal.node; + if old_host != portal.host || old_sibling != portal.next_sibling { + // Remount the inner node somewhere else instead of diffing + // Move the node, but keep the state + node.move_before(&portal.host, &portal.next_sibling.get()); } - Some(node) => { - node.detach(parent); - None - } - None => None, - }; - - self.node.apply( - parent_scope, - &self.host, - self.next_sibling.clone(), - inner_ancestor, - ); - self.sibling_ref = next_sibling.clone(); + let inner_ancestor = node.borrow_mut(); + self.node + .apply(parent_scope, parent, next_sibling.clone(), inner_ancestor); + return next_sibling; + } + + let (_, self_) = self.attach(parent_scope, parent, next_sibling.clone()); + ancestor.replace(parent, self_.into()); next_sibling } } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 8c377b78549..f965000ba58 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -1,25 +1,22 @@ use super::{Key, VNode}; use crate::{ - dom_bundle::VDiff, + dom_bundle::{DomBundle, VDiff}, html::{AnyScope, NodeRef}, }; -use web_sys::{Element, Node}; +use std::borrow::BorrowMut; +use web_sys::Element; /// This struct represents a suspendable DOM fragment. #[derive(Clone, Debug, PartialEq)] pub struct VSuspense { /// Child nodes. children: Box, - /// Fallback nodes when suspended. fallback: Box, - /// The element to attach to when children is not attached to DOM detached_parent: Element, - /// Whether the current status is suspended. suspended: bool, - /// The Key. pub(crate) key: Option, } @@ -41,16 +38,16 @@ impl VSuspense { } } - pub(crate) fn first_node(&self) -> Option { + pub(crate) fn active_node(&self) -> &VNode { if self.suspended { - self.fallback.first_node() + &self.fallback } else { - self.children.first_node() + &self.children } } } -impl VDiff for VSuspense { +impl DomBundle for VSuspense { fn detach(self, parent: &Element) { if self.suspended { self.fallback.detach(parent); @@ -60,44 +57,68 @@ impl VDiff for VSuspense { } } - fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { - if self.suspended { - self.fallback - .shift(previous_parent, next_parent, next_sibling); + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + self.active_node().shift(next_parent, next_sibling) + } +} + +impl VDiff for VSuspense { + type Bundle = VSuspense; + + fn attach( + mut self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + // When it's suspended, we render children into an element that is detached from the dom + // tree while rendering fallback UI into the original place where children resides in. + let node_ref = if self.suspended { + let (_child_ref, children) = + self.children + .attach(parent_scope, &self.detached_parent, NodeRef::default()); + self.children = children.into(); + let (fallback_ref, fallback) = self.fallback.attach(parent_scope, parent, next_sibling); + self.fallback = fallback.into(); + fallback_ref } else { - self.children - .shift(previous_parent, next_parent, next_sibling); - } + let (child_ref, children) = self.children.attach(parent_scope, parent, next_sibling); + self.children = children.into(); + child_ref + }; + (node_ref, self) } fn apply( - &mut self, + self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: Option, + ancestor: &mut VNode, ) -> NodeRef { - let (already_suspended, children_ancestor, fallback_ancestor) = match ancestor { - Some(VNode::VSuspense(m)) => { - // We only preserve the child state if they are the same suspense. - if m.key != self.key || self.detached_parent != m.detached_parent { - m.detach(parent); - - (false, None, None) - } else { - (m.suspended, Some(*m.children), Some(*m.fallback)) - } + let suspense = match ancestor { + // We only preserve the child state if they are the same suspense. + VNode::VSuspense(m) + if m.key == self.key && self.detached_parent == m.detached_parent => + { + m } - Some(m) => { - m.detach(parent); - (false, None, None) + _ => { + let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); + ancestor.replace(parent, self_.into()); + return self_ref; } - None => (false, None, None), }; + let was_suspended = suspense.suspended; + let children_ancestor = suspense.children.borrow_mut(); + let fallback_ancestor = suspense.fallback.borrow_mut(); + + suspense.suspended = self.suspended; + // no need to update key & detached_parent // When it's suspended, we render children into an element that is detached from the dom // tree while rendering fallback UI into the original place where children resides in. - match (self.suspended, already_suspended) { + match (self.suspended, was_suspended) { (true, true) => { self.children.apply( parent_scope, @@ -116,11 +137,7 @@ impl VDiff for VSuspense { } (true, false) => { - children_ancestor.as_ref().unwrap().shift( - parent, - &self.detached_parent, - NodeRef::default(), - ); + children_ancestor.shift(&self.detached_parent, NodeRef::default()); self.children.apply( parent_scope, @@ -128,20 +145,17 @@ impl VDiff for VSuspense { NodeRef::default(), children_ancestor, ); - - // first render of fallback, ancestor needs to be None. - self.fallback - .apply(parent_scope, parent, next_sibling, None) + // first render of fallback + let (fallback_ref, fallback) = + self.fallback.attach(parent_scope, parent, next_sibling); + *fallback_ancestor = fallback; + fallback_ref } (false, true) => { - fallback_ancestor.unwrap().detach(parent); + fallback_ancestor.replace(parent, VNode::default()); - children_ancestor.as_ref().unwrap().shift( - &self.detached_parent, - parent, - next_sibling.clone(), - ); + children_ancestor.shift(parent, next_sibling.clone()); self.children .apply(parent_scope, parent, next_sibling, children_ancestor) } diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 3f1b5bff6c2..6e1838048ee 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1,7 +1,7 @@ //! This module contains the implementation of a virtual element node [VTag]. use super::{Apply, AttrValue, Attributes, Key, Listener, Listeners, VList, VNode}; -use crate::dom_bundle::VDiff; +use crate::dom_bundle::{DomBundle, VDiff}; use crate::html::{AnyScope, IntoPropValue, NodeRef}; use gloo::console; use gloo_utils::document; @@ -10,7 +10,7 @@ use std::cmp::PartialEq; use std::hint::unreachable_unchecked; use std::marker::PhantomData; use std::mem; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; use std::rc::Rc; use wasm_bindgen::JsCast; use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; @@ -40,7 +40,7 @@ impl Apply for Value { } } - fn apply_diff(&mut self, el: &Self::Element, ancestor: Self) { + fn apply_diff(self, el: &Self::Element, ancestor: &mut Self) { match (&self.0, &ancestor.0) { (Some(new), Some(_)) => { // Refresh value from the DOM. It might have changed. @@ -107,12 +107,12 @@ impl Apply for InputFields { self.value.apply(el); } - fn apply_diff(&mut self, el: &Self::Element, ancestor: Self) { + fn apply_diff(self, el: &Self::Element, ancestor: &mut Self) { // IMPORTANT! This parameter has to be set every time // to prevent strange behaviour in the browser when the DOM changes el.set_checked(self.checked); - self.value.apply_diff(el, ancestor.value); + self.value.apply_diff(el, &mut ancestor.value); } } @@ -140,7 +140,7 @@ enum VTagInner { tag: Cow<'static, str>, /// List of child nodes - children: VList, + children: VNode, }, } @@ -281,6 +281,7 @@ impl VTag { listeners: Listeners, children: VList, ) -> Self { + let children = children.into(); VTag::new_base( VTagInner::Other { tag, children }, node_ref, @@ -322,21 +323,30 @@ impl VTag { /// Add [VNode] child. pub fn add_child(&mut self, child: VNode) { if let VTagInner::Other { children, .. } = &mut self.inner { - children.add_child(child); + match children { + VNode::VList(children) => children.add_child(child), + _ => unreachable!(), + } } } /// Add multiple [VNode] children. pub fn add_children(&mut self, children: impl IntoIterator) { if let VTagInner::Other { children: dst, .. } = &mut self.inner { - dst.add_children(children) + match dst { + VNode::VList(dst) => dst.add_children(children), + _ => unreachable!(), + } } } /// Returns a reference to the children of this [VTag] pub fn children(&self) -> &VList { match &self.inner { - VTagInner::Other { children, .. } => children, + VTagInner::Other { children, .. } => match children { + VNode::VList(children) => children, + _ => unreachable!(), + }, _ => { // This is mutable because the VList is not Sync static mut EMPTY: VList = VList::new(); @@ -351,7 +361,10 @@ impl VTag { // children pub fn children_mut(&mut self) -> Option<&mut VList> { match &mut self.inner { - VTagInner::Other { children, .. } => Some(children), + VTagInner::Other { children, .. } => match children { + VNode::VList(children) => Some(children), + _ => unreachable!(), + }, _ => None, } } @@ -470,7 +483,7 @@ impl VTag { } } -impl VDiff for VTag { +impl DomBundle for VTag { /// Remove VTag from parent. fn detach(mut self, parent: &Element) { let node = self @@ -494,123 +507,121 @@ impl VDiff for VTag { } } - fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { let node = self .reference .as_ref() .expect("tried to shift not rendered VTag from DOM"); - previous_parent.remove_child(node).unwrap(); next_parent .insert_before(node, next_sibling.get().as_ref()) .unwrap(); } +} + +impl VDiff for VTag { + type Bundle = VTag; + + fn attach( + mut self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let el = self.create_element(parent); + super::insert_node(&el, parent, next_sibling.get().as_ref()); + + self.attributes.apply(&el); + self.listeners.apply(&el); + self.inner = match self.inner { + VTagInner::Input(mut f) => { + f.apply(el.unchecked_ref()); + VTagInner::Input(f) + } + VTagInner::Textarea { mut value } => { + value.apply(el.unchecked_ref()); + VTagInner::Textarea { value } + } + VTagInner::Other { children, tag } => { + let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default()); + VTagInner::Other { + children: child_bundle, + tag, + } + } + }; + self.node_ref.set(Some(el.deref().clone())); + self.reference = el.into(); + (self.node_ref.clone(), self) + } /// Renders virtual tag over DOM [Element], but it also compares this with an ancestor [VTag] /// to compute what to patch in the actual DOM nodes. fn apply( - &mut self, + self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: Option, + node_bundle: &mut VNode, ) -> NodeRef { // This kind of branching patching routine reduces branch predictor misses and the need to // unpack the enums (including `Option`s) all the time, resulting in a more streamlined // patching flow - let (ancestor_tag, el) = match ancestor { - Some(ancestor) => { - // If the ancestor is a tag of the same type, don't recreate, keep the - // old tag and update its attributes and children. - if match &ancestor { - VNode::VTag(a) => { - self.key == a.key - && match (&self.inner, &a.inner) { - (VTagInner::Input(_), VTagInner::Input(_)) - | (VTagInner::Textarea { .. }, VTagInner::Textarea { .. }) => true, - ( - VTagInner::Other { tag: l, .. }, - VTagInner::Other { tag: r, .. }, - ) => l == r, - _ => false, - } - } - _ => false, - } { - match ancestor { - VNode::VTag(mut a) => { - // Preserve the reference that already exists - let el = a.reference.take().unwrap(); - if self.node_ref.get().as_ref() == self.reference.as_deref() { - a.node_ref.set(None); - } - (Some(a), el) - } - _ => unsafe { unreachable_unchecked() }, - } - } else { - let el = self.create_element(parent); - super::insert_node(&el, parent, ancestor.first_node().as_ref()); - ancestor.detach(parent); - (None, el) + let is_matching_tag = match node_bundle { + VNode::VTag(ex) if self.key == ex.key => match (&self.inner, &ex.inner) { + (VTagInner::Input(_), VTagInner::Input(_)) => true, + (VTagInner::Textarea { .. }, VTagInner::Textarea { .. }) => true, + (VTagInner::Other { tag: l, .. }, VTagInner::Other { tag: r, .. }) if l == r => { + true } + _ => false, + }, + _ => false, + }; + // If the ancestor is a tag of the same type, don't recreate, keep the + // old tag and update its attributes and children. + let tag = if is_matching_tag { + match node_bundle { + VNode::VTag(a) => { + // Preserve the reference that already exists + a.deref_mut() + } + _ => unsafe { unreachable_unchecked() }, } - None => (None, { - let el = self.create_element(parent); - super::insert_node(&el, parent, next_sibling.get().as_ref()); - el - }), + } else { + let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); + node_bundle.replace(parent, self_.into()); + return self_ref; }; + let el = tag.reference.take().unwrap(); - match ancestor_tag { - None => { - self.attributes.apply(&el); - self.listeners.apply(&el); + self.attributes.apply_diff(&el, &mut tag.attributes); + self.listeners.apply_diff(&el, &mut tag.listeners); - match &mut self.inner { - VTagInner::Input(f) => { - f.apply(el.unchecked_ref()); - } - VTagInner::Textarea { value } => { - value.apply(el.unchecked_ref()); - } - VTagInner::Other { children, .. } => { - if !children.is_empty() { - children.apply(parent_scope, &el, NodeRef::default(), None); - } - } - } + match (self.inner, &mut tag.inner) { + (VTagInner::Input(new), VTagInner::Input(old)) => { + new.apply_diff(el.unchecked_ref(), old); } - Some(ancestor) => { - self.attributes.apply_diff(&el, ancestor.attributes); - self.listeners.apply_diff(&el, ancestor.listeners); - - match (&mut self.inner, ancestor.inner) { - (VTagInner::Input(new), VTagInner::Input(old)) => { - new.apply_diff(el.unchecked_ref(), old); - } - (VTagInner::Textarea { value: new }, VTagInner::Textarea { value: old }) => { - new.apply_diff(el.unchecked_ref(), old); - } - ( - VTagInner::Other { children: new, .. }, - VTagInner::Other { children: old, .. }, - ) => { - if !new.is_empty() { - new.apply(parent_scope, &el, NodeRef::default(), Some(old.into())); - } else if !old.is_empty() { - old.detach(&el); - } - } - // Can not happen, because we checked for tag equability above - _ => unsafe { unreachable_unchecked() }, - } + (VTagInner::Textarea { value: new }, VTagInner::Textarea { value: old }) => { + new.apply_diff(el.unchecked_ref(), old); } - }; + (VTagInner::Other { children: new, .. }, VTagInner::Other { children: old, .. }) => { + new.apply(parent_scope, &el, NodeRef::default(), old); + } + // Can not happen, because we checked for tag equability above + _ => unsafe { unreachable_unchecked() }, + } - self.node_ref.set(Some(el.deref().clone())); - self.reference = el.into(); - self.node_ref.clone() + tag.key = self.key; + + if self.node_ref != tag.node_ref && tag.node_ref.get().as_ref() == Some(&el) { + tag.node_ref.set(None); + } + tag.node_ref = self.node_ref; + tag.node_ref.set(Some(el.deref().clone())); + + tag.reference = el.into(); + tag.node_ref.clone() } } @@ -619,10 +630,7 @@ impl PartialEq for VTag { use VTagInner::*; (match (&self.inner, &other.inner) { - ( - Input(l), - Input (r), - ) => l == r, + (Input(l), Input(r)) => l == r, (Textarea { value: value_l }, Textarea { value: value_r }) => value_l == value_r, (Other { tag: tag_l, .. }, Other { tag: tag_r, .. }) => tag_l == tag_r, _ => false, @@ -793,7 +801,14 @@ mod tests { assert_ne!(a, d); } - fn assert_vtag(node: &VNode) -> &VTag { + fn assert_vtag(node: VNode) -> VTag { + if let VNode::VTag(vtag) = node { + return *vtag; + } + panic!("should be vtag"); + } + + fn assert_vtag_ref(node: &VNode) -> &VTag { if let VNode::VTag(vtag) = node { return vtag; } @@ -824,23 +839,23 @@ mod tests { let namespace = Some(namespace); let svg_el = document.create_element_ns(namespace, "svg").unwrap(); - let mut g_node = html! { }; + let g_node = html! { }; let path_node = html! { }; - let mut svg_node = html! { {path_node} }; + let svg_node = html! { {path_node} }; - let svg_tag = assert_vtag_mut(&mut svg_node); - svg_tag.apply(&scope, &div_el, NodeRef::default(), None); - assert_namespace(svg_tag, SVG_NAMESPACE); - let path_tag = assert_vtag(svg_tag.children().get(0).unwrap()); + let svg_tag = assert_vtag(svg_node); + let (_, svg_tag) = svg_tag.attach(&scope, &div_el, NodeRef::default()); + assert_namespace(&svg_tag, SVG_NAMESPACE); + let path_tag = assert_vtag_ref(svg_tag.children().get(0).unwrap()); assert_namespace(path_tag, SVG_NAMESPACE); - let g_tag = assert_vtag_mut(&mut g_node); - g_tag.apply(&scope, &div_el, NodeRef::default(), None); - assert_namespace(g_tag, HTML_NAMESPACE); + let g_tag = assert_vtag(g_node); + let (_, mut g_tag) = g_tag.attach(&scope, &div_el, NodeRef::default()); + assert_namespace(&g_tag, HTML_NAMESPACE); g_tag.reference = None; - g_tag.apply(&scope, &svg_el, NodeRef::default(), None); - assert_namespace(g_tag, SVG_NAMESPACE); + let (_, g_tag) = g_tag.attach(&scope, &svg_el, NodeRef::default()); + assert_namespace(&g_tag, SVG_NAMESPACE); } #[test] @@ -940,8 +955,8 @@ mod tests { document().body().unwrap().append_child(&parent).unwrap(); - let mut elem = html! {
}; - VDiff::apply(&mut elem, &scope, &parent, NodeRef::default(), None); + let elem = html! {
}; + let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); let vtag = assert_vtag_mut(&mut elem); // test if the className has not been set assert!(!vtag.reference.as_ref().unwrap().has_attribute("class")); @@ -953,8 +968,8 @@ mod tests { document().body().unwrap().append_child(&parent).unwrap(); - let mut elem = gen_html(); - VDiff::apply(&mut elem, &scope, &parent, NodeRef::default(), None); + let elem = gen_html(); + let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); let vtag = assert_vtag_mut(&mut elem); // test if the className has been set assert!(vtag.reference.as_ref().unwrap().has_attribute("class")); @@ -980,30 +995,21 @@ mod tests { let expected = "not_changed_value"; // Initial state - let mut elem = html! { }; - VDiff::apply(&mut elem, &scope, &parent, NodeRef::default(), None); - let vtag = if let VNode::VTag(vtag) = elem { - vtag - } else { - panic!("should be vtag") - }; + let elem = html! { }; + let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_vtag_ref(&elem); // User input let input_ref = vtag.reference.as_ref().unwrap(); let input = input_ref.dyn_ref::(); input.unwrap().set_value("User input"); - let ancestor = vtag; - let mut elem = html! { }; - let vtag = assert_vtag_mut(&mut elem); + let next_elem = html! { }; + let elem_vtag = assert_vtag(next_elem); // Sync happens here - vtag.apply( - &scope, - &parent, - NodeRef::default(), - Some(VNode::VTag(ancestor)), - ); + elem_vtag.apply(&scope, &parent, NodeRef::default(), &mut elem); + let vtag = assert_vtag_ref(&elem); // Get new current value of the input element let input_ref = vtag.reference.as_ref().unwrap(); @@ -1023,30 +1029,21 @@ mod tests { document().body().unwrap().append_child(&parent).unwrap(); // Initial state - let mut elem = html! { }; - VDiff::apply(&mut elem, &scope, &parent, NodeRef::default(), None); - let vtag = if let VNode::VTag(vtag) = elem { - vtag - } else { - panic!("should be vtag") - }; + let elem = html! { }; + let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_vtag_ref(&elem); // User input let input_ref = vtag.reference.as_ref().unwrap(); let input = input_ref.dyn_ref::(); input.unwrap().set_value("User input"); - let ancestor = vtag; - let mut elem = html! { }; - let vtag = assert_vtag_mut(&mut elem); + let next_elem = html! { }; + let elem_vtag = assert_vtag(next_elem); // Value should not be refreshed - vtag.apply( - &scope, - &parent, - NodeRef::default(), - Some(VNode::VTag(ancestor)), - ); + elem_vtag.apply(&scope, &parent, NodeRef::default(), &mut elem); + let vtag = assert_vtag_ref(&elem); // Get user value of the input element let input_ref = vtag.reference.as_ref().unwrap(); @@ -1069,13 +1066,13 @@ mod tests { document().body().unwrap().append_child(&parent).unwrap(); - let mut elem = html! { <@{ + let elem = html! { <@{ let mut builder = String::new(); builder.push('a'); builder }/> }; - VDiff::apply(&mut elem, &scope, &parent, NodeRef::default(), None); + let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); let vtag = assert_vtag_mut(&mut elem); // make sure the new tag name is used internally assert_eq!(vtag.tag(), "a"); @@ -1125,7 +1122,7 @@ mod tests { let node_ref = NodeRef::default(); let mut elem: VNode = html! {
}; assert_vtag_mut(&mut elem); - elem.apply(&scope, &parent, NodeRef::default(), None); + let (_, elem) = elem.attach(&scope, &parent, NodeRef::default()); let parent_node = parent.deref(); assert_eq!(node_ref.get(), parent_node.first_child()); elem.detach(&parent); @@ -1139,15 +1136,15 @@ mod tests { document().body().unwrap().append_child(&parent).unwrap(); let node_ref_a = NodeRef::default(); - let mut elem_a = html! {
}; - elem_a.apply(&scope, &parent, NodeRef::default(), None); + let elem_a = html! {
}; + let (_, mut elem) = elem_a.attach(&scope, &parent, NodeRef::default()); // save the Node to check later that it has been reused. let node_a = node_ref_a.get().unwrap(); let node_ref_b = NodeRef::default(); - let mut elem_b = html! {
}; - elem_b.apply(&scope, &parent, NodeRef::default(), Some(elem_a)); + let elem_b = html! {
}; + elem_b.apply(&scope, &parent, NodeRef::default(), &mut elem); let node_b = node_ref_b.get().unwrap(); @@ -1165,12 +1162,12 @@ mod tests { document().body().unwrap().append_child(&parent).unwrap(); let test_ref = NodeRef::default(); - let mut before = html! { + let before = html! { <>
}; - let mut after = html! { + let after = html! { <>
@@ -1179,8 +1176,8 @@ mod tests { // The point of this diff is to first render the "after" div and then detach the "before" div, // while both should be bound to the same node ref - before.apply(&scope, &parent, NodeRef::default(), None); - after.apply(&scope, &parent, NodeRef::default(), Some(before)); + let (_, mut elem) = before.attach(&scope, &parent, NodeRef::default()); + after.apply(&scope, &parent, NodeRef::default(), &mut elem); assert_eq!( test_ref diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index a3a3d6a77d3..7e8decf7ffc 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -1,7 +1,7 @@ //! This module contains the implementation of a virtual text node `VText`. use super::{AttrValue, VNode}; -use crate::dom_bundle::VDiff; +use crate::dom_bundle::{DomBundle, VDiff}; use crate::html::{AnyScope, NodeRef}; use gloo::console; use gloo_utils::document; @@ -43,7 +43,7 @@ impl std::fmt::Debug for VText { } } -impl VDiff for VText { +impl DomBundle for VText { /// Remove VText from parent. fn detach(mut self, parent: &Element) { let node = self @@ -55,47 +55,58 @@ impl VDiff for VText { } } - fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { let node = self .reference .as_ref() .expect("tried to shift not rendered VTag from DOM"); - previous_parent.remove_child(node).unwrap(); next_parent .insert_before(node, next_sibling.get().as_ref()) .unwrap(); } +} + +impl VDiff for VText { + type Bundle = VText; + + fn attach( + mut self, + _parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let text_node = document().create_text_node(&self.text); + super::insert_node(&text_node, parent, next_sibling.get().as_ref()); + self.reference = Some(text_node.clone()); + let node_ref = NodeRef::new(text_node.into()); + (node_ref, self) + } /// Renders virtual node over existing `TextNode`, but only if value of text has changed. fn apply( - &mut self, - _parent_scope: &AnyScope, + self, + parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: Option, + ancestor: &mut VNode, ) -> NodeRef { - if let Some(ancestor) = ancestor { - if let VNode::VText(mut vtext) = ancestor { - self.reference = vtext.reference.take(); - let text_node = self - .reference - .clone() - .expect("Rendered VText nodes should have a ref"); - if self.text != vtext.text { - text_node.set_node_value(Some(&self.text)); - } - - return NodeRef::new(text_node.into()); + if let VNode::VText(ref mut vtext) = ancestor { + let ancestor = std::mem::replace(vtext, self); + vtext.reference = ancestor.reference; + let text_node = vtext + .reference + .clone() + .expect("Rendered VText nodes should have a ref"); + if vtext.text != ancestor.text { + text_node.set_node_value(Some(&vtext.text)); } - ancestor.detach(parent); + return NodeRef::new(text_node.into()); } - - let text_node = document().create_text_node(&self.text); - super::insert_node(&text_node, parent, next_sibling.get().as_ref()); - self.reference = Some(text_node.clone()); - NodeRef::new(text_node.into()) + let (node_ref, self_) = self.attach(parent_scope, parent, next_sibling); + ancestor.replace(parent, self_.into()); + node_ref } } From 38fb92517dcd6c2255e48198684d9bdd668e8e8a Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 5 Jan 2022 15:37:12 +0100 Subject: [PATCH 04/26] port over the infrastructure the new bcomp is especially nice and lost a few unwraps owed to not having to reserve space for a scope before rendering. Note also that bsuspense has been slimmed a bit, storing the suspended flag implicitly in the state. some naming is not perfect yet and has to be adjusted still. --- packages/yew/src/app_handle.rs | 6 +- packages/yew/src/dom_bundle/attributes.rs | 362 +++++ packages/yew/src/dom_bundle/bcomp.rs | 800 +++++++++++ packages/yew/src/dom_bundle/blist.rs | 1283 ++++++++++++++++++ packages/yew/src/dom_bundle/bnode.rs | 291 ++++ packages/yew/src/dom_bundle/bportal.rs | 177 +++ packages/yew/src/dom_bundle/bsuspense.rs | 156 +++ packages/yew/src/dom_bundle/btag.rs | 1057 +++++++++++++++ packages/yew/src/dom_bundle/listeners.rs | 696 ++++++++++ packages/yew/src/dom_bundle/mod.rs | 62 +- packages/yew/src/html/component/lifecycle.rs | 25 +- packages/yew/src/html/component/scope.rs | 21 +- packages/yew/src/lib.rs | 2 +- packages/yew/src/tests/layout_tests.rs | 8 +- packages/yew/src/virtual_dom/listeners.rs | 723 +--------- packages/yew/src/virtual_dom/mod.rs | 213 +-- packages/yew/src/virtual_dom/vcomp.rs | 800 +---------- packages/yew/src/virtual_dom/vlist.rs | 1266 +---------------- packages/yew/src/virtual_dom/vnode.rs | 239 +--- packages/yew/src/virtual_dom/vportal.rs | 166 +-- packages/yew/src/virtual_dom/vsuspense.rs | 137 +- packages/yew/src/virtual_dom/vtag.rs | 1144 +--------------- packages/yew/src/virtual_dom/vtext.rs | 10 +- 23 files changed, 4991 insertions(+), 4653 deletions(-) create mode 100644 packages/yew/src/dom_bundle/attributes.rs create mode 100644 packages/yew/src/dom_bundle/bcomp.rs create mode 100644 packages/yew/src/dom_bundle/blist.rs create mode 100644 packages/yew/src/dom_bundle/bnode.rs create mode 100644 packages/yew/src/dom_bundle/bportal.rs create mode 100644 packages/yew/src/dom_bundle/bsuspense.rs create mode 100644 packages/yew/src/dom_bundle/btag.rs create mode 100644 packages/yew/src/dom_bundle/listeners.rs diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs index 7f66c78ff45..94e1c916338 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/app_handle.rs @@ -25,7 +25,11 @@ where pub(crate) fn mount_with_props(element: Element, props: Rc) -> Self { clear_element(&element); let app = Self { - scope: Scope::new(None), + scope: Scope::new( + None, + #[cfg(debug_assertions)] + u64::MAX, + ), }; app.scope .mount_in_place(element, NodeRef::default(), NodeRef::default(), props); diff --git a/packages/yew/src/dom_bundle/attributes.rs b/packages/yew/src/dom_bundle/attributes.rs new file mode 100644 index 00000000000..912329eace5 --- /dev/null +++ b/packages/yew/src/dom_bundle/attributes.rs @@ -0,0 +1,362 @@ +use crate::virtual_dom::{AttrValue, Attributes}; +use indexmap::IndexMap; +use std::{ + collections::HashMap, + iter, + marker::PhantomData, + ops::{Deref, DerefMut}, +}; +use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; + +// Value field corresponding to an [Element]'s `value` property +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct Value(Option, PhantomData); + +impl Default for Value { + fn default() -> Self { + Value(None, PhantomData) + } +} + +impl Value { + /// Create a new value. The caller should take care that the value is valid for the element's `value` property + pub(crate) fn new(value: Option) -> Self { + Value(value, PhantomData) + } + /// Set a new value. The caller should take care that the value is valid for the element's `value` property + pub(crate) fn set(&mut self, value: Option) { + self.0 = value; + } +} + +impl Deref for Value { + type Target = Option; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Apply for Value { + type Element = T; + type Bundle = Self; + + fn apply(self, el: &Self::Element) -> Self { + if let Some(v) = &self.0 { + el.set_value(v); + } + self + } + + fn apply_diff(self, el: &Self::Element, ancestor: &mut Self) { + match (&self.0, &ancestor.0) { + (Some(new), Some(_)) => { + // Refresh value from the DOM. It might have changed. + if new.as_ref() != el.value() { + el.set_value(new); + } + } + (Some(new), None) => el.set_value(new), + (None, Some(_)) => el.set_value(""), + (None, None) => (), + } + } +} + +macro_rules! impl_access_value { + ($( $type:ty )*) => { + $( + impl AccessValue for $type { + #[inline] + fn value(&self) -> String { + <$type>::value(&self) + } + + #[inline] + fn set_value(&self, v: &str) { + <$type>::set_value(&self, v) + } + } + )* + }; +} +impl_access_value! {InputElement TextAreaElement} + +/// Able to have its value read or set +pub(crate) trait AccessValue { + fn value(&self) -> String; + fn set_value(&self, v: &str); +} + +/// Applies contained changes to DOM [Element] +pub(crate) trait Apply { + /// [Element] type to apply the changes to + type Element; + type Bundle; + + /// Apply contained values to [Element] with no ancestor + fn apply(self, el: &Self::Element) -> Self::Bundle; + + /// Apply diff between [self] and `ancestor` to [Element]. + fn apply_diff(self, el: &Self::Element, ancestor: &mut Self::Bundle); +} + +/// Fields specific to +/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag]s +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub(crate) struct InputFields { + /// Contains a value of an + /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). + value: Value, + /// Represents `checked` attribute of + /// [input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-checked). + /// It exists to override standard behavior of `checked` attribute, because + /// in original HTML it sets `defaultChecked` value of `InputElement`, but for reactive + /// frameworks it's more useful to control `checked` value of an `InputElement`. + checked: bool, +} + +impl Deref for InputFields { + type Target = Value; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl DerefMut for InputFields { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.value + } +} + +impl InputFields { + pub(crate) fn new(value: Option, checked: bool) -> Self { + Self { + value: Value::new(value), + checked, + } + } + + pub(crate) fn checked(&self) -> bool { + self.checked + } + + pub(crate) fn set_checked(&mut self, checked: bool) { + self.checked = checked; + } +} + +impl Apply for InputFields { + type Element = InputElement; + type Bundle = Self; + + fn apply(mut self, el: &Self::Element) -> Self { + // IMPORTANT! This parameter has to be set every time + // to prevent strange behaviour in the browser when the DOM changes + el.set_checked(self.checked); + + self.value = self.value.apply(el); + self + } + + fn apply_diff(self, el: &Self::Element, ancestor: &mut Self) { + // IMPORTANT! This parameter has to be set every time + // to prevent strange behaviour in the browser when the DOM changes + el.set_checked(self.checked); + + self.value.apply_diff(el, &mut ancestor.value); + } +} + +impl Attributes { + #[cold] + fn apply_diff_index_maps<'a, A, B>( + el: &Element, + // this makes it possible to diff `&'a IndexMap<_, A>` and `IndexMap<_, &'a A>`. + mut new_iter: impl Iterator, + new: &IndexMap<&'static str, A>, + old: &IndexMap<&'static str, B>, + ) where + A: AsRef, + B: AsRef, + { + let mut old_iter = old.iter(); + loop { + match (new_iter.next(), old_iter.next()) { + (Some((new_key, new_value)), Some((old_key, old_value))) => { + if new_key != *old_key { + break; + } + if new_value != old_value.as_ref() { + Self::set_attribute(el, new_key, new_value); + } + } + // new attributes + (Some(attr), None) => { + for (key, value) in iter::once(attr).chain(new_iter) { + match old.get(key) { + Some(old_value) => { + if value != old_value.as_ref() { + Self::set_attribute(el, key, value); + } + } + None => { + Self::set_attribute(el, key, value); + } + } + } + break; + } + // removed attributes + (None, Some(attr)) => { + for (key, _) in iter::once(attr).chain(old_iter) { + if !new.contains_key(key) { + Self::remove_attribute(el, key); + } + } + break; + } + (None, None) => break, + } + } + } + + /// Convert [Attributes] pair to [HashMap]s and patch changes to `el`. + /// Works with any [Attributes] variants. + #[cold] + fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) { + fn collect<'a>(src: &'a Attributes) -> HashMap<&'static str, &'a str> { + use Attributes::*; + + match src { + Static(arr) => (*arr).iter().map(|[k, v]| (*k, *v)).collect(), + Dynamic { keys, values } => keys + .iter() + .zip(values.iter()) + .filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref()))) + .collect(), + IndexMap(m) => m.iter().map(|(k, v)| (*k, v.as_ref())).collect(), + } + } + + let new = collect(new); + let old = collect(old); + + // Update existing or set new + for (k, new) in new.iter() { + if match old.get(k) { + Some(old) => old != new, + None => true, + } { + el.set_attribute(k, new).unwrap(); + } + } + + // Remove missing + for k in old.keys() { + if !new.contains_key(k) { + Self::remove_attribute(el, k); + } + } + } + + fn set_attribute(el: &Element, key: &str, value: &str) { + el.set_attribute(key, value).expect("invalid attribute key") + } + + fn remove_attribute(el: &Element, key: &str) { + el.remove_attribute(key) + .expect("could not remove attribute") + } +} + +impl Apply for Attributes { + type Element = Element; + type Bundle = Self; + + fn apply(self, el: &Element) -> Self { + match &self { + Self::Static(arr) => { + for kv in arr.iter() { + Self::set_attribute(el, kv[0], kv[1]); + } + } + Self::Dynamic { keys, values } => { + for (k, v) in keys.iter().zip(values.iter()) { + if let Some(v) = v { + Self::set_attribute(el, k, v) + } + } + } + Self::IndexMap(m) => { + for (k, v) in m.iter() { + Self::set_attribute(el, k, v) + } + } + } + self + } + + fn apply_diff(self, el: &Element, bundle: &mut Self) { + #[inline] + fn ptr_eq(a: &[T], b: &[T]) -> bool { + a.as_ptr() == b.as_ptr() + } + + let ancestor = std::mem::replace(bundle, self); + match (bundle, ancestor) { + // Hot path + (Self::Static(new), Self::Static(old)) if ptr_eq(new, old) => (), + // Hot path + ( + Self::Dynamic { + keys: new_k, + values: new_v, + }, + Self::Dynamic { + keys: old_k, + values: old_v, + }, + ) if ptr_eq(new_k, old_k) => { + // Double zipping does not optimize well, so use asserts and unsafe instead + assert!(new_k.len() == new_v.len()); + assert!(new_k.len() == old_v.len()); + for i in 0..new_k.len() { + macro_rules! key { + () => { + unsafe { new_k.get_unchecked(i) } + }; + } + macro_rules! set { + ($new:expr) => { + Self::set_attribute(el, key!(), $new) + }; + } + + match unsafe { (new_v.get_unchecked(i), old_v.get_unchecked(i)) } { + (Some(new), Some(old)) => { + if new != old { + set!(new); + } + } + (Some(new), None) => set!(new), + (None, Some(_)) => { + Self::remove_attribute(el, key!()); + } + (None, None) => (), + } + } + } + // For VTag's constructed outside the html! macro + (Self::IndexMap(new), Self::IndexMap(old)) => { + let new_iter = new.iter().map(|(k, v)| (*k, v.as_ref())); + Self::apply_diff_index_maps(el, new_iter, new, &old); + } + // Cold path. Happens only with conditional swapping and reordering of `VTag`s with the + // same tag and no keys. + (new, ancestor) => { + Self::apply_diff_as_maps(el, new, &ancestor); + } + } + } +} diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs new file mode 100644 index 00000000000..d1356b9acfd --- /dev/null +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -0,0 +1,800 @@ +//! This module contains the bundle implementation of a virtual component `BComp`. + +use super::{BNode, DomBundle, VDiff}; +use crate::{ + html::{AnyScope, BaseComponent, Scope, Scoped}, + virtual_dom::{Key, VComp}, + NodeRef, +}; +use std::{any::TypeId, borrow::Borrow, ops::Deref}; +use std::{fmt, rc::Rc}; +use web_sys::Element; + +thread_local! { + #[cfg(debug_assertions)] + static EVENT_HISTORY: std::cell::RefCell>> + = Default::default(); +} + +/// Push [VComp] event to lifecycle debugging registry +#[cfg(debug_assertions)] +pub(crate) fn log_event(vcomp_id: u64, event: impl ToString) { + EVENT_HISTORY.with(|h| { + h.borrow_mut() + .entry(vcomp_id) + .or_default() + .push(event.to_string()) + }); +} + +/// Get [VComp] event log from lifecycle debugging registry +#[cfg(debug_assertions)] +#[allow(dead_code)] +pub(crate) fn get_event_log(vcomp_id: u64) -> Vec { + EVENT_HISTORY.with(|h| { + h.borrow() + .get(&vcomp_id) + .map(|l| (*l).clone()) + .unwrap_or_default() + }) +} + +/// A virtual component. +pub struct BComp { + type_id: TypeId, + scope: Box, + node_ref: NodeRef, + key: Option, +} + +impl BComp { + pub(crate) fn root_bnode(&self) -> Option + '_> { + self.scope.root_bnode() + } + pub(crate) fn key(&self) -> Option<&Key> { + self.key.as_ref() + } +} + +impl fmt::Debug for BComp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "BComp {{ root: {:?} }}", self.root_bnode().as_deref()) + } +} + +pub(crate) trait Mountable { + fn copy(&self) -> Box; + fn mount( + self: Box, + node_ref: NodeRef, + parent_scope: &AnyScope, + parent: Element, + next_sibling: NodeRef, + #[cfg(debug_assertions)] id: u64, + ) -> Box; + fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); +} + +pub(crate) struct PropsWrapper { + props: Rc, +} + +impl PropsWrapper { + pub fn new(props: Rc) -> Self { + Self { props } + } +} + +impl Mountable for PropsWrapper { + fn copy(&self) -> Box { + let wrapper: PropsWrapper = PropsWrapper { + props: Rc::clone(&self.props), + }; + Box::new(wrapper) + } + + fn mount( + self: Box, + node_ref: NodeRef, + parent_scope: &AnyScope, + parent: Element, + next_sibling: NodeRef, + #[cfg(debug_assertions)] id: u64, + ) -> Box { + let scope: Scope = Scope::new( + Some(parent_scope.clone()), + #[cfg(debug_assertions)] + id, + ); + scope.mount_in_place(parent, next_sibling, node_ref, self.props); + + Box::new(scope) + } + + fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { + let scope: Scope = scope.to_any().downcast(); + scope.reuse(self.props, node_ref, next_sibling); + } +} + +impl DomBundle for BComp { + fn detach(mut self, _parent: &Element) { + self.scope.destroy(); + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + self.scope.shift_node(next_parent.clone(), next_sibling); + } +} + +impl VDiff for VComp { + type Bundle = BComp; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let VComp { + type_id, + props, + node_ref, + key, + } = self; + + let scope = props.mount( + node_ref.clone(), + parent_scope, + parent.to_owned(), + next_sibling, + #[cfg(debug_assertions)] + { + thread_local! { + static ID_COUNTER: std::cell::RefCell = Default::default(); + } + + ID_COUNTER.with(|c| { + let c = &mut *c.borrow_mut(); + *c += 1; + *c + }) + }, + ); + + ( + node_ref.clone(), + BComp { + type_id, + node_ref, + key, + scope, + }, + ) + } + + fn apply( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ancestor: &mut BNode, + ) -> NodeRef { + let bcomp = match ancestor { + // If the ancestor is the same type, reuse it and update its properties + BNode::BComp(ref mut bcomp) + if self.type_id == bcomp.type_id && self.key == bcomp.key => + { + bcomp + } + _ => { + let (node_ref, self_) = self.attach(parent_scope, parent, next_sibling); + ancestor.replace(parent, self_.into()); + return node_ref; + } + }; + let VComp { + props, + node_ref, + key, + type_id: _, + } = self; + bcomp.key = key; + let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref.clone()); + bcomp.node_ref.reuse(old_ref); + props.reuse(node_ref.clone(), bcomp.scope.borrow(), next_sibling); + node_ref + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + html, + virtual_dom::{VChild, VNode}, + Children, Component, Context, Html, NodeRef, Properties, + }; + use gloo_utils::document; + use web_sys::Node; + + #[cfg(feature = "wasm_test")] + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + #[cfg(feature = "wasm_test")] + wasm_bindgen_test_configure!(run_in_browser); + + struct Comp; + + #[derive(Clone, PartialEq, Properties)] + struct Props { + #[prop_or_default] + field_1: u32, + #[prop_or_default] + field_2: u32, + } + + impl Component for Comp { + type Message = (); + type Properties = Props; + + fn create(_: &Context) -> Self { + Comp + } + + fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { + unimplemented!(); + } + + fn view(&self, _ctx: &Context) -> Html { + html! {
} + } + } + + #[test] + fn update_loop() { + let document = gloo_utils::document(); + let parent_scope: AnyScope = AnyScope::test(); + let parent_element = document.create_element("div").unwrap(); + + let ancestor = html! { }; + let (_, mut comp) = ancestor.attach(&parent_scope, &parent_element, NodeRef::default()); + + for _ in 0..10000 { + let node = html! { }; + node.apply( + &parent_scope, + &parent_element, + NodeRef::default(), + &mut comp, + ); + } + } + + #[test] + fn set_properties_to_component() { + html! { + + }; + + html! { + + }; + + html! { + + }; + + html! { + + }; + + let props = Props { + field_1: 1, + field_2: 1, + }; + + html! { + + }; + } + + #[test] + fn set_component_key() { + let test_key: Key = "test".to_string().into(); + let check_key = |vnode: VNode| { + assert_eq!(vnode.key(), Some(&test_key)); + }; + + let props = Props { + field_1: 1, + field_2: 1, + }; + let props_2 = props.clone(); + + check_key(html! { }); + check_key(html! { }); + check_key(html! { }); + check_key(html! { }); + check_key(html! { }); + } + + #[test] + fn set_component_node_ref() { + let test_node: Node = document().create_text_node("test").into(); + let test_node_ref = NodeRef::new(test_node); + let check_node_ref = |vnode: VNode| { + let vcomp = match vnode { + VNode::VComp(vcomp) => vcomp, + _ => unreachable!("should be a vcomp"), + }; + assert_eq!(vcomp.node_ref, test_node_ref); + }; + + let props = Props { + field_1: 1, + field_2: 1, + }; + let props_2 = props.clone(); + + check_node_ref(html! { }); + check_node_ref(html! { }); + check_node_ref(html! { }); + check_node_ref(html! { }); + check_node_ref(html! { }); + } + + #[test] + fn vchild_partialeq() { + let vchild1: VChild = VChild::new( + Props { + field_1: 1, + field_2: 1, + }, + NodeRef::default(), + None, + ); + + let vchild2: VChild = VChild::new( + Props { + field_1: 1, + field_2: 1, + }, + NodeRef::default(), + None, + ); + + let vchild3: VChild = VChild::new( + Props { + field_1: 2, + field_2: 2, + }, + NodeRef::default(), + None, + ); + + assert_eq!(vchild1, vchild2); + assert_ne!(vchild1, vchild3); + assert_ne!(vchild2, vchild3); + } + + #[derive(Clone, Properties, PartialEq)] + pub struct ListProps { + pub children: Children, + } + pub struct List; + impl Component for List { + type Message = (); + type Properties = ListProps; + + fn create(_: &Context) -> Self { + Self + } + fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { + unimplemented!(); + } + fn changed(&mut self, _ctx: &Context) -> bool { + unimplemented!(); + } + fn view(&self, ctx: &Context) -> Html { + let item_iter = ctx + .props() + .children + .iter() + .map(|item| html! {
  • { item }
  • }); + html! { +
      { for item_iter }
    + } + } + } + + use super::{AnyScope, Element}; + + fn setup_parent() -> (AnyScope, Element) { + let scope = AnyScope::test(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + (scope, parent) + } + + fn get_html(node: Html, scope: &AnyScope, parent: &Element) -> String { + // clear parent + parent.set_inner_html(""); + + node.attach(scope, parent, NodeRef::default()); + parent.inner_html() + } + + #[test] + fn all_ways_of_passing_children_work() { + let (scope, parent) = setup_parent(); + + let children: Vec<_> = vec!["a", "b", "c"] + .drain(..) + .map(|text| html! {{ text }}) + .collect(); + let children_renderer = Children::new(children.clone()); + let expected_html = "\ +
      \ +
    • a
    • \ +
    • b
    • \ +
    • c
    • \ +
    "; + + let prop_method = html! { + + }; + assert_eq!(get_html(prop_method, &scope, &parent), expected_html); + + let children_renderer_method = html! { + + { children_renderer } + + }; + assert_eq!( + get_html(children_renderer_method, &scope, &parent), + expected_html + ); + + let direct_method = html! { + + { children.clone() } + + }; + assert_eq!(get_html(direct_method, &scope, &parent), expected_html); + + let for_method = html! { + + { for children } + + }; + assert_eq!(get_html(for_method, &scope, &parent), expected_html); + } + + #[test] + fn reset_node_ref() { + let scope = AnyScope::test(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + let node_ref = NodeRef::default(); + let elem = html! { }; + let (_, elem) = elem.attach(&scope, &parent, NodeRef::default()); + let parent_node = parent.deref(); + assert_eq!(node_ref.get(), parent_node.first_child()); + elem.detach(&parent); + assert!(node_ref.get().is_none()); + } +} + +#[cfg(test)] +mod layout_tests { + extern crate self as yew; + + use crate::html; + use crate::tests::layout_tests::{diff_layouts, TestLayout}; + use crate::{Children, Component, Context, Html, Properties}; + use std::marker::PhantomData; + + #[cfg(feature = "wasm_test")] + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + #[cfg(feature = "wasm_test")] + wasm_bindgen_test_configure!(run_in_browser); + + struct Comp { + _marker: PhantomData, + } + + #[derive(Properties, Clone, PartialEq)] + struct CompProps { + #[prop_or_default] + children: Children, + } + + impl Component for Comp { + type Message = (); + type Properties = CompProps; + + fn create(_: &Context) -> Self { + Comp { + _marker: PhantomData::default(), + } + } + + fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { + unimplemented!(); + } + + fn view(&self, ctx: &Context) -> Html { + html! { + <>{ ctx.props().children.clone() } + } + } + } + + struct A; + struct B; + + #[test] + fn diff() { + let layout1 = TestLayout { + name: "1", + node: html! { + > + >> + {"C"} + > + }, + expected: "C", + }; + + let layout2 = TestLayout { + name: "2", + node: html! { + > + {"A"} + > + }, + expected: "A", + }; + + let layout3 = TestLayout { + name: "3", + node: html! { + > + >> + {"B"} + > + }, + expected: "B", + }; + + let layout4 = TestLayout { + name: "4", + node: html! { + > + >{"A"}> + {"B"} + > + }, + expected: "AB", + }; + + let layout5 = TestLayout { + name: "5", + node: html! { + > + <> + > + {"A"} + > + + {"B"} + > + }, + expected: "AB", + }; + + let layout6 = TestLayout { + name: "6", + node: html! { + > + <> + > + {"A"} + > + {"B"} + + {"C"} + > + }, + expected: "ABC", + }; + + let layout7 = TestLayout { + name: "7", + node: html! { + > + <> + > + {"A"} + > + > + {"B"} + > + + {"C"} + > + }, + expected: "ABC", + }; + + let layout8 = TestLayout { + name: "8", + node: html! { + > + <> + > + {"A"} + > + > + > + {"B"} + > + > + + {"C"} + > + }, + expected: "ABC", + }; + + let layout9 = TestLayout { + name: "9", + node: html! { + > + <> + <> + {"A"} + + > + > + {"B"} + > + > + + {"C"} + > + }, + expected: "ABC", + }; + + let layout10 = TestLayout { + name: "10", + node: html! { + > + <> + > + > + {"A"} + > + > + <> + {"B"} + + + {"C"} + > + }, + expected: "ABC", + }; + + let layout11 = TestLayout { + name: "11", + node: html! { + > + <> + <> + > + > + {"A"} + > + {"B"} + > + + + {"C"} + > + }, + expected: "ABC", + }; + + let layout12 = TestLayout { + name: "12", + node: html! { + > + <> + >> + <> + > + <> + > + {"A"} + > + <> + > + >> + <> + {"B"} + <> + >> + > + + > + <> + + >> + + {"C"} + >> + <> + > + }, + expected: "ABC", + }; + + diff_layouts(vec![ + layout1, layout2, layout3, layout4, layout5, layout6, layout7, layout8, layout9, + layout10, layout11, layout12, + ]); + } + + #[test] + fn component_with_children() { + #[derive(Properties, PartialEq)] + struct Props { + children: Children, + } + + struct ComponentWithChildren; + + impl Component for ComponentWithChildren { + type Message = (); + type Properties = Props; + + fn create(_ctx: &Context) -> Self { + Self + } + + fn view(&self, ctx: &Context) -> Html { + html! { +
      + { for ctx.props().children.iter().map(|child| html! {
    • { child }
    • }) } +
    + } + } + } + + let layout = TestLayout { + name: "13", + node: html! { + + if true { + { "hello" } + { "world" } + } else { + { "goodbye" } + { "world" } + } + + }, + expected: "
    • helloworld
    ", + }; + + diff_layouts(vec![layout]); + } +} diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs new file mode 100644 index 00000000000..c813075e334 --- /dev/null +++ b/packages/yew/src/dom_bundle/blist.rs @@ -0,0 +1,1283 @@ +//! This module contains fragments bundles. +use super::test_log; +use super::BNode; +use crate::dom_bundle::{DomBundle, VDiff}; +use crate::html::{AnyScope, NodeRef}; +use crate::virtual_dom::{Key, VList, VNode, VText}; +use std::borrow::Borrow; +use std::collections::HashSet; +use std::hash::Hash; +use std::ops::Deref; +use web_sys::Element; + +/// This struct represents a fragment of the Virtual DOM tree. +#[derive(Debug)] +pub struct BList { + /// The reverse (render order) list of child [BNode]s + rev_children: Vec, + /// All [BNode]s in the BList have keys + fully_keyed: bool, + key: Option, +} + +impl Deref for BList { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.rev_children + } +} + +struct ElementWriter<'s> { + parent_scope: &'s AnyScope, + parent: &'s Element, + next_sibling: NodeRef, +} + +impl<'s> ElementWriter<'s> { + fn add(self, node: VNode) -> (Self, BNode) { + test_log!("adding: {:?}", node); + test_log!(" parent={:?}, next_sibling={:?}", self.parent.outer_html(), self.next_sibling); + let (next, bundle) = node.attach(self.parent_scope, self.parent, self.next_sibling); + test_log!(" next_position: {:?}", next); + ( + Self { + next_sibling: next, + ..self + }, + bundle, + ) + } + + fn patch(self, node: VNode, ancestor: &mut BNode) -> Self { + test_log!("patching: {:?} -> {:?}", ancestor, node); + test_log!(" parent={:?}, next_sibling={:?}", self.parent.outer_html(), self.next_sibling); + // Advance the next sibling reference (from right to left) + ancestor.move_before(self.parent, &self.next_sibling.get()); + let next = node.apply(self.parent_scope, self.parent, self.next_sibling, ancestor); + test_log!(" next_position: {:?}", next); + Self { + next_sibling: next, + ..self + } + } +} + +struct NodeEntry(BNode); +impl Borrow for NodeEntry { + fn borrow(&self) -> &Key { + self.0.key().expect("unkeyed child in fully keyed list") + } +} +impl Hash for NodeEntry { + fn hash(&self, state: &mut H) { + >::borrow(self).hash(state) + } +} +impl PartialEq for NodeEntry { + fn eq(&self, other: &Self) -> bool { + >::borrow(self) == >::borrow(other) + } +} +impl Eq for NodeEntry {} + +impl BNode { + fn make_list(&mut self) -> &mut BList { + match self { + Self::BList(blist) => blist, + self_ => { + let b = std::mem::replace(self_, BNode::BList(BList::new())); + let self_list = match self_ { + BNode::BList(blist) => blist, + _ => unreachable!("just been set to the variant"), + }; + self_list.fully_keyed = b.has_key(); + self_list.key = b.key().cloned(); + self_list.rev_children.push(b); + self_list + } + } + } +} + +impl BList { + pub(crate) const fn new() -> BList { + BList { + rev_children: vec![], + fully_keyed: true, + key: None, + } + } + + pub(crate) fn key(&self) -> Option<&Key> { + self.key.as_ref() + } + + /// Diff and patch unkeyed child lists + fn apply_unkeyed( + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + lefts: Vec, + rights: &mut Vec, + ) -> NodeRef { + let mut writer = ElementWriter { + parent_scope, + parent, + next_sibling, + }; + + // Remove extra nodes + if lefts.len() < rights.len() { + for r in rights.drain(lefts.len()..) { + test_log!("removing: {:?}", r); + r.detach(parent); + } + } + + let mut lefts_it = lefts.into_iter().rev(); + for (r, l) in rights.iter_mut().zip(&mut lefts_it) { + writer = writer.patch(l, r); + } + + // Add missing nodes + for l in lefts_it { + let (next_writer, el) = writer.add(l); + rights.push(el); + writer = next_writer; + } + writer.next_sibling + } + + /// Diff and patch fully keyed child lists. + /// + /// Optimized for node addition or removal from either end of the list and small changes in the + /// middle. + fn apply_keyed( + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + lefts: Vec, + rights: &mut Vec, + ) -> NodeRef { + /// Find the first differing key in 2 iterators + fn matching_len<'a, 'b>( + a: impl Iterator, + b: impl Iterator, + ) -> usize { + a.zip(b).take_while(|(a, b)| a == b).count() + } + + // Find first key mismatch from the back + let matching_len_end = matching_len( + lefts + .iter() + .map(|v| v.key().expect("unkeyed child in fully keyed list")) + .rev(), + rights + .iter() + .map(|v| v.key().expect("unkeyed child in fully keyed list")), + ); + + if matching_len_end == std::cmp::min(lefts.len(), rights.len()) { + // No key changes + return Self::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights); + } + // We partially deconstruct the new vector in several steps. + let mut lefts = lefts; + let mut writer = ElementWriter { + parent_scope, + parent, + next_sibling, + }; + // Diff matching children at the end + let lefts_to = lefts.len() - matching_len_end; + for (l, r) in lefts + .drain(lefts_to..) + .rev() + .zip(rights[..matching_len_end].iter_mut()) + { + writer = writer.patch(l, r); + } + // Find first key mismatch from the front + let matching_len_start = matching_len( + lefts + .iter() + .map(|v| v.key().expect("unkeyed child in fully keyed list")), + rights + .iter() + .map(|v| v.key().expect("unkeyed child in fully keyed list")) + .rev(), + ); + + // Diff mismatched children in the middle + let rights_to = rights.len() - matching_len_start; + let mut spliced_middle = rights.splice(matching_len_end..rights_to, std::iter::empty()); + let mut rights_diff: HashSet = + HashSet::with_capacity((matching_len_end..rights_to).len()); + for r in &mut spliced_middle { + rights_diff.insert(NodeEntry(r)); + } + let mut replacements: Vec = Vec::with_capacity((matching_len_start..lefts_to).len()); + for l in lefts + .drain(matching_len_start..) // lefts_to.. has been drained + .rev() + { + let l_key = l.key().expect("unkeyed child in fully keyed list"); + let bundle = match rights_diff.take(l_key) { + Some(NodeEntry(mut r_bundle)) => { + writer = writer.patch(l, &mut r_bundle); + r_bundle + } + None => { + let (next_writer, bundle) = writer.add(l); + writer = next_writer; + bundle + } + }; + replacements.push(bundle); + } + // now drop the splice iterator + std::mem::drop(spliced_middle); + rights.splice(matching_len_end..matching_len_end, replacements); + + // Remove any extra rights + for NodeEntry(r) in rights_diff.drain() { + test_log!("removing: {:?}", r); + r.detach(parent); + } + + // Diff matching children at the start + let rights_to = rights.len() - matching_len_start; + for (l, r) in lefts + .drain(..) // matching_len_start.. has been drained already + .rev() + .zip(rights[rights_to..].iter_mut()) + { + writer = writer.patch(l, r); + } + + writer.next_sibling + } +} + +impl DomBundle for BList { + fn detach(self, parent: &Element) { + for child in self.rev_children.into_iter() { + child.detach(parent); + } + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + for node in self.rev_children.iter().rev() { + node.shift(next_parent, next_sibling.clone()); + } + } +} + +impl VDiff for VList { + type Bundle = BList; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let mut self_ = BNode::BList(BList::new()); + let node_ref = self.apply(parent_scope, parent, next_sibling, &mut self_); + let self_ = match self_ { + BNode::BList(self_) => self_, + _ => unreachable!("applying list should leave a VList in bundle ref"), + }; + (node_ref, self_) + } + + fn apply( + mut self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ancestor: &mut BNode, + ) -> NodeRef { + // Here, we will try to diff the previous list elements with the new + // ones we want to insert. For that, we will use two lists: + // - lefts: new elements to render in the DOM + // - rights: previously rendered elements. + // + // The left items are known since we want to insert them + // (self.children). For the right ones, we will look at the ancestor, + // i.e. the current DOM list element that we want to replace with self. + + if self.children.is_empty() { + // Without a placeholder the next element becomes first + // and corrupts the order of rendering + // We use empty text element to stake out a place + self.add_child(VText::new("").into()); + } + + let lefts = self.children; + let blist = ancestor.make_list(); + let rights = &mut blist.rev_children; + test_log!("lefts: {:?}", lefts); + test_log!("rights: {:?}", rights); + + if let Some(additional) = rights.len().checked_sub(lefts.len()) { + rights.reserve_exact(additional); + } + #[allow(clippy::let_and_return)] + let first = if self.fully_keyed && blist.fully_keyed { + BList::apply_keyed(parent_scope, parent, next_sibling, lefts, rights) + } else { + BList::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights) + }; + blist.fully_keyed = self.fully_keyed; + blist.key = self.key; + test_log!("result: {:?}", rights); + first + } +} + +#[cfg(test)] +mod layout_tests { + extern crate self as yew; + + use crate::html; + use crate::tests::layout_tests::{diff_layouts, TestLayout}; + + #[cfg(feature = "wasm_test")] + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + #[cfg(feature = "wasm_test")] + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn diff() { + let layout1 = TestLayout { + name: "1", + node: html! { + <> + {"a"} + {"b"} + <> + {"c"} + {"d"} + + {"e"} + + }, + expected: "abcde", + }; + + let layout2 = TestLayout { + name: "2", + node: html! { + <> + {"a"} + {"b"} + <> + {"e"} + {"f"} + + }, + expected: "abef", + }; + + let layout3 = TestLayout { + name: "3", + node: html! { + <> + {"a"} + <> + {"b"} + {"e"} + + }, + expected: "abe", + }; + + let layout4 = TestLayout { + name: "4", + node: html! { + <> + {"a"} + <> + {"c"} + {"d"} + + {"b"} + {"e"} + + }, + expected: "acdbe", + }; + + diff_layouts(vec![layout1, layout2, layout3, layout4]); + } +} + +#[cfg(test)] +mod layout_tests_keys { + extern crate self as yew; + + use crate::html; + use crate::tests::layout_tests::{diff_layouts, TestLayout}; + use crate::virtual_dom::VNode; + use crate::{Children, Component, Context, Html, Properties}; + use web_sys::Node; + + #[cfg(feature = "wasm_test")] + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + #[cfg(feature = "wasm_test")] + wasm_bindgen_test_configure!(run_in_browser); + + struct Comp {} + + #[derive(Properties, Clone, PartialEq)] + struct CountingCompProps { + id: usize, + #[prop_or(false)] + can_change: bool, + } + + impl Component for Comp { + type Message = (); + type Properties = CountingCompProps; + + fn create(_: &Context) -> Self { + Comp {} + } + + fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { + unimplemented!(); + } + + fn view(&self, ctx: &Context) -> Html { + html! {

    { ctx.props().id }

    } + } + } + + #[derive(Clone, Properties, PartialEq)] + pub struct ListProps { + pub children: Children, + } + + pub struct List(); + + impl Component for List { + type Message = (); + type Properties = ListProps; + + fn create(_: &Context) -> Self { + Self() + } + + fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { + unimplemented!(); + } + + fn view(&self, ctx: &Context) -> Html { + html! { <>{ for ctx.props().children.iter() } } + } + } + + #[test] + fn diff() { + let mut layouts = vec![]; + + let vref_node: Node = gloo_utils::document().create_element("i").unwrap().into(); + layouts.push(TestLayout { + name: "All VNode types as children", + node: html! { + <> + {"a"} + + {"c"} + {"d"} + + + {"foo"} + {"bar"} + + {VNode::VRef(vref_node)} + + }, + expected: "acd

    0

    foobar", + }); + + layouts.extend(vec![ + TestLayout { + name: "Inserting into VList first child - before", + node: html! { + <> + + + +

    + + }, + expected: "

    ", + }, + TestLayout { + name: "Inserting into VList first child - after", + node: html! { + <> + + + + +

    + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "No matches - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "No matches - after", + node: html! { + <> + +

    + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Append - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "Append - after", + node: html! { + <> + + +

    + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Prepend - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "Prepend - after", + node: html! { + <> +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Delete first - before", + node: html! { + <> + + +

    + + }, + expected: "

    ", + }, + TestLayout { + name: "Delete first - after", + node: html! { + <> + +

    + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Delete last - before", + node: html! { + <> + + +

    + + }, + expected: "

    ", + }, + TestLayout { + name: "Delete last - after", + node: html! { + <> + + + + }, + expected: "", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Delete last and change node type - before", + node: html! { + <> + + +

    + + }, + expected: "

    ", + }, + TestLayout { + name: "Delete last - after", + node: html! { + <> + + + + + }, + expected: "", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Delete middle - before", + node: html! { + <> + + +

    + + + }, + expected: "

    ", + }, + TestLayout { + name: "Delete middle - after", + node: html! { + <> + + +

    + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Delete middle and change node type - before", + node: html! { + <> + + +

    + + + }, + expected: "

    ", + }, + TestLayout { + name: "Delete middle and change node type- after", + node: html! { + <> + + +

    + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Reverse - before", + node: html! { + <> + + +

    + + + }, + expected: "

    ", + }, + TestLayout { + name: "Reverse - after", + node: html! { + <> + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Reverse and change node type - before", + node: html! { + <> + + + + + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Reverse and change node type - after", + node: html! { + <> + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap 1&2 - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 1&2 - after", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap 1&2 and change node type - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 1&2 and change node type - after", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "test - before", + node: html! { + <> + + +

    + + + + + +

    + + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 4&5 - after", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap 4&5 - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 4&5 - after", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap 1&5 - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 1&5 - after", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Move 2 after 4 - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Move 2 after 4 - after", + node: html! { + <> + +

    + + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap 1,2 <-> 3,4 - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 1,2 <-> 3,4 - after", + node: html! { + <> +

    + + + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap lists - before", + node: html! { + <> + + + + + + + + + + }, + expected: "", + }, + TestLayout { + name: "Swap lists - after", + node: html! { + <> + + + + + + + + + + }, + expected: "", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap lists with in-between - before", + node: html! { + <> + + + + +

    + + + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap lists with in-between - after", + node: html! { + <> + + + + +

    + + + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Insert VComp front - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "Insert VComp front - after", + node: html! { + <> + + + + + }, + expected: "

    0

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Insert VComp middle - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "Insert VComp middle - after", + node: html! { + <> + + + + + }, + expected: "

    0

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Insert VComp back - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "Insert VComp back - after", + node: html! { + <> + + + + + }, + expected: "

    0

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Reverse VComp children - before", + node: html! { + <> + + + + + }, + expected: "

    1

    2

    3

    ", + }, + TestLayout { + name: "Reverse VComp children - after", + node: html! { + <> + + + + + }, + expected: "

    3

    2

    1

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Reverse VComp children with children - before", + node: html! { + <> +

    {"11"}

    {"12"}

    +

    {"21"}

    {"22"}

    +

    {"31"}

    {"32"}

    + + }, + expected: "

    11

    12

    21

    22

    31

    32

    ", + }, + TestLayout { + name: "Reverse VComp children with children - after", + node: html! { + <> +

    {"31"}

    {"32"}

    +

    {"21"}

    {"22"}

    +

    {"11"}

    {"12"}

    + + }, + expected: "

    31

    32

    21

    22

    11

    12

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Complex component update - before", + node: html! { + + + + + }, + expected: "

    1

    2

    ", + }, + TestLayout { + name: "Complex component update - after", + node: html! { + + + + + +

    {"2"}

    +
    +
    + }, + expected: "

    1

    2

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Reorder VComp children with children - before", + node: html! { + <> +

    {"1"}

    +

    {"3"}

    +

    {"5"}

    +

    {"2"}

    +

    {"4"}

    +

    {"6"}

    + + }, + expected: "

    1

    3

    5

    2

    4

    6

    ", + }, + TestLayout { + name: "Reorder VComp children with children - after", + node: html! { + <> + + + + + + + + }, + expected: "

    6

    5

    4

    3

    2

    1

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Replace and reorder components - before", + node: html! { + +

    {"1"}

    +

    {"2"}

    +

    {"3"}

    +
    + }, + expected: "

    1

    2

    3

    ", + }, + TestLayout { + name: "Replace and reorder components - after", + node: html! { + + + + + + }, + expected: "

    3

    2

    1

    ", + }, + ]); + + diff_layouts(layouts); + } +} diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs new file mode 100644 index 00000000000..05a6d97e97e --- /dev/null +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -0,0 +1,291 @@ +//! This module contains the bundle version of an abstract node. + +use super::{BComp, BList, BPortal, BSuspense, BTag}; +use crate::dom_bundle::{DomBundle, VDiff}; +use crate::html::{AnyScope, NodeRef}; +use crate::virtual_dom::{Key, VNode, VText}; +use gloo::console; +use std::fmt; +use web_sys::{Element, Node}; + +/// Bind virtual element to a DOM reference. +pub enum BNode { + /// A bind between `VTag` and `Element`. + BTag(Box), + /// A bind between `VText` and `TextNode`. + BText(VText), + /// A bind between `VComp` and `Element`. + BComp(BComp), + /// A holder for a list of other nodes. + BList(BList), + /// A portal to another part of the document + BPortal(BPortal), + /// A holder for any `Node` (necessary for replacing node). + BRef(Node), + /// A suspendible document fragment. + BSuspense(Box), +} + +impl BNode { + pub(crate) fn key(&self) -> Option<&Key> { + match self { + Self::BComp(bsusp) => bsusp.key(), + Self::BList(blist) => blist.key(), + Self::BRef(_) => None, + Self::BTag(btag) => btag.key(), + Self::BText(_) => None, + Self::BPortal(bportal) => bportal.key(), + Self::BSuspense(bsusp) => bsusp.key(), + } + } + + /// Returns true if the [VNode] has a key without needlessly cloning the key. + pub(crate) fn has_key(&self) -> bool { + match self { + Self::BComp(bsusp) => bsusp.key().is_some(), + Self::BList(blist) => blist.key().is_some(), + Self::BRef(_) | Self::BText(_) => false, + Self::BTag(vtag) => vtag.key().is_some(), + Self::BPortal(bportal) => bportal.key().is_some(), + Self::BSuspense(bsusp) => bsusp.key().is_some(), + } + } + + /// Returns the first DOM node that is used to designate the position of the virtual DOM node. + fn unchecked_first_node(&self) -> Node { + match self { + Self::BTag(btag) => btag.reference().clone().into(), + Self::BText(vtext) => { + let text_node = vtext.reference.as_ref().expect("VText is not mounted"); + text_node.clone().into() + } + Self::BRef(node) => node.clone(), + Self::BList(_) => unreachable!("no need to get first node of blist"), + Self::BComp(_) => unreachable!("no need to get first node of bcomp"), + Self::BSuspense(_) => unreachable!("no need to get first node of bsuspense"), + Self::BPortal(_) => unreachable!("portals have no first node, they are empty inside"), + } + } + + pub(crate) fn move_before(&self, parent: &Element, next_sibling: &Option) { + match self { + Self::BList(blist) => { + for node in blist.iter().rev() { + node.move_before(parent, next_sibling); + } + } + Self::BComp(bcomp) => { + bcomp + .root_bnode() + .expect("VComp has no root vnode") + .move_before(parent, next_sibling); + } + Self::BPortal(_) => {} // no need to move portals + Self::BSuspense(bsusp) => bsusp.active_node().move_before(parent, next_sibling), + _ => super::insert_node(&self.unchecked_first_node(), parent, next_sibling.as_ref()), + }; + } +} + +impl DomBundle for BNode { + /// Remove VNode from parent. + fn detach(self, parent: &Element) { + match self { + Self::BTag(vtag) => vtag.detach(parent), + Self::BText(vtext) => vtext.detach(parent), + Self::BComp(bsusp) => bsusp.detach(parent), + Self::BList(blist) => blist.detach(parent), + Self::BRef(ref node) => { + if parent.remove_child(node).is_err() { + console::warn!("Node not found to remove VRef"); + } + } + Self::BPortal(bportal) => bportal.detach(parent), + Self::BSuspense(bsusp) => bsusp.detach(parent), + } + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + match self { + Self::BTag(ref vtag) => vtag.shift(next_parent, next_sibling), + Self::BText(ref vtext) => vtext.shift(next_parent, next_sibling), + Self::BComp(ref bsusp) => bsusp.shift(next_parent, next_sibling), + Self::BList(ref vlist) => vlist.shift(next_parent, next_sibling), + Self::BRef(ref node) => { + next_parent + .insert_before(node, next_sibling.get().as_ref()) + .unwrap(); + } + Self::BPortal(ref vportal) => vportal.shift(next_parent, next_sibling), + Self::BSuspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling), + } + } +} + +impl VDiff for VNode { + type Bundle = BNode; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + match self { + VNode::VTag(vtag) => { + let (node_ref, tag) = vtag.attach(parent_scope, parent, next_sibling); + (node_ref, tag.into()) + } + VNode::VText(vtext) => { + let (node_ref, text) = vtext.attach(parent_scope, parent, next_sibling); + (node_ref, text.into()) + } + VNode::VComp(vcomp) => { + let (node_ref, comp) = vcomp.attach(parent_scope, parent, next_sibling); + (node_ref, comp.into()) + } + VNode::VList(vlist) => { + let (node_ref, list) = vlist.attach(parent_scope, parent, next_sibling); + (node_ref, list.into()) + } + VNode::VRef(node) => { + super::insert_node(&node, parent, next_sibling.get().as_ref()); + (NodeRef::new(node.clone()), BNode::BRef(node)) + } + VNode::VPortal(vportal) => { + let (node_ref, portal) = vportal.attach(parent_scope, parent, next_sibling); + (node_ref, portal.into()) + } + VNode::VSuspense(vsuspsense) => { + let (node_ref, suspsense) = vsuspsense.attach(parent_scope, parent, next_sibling); + (node_ref, suspsense.into()) + } + } + } + + fn apply( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ancestor: &mut BNode, + ) -> NodeRef { + match self { + VNode::VTag(vtag) => vtag.apply(parent_scope, parent, next_sibling, ancestor), + VNode::VText(vtext) => vtext.apply(parent_scope, parent, next_sibling, ancestor), + VNode::VComp(vcomp) => vcomp.apply(parent_scope, parent, next_sibling, ancestor), + VNode::VList(vlist) => vlist.apply(parent_scope, parent, next_sibling, ancestor), + VNode::VRef(node) => { + if let BNode::BRef(ref n) = ancestor { + if &node == n { + return NodeRef::new(node); + } + } + let (node_ref, self_) = + VNode::VRef(node).attach(parent_scope, parent, next_sibling); + ancestor.replace(parent, self_); + node_ref + } + VNode::VPortal(vportal) => vportal.apply(parent_scope, parent, next_sibling, ancestor), + VNode::VSuspense(vsuspsense) => { + vsuspsense.apply(parent_scope, parent, next_sibling, ancestor) + } + } + } +} + +impl From for BNode { + #[inline] + fn from(vtext: VText) -> Self { + Self::BText(vtext) + } +} + +impl From for BNode { + #[inline] + fn from(blist: BList) -> Self { + Self::BList(blist) + } +} + +impl From for BNode { + #[inline] + fn from(btag: BTag) -> Self { + Self::BTag(Box::new(btag)) + } +} + +impl From for BNode { + #[inline] + fn from(bcomp: BComp) -> Self { + Self::BComp(bcomp) + } +} + +impl From for BNode { + #[inline] + fn from(bportal: BPortal) -> Self { + Self::BPortal(bportal) + } +} + +impl From for BNode { + #[inline] + fn from(bsusp: BSuspense) -> Self { + Self::BSuspense(Box::new(bsusp)) + } +} + +impl fmt::Debug for BNode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Self::BTag(ref vtag) => vtag.fmt(f), + Self::BText(ref vtext) => vtext.fmt(f), + Self::BComp(ref bsusp) => bsusp.fmt(f), + Self::BList(ref vlist) => vlist.fmt(f), + Self::BRef(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)), + Self::BPortal(ref vportal) => vportal.fmt(f), + Self::BSuspense(ref bsusp) => bsusp.fmt(f), + } + } +} + +impl BNode { + pub(crate) fn replace(&mut self, parent: &Element, next_node: BNode) { + let ancestor = std::mem::replace(self, next_node); + ancestor.detach(parent); + } +} + +#[cfg(test)] +mod layout_tests { + use super::*; + use crate::tests::layout_tests::{diff_layouts, TestLayout}; + + #[cfg(feature = "wasm_test")] + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + #[cfg(feature = "wasm_test")] + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn diff() { + let document = gloo_utils::document(); + let vref_node_1 = VNode::VRef(document.create_element("i").unwrap().into()); + let vref_node_2 = VNode::VRef(document.create_element("b").unwrap().into()); + + let layout1 = TestLayout { + name: "1", + node: vref_node_1, + expected: "", + }; + + let layout2 = TestLayout { + name: "2", + node: vref_node_2, + expected: "", + }; + + diff_layouts(vec![layout1, layout2]); + } +} diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs new file mode 100644 index 00000000000..01e92613983 --- /dev/null +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -0,0 +1,177 @@ +//! This module contains the bundle implementation of a portal `BPortal`. + +use super::test_log; +use super::BNode; +use crate::dom_bundle::{DomBundle, VDiff}; +use crate::html::{AnyScope, NodeRef}; +use crate::virtual_dom::Key; +use crate::virtual_dom::VPortal; +use std::borrow::BorrowMut; +use web_sys::Element; + +/// The bundle implementation to [VPortal]. +#[derive(Debug)] +pub struct BPortal { + /// The element under which the content is inserted. + host: Element, + /// The next sibling after the inserted content + next_sibling: NodeRef, + /// The inserted node + node: Box, +} + +impl DomBundle for BPortal { + fn detach(self, _: &Element) { + test_log!("Detaching portal from host{:?}", self.host.outer_html()); + self.node.detach(&self.host); + test_log!("Detached portal from host{:?}", self.host.outer_html()); + } + + fn shift(&self, _next_parent: &Element, _next_sibling: NodeRef) { + // portals have nothing in it's original place of DOM, we also do nothing. + } +} + +impl VDiff for VPortal { + type Bundle = BPortal; + + fn attach( + self, + parent_scope: &AnyScope, + _parent: &Element, + host_next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let VPortal { + host, + next_sibling, + node, + } = self; + let (_, inner) = node.attach(parent_scope, &host, next_sibling.clone()); + ( + host_next_sibling, + BPortal { + host, + node: Box::new(inner), + next_sibling, + }, + ) + } + + fn apply( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ancestor: &mut BNode, + ) -> NodeRef { + if let BNode::BPortal(portal) = ancestor { + let old_host = std::mem::replace(&mut portal.host, self.host); + let old_sibling = std::mem::replace(&mut portal.next_sibling, self.next_sibling); + let node = &mut portal.node; + if old_host != portal.host || old_sibling != portal.next_sibling { + // Remount the inner node somewhere else instead of diffing + // Move the node, but keep the state + node.move_before(&portal.host, &portal.next_sibling.get()); + } + let inner_ancestor = node.borrow_mut(); + self.node + .apply(parent_scope, parent, next_sibling.clone(), inner_ancestor); + return next_sibling; + } + + let (_, self_) = self.attach(parent_scope, parent, next_sibling.clone()); + ancestor.replace(parent, self_.into()); + next_sibling + } +} + +impl BPortal { + pub(crate) fn key(&self) -> Option<&Key> { + self.node.key() + } +} + +#[cfg(test)] +mod layout_tests { + extern crate self as yew; + + use crate::html; + use crate::tests::layout_tests::{diff_layouts, TestLayout}; + use crate::virtual_dom::VNode; + use yew::virtual_dom::VPortal; + + #[cfg(feature = "wasm_test")] + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + #[cfg(feature = "wasm_test")] + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn diff() { + let mut layouts = vec![]; + let first_target = gloo_utils::document().create_element("i").unwrap(); + let second_target = gloo_utils::document().create_element("o").unwrap(); + let target_with_child = gloo_utils::document().create_element("i").unwrap(); + let target_child = gloo_utils::document().create_element("s").unwrap(); + target_with_child.append_child(&target_child).unwrap(); + + layouts.push(TestLayout { + name: "Portal - first target", + node: html! { +
    + {VNode::VRef(first_target.clone().into())} + {VNode::VRef(second_target.clone().into())} + {VNode::VPortal(VPortal::new( + html! { {"PORTAL"} }, + first_target.clone(), + ))} + {"AFTER"} +
    + }, + expected: "
    PORTALAFTER
    ", + }); + layouts.push(TestLayout { + name: "Portal - second target", + node: html! { +
    + {VNode::VRef(first_target.clone().into())} + {VNode::VRef(second_target.clone().into())} + {VNode::VPortal(VPortal::new( + html! { {"PORTAL"} }, + second_target.clone(), + ))} + {"AFTER"} +
    + }, + expected: "
    PORTALAFTER
    ", + }); + layouts.push(TestLayout { + name: "Portal - replaced by text", + node: html! { +
    + {VNode::VRef(first_target.clone().into())} + {VNode::VRef(second_target.clone().into())} + {"FOO"} + {"AFTER"} +
    + }, + expected: "
    FOOAFTER
    ", + }); + layouts.push(TestLayout { + name: "Portal - next sibling", + node: html! { +
    + {VNode::VRef(target_with_child.clone().into())} + {VNode::VPortal(VPortal::new_before( + html! { {"PORTAL"} }, + target_with_child.clone(), + Some(target_child.clone().into()), + ))} +
    + }, + expected: "
    PORTAL
    ", + }); + + diff_layouts(layouts) + } +} diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs new file mode 100644 index 00000000000..960a47665e0 --- /dev/null +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -0,0 +1,156 @@ +use super::{BNode, DomBundle, VDiff}; +use crate::html::AnyScope; +use crate::virtual_dom::{Key, VSuspense}; +use crate::NodeRef; +use web_sys::Element; + +/// This struct represents a suspendable DOM fragment. +#[derive(Debug)] +pub struct BSuspense { + children: BNode, + // suspended if fallback is Some + fallback: Option, + detached_parent: Element, + key: Option, +} + +impl BSuspense { + pub(crate) fn key(&self) -> Option<&Key> { + self.key.as_ref() + } + + pub(crate) fn active_node(&self) -> &BNode { + self.fallback.as_ref().unwrap_or(&self.children) + } +} + +impl DomBundle for BSuspense { + fn detach(self, parent: &Element) { + if let Some(fallback) = self.fallback { + fallback.detach(parent); + self.children.detach(&self.detached_parent); + } else { + self.children.detach(parent); + } + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + self.active_node().shift(next_parent, next_sibling) + } +} + +impl VDiff for VSuspense { + type Bundle = BSuspense; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let VSuspense { + children, + fallback, + detached_parent, + suspended, + key, + } = self; + + // When it's suspended, we render children into an element that is detached from the dom + // tree while rendering fallback UI into the original place where children resides in. + if suspended { + let (_child_ref, children) = + children.attach(parent_scope, &detached_parent, NodeRef::default()); + let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling); + ( + fallback_ref, + BSuspense { + children, + fallback: Some(fallback), + detached_parent, + key, + }, + ) + } else { + let (child_ref, children) = children.attach(parent_scope, parent, next_sibling); + ( + child_ref, + BSuspense { + children, + fallback: None, + detached_parent, + key, + }, + ) + } + } + + fn apply( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ancestor: &mut BNode, + ) -> NodeRef { + let suspense = match ancestor { + // We only preserve the child state if they are the same suspense. + BNode::BSuspense(m) + if m.key == self.key && self.detached_parent == m.detached_parent => + { + m + } + _ => { + let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); + ancestor.replace(parent, self_.into()); + return self_ref; + } + }; + let children_ancestor = &mut suspense.children; + // no need to update key & detached_parent + + // When it's suspended, we render children into an element that is detached from the dom + // tree while rendering fallback UI into the original place where children resides in. + match (self.suspended, &mut suspense.fallback) { + (true, Some(fallback_ancestor)) => { + self.children.apply( + parent_scope, + &self.detached_parent, + NodeRef::default(), + children_ancestor, + ); + + self.fallback + .apply(parent_scope, parent, next_sibling, fallback_ancestor) + } + + (false, None) => { + self.children + .apply(parent_scope, parent, next_sibling, children_ancestor) + } + + (true, None) => { + children_ancestor.shift(&self.detached_parent, NodeRef::default()); + + self.children.apply( + parent_scope, + &self.detached_parent, + NodeRef::default(), + children_ancestor, + ); + // first render of fallback + let (fallback_ref, fallback) = + self.fallback.attach(parent_scope, parent, next_sibling); + suspense.fallback = Some(fallback); + fallback_ref + } + + (false, Some(_)) => { + suspense.fallback.take().unwrap().detach(parent); + + children_ancestor.shift(parent, next_sibling.clone()); + self.children + .apply(parent_scope, parent, next_sibling, children_ancestor) + } + } + } +} diff --git a/packages/yew/src/dom_bundle/btag.rs b/packages/yew/src/dom_bundle/btag.rs new file mode 100644 index 00000000000..0400be90f2e --- /dev/null +++ b/packages/yew/src/dom_bundle/btag.rs @@ -0,0 +1,1057 @@ +//! This module contains the bundle implementation of a tag `BTag`. + +use super::listeners::ListenerRegistration; +use super::{BNode, DomBundle, InputFields, VDiff, Value}; +use crate::dom_bundle::attributes::Apply; +use crate::html::AnyScope; +use crate::virtual_dom::{vtag::VTagInner, vtag::SVG_NAMESPACE, Attributes, Key, VTag}; +use crate::NodeRef; +use gloo::console; +use gloo_utils::document; +use std::ops::DerefMut; +use std::{borrow::Cow, hint::unreachable_unchecked}; +use wasm_bindgen::JsCast; +use web_sys::{Element, HtmlTextAreaElement as TextAreaElement}; + +/// [BTag] fields that are specific to different [BTag] kinds. +/// Decreases the memory footprint of [BTag] by avoiding impossible field and value combinations. +#[derive(Debug)] +enum BTagInner { + /// Fields specific to + /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) + Input(InputFields), + /// Fields specific to + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + Textarea { + /// Contains a value of an + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + value: Value, + }, + /// Fields for all other kinds of [VTag]s + Other { + /// A tag of the element. + tag: Cow<'static, str>, + /// List of child nodes + child_bundle: BNode, + }, +} + +/// The bundle implementation to [VTag] +#[derive(Debug)] +pub struct BTag { + /// [BTag] fields that are specific to different [BTag] kinds. + inner: BTagInner, + listeners: ListenerRegistration, + /// A reference to the DOM [`Element`]. + reference: Element, + /// A node reference used for DOM access in Component lifecycle methods + node_ref: NodeRef, + attributes: Attributes, + key: Option, +} + +impl DomBundle for BTag { + /// Remove VTag from parent. + fn detach(self, parent: &Element) { + self.listeners.unregister(); + + let node = self.reference; + // recursively remove its children + if let BTagInner::Other { child_bundle, .. } = self.inner { + child_bundle.detach(&node); + } + if parent.remove_child(&node).is_err() { + console::warn!("Node not found to remove VTag"); + } + // It could be that the ref was already reused when rendering another element. + // Only unset the ref it still belongs to our node + if self.node_ref.get().as_ref() == Some(&node) { + self.node_ref.set(None); + } + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + next_parent + .insert_before(&self.reference, next_sibling.get().as_ref()) + .unwrap(); + } +} + +impl VDiff for VTag { + type Bundle = BTag; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let el = self.create_element(parent); + let Self { + listeners, + attributes, + node_ref, + key, + .. + } = self; + super::insert_node(&el, parent, next_sibling.get().as_ref()); + + let attributes = attributes.apply(&el); + let listeners = listeners.apply(&el); + + let inner = match self.inner { + VTagInner::Input(f) => { + let f = f.apply(el.unchecked_ref()); + BTagInner::Input(f) + } + VTagInner::Textarea { value } => { + let value = value.apply(el.unchecked_ref()); + BTagInner::Textarea { value } + } + VTagInner::Other { children, tag } => { + let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default()); + BTagInner::Other { + child_bundle: child_bundle.into(), + tag, + } + } + }; + node_ref.set(Some(el.clone().into())); + ( + node_ref.clone(), + BTag { + inner, + listeners, + reference: el, + attributes, + key, + node_ref, + }, + ) + } + /// Renders virtual tag over DOM [Element], but it also compares this with an ancestor [VTag] + /// to compute what to patch in the actual DOM nodes. + fn apply( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + node_bundle: &mut BNode, + ) -> NodeRef { + // This kind of branching patching routine reduces branch predictor misses and the need to + // unpack the enums (including `Option`s) all the time, resulting in a more streamlined + // patching flow + let is_matching_tag = match node_bundle { + BNode::BTag(ex) if self.key == ex.key => match (&self.inner, &ex.inner) { + (VTagInner::Input(_), BTagInner::Input(_)) => true, + (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true, + (VTagInner::Other { tag: l, .. }, BTagInner::Other { tag: r, .. }) if l == r => { + true + } + _ => false, + }, + _ => false, + }; + // If the ancestor is a tag of the same type, don't recreate, keep the + // old tag and update its attributes and children. + let tag = if is_matching_tag { + match node_bundle { + BNode::BTag(a) => { + // Preserve the reference that already exists + a.deref_mut() + } + _ => unsafe { unreachable_unchecked() }, + } + } else { + let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); + node_bundle.replace(parent, self_.into()); + return self_ref; + }; + + let el = &tag.reference; + self.attributes.apply_diff(el, &mut tag.attributes); + self.listeners.apply_diff(el, &mut tag.listeners); + + match (self.inner, &mut tag.inner) { + (VTagInner::Input(new), BTagInner::Input(old)) => { + new.apply_diff(el.unchecked_ref(), old); + } + (VTagInner::Textarea { value: new }, BTagInner::Textarea { value: old }) => { + new.apply_diff(el.unchecked_ref(), old); + } + ( + VTagInner::Other { children: new, .. }, + BTagInner::Other { + child_bundle: old, .. + }, + ) => { + new.apply(parent_scope, el, NodeRef::default(), old); + } + // Can not happen, because we checked for tag equability above + _ => unsafe { unreachable_unchecked() }, + } + + tag.key = self.key; + + if self.node_ref != tag.node_ref && tag.node_ref.get().as_ref() == Some(el) { + tag.node_ref.set(None); + } + if self.node_ref != tag.node_ref { + tag.node_ref = self.node_ref; + tag.node_ref.set(Some(el.clone().into())); + } + + tag.node_ref.clone() + } +} + +impl VTag { + fn create_element(&self, parent: &Element) -> Element { + let tag = self.tag(); + if tag == "svg" + || parent + .namespace_uri() + .map_or(false, |ns| ns == SVG_NAMESPACE) + { + let namespace = Some(SVG_NAMESPACE); + document() + .create_element_ns(namespace, tag) + .expect("can't create namespaced element for vtag") + } else { + document() + .create_element(tag) + .expect("can't create element for vtag") + } + } +} + +impl BTag { + pub(crate) fn key(&self) -> Option<&Key> { + self.key.as_ref() + } + + pub(crate) fn reference(&self) -> &Element { + &self.reference + } + + #[cfg(test)] + fn children(&self) -> &[BNode] { + match &self.inner { + BTagInner::Other { child_bundle, .. } => match child_bundle { + BNode::BList(blist) => blist, + _ => unreachable!("should be blist"), + }, + _ => &[], + } + } + + #[cfg(test)] + fn tag(&self) -> &str { + match &self.inner { + BTagInner::Input { .. } => "input", + BTagInner::Textarea { .. } => "textarea", + BTagInner::Other { tag, .. } => tag.as_ref(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + html, + virtual_dom::{vtag::HTML_NAMESPACE, AttrValue, VNode}, + Html, + }; + use wasm_bindgen::JsCast; + use web_sys::HtmlInputElement as InputElement; + + #[cfg(feature = "wasm_test")] + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + #[cfg(feature = "wasm_test")] + wasm_bindgen_test_configure!(run_in_browser); + + fn test_scope() -> AnyScope { + AnyScope::test() + } + + #[test] + fn it_compares_tags() { + let a = html! { +
    + }; + + let b = html! { +
    + }; + + let c = html! { +

    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_text() { + let a = html! { +
    { "correct" }
    + }; + + let b = html! { +
    { "correct" }
    + }; + + let c = html! { +
    { "incorrect" }
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_attributes_static() { + let a = html! { +
    + }; + + let b = html! { +
    + }; + + let c = html! { +
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_attributes_dynamic() { + let a = html! { +
    + }; + + let b = html! { +
    + }; + + let c = html! { +
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_children() { + let a = html! { +
    +

    +
    + }; + + let b = html! { +
    +

    +
    + }; + + let c = html! { +
    + +
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_classes_static() { + let a = html! { +
    + }; + + let b = html! { +
    + }; + + let c = html! { +
    + }; + + let d = html! { +
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + assert_ne!(a, d); + } + + #[test] + fn it_compares_classes_dynamic() { + let a = html! { +
    + }; + + let b = html! { +
    + }; + + let c = html! { +
    + }; + + let d = html! { +
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + assert_ne!(a, d); + } + + fn assert_vtag(node: VNode) -> VTag { + if let VNode::VTag(vtag) = node { + return *vtag; + } + panic!("should be vtag"); + } + + fn assert_btag_ref(node: &BNode) -> &BTag { + if let BNode::BTag(vtag) = node { + return vtag; + } + panic!("should be btag"); + } + + fn assert_vtag_ref(node: &VNode) -> &VTag { + if let VNode::VTag(vtag) = node { + return vtag; + } + panic!("should be vtag"); + } + + fn assert_btag_mut(node: &mut BNode) -> &mut BTag { + if let BNode::BTag(btag) = node { + return btag; + } + panic!("should be btag"); + } + + fn assert_namespace(vtag: &BTag, namespace: &'static str) { + assert_eq!(vtag.reference.namespace_uri().unwrap(), namespace); + } + + #[test] + fn supports_svg() { + let document = web_sys::window().unwrap().document().unwrap(); + + let scope = test_scope(); + let div_el = document.create_element("div").unwrap(); + let namespace = SVG_NAMESPACE; + let namespace = Some(namespace); + let svg_el = document.create_element_ns(namespace, "svg").unwrap(); + + let g_node = html! { }; + let path_node = html! { }; + let svg_node = html! { {path_node} }; + + let svg_tag = assert_vtag(svg_node); + let (_, svg_tag) = svg_tag.attach(&scope, &div_el, NodeRef::default()); + assert_namespace(&svg_tag, SVG_NAMESPACE); + let path_tag = assert_btag_ref(svg_tag.children().get(0).unwrap()); + assert_namespace(path_tag, SVG_NAMESPACE); + + let g_tag = assert_vtag(g_node.clone()); + let (_, g_tag) = g_tag.attach(&scope, &div_el, NodeRef::default()); + assert_namespace(&g_tag, HTML_NAMESPACE); + + let g_tag = assert_vtag(g_node); + let (_, g_tag) = g_tag.attach(&scope, &svg_el, NodeRef::default()); + assert_namespace(&g_tag, SVG_NAMESPACE); + } + + #[test] + fn it_compares_values() { + let a = html! { + + }; + + let b = html! { + + }; + + let c = html! { + + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_kinds() { + let a = html! { + + }; + + let b = html! { + + }; + + let c = html! { + + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_checked() { + let a = html! { + + }; + + let b = html! { + + }; + + let c = html! { + + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_allows_aria_attributes() { + let a = html! { +

    + + +

    +

    + }; + if let VNode::VTag(vtag) = a { + assert_eq!( + vtag.attributes + .iter() + .find(|(k, _)| k == &"aria-controls") + .map(|(_, v)| v), + Some("it-works") + ); + } else { + panic!("vtag expected"); + } + } + + #[test] + fn it_does_not_set_missing_class_name() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + let elem = html! {
    }; + let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_btag_mut(&mut elem); + // test if the className has not been set + assert!(!vtag.reference.has_attribute("class")); + } + + fn test_set_class_name(gen_html: impl FnOnce() -> Html) { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + let elem = gen_html(); + let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_btag_mut(&mut elem); + // test if the className has been set + assert!(vtag.reference.has_attribute("class")); + } + + #[test] + fn it_sets_class_name_static() { + test_set_class_name(|| html! {
    }); + } + + #[test] + fn it_sets_class_name_dynamic() { + test_set_class_name(|| html! {
    }); + } + + #[test] + fn controlled_input_synced() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + let expected = "not_changed_value"; + + // Initial state + let elem = html! { }; + let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_btag_ref(&elem); + + // User input + let input_ref = &vtag.reference; + let input = input_ref.dyn_ref::(); + input.unwrap().set_value("User input"); + + let next_elem = html! { }; + let elem_vtag = assert_vtag(next_elem); + + // Sync happens here + elem_vtag.apply(&scope, &parent, NodeRef::default(), &mut elem); + let vtag = assert_btag_ref(&elem); + + // Get new current value of the input element + let input_ref = &vtag.reference; + let input = input_ref.dyn_ref::().unwrap(); + + let current_value = input.value(); + + // check whether not changed virtual dom value has been set to the input element + assert_eq!(current_value, expected); + } + + #[test] + fn uncontrolled_input_unsynced() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + // Initial state + let elem = html! { }; + let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_btag_ref(&elem); + + // User input + let input_ref = &vtag.reference; + let input = input_ref.dyn_ref::(); + input.unwrap().set_value("User input"); + + let next_elem = html! { }; + let elem_vtag = assert_vtag(next_elem); + + // Value should not be refreshed + elem_vtag.apply(&scope, &parent, NodeRef::default(), &mut elem); + let vtag = assert_btag_ref(&elem); + + // Get user value of the input element + let input_ref = &vtag.reference; + let input = input_ref.dyn_ref::().unwrap(); + + let current_value = input.value(); + + // check whether not changed virtual dom value has been set to the input element + assert_eq!(current_value, "User input"); + + // Need to remove the element to clean up the dirty state of the DOM. Failing this causes + // event listener tests to fail. + parent.remove(); + } + + #[test] + fn dynamic_tags_work() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + let elem = html! { <@{ + let mut builder = String::new(); + builder.push('a'); + builder + }/> }; + + let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_btag_mut(&mut elem); + // make sure the new tag name is used internally + assert_eq!(vtag.tag(), "a"); + + // Element.tagName is always in the canonical upper-case form. + assert_eq!(vtag.reference.tag_name(), "A"); + } + + #[test] + fn dynamic_tags_handle_value_attribute() { + let div_el = html! { + <@{"div"} value="Hello"/> + }; + let div_vtag = assert_vtag_ref(&div_el); + assert!(div_vtag.value().is_none()); + let v: Option<&str> = div_vtag + .attributes + .iter() + .find(|(k, _)| k == &"value") + .map(|(_, v)| AsRef::as_ref(v)); + assert_eq!(v, Some("Hello")); + + let input_el = html! { + <@{"input"} value="World"/> + }; + let input_vtag = assert_vtag_ref(&input_el); + assert_eq!(input_vtag.value(), Some(&AttrValue::Static("World"))); + assert!(!input_vtag.attributes.iter().any(|(k, _)| k == "value")); + } + + #[test] + fn dynamic_tags_handle_weird_capitalization() { + let el = html! { + <@{"tExTAREa"}/> + }; + let vtag = assert_vtag_ref(&el); + assert_eq!(vtag.tag(), "textarea"); + } + + #[test] + fn reset_node_ref() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + let node_ref = NodeRef::default(); + let elem: VNode = html! {
    }; + assert_vtag_ref(&elem); + let (_, elem) = elem.attach(&scope, &parent, NodeRef::default()); + assert_eq!(node_ref.get(), parent.first_child()); + elem.detach(&parent); + assert!(node_ref.get().is_none()); + } + + #[test] + fn vtag_reuse_should_reset_ancestors_node_ref() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + document().body().unwrap().append_child(&parent).unwrap(); + + let node_ref_a = NodeRef::default(); + let elem_a = html! {
    }; + let (_, mut elem) = elem_a.attach(&scope, &parent, NodeRef::default()); + + // save the Node to check later that it has been reused. + let node_a = node_ref_a.get().unwrap(); + + let node_ref_b = NodeRef::default(); + let elem_b = html! {
    }; + elem_b.apply(&scope, &parent, NodeRef::default(), &mut elem); + + let node_b = node_ref_b.get().unwrap(); + + assert_eq!(node_a, node_b, "VTag should have reused the element"); + assert!( + node_ref_a.get().is_none(), + "node_ref_a should have been reset when the element was reused." + ); + } + + #[test] + fn vtag_should_not_touch_newly_bound_refs() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + document().body().unwrap().append_child(&parent).unwrap(); + + let test_ref = NodeRef::default(); + let before = html! { + <> +
    + + }; + let after = html! { + <> +
    +
    + + }; + // The point of this diff is to first render the "after" div and then detach the "before" div, + // while both should be bound to the same node ref + + let (_, mut elem) = before.attach(&scope, &parent, NodeRef::default()); + after.apply(&scope, &parent, NodeRef::default(), &mut elem); + + assert_eq!( + test_ref + .get() + .unwrap() + .dyn_ref::() + .unwrap() + .outer_html(), + "
    " + ); + } +} + +#[cfg(test)] +mod layout_tests { + extern crate self as yew; + + use crate::html; + use crate::tests::layout_tests::{diff_layouts, TestLayout}; + + #[cfg(feature = "wasm_test")] + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + #[cfg(feature = "wasm_test")] + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn diff() { + let layout1 = TestLayout { + name: "1", + node: html! { +
      +
    • + {"a"} +
    • +
    • + {"b"} +
    • +
    + }, + expected: "
    • a
    • b
    ", + }; + + let layout2 = TestLayout { + name: "2", + node: html! { +
      +
    • + {"a"} +
    • +
    • + {"b"} +
    • +
    • + {"d"} +
    • +
    + }, + expected: "
    • a
    • b
    • d
    ", + }; + + let layout3 = TestLayout { + name: "3", + node: html! { +
      +
    • + {"a"} +
    • +
    • + {"b"} +
    • +
    • + {"c"} +
    • +
    • + {"d"} +
    • +
    + }, + expected: "
    • a
    • b
    • c
    • d
    ", + }; + + let layout4 = TestLayout { + name: "4", + node: html! { +
      +
    • + <> + {"a"} + +
    • +
    • + {"b"} +
    • + {"c"} +
    • +
    • + {"d"} +
    • + +
    + }, + expected: "
    • a
    • b
    • c
    • d
    ", + }; + + diff_layouts(vec![layout1, layout2, layout3, layout4]); + } +} + +#[cfg(test)] +mod tests_without_browser { + use crate::html; + + #[test] + fn html_if_bool() { + assert_eq!( + html! { + if true { +
    + } + }, + html! {
    }, + ); + assert_eq!( + html! { + if false { +
    + } else { +
    + } + }, + html! { +
    + }, + ); + assert_eq!( + html! { + if false { +
    + } + }, + html! {}, + ); + + // non-root tests + assert_eq!( + html! { +
    + if true { +
    + } +
    + }, + html! { +
    +
    +
    + }, + ); + assert_eq!( + html! { +
    + if false { +
    + } else { +
    + } +
    + }, + html! { +
    +
    +
    + }, + ); + assert_eq!( + html! { +
    + if false { +
    + } +
    + }, + html! { +
    + <> +
    + }, + ); + } + + #[test] + fn html_if_option() { + let option_foo = Some("foo"); + let none: Option<&'static str> = None; + assert_eq!( + html! { + if let Some(class) = option_foo { +
    + } + }, + html! {
    }, + ); + assert_eq!( + html! { + if let Some(class) = none { +
    + } else { +
    + } + }, + html! {
    }, + ); + assert_eq!( + html! { + if let Some(class) = none { +
    + } + }, + html! {}, + ); + + // non-root tests + assert_eq!( + html! { +
    + if let Some(class) = option_foo { +
    + } +
    + }, + html! {
    }, + ); + assert_eq!( + html! { +
    + if let Some(class) = none { +
    + } else { +
    + } +
    + }, + html! {
    }, + ); + assert_eq!( + html! { +
    + if let Some(class) = none { +
    + } +
    + }, + html! {
    <>
    }, + ); + } +} diff --git a/packages/yew/src/dom_bundle/listeners.rs b/packages/yew/src/dom_bundle/listeners.rs new file mode 100644 index 00000000000..fde7171cb10 --- /dev/null +++ b/packages/yew/src/dom_bundle/listeners.rs @@ -0,0 +1,696 @@ +use crate::dom_bundle::test_log; +use crate::virtual_dom::{Listener, ListenerKind, Listeners}; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::ops::Deref; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use wasm_bindgen::{prelude::Closure, JsCast}; +use web_sys::{Element, Event}; + +thread_local! { + /// Global event listener registry + static REGISTRY: RefCell = Default::default(); + + /// Key used to store listener id on element + static LISTENER_ID_PROP: wasm_bindgen::JsValue = "__yew_listener_id".into(); + + /// Cached reference to the document body + static BODY: web_sys::HtmlElement = gloo_utils::document().body().unwrap(); +} + +/// Bubble events during delegation +static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); + +/// Set, if events should bubble up the DOM tree, calling any matching callbacks. +/// +/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event +/// handling performance. +/// +/// Note that yew uses event delegation and implements internal even bubbling for performance +/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event +/// handler has no effect. +/// +/// This function should be called before any component is mounted. +pub fn set_event_bubbling(bubble: bool) { + BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); +} + +#[derive(Debug)] +pub(crate) enum ListenerRegistration { + /// No listeners registered. + NoReg, + /// Added to global registry by ID + Registered(u32), +} + +impl super::Apply for Listeners { + type Element = Element; + type Bundle = ListenerRegistration; + + fn apply(self, el: &Self::Element) -> ListenerRegistration { + match self { + Self::Pending(pending) => ListenerRegistration::register(el, &pending), + Self::None => ListenerRegistration::NoReg, + } + } + + fn apply_diff(self, el: &Self::Element, bundle: &mut ListenerRegistration) { + use ListenerRegistration::*; + use Listeners::*; + + match (self, bundle) { + (Pending(pending), Registered(ref id)) => { + // Reuse the ID + test_log!("reusing listeners for {}", id); + Registry::with(|reg| reg.patch(id, &*pending)); + } + (Pending(pending), bundle @ NoReg) => { + *bundle = ListenerRegistration::register(el, &pending); + test_log!( + "registering listeners for {}", + match bundle { + ListenerRegistration::Registered(id) => id, + _ => unreachable!(), + } + ); + } + (None, bundle @ Registered(_)) => { + let id = match bundle { + ListenerRegistration::Registered(ref id) => id, + _ => unreachable!(), + }; + test_log!("unregistering listeners for {}", id); + Registry::with(|reg| reg.unregister(id)); + *bundle = NoReg; + } + (None, NoReg) => { + test_log!("{}", &"unchanged empty listeners"); + } + }; + } +} + +impl ListenerRegistration { + /// Register listeners and return their handle ID + fn register(el: &Element, pending: &[Option>]) -> Self { + Self::Registered(Registry::with(|reg| { + let id = reg.set_listener_id(el); + reg.register(id, pending); + id + })) + } + + /// Remove any registered event listeners from the global registry + pub(super) fn unregister(&self) { + if let Self::Registered(id) = self { + Registry::with(|r| r.unregister(id)); + } + } +} + +#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] +struct EventDescriptor { + kind: ListenerKind, + passive: bool, +} + +impl From<&dyn Listener> for EventDescriptor { + fn from(l: &dyn Listener) -> Self { + Self { + kind: l.kind(), + passive: l.passive(), + } + } +} + +/// Ensures global event handler registration. +// +// Separate struct to DRY, while avoiding partial struct mutability. +#[derive(Default, Debug)] +struct GlobalHandlers { + /// Events with registered handlers that are possibly passive + handling: HashSet, + + /// Keep track of all listeners to drop them on registry drop. + /// The registry is never dropped in production. + #[cfg(test)] + #[allow(clippy::type_complexity)] + registered: Vec<(ListenerKind, Closure)>, +} + +impl GlobalHandlers { + /// Ensure a descriptor has a global event handler assigned + fn ensure_handled(&mut self, desc: EventDescriptor) { + if !self.handling.contains(&desc) { + let cl = BODY.with(|body| { + let cl = Closure::wrap( + Box::new(move |e: Event| Registry::handle(desc, e)) as Box + ); + AsRef::::as_ref(body) + .add_event_listener_with_callback_and_add_event_listener_options( + &desc.kind.as_ref()[2..], + cl.as_ref().unchecked_ref(), + &{ + let mut opts = web_sys::AddEventListenerOptions::new(); + opts.capture(true); + // We need to explicitly set passive to override any browser defaults + opts.passive(desc.passive); + opts + }, + ) + .map_err(|e| format!("could not register global listener: {:?}", e)) + .unwrap(); + cl + }); + + // Never drop the closure as this event handler is static + #[cfg(not(test))] + cl.forget(); + #[cfg(test)] + self.registered.push((desc.kind, cl)); + + self.handling.insert(desc); + } + } +} + +// Enable resetting between tests +#[cfg(test)] +impl Drop for GlobalHandlers { + fn drop(&mut self) { + BODY.with(|body| { + for (kind, cl) in std::mem::take(&mut self.registered) { + AsRef::::as_ref(body) + .remove_event_listener_with_callback( + &kind.as_ref()[2..], + cl.as_ref().unchecked_ref(), + ) + .unwrap(); + } + }); + } +} + +/// Global multiplexing event handler registry +#[derive(Default, Debug)] +struct Registry { + /// Counter for assigning new IDs + id_counter: u32, + + /// Registered global event handlers + global: GlobalHandlers, + + /// Contains all registered event listeners by listener ID + by_id: HashMap>>>, +} + +impl Registry { + /// Run f with access to global Registry + #[inline] + fn with(f: impl FnOnce(&mut Registry) -> R) -> R { + REGISTRY.with(|r| f(&mut *r.borrow_mut())) + } + + /// Register all passed listeners under ID + fn register(&mut self, id: u32, listeners: &[Option>]) { + let mut by_desc = + HashMap::>>::with_capacity(listeners.len()); + for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() { + let desc = EventDescriptor::from(l.deref()); + self.global.ensure_handled(desc); + by_desc.entry(desc).or_default().push(l); + } + self.by_id.insert(id, by_desc); + } + + /// Patch an already registered set of handlers + fn patch(&mut self, id: &u32, listeners: &[Option>]) { + if let Some(by_desc) = self.by_id.get_mut(id) { + // Keeping empty vectors is fine. Those don't do much and should happen rarely. + for v in by_desc.values_mut() { + v.clear() + } + + for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() { + let desc = EventDescriptor::from(l.deref()); + self.global.ensure_handled(desc); + by_desc.entry(desc).or_default().push(l); + } + } + } + + /// Unregister any existing listeners for ID + fn unregister(&mut self, id: &u32) { + self.by_id.remove(id); + } + + /// Set unique listener ID onto element and return it + fn set_listener_id(&mut self, el: &Element) -> u32 { + let id = self.id_counter; + self.id_counter += 1; + + LISTENER_ID_PROP.with(|prop| { + if !js_sys::Reflect::set(el, prop, &js_sys::Number::from(id)).unwrap() { + panic!("failed to set listener ID property"); + } + }); + + id + } + + /// Handle a global event firing + fn handle(desc: EventDescriptor, event: Event) { + let target = match event + .target() + .map(|el| el.dyn_into::().ok()) + .flatten() + { + Some(el) => el, + None => return, + }; + + Self::run_handlers(desc, event, target); + } + + fn run_handlers(desc: EventDescriptor, event: Event, target: web_sys::Element) { + let run_handler = |el: &web_sys::Element| { + if let Some(l) = LISTENER_ID_PROP + .with(|prop| js_sys::Reflect::get(el, prop).ok()) + .map(|v| v.dyn_into().ok()) + .flatten() + .map(|num: js_sys::Number| { + Registry::with(|r| { + r.by_id + .get(&(num.value_of() as u32)) + .map(|s| s.get(&desc)) + .flatten() + .cloned() + }) + }) + .flatten() + { + for l in l { + l.handle(event.clone()); + } + } + }; + + run_handler(&target); + + if BUBBLE_EVENTS.load(Ordering::Relaxed) { + let mut el = target; + while !event.cancel_bubble() { + el = match el.parent_element() { + Some(el) => el, + None => break, + }; + run_handler(&el); + } + } + } +} + +#[cfg(test)] +mod tests { + use std::marker::PhantomData; + + use web_sys::{Event, EventInit, MouseEvent}; + + #[cfg(feature = "wasm_test")] + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + #[cfg(feature = "wasm_test")] + wasm_bindgen_test_configure!(run_in_browser); + + use crate::{html, html::TargetCast, AppHandle, Component, Context, Html}; + use gloo_utils::document; + use wasm_bindgen::JsCast; + + #[derive(Clone)] + enum Message { + Action, + StopListening, + SetText(String), + } + + #[derive(Default)] + struct State { + stop_listening: bool, + action: u32, + text: String, + } + + trait Mixin { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + if state.stop_listening { + html! { + {state.action} + } + } else { + html! { + + {state.action} + + } + } + } + } + + struct Comp + where + M: Mixin + 'static, + { + state: State, + pd: PhantomData, + } + + impl Component for Comp + where + M: Mixin + 'static, + { + type Message = Message; + type Properties = (); + + fn create(_: &Context) -> Self { + Comp { + state: Default::default(), + pd: PhantomData, + } + } + + fn update(&mut self, _: &Context, msg: Self::Message) -> bool { + match msg { + Message::Action => { + self.state.action += 1; + } + Message::StopListening => { + self.state.stop_listening = true; + } + Message::SetText(s) => { + self.state.text = s; + } + }; + true + } + + fn view(&self, ctx: &Context) -> crate::Html { + M::view(ctx, &self.state) + } + } + + fn assert_count(el: &web_sys::HtmlElement, count: isize) { + assert_eq!(el.text_content(), Some(count.to_string())) + } + + fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement { + document() + .query_selector(tag) + .unwrap() + .unwrap() + .dyn_into::() + .unwrap() + } + + fn init(tag: &str) -> (AppHandle>, web_sys::HtmlElement) + where + M: Mixin, + { + // Remove any existing listeners and elements + super::Registry::with(|r| *r = Default::default()); + if let Some(el) = document().query_selector(tag).unwrap() { + el.parent_element().unwrap().remove(); + } + + let root = document().create_element("div").unwrap(); + document().body().unwrap().append_child(&root).unwrap(); + let app = crate::start_app_in_element::>(root); + + (app, get_el_by_tag(tag)) + } + + #[test] + fn synchronous() { + struct Synchronous; + + impl Mixin for Synchronous {} + + let (link, el) = init::("a"); + + assert_count(&el, 0); + + el.click(); + assert_count(&el, 1); + + el.click(); + assert_count(&el, 2); + + link.send_message(Message::StopListening); + el.click(); + assert_count(&el, 2); + } + + #[test] + fn non_bubbling_event() { + struct NonBubbling; + + impl Mixin for NonBubbling { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + let onblur = ctx.link().callback(|_| Message::Action); + html! { + + } + } + } + + let (_, el) = init::("a"); + + assert_count(&el, 0); + + let input = document().get_element_by_id("input").unwrap(); + + input + .dispatch_event( + &Event::new_with_event_init_dict("blur", &{ + let mut dict = EventInit::new(); + dict.bubbles(false); + dict + }) + .unwrap(), + ) + .unwrap(); + + assert_count(&el, 1); + } + + #[test] + fn bubbling() { + struct Bubbling; + + impl Mixin for Bubbling { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + if state.stop_listening { + html! { + + } + } else { + let cb = ctx.link().callback(|_| Message::Action); + html! { + + } + } + } + } + + let (link, el) = init::("a"); + + assert_count(&el, 0); + + el.click(); + assert_count(&el, 2); + + el.click(); + assert_count(&el, 4); + + link.send_message(Message::StopListening); + el.click(); + assert_count(&el, 4); + } + + #[test] + fn cancel_bubbling() { + struct CancelBubbling; + + impl Mixin for CancelBubbling { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + html! { + + } + } + } + + let (_, el) = init::("a"); + + assert_count(&el, 0); + + el.click(); + assert_count(&el, 1); + + el.click(); + assert_count(&el, 2); + } + + #[test] + fn cancel_bubbling_nested() { + // Here an event is being delivered to a DOM node which does + // _not_ have a listener but which is contained within an + // element that does and which cancels the bubble. + struct CancelBubbling; + + impl Mixin for CancelBubbling { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + html! { + + } + } + } + + let (_, el) = init::("a"); + + assert_count(&el, 0); + + el.click(); + assert_count(&el, 1); + + el.click(); + assert_count(&el, 2); + } + + fn test_input_listener(make_event: impl Fn() -> E) + where + E: JsCast + std::fmt::Debug, + { + struct Input; + + impl Mixin for Input { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + if state.stop_listening { + html! { +
    + +

    {state.text.clone()}

    +
    + } + } else { + html! { +
    + +

    {state.text.clone()}

    +
    + } + } + } + } + + let (link, input_el) = init::("input"); + let input_el = input_el.dyn_into::().unwrap(); + let p_el = get_el_by_tag("p"); + + assert_eq!(&p_el.text_content().unwrap(), ""); + for mut s in ["foo", "bar", "baz"].iter() { + input_el.set_value(s); + if s == &"baz" { + link.send_message(Message::StopListening); + s = &"bar"; + } + input_el + .dyn_ref::() + .unwrap() + .dispatch_event(&make_event().dyn_into().unwrap()) + .unwrap(); + assert_eq!(&p_el.text_content().unwrap(), s); + } + } + + #[test] + fn oninput() { + test_input_listener(|| { + web_sys::InputEvent::new_with_event_init_dict( + "input", + web_sys::InputEventInit::new().bubbles(true), + ) + .unwrap() + }) + } + + #[test] + fn onchange() { + test_input_listener(|| { + web_sys::Event::new_with_event_init_dict( + "change", + web_sys::EventInit::new().bubbles(true), + ) + .unwrap() + }) + } +} diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index e23980af65b..5ae6a9b478c 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -3,15 +3,34 @@ //! A bundle, borrowed from the mathematical meaning, is any structure over some base space. //! In our case, the base space is the virtual dom we're trying to render. //! In order to efficiently implement updates, and diffing, additional information has to be -//! kept around. +//! kept around. This information is carried in the bundle. -use web_sys::Element; +mod attributes; +mod bcomp; +mod blist; +mod bnode; +mod bportal; +mod bsuspense; +mod btag; +mod listeners; -use crate::{html::AnyScope, virtual_dom::VNode, NodeRef}; +#[cfg(debug_assertions)] +pub(crate) use self::bcomp::log_event; -// TODO(#938): What about implementing `VDiff` for `Element`? -// It would make it possible to include ANY element into the tree. -// `Ace` editor embedding for example? +pub use self::bcomp::BComp; +pub use self::blist::BList; +pub use self::bnode::BNode; +pub use self::bportal::BPortal; +pub use self::bsuspense::BSuspense; +pub use self::btag::BTag; + +pub(crate) use self::attributes::{Apply, InputFields, Value}; +pub(crate) use self::bcomp::{Mountable, PropsWrapper}; +#[doc(hidden)] +pub use self::listeners::set_event_bubbling; + +use crate::{html::AnyScope, NodeRef}; +use web_sys::{Element, Node}; pub(crate) trait DomBundle { /// Remove self from parent. @@ -23,6 +42,10 @@ pub(crate) trait DomBundle { fn shift(&self, next_parent: &Element, next_sibling: NodeRef); } +// TODO(#938): What about implementing `VDiff` for `Element`? +// It would make it possible to include ANY element into the tree. +// `Ace` editor embedding for example? + /// This trait provides features to update a tree by calculating a difference against another tree. pub(crate) trait VDiff { type Bundle: DomBundle; @@ -64,6 +87,31 @@ pub(crate) trait VDiff { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: &mut VNode, + ancestor: &mut BNode, ) -> NodeRef; } + +pub(crate) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) { + match next_sibling { + Some(next_sibling) => parent + .insert_before(node, Some(next_sibling)) + .expect("failed to insert tag before next sibling"), + None => parent.append_child(node).expect("failed to append child"), + }; +} + +/// Log an operation during tests for debugging purposes +/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. +#[cfg(all(test, feature = "wasm_test", verbose_tests))] +macro_rules! test_log { + ($fmt:literal, $($arg:expr),* $(,)?) => { + ::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*); + }; +} +#[cfg(not(all(test, feature = "wasm_test", verbose_tests)))] +macro_rules! test_log { + ($fmt:literal, $($arg:expr),* $(,)?) => { + let _ = std::format_args!(concat!("\t ", $fmt), $($arg),*); + }; +} +pub(self) use test_log; diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index eba82d5532c..e2bc433a72b 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -1,11 +1,10 @@ //! Component lifecycle module use super::{AnyScope, BaseComponent, Scope}; -use crate::dom_bundle::{DomBundle, VDiff}; +use crate::dom_bundle::{BNode, DomBundle, VDiff}; use crate::html::RenderError; use crate::scheduler::{self, Runnable, Shared}; use crate::suspense::{Suspense, Suspension}; -use crate::virtual_dom::VNode; use crate::Callback; use crate::{Context, NodeRef}; use std::rc::Rc; @@ -13,7 +12,7 @@ use web_sys::Element; pub(crate) struct ComponentState { pub(crate) component: Box, - pub(crate) root_node: VNode, + pub(crate) root_node: BNode, context: Context, parent: Element, @@ -32,7 +31,7 @@ impl ComponentState { pub(crate) fn new( parent: Element, next_sibling: NodeRef, - root_node: VNode, + root_node: BNode, node_ref: NodeRef, scope: Scope, props: Rc, @@ -65,7 +64,7 @@ impl ComponentState { pub(crate) struct CreateRunner { pub(crate) parent: Element, pub(crate) next_sibling: NodeRef, - pub(crate) placeholder: VNode, + pub(crate) placeholder: BNode, pub(crate) node_ref: NodeRef, pub(crate) props: Rc, pub(crate) scope: Scope, @@ -76,7 +75,7 @@ impl Runnable for CreateRunner { let mut current_state = self.scope.state.borrow_mut(); if current_state.is_none() { #[cfg(debug_assertions)] - crate::virtual_dom::vcomp::log_event(self.scope.vcomp_id, "create"); + crate::dom_bundle::log_event(self.scope.vcomp_id, "create"); *current_state = Some(ComponentState::new( self.parent, @@ -140,7 +139,7 @@ impl Runnable for UpdateRunner { }; #[cfg(debug_assertions)] - crate::virtual_dom::vcomp::log_event( + crate::dom_bundle::log_event( state.vcomp_id, format!("update(schedule_render={})", schedule_render), ); @@ -169,7 +168,7 @@ impl Runnable for DestroyRunner { fn run(self: Box) { if let Some(mut state) = self.state.borrow_mut().take() { #[cfg(debug_assertions)] - crate::virtual_dom::vcomp::log_event(state.vcomp_id, "destroy"); + crate::dom_bundle::log_event(state.vcomp_id, "destroy"); state.component.destroy(&state.context); state.root_node.detach(&state.parent); @@ -186,7 +185,7 @@ impl Runnable for RenderRunner { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { #[cfg(debug_assertions)] - crate::virtual_dom::vcomp::log_event(state.vcomp_id, "render"); + crate::dom_bundle::log_event(state.vcomp_id, "render"); match state.component.view(&state.context) { Ok(root) => { @@ -270,7 +269,7 @@ impl Runnable for RenderedRunner { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { #[cfg(debug_assertions)] - crate::virtual_dom::vcomp::log_event(state.vcomp_id, "rendered"); + crate::dom_bundle::log_event(state.vcomp_id, "rendered"); let first_render = !state.has_rendered; state.component.rendered(&state.context, first_render); @@ -402,7 +401,11 @@ mod tests { fn test_lifecycle(props: Props, expected: &[&str]) { let document = gloo_utils::document(); - let scope = Scope::::new(None); + let scope = Scope::::new( + None, + #[cfg(debug_assertions)] + 0, + ); let el = document.create_element("div").unwrap(); let lifecycle = props.lifecycle.clone(); diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 410f9c34530..f0fb565a8a7 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -7,11 +7,11 @@ use super::{ }, BaseComponent, }; -use crate::callback::Callback; use crate::context::{ContextHandle, ContextProvider}; +use crate::dom_bundle::insert_node; use crate::html::NodeRef; use crate::scheduler::{self, Shared}; -use crate::virtual_dom::{insert_node, VNode}; +use crate::{callback::Callback, dom_bundle::BNode}; use gloo_utils::document; use std::any::{Any, TypeId}; use std::cell::{Ref, RefCell}; @@ -117,7 +117,7 @@ impl AnyScope { pub(crate) trait Scoped { fn to_any(&self) -> AnyScope; - fn root_vnode(&self) -> Option>; + fn root_bnode(&self) -> Option>; fn destroy(&mut self); fn shift_node(&self, parent: Element, next_sibling: NodeRef); } @@ -127,7 +127,7 @@ impl Scoped for Scope { self.clone().into() } - fn root_vnode(&self) -> Option> { + fn root_bnode(&self) -> Option> { let state_ref = self.state.borrow(); // check that component hasn't been destroyed @@ -199,19 +199,16 @@ impl Scope { }) } - pub(crate) fn new(parent: Option) -> Self { + pub(crate) fn new(parent: Option, #[cfg(debug_assertions)] id: u64) -> Self { let parent = parent.map(Rc::new); let state = Rc::new(RefCell::new(None)); - #[cfg(debug_assertions)] - let vcomp_id = parent.as_ref().map(|p| p.vcomp_id).unwrap_or_default(); - Scope { state, parent, #[cfg(debug_assertions)] - vcomp_id, + vcomp_id: id, } } @@ -224,12 +221,12 @@ impl Scope { props: Rc, ) { #[cfg(debug_assertions)] - crate::virtual_dom::vcomp::log_event(self.vcomp_id, "create placeholder"); + crate::dom_bundle::log_event(self.vcomp_id, "create placeholder"); let placeholder = { let placeholder: Node = document().create_text_node("").into(); insert_node(&placeholder, &parent, next_sibling.get().as_ref()); node_ref.set(Some(placeholder.clone())); - VNode::VRef(placeholder) + BNode::BRef(placeholder) }; scheduler::push_component_create( @@ -259,7 +256,7 @@ impl Scope { next_sibling: NodeRef, ) { #[cfg(debug_assertions)] - crate::virtual_dom::vcomp::log_event(self.vcomp_id, "reuse"); + crate::dom_bundle::log_event(self.vcomp_id, "reuse"); self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling)); } diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index 467d9be7381..53ba7b0d410 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -273,7 +273,7 @@ pub mod virtual_dom; pub mod events { pub use crate::html::TargetCast; - pub use crate::virtual_dom::listeners::set_event_bubbling; + pub use crate::dom_bundle::set_event_bubbling; #[doc(no_inline)] pub use web_sys::{ diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/tests/layout_tests.rs index e2ec170eeb3..9511745bbef 100644 --- a/packages/yew/src/tests/layout_tests.rs +++ b/packages/yew/src/tests/layout_tests.rs @@ -1,5 +1,5 @@ -use crate::dom_bundle::VDiff; -use crate::html::{AnyScope, Scope}; +use crate::dom_bundle::{BNode, VDiff}; +use crate::html::AnyScope; use crate::virtual_dom::{VNode, VText}; use crate::{Component, Context, Html}; use gloo::console::log; @@ -37,7 +37,7 @@ pub struct TestLayout<'a> { pub fn diff_layouts(layouts: Vec>) { let document = gloo_utils::document(); - let parent_scope: AnyScope = Scope::::new(None).into(); + let parent_scope: AnyScope = AnyScope::test(); let parent_element = document.create_element("div").unwrap(); let parent_node: Node = parent_element.clone().into(); let end_node = document.create_text_node("END"); @@ -93,7 +93,7 @@ pub fn diff_layouts(layouts: Vec>) { } // Sequentially apply each layout - let mut ancestor: Option = None; + let mut ancestor: Option = None; for layout in layouts.iter() { let next_vnode = layout.node.clone(); diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs index dce8db00602..ed27ba86903 100644 --- a/packages/yew/src/virtual_dom/listeners.rs +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -1,49 +1,4 @@ -use std::{ - cell::RefCell, - collections::{HashMap, HashSet}, - ops::Deref, - rc::Rc, - sync::atomic::{AtomicBool, Ordering}, -}; -use wasm_bindgen::{prelude::*, JsCast}; -use web_sys::{Element, Event}; - -/// Log an operation during tests for debugging purposes -/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. -macro_rules! test_log { - ($fmt:literal, $($arg:expr),* $(,)?) => { - #[cfg(all(test, feature = "wasm_test", verbose_tests))] - ::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*) - }; -} - -thread_local! { - /// Global event listener registry - static REGISTRY: RefCell = Default::default(); - - /// Key used to store listener id on element - static LISTENER_ID_PROP: wasm_bindgen::JsValue = "__yew_listener_id".into(); - - /// Cached reference to the document body - static BODY: web_sys::HtmlElement = gloo_utils::document().body().unwrap(); -} - -/// Bubble events during delegation -static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); - -/// Set, if events should bubble up the DOM tree, calling any matching callbacks. -/// -/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event -/// handling performance. -/// -/// Note that yew uses event delegation and implements internal even bubbling for performance -/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event -/// handler has no effect. -/// -/// This function should be called before any component is mounted. -pub fn set_event_bubbling(bubble: bool) { - BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); -} +use std::rc::Rc; /// The [Listener] trait is an universal implementation of an event listener /// which is used to bind Rust-listener to JS-listener (DOM). @@ -198,106 +153,16 @@ pub enum Listeners { /// Distinct from `Pending` with an empty slice to avoid an allocation. None, - /// Added to global registry by ID - Registered(u32), - /// Not yet added to the element or registry Pending(Box<[Option>]>), } -impl Listeners { - /// Register listeners and return their handle ID - fn register(el: &Element, pending: &[Option>]) -> Self { - Self::Registered(Registry::with(|reg| { - let id = reg.set_listener_id(el); - reg.register(id, pending); - id - })) - } - - /// Remove any registered event listeners from the global registry - pub(super) fn unregister(&self) { - if let Self::Registered(id) = self { - Registry::with(|r| r.unregister(id)); - } - } -} - -impl super::Apply for Listeners { - type Element = Element; - - fn apply(&mut self, el: &Self::Element) { - if let Self::Pending(pending) = self { - *self = Self::register(el, pending); - } - } - - fn apply_diff(self, el: &Self::Element, bundle: &mut Self) { - use Listeners::*; - - match (self, bundle) { - (Pending(pending), Registered(ref id)) => { - // Reuse the ID - test_log!("reusing listeners for {}", id); - Registry::with(|reg| reg.patch(id, &*pending)); - } - (Pending(pending), bundle @ None) => { - *bundle = Self::register(el, &pending); - test_log!( - "registering listeners for {}", - match bundle { - Self::Registered(id) => id, - _ => unreachable!(), - } - ); - } - (None, bundle @ Registered(_)) => { - let id = match bundle { - Self::Registered(ref id) => id, - _ => unreachable!(), - }; - test_log!("unregistering listeners for {}", id); - Registry::with(|reg| reg.unregister(id)); - *bundle = None; - } - (None, None) => { - test_log!("{}", &"unchanged empty listeners"); - } - (self_, bundle) => unreachable!("{:?} -> {:?}", bundle, &self_), - }; - } -} - impl PartialEq for Listeners { fn eq(&self, rhs: &Self) -> bool { use Listeners::*; match (self, rhs) { (None, None) => true, - (Registered(lhs), Registered(rhs)) => lhs == rhs, - (Registered(registered_id), Pending(pending)) - | (Pending(pending), Registered(registered_id)) => { - use std::option::Option::None; - - Registry::with(|reg| match reg.by_id.get(registered_id) { - Some(reg) => { - if reg.len() != pending.len() { - return false; - } - - pending.iter().filter_map(|l| l.as_ref()).all(|l| { - match reg.get(&EventDescriptor::from(l.deref())) { - Some(reg) => reg.iter().any(|reg| { - #[allow(clippy::vtable_address_comparisons)] - Rc::ptr_eq(reg, l) - }), - None => false, - } - }) - } - None => false, - }) - } (Pending(lhs), Pending(rhs)) => { if lhs.len() != rhs.len() { false @@ -317,7 +182,7 @@ impl PartialEq for Listeners { }) } } - _ => false, + (None, Pending(pending)) | (Pending(pending), None) => pending.len() == 0, } } } @@ -325,7 +190,7 @@ impl PartialEq for Listeners { impl Clone for Listeners { fn clone(&self) -> Self { match self { - Self::None | Self::Registered(_) => Self::None, + Self::None => Self::None, Self::Pending(v) => Self::Pending(v.clone()), } } @@ -336,585 +201,3 @@ impl Default for Listeners { Self::None } } - -#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] -struct EventDescriptor { - kind: ListenerKind, - passive: bool, -} - -impl From<&dyn Listener> for EventDescriptor { - fn from(l: &dyn Listener) -> Self { - Self { - kind: l.kind(), - passive: l.passive(), - } - } -} - -/// Ensures global event handler registration. -// -// Separate struct to DRY, while avoiding partial struct mutability. -#[derive(Default, Debug)] -struct GlobalHandlers { - /// Events with registered handlers that are possibly passive - handling: HashSet, - - /// Keep track of all listeners to drop them on registry drop. - /// The registry is never dropped in production. - #[cfg(test)] - #[allow(clippy::type_complexity)] - registered: Vec<(ListenerKind, Closure)>, -} - -impl GlobalHandlers { - /// Ensure a descriptor has a global event handler assigned - fn ensure_handled(&mut self, desc: EventDescriptor) { - if !self.handling.contains(&desc) { - let cl = BODY.with(|body| { - let cl = Closure::wrap( - Box::new(move |e: Event| Registry::handle(desc, e)) as Box - ); - AsRef::::as_ref(body) - .add_event_listener_with_callback_and_add_event_listener_options( - &desc.kind.as_ref()[2..], - cl.as_ref().unchecked_ref(), - &{ - let mut opts = web_sys::AddEventListenerOptions::new(); - opts.capture(true); - // We need to explicitly set passive to override any browser defaults - opts.passive(desc.passive); - opts - }, - ) - .map_err(|e| format!("could not register global listener: {:?}", e)) - .unwrap(); - cl - }); - - // Never drop the closure as this event handler is static - #[cfg(not(test))] - cl.forget(); - #[cfg(test)] - self.registered.push((desc.kind, cl)); - - self.handling.insert(desc); - } - } -} - -// Enable resetting between tests -#[cfg(test)] -impl Drop for GlobalHandlers { - fn drop(&mut self) { - BODY.with(|body| { - for (kind, cl) in std::mem::take(&mut self.registered) { - AsRef::::as_ref(body) - .remove_event_listener_with_callback( - &kind.as_ref()[2..], - cl.as_ref().unchecked_ref(), - ) - .unwrap(); - } - }); - } -} - -/// Global multiplexing event handler registry -#[derive(Default, Debug)] -struct Registry { - /// Counter for assigning new IDs - id_counter: u32, - - /// Registered global event handlers - global: GlobalHandlers, - - /// Contains all registered event listeners by listener ID - by_id: HashMap>>>, -} - -impl Registry { - /// Run f with access to global Registry - #[inline] - fn with(f: impl FnOnce(&mut Registry) -> R) -> R { - REGISTRY.with(|r| f(&mut *r.borrow_mut())) - } - - /// Register all passed listeners under ID - fn register(&mut self, id: u32, listeners: &[Option>]) { - let mut by_desc = - HashMap::>>::with_capacity(listeners.len()); - for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() { - let desc = EventDescriptor::from(l.deref()); - self.global.ensure_handled(desc); - by_desc.entry(desc).or_default().push(l); - } - self.by_id.insert(id, by_desc); - } - - /// Patch an already registered set of handlers - fn patch(&mut self, id: &u32, listeners: &[Option>]) { - if let Some(by_desc) = self.by_id.get_mut(id) { - // Keeping empty vectors is fine. Those don't do much and should happen rarely. - for v in by_desc.values_mut() { - v.clear() - } - - for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() { - let desc = EventDescriptor::from(l.deref()); - self.global.ensure_handled(desc); - by_desc.entry(desc).or_default().push(l); - } - } - } - - /// Unregister any existing listeners for ID - fn unregister(&mut self, id: &u32) { - self.by_id.remove(id); - } - - /// Set unique listener ID onto element and return it - fn set_listener_id(&mut self, el: &Element) -> u32 { - let id = self.id_counter; - self.id_counter += 1; - - LISTENER_ID_PROP.with(|prop| { - if !js_sys::Reflect::set(el, prop, &js_sys::Number::from(id)).unwrap() { - panic!("failed to set listener ID property"); - } - }); - - id - } - - /// Handle a global event firing - fn handle(desc: EventDescriptor, event: Event) { - let target = match event - .target() - .map(|el| el.dyn_into::().ok()) - .flatten() - { - Some(el) => el, - None => return, - }; - - Self::run_handlers(desc, event, target); - } - - fn run_handlers(desc: EventDescriptor, event: Event, target: web_sys::Element) { - let run_handler = |el: &web_sys::Element| { - if let Some(l) = LISTENER_ID_PROP - .with(|prop| js_sys::Reflect::get(el, prop).ok()) - .map(|v| v.dyn_into().ok()) - .flatten() - .map(|num: js_sys::Number| { - Registry::with(|r| { - r.by_id - .get(&(num.value_of() as u32)) - .map(|s| s.get(&desc)) - .flatten() - .cloned() - }) - }) - .flatten() - { - for l in l { - l.handle(event.clone()); - } - } - }; - - run_handler(&target); - - if BUBBLE_EVENTS.load(Ordering::Relaxed) { - let mut el = target; - while !event.cancel_bubble() { - el = match el.parent_element() { - Some(el) => el, - None => break, - }; - run_handler(&el); - } - } - } -} - -#[cfg(all(test, feature = "wasm_test"))] -mod tests { - use std::marker::PhantomData; - - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - use web_sys::{Event, EventInit, MouseEvent}; - wasm_bindgen_test_configure!(run_in_browser); - - use crate::{html, html::TargetCast, AppHandle, Component, Context, Html}; - use gloo_utils::document; - use wasm_bindgen::JsCast; - - #[derive(Clone)] - enum Message { - Action, - StopListening, - SetText(String), - } - - #[derive(Default)] - struct State { - stop_listening: bool, - action: u32, - text: String, - } - - trait Mixin { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component, - { - if state.stop_listening { - html! { - {state.action} - } - } else { - html! { - - {state.action} - - } - } - } - } - - struct Comp - where - M: Mixin + 'static, - { - state: State, - pd: PhantomData, - } - - impl Component for Comp - where - M: Mixin + 'static, - { - type Message = Message; - type Properties = (); - - fn create(_: &Context) -> Self { - Comp { - state: Default::default(), - pd: PhantomData, - } - } - - fn update(&mut self, _: &Context, msg: Self::Message) -> bool { - match msg { - Message::Action => { - self.state.action += 1; - } - Message::StopListening => { - self.state.stop_listening = true; - } - Message::SetText(s) => { - self.state.text = s; - } - }; - true - } - - fn view(&self, ctx: &Context) -> crate::Html { - M::view(ctx, &self.state) - } - } - - fn assert_count(el: &web_sys::HtmlElement, count: isize) { - assert_eq!(el.text_content(), Some(count.to_string())) - } - - fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement { - document() - .query_selector(tag) - .unwrap() - .unwrap() - .dyn_into::() - .unwrap() - } - - fn init(tag: &str) -> (AppHandle>, web_sys::HtmlElement) - where - M: Mixin, - { - // Remove any existing listeners and elements - super::Registry::with(|r| *r = Default::default()); - if let Some(el) = document().query_selector(tag).unwrap() { - el.parent_element().unwrap().remove(); - } - - let root = document().create_element("div").unwrap(); - document().body().unwrap().append_child(&root).unwrap(); - let app = crate::start_app_in_element::>(root); - - (app, get_el_by_tag(tag)) - } - - #[test] - fn synchronous() { - struct Synchronous; - - impl Mixin for Synchronous {} - - let (link, el) = init::("a"); - - assert_count(&el, 0); - - el.click(); - assert_count(&el, 1); - - el.click(); - assert_count(&el, 2); - - link.send_message(Message::StopListening); - el.click(); - assert_count(&el, 2); - } - - #[test] - async fn non_bubbling_event() { - struct NonBubbling; - - impl Mixin for NonBubbling { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component, - { - let onblur = ctx.link().callback(|_| Message::Action); - html! { - - } - } - } - - let (_, el) = init::("a"); - - assert_count(&el, 0); - - let input = document().get_element_by_id("input").unwrap(); - - input - .dispatch_event( - &Event::new_with_event_init_dict("blur", &{ - let mut dict = EventInit::new(); - dict.bubbles(false); - dict - }) - .unwrap(), - ) - .unwrap(); - - assert_count(&el, 1); - } - - #[test] - fn bubbling() { - struct Bubbling; - - impl Mixin for Bubbling { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component, - { - if state.stop_listening { - html! { - - } - } else { - let cb = ctx.link().callback(|_| Message::Action); - html! { - - } - } - } - } - - let (link, el) = init::("a"); - - assert_count(&el, 0); - - el.click(); - assert_count(&el, 2); - - el.click(); - assert_count(&el, 4); - - link.send_message(Message::StopListening); - el.click(); - assert_count(&el, 4); - } - - #[test] - fn cancel_bubbling() { - struct CancelBubbling; - - impl Mixin for CancelBubbling { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component, - { - html! { - - } - } - } - - let (_, el) = init::("a"); - - assert_count(&el, 0); - - el.click(); - assert_count(&el, 1); - - el.click(); - assert_count(&el, 2); - } - - #[test] - fn cancel_bubbling_nested() { - // Here an event is being delivered to a DOM node which does - // _not_ have a listener but which is contained within an - // element that does and which cancels the bubble. - struct CancelBubbling; - - impl Mixin for CancelBubbling { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component, - { - html! { - - } - } - } - - let (_, el) = init::("a"); - - assert_count(&el, 0); - - el.click(); - assert_count(&el, 1); - - el.click(); - assert_count(&el, 2); - } - - fn test_input_listener(make_event: impl Fn() -> E) - where - E: JsCast + std::fmt::Debug, - { - struct Input; - - impl Mixin for Input { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component, - { - if state.stop_listening { - html! { -
    - -

    {state.text.clone()}

    -
    - } - } else { - html! { -
    - -

    {state.text.clone()}

    -
    - } - } - } - } - - let (link, input_el) = init::("input"); - let input_el = input_el.dyn_into::().unwrap(); - let p_el = get_el_by_tag("p"); - - assert_eq!(&p_el.text_content().unwrap(), ""); - for mut s in ["foo", "bar", "baz"].iter() { - input_el.set_value(s); - if s == &"baz" { - link.send_message(Message::StopListening); - s = &"bar"; - } - input_el - .dyn_ref::() - .unwrap() - .dispatch_event(&make_event().dyn_into().unwrap()) - .unwrap(); - assert_eq!(&p_el.text_content().unwrap(), s); - } - } - - #[test] - fn oninput() { - test_input_listener(|| { - web_sys::InputEvent::new_with_event_init_dict( - "input", - web_sys::InputEventInit::new().bubbles(true), - ) - .unwrap() - }) - } - - #[test] - fn onchange() { - test_input_listener(|| { - web_sys::Event::new_with_event_init_dict( - "change", - web_sys::EventInit::new().bubbles(true), - ) - .unwrap() - }) - } -} diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index e7bf896dc7c..77cefd1756f 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -43,8 +43,7 @@ use std::borrow::Cow; use std::fmt::Formatter; use std::ops::Deref; use std::rc::Rc; -use std::{collections::HashMap, fmt, hint::unreachable_unchecked, iter}; -use web_sys::{Element, Node}; +use std::{fmt, hint::unreachable_unchecked}; /// Attribute value #[derive(Debug)] @@ -197,18 +196,6 @@ mod tests_attr_value { } } -/// Applies contained changes to DOM [Element] -trait Apply { - /// [Element] type to apply the changes to - type Element; - - /// Apply contained values to [Element] with no ancestor - fn apply(&mut self, el: &Self::Element); - - /// Apply diff between [self] and `ancestor` to [Element]. - fn apply_diff(self, el: &Self::Element, ancestor: &mut Self); -} - /// A collection of attributes for an element #[derive(PartialEq, Eq, Clone, Debug)] pub enum Attributes { @@ -287,195 +274,6 @@ impl Attributes { } } } - - #[cold] - fn apply_diff_index_maps<'a, A, B>( - el: &Element, - // this makes it possible to diff `&'a IndexMap<_, A>` and `IndexMap<_, &'a A>`. - mut new_iter: impl Iterator, - new: &IndexMap<&'static str, A>, - old: &IndexMap<&'static str, B>, - ) where - A: AsRef, - B: AsRef, - { - let mut old_iter = old.iter(); - loop { - match (new_iter.next(), old_iter.next()) { - (Some((new_key, new_value)), Some((old_key, old_value))) => { - if new_key != *old_key { - break; - } - if new_value != old_value.as_ref() { - Self::set_attribute(el, new_key, new_value); - } - } - // new attributes - (Some(attr), None) => { - for (key, value) in iter::once(attr).chain(new_iter) { - match old.get(key) { - Some(old_value) => { - if value != old_value.as_ref() { - Self::set_attribute(el, key, value); - } - } - None => { - Self::set_attribute(el, key, value); - } - } - } - break; - } - // removed attributes - (None, Some(attr)) => { - for (key, _) in iter::once(attr).chain(old_iter) { - if !new.contains_key(key) { - Self::remove_attribute(el, key); - } - } - break; - } - (None, None) => break, - } - } - } - - /// Convert [Attributes] pair to [HashMap]s and patch changes to `el`. - /// Works with any [Attributes] variants. - #[cold] - fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) { - fn collect<'a>(src: &'a Attributes) -> HashMap<&'static str, &'a str> { - use Attributes::*; - - match src { - Static(arr) => (*arr).iter().map(|[k, v]| (*k, *v)).collect(), - Dynamic { keys, values } => keys - .iter() - .zip(values.iter()) - .filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref()))) - .collect(), - IndexMap(m) => m.iter().map(|(k, v)| (*k, v.as_ref())).collect(), - } - } - - let new = collect(new); - let old = collect(old); - - // Update existing or set new - for (k, new) in new.iter() { - if match old.get(k) { - Some(old) => old != new, - None => true, - } { - el.set_attribute(k, new).unwrap(); - } - } - - // Remove missing - for k in old.keys() { - if !new.contains_key(k) { - Self::remove_attribute(el, k); - } - } - } - - fn set_attribute(el: &Element, key: &str, value: &str) { - el.set_attribute(key, value).expect("invalid attribute key") - } - - fn remove_attribute(el: &Element, key: &str) { - el.remove_attribute(key) - .expect("could not remove attribute") - } -} - -impl Apply for Attributes { - type Element = Element; - - fn apply(&mut self, el: &Element) { - match self { - Self::Static(arr) => { - for kv in arr.iter() { - Self::set_attribute(el, kv[0], kv[1]); - } - } - Self::Dynamic { keys, values } => { - for (k, v) in keys.iter().zip(values.iter()) { - if let Some(v) = v { - Self::set_attribute(el, k, v) - } - } - } - Self::IndexMap(m) => { - for (k, v) in m.iter() { - Self::set_attribute(el, k, v) - } - } - } - } - - fn apply_diff(self, el: &Element, bundle: &mut Self) { - #[inline] - fn ptr_eq(a: &[T], b: &[T]) -> bool { - a.as_ptr() == b.as_ptr() - } - - let ancestor = std::mem::replace(bundle, self); - match (bundle, ancestor) { - // Hot path - (Self::Static(new), Self::Static(old)) if ptr_eq(new, old) => (), - // Hot path - ( - Self::Dynamic { - keys: new_k, - values: new_v, - }, - Self::Dynamic { - keys: old_k, - values: old_v, - }, - ) if ptr_eq(new_k, old_k) => { - // Double zipping does not optimize well, so use asserts and unsafe instead - assert!(new_k.len() == new_v.len()); - assert!(new_k.len() == old_v.len()); - for i in 0..new_k.len() { - macro_rules! key { - () => { - unsafe { new_k.get_unchecked(i) } - }; - } - macro_rules! set { - ($new:expr) => { - Self::set_attribute(el, key!(), $new) - }; - } - - match unsafe { (new_v.get_unchecked(i), old_v.get_unchecked(i)) } { - (Some(new), Some(old)) => { - if new != old { - set!(new); - } - } - (Some(new), None) => set!(new), - (None, Some(_)) => { - Self::remove_attribute(el, key!()); - } - (None, None) => (), - } - } - } - // For VTag's constructed outside the html! macro - (Self::IndexMap(new), Self::IndexMap(old)) => { - let new_iter = new.iter().map(|(k, v)| (*k, v.as_ref())); - Self::apply_diff_index_maps(el, new_iter, new, &old); - } - // Cold path. Happens only with conditional swapping and reordering of `VTag`s with the - // same tag and no keys. - (new, ancestor) => { - Self::apply_diff_as_maps(el, new, &ancestor); - } - } - } } impl From> for Attributes { @@ -490,15 +288,6 @@ impl Default for Attributes { } } -pub(crate) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) { - match next_sibling { - Some(next_sibling) => parent - .insert_before(node, Some(next_sibling)) - .expect("failed to insert tag before next sibling"), - None => parent.append_child(node).expect("failed to append child"), - }; -} - #[cfg(all(test, feature = "wasm_bench"))] mod benchmarks { use super::*; diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index a94fe5d736d..c1fd80453e4 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -1,74 +1,38 @@ //! This module contains the implementation of a virtual component (`VComp`). -use super::{Key, VNode}; -use crate::dom_bundle::{DomBundle, VDiff}; -use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped}; +use super::Key; +use crate::dom_bundle::{Mountable, PropsWrapper}; +use crate::html::{BaseComponent, NodeRef}; use std::any::TypeId; -use std::borrow::Borrow; use std::fmt; -use std::ops::Deref; use std::rc::Rc; -use web_sys::Element; - -thread_local! { - #[cfg(debug_assertions)] - static EVENT_HISTORY: std::cell::RefCell>> - = Default::default(); -} - -/// Push [VComp] event to lifecycle debugging registry -#[cfg(debug_assertions)] -pub(crate) fn log_event(vcomp_id: u64, event: impl ToString) { - EVENT_HISTORY.with(|h| { - h.borrow_mut() - .entry(vcomp_id) - .or_default() - .push(event.to_string()) - }); -} - -/// Get [VComp] event log from lifecycle debugging registry -#[cfg(debug_assertions)] -pub(crate) fn get_event_log(vcomp_id: u64) -> Vec { - EVENT_HISTORY.with(|h| { - h.borrow() - .get(&vcomp_id) - .map(|l| (*l).clone()) - .unwrap_or_default() - }) -} /// A virtual component. pub struct VComp { - type_id: TypeId, - scope: Option>, - props: Option>, + pub(crate) type_id: TypeId, + pub(crate) props: Box, pub(crate) node_ref: NodeRef, pub(crate) key: Option, +} - /// Used for debug logging - #[cfg(debug_assertions)] - pub(crate) id: u64, +impl fmt::Debug for VComp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("VComp") + .field("type_id", &self.type_id) + .field("node_ref", &self.node_ref) + .field("props", &"..") + .field("key", &self.key) + .finish() + } } impl Clone for VComp { fn clone(&self) -> Self { - if self.scope.is_some() { - panic!("Mounted components are not allowed to be cloned!"); - } - - #[cfg(debug_assertions)] - log_event(self.id, "clone"); - Self { type_id: self.type_id, - scope: None, - props: self.props.as_ref().map(|m| m.copy()), + props: self.props.copy(), node_ref: self.node_ref.clone(), key: self.key.clone(), - - #[cfg(debug_assertions)] - id: self.id, } } } @@ -133,150 +97,10 @@ impl VComp { VComp { type_id: TypeId::of::(), node_ref, - props: Some(Box::new(PropsWrapper::::new(props))), - scope: None, + props: Box::new(PropsWrapper::::new(props)), key, - - #[cfg(debug_assertions)] - id: { - thread_local! { - static ID_COUNTER: std::cell::RefCell = Default::default(); - } - - ID_COUNTER.with(|c| { - let c = &mut *c.borrow_mut(); - *c += 1; - *c - }) - }, } } - - pub(crate) fn root_vnode(&self) -> Option + '_> { - self.scope.as_ref().and_then(|scope| scope.root_vnode()) - } - - /// Take ownership of [Box] or panic with error message, if component is not mounted - #[inline] - fn take_scope(&mut self) -> Box { - self.scope.take().unwrap_or_else(|| { - #[cfg(not(debug_assertions))] - panic!("no scope; VComp should be mounted"); - - #[cfg(debug_assertions)] - panic!( - "no scope; VComp should be mounted after: {:?}", - get_event_log(self.id) - ); - }) - } -} - -trait Mountable { - fn copy(&self) -> Box; - fn mount( - self: Box, - node_ref: NodeRef, - parent_scope: &AnyScope, - parent: Element, - next_sibling: NodeRef, - ) -> Box; - fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); -} - -struct PropsWrapper { - props: Rc, -} - -impl PropsWrapper { - pub fn new(props: Rc) -> Self { - Self { props } - } -} - -impl Mountable for PropsWrapper { - fn copy(&self) -> Box { - let wrapper: PropsWrapper = PropsWrapper { - props: Rc::clone(&self.props), - }; - Box::new(wrapper) - } - - fn mount( - self: Box, - node_ref: NodeRef, - parent_scope: &AnyScope, - parent: Element, - next_sibling: NodeRef, - ) -> Box { - let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.mount_in_place(parent, next_sibling, node_ref, self.props); - - Box::new(scope) - } - - fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { - let scope: Scope = scope.to_any().downcast(); - scope.reuse(self.props, node_ref, next_sibling); - } -} - -impl DomBundle for VComp { - fn detach(mut self, _parent: &Element) { - self.take_scope().destroy(); - } - - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - let scope = self.scope.as_ref().unwrap(); - scope.shift_node(next_parent.clone(), next_sibling); - } -} - -impl VDiff for VComp { - type Bundle = VComp; - - fn attach( - mut self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { - let mountable = self.props.take().expect("VComp has already been mounted"); - - self.scope = Some(mountable.mount( - self.node_ref.clone(), - parent_scope, - parent.to_owned(), - next_sibling, - )); - - (self.node_ref.clone(), self) - } - - fn apply( - mut self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ancestor: &mut VNode, - ) -> NodeRef { - if let VNode::VComp(ref mut vcomp) = ancestor { - // If the ancestor is the same type, reuse it and update its properties - if self.type_id == vcomp.type_id && self.key == vcomp.key { - let mountable = self.props.take().expect("VComp has already been mounted"); - let mut ancestor = std::mem::replace(vcomp, self); - - vcomp.node_ref.reuse(ancestor.node_ref.clone()); - let scope = ancestor.take_scope(); - mountable.reuse(vcomp.node_ref.clone(), scope.borrow(), next_sibling); - vcomp.scope = Some(scope); - return vcomp.node_ref.clone(); - } - } - let (node_ref, self_) = self.attach(parent_scope, parent, next_sibling); - ancestor.replace(parent, self_.into()); - node_ref - } } impl PartialEq for VComp { @@ -285,598 +109,8 @@ impl PartialEq for VComp { } } -impl fmt::Debug for VComp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "VComp {{ root: {:?} }}", self.root_vnode().as_deref()) - } -} - impl fmt::Debug for VChild { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("VChild<_>") } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{html, Children, Component, Context, Html, NodeRef, Properties}; - use gloo_utils::document; - use web_sys::Node; - - #[cfg(feature = "wasm_test")] - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - #[cfg(feature = "wasm_test")] - wasm_bindgen_test_configure!(run_in_browser); - - struct Comp; - - #[derive(Clone, PartialEq, Properties)] - struct Props { - #[prop_or_default] - field_1: u32, - #[prop_or_default] - field_2: u32, - } - - impl Component for Comp { - type Message = (); - type Properties = Props; - - fn create(_: &Context) -> Self { - Comp - } - - fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { - unimplemented!(); - } - - fn view(&self, _ctx: &Context) -> Html { - html! {
    } - } - } - - #[test] - fn update_loop() { - let document = gloo_utils::document(); - let parent_scope: AnyScope = crate::html::Scope::::new(None).into(); - let parent_element = document.create_element("div").unwrap(); - - let ancestor = html! { }; - let (_, mut comp) = ancestor.attach(&parent_scope, &parent_element, NodeRef::default()); - - for _ in 0..10000 { - let node = html! { }; - node.apply( - &parent_scope, - &parent_element, - NodeRef::default(), - &mut comp, - ); - } - } - - #[test] - fn set_properties_to_component() { - html! { - - }; - - html! { - - }; - - html! { - - }; - - html! { - - }; - - let props = Props { - field_1: 1, - field_2: 1, - }; - - html! { - - }; - } - - #[test] - fn set_component_key() { - let test_key: Key = "test".to_string().into(); - let check_key = |vnode: VNode| { - assert_eq!(vnode.key(), Some(&test_key)); - }; - - let props = Props { - field_1: 1, - field_2: 1, - }; - let props_2 = props.clone(); - - check_key(html! { }); - check_key(html! { }); - check_key(html! { }); - check_key(html! { }); - check_key(html! { }); - } - - #[test] - fn set_component_node_ref() { - let test_node: Node = document().create_text_node("test").into(); - let test_node_ref = NodeRef::new(test_node); - let check_node_ref = |vnode: VNode| { - assert_eq!(vnode.unchecked_first_node(), test_node_ref.get().unwrap()); - }; - - let props = Props { - field_1: 1, - field_2: 1, - }; - let props_2 = props.clone(); - - check_node_ref(html! { }); - check_node_ref(html! { }); - check_node_ref(html! { }); - check_node_ref(html! { }); - check_node_ref(html! { }); - } - - #[test] - fn vchild_partialeq() { - let vchild1: VChild = VChild::new( - Props { - field_1: 1, - field_2: 1, - }, - NodeRef::default(), - None, - ); - - let vchild2: VChild = VChild::new( - Props { - field_1: 1, - field_2: 1, - }, - NodeRef::default(), - None, - ); - - let vchild3: VChild = VChild::new( - Props { - field_1: 2, - field_2: 2, - }, - NodeRef::default(), - None, - ); - - assert_eq!(vchild1, vchild2); - assert_ne!(vchild1, vchild3); - assert_ne!(vchild2, vchild3); - } - - #[derive(Clone, Properties, PartialEq)] - pub struct ListProps { - pub children: Children, - } - pub struct List; - impl Component for List { - type Message = (); - type Properties = ListProps; - - fn create(_: &Context) -> Self { - Self - } - fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { - unimplemented!(); - } - fn changed(&mut self, _ctx: &Context) -> bool { - unimplemented!(); - } - fn view(&self, ctx: &Context) -> Html { - let item_iter = ctx - .props() - .children - .iter() - .map(|item| html! {
  • { item }
  • }); - html! { -
      { for item_iter }
    - } - } - } - - use super::{AnyScope, Element}; - - fn setup_parent() -> (AnyScope, Element) { - let scope = AnyScope::test(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - (scope, parent) - } - - fn get_html(node: Html, scope: &AnyScope, parent: &Element) -> String { - // clear parent - parent.set_inner_html(""); - - node.attach(scope, parent, NodeRef::default()); - parent.inner_html() - } - - #[test] - fn all_ways_of_passing_children_work() { - let (scope, parent) = setup_parent(); - - let children: Vec<_> = vec!["a", "b", "c"] - .drain(..) - .map(|text| html! {{ text }}) - .collect(); - let children_renderer = Children::new(children.clone()); - let expected_html = "\ -
      \ -
    • a
    • \ -
    • b
    • \ -
    • c
    • \ -
    "; - - let prop_method = html! { - - }; - assert_eq!(get_html(prop_method, &scope, &parent), expected_html); - - let children_renderer_method = html! { - - { children_renderer } - - }; - assert_eq!( - get_html(children_renderer_method, &scope, &parent), - expected_html - ); - - let direct_method = html! { - - { children.clone() } - - }; - assert_eq!(get_html(direct_method, &scope, &parent), expected_html); - - let for_method = html! { - - { for children } - - }; - assert_eq!(get_html(for_method, &scope, &parent), expected_html); - } - - #[test] - fn reset_node_ref() { - let scope = AnyScope::test(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - let node_ref = NodeRef::default(); - let elem: VNode = html! { }; - let (_, elem) = elem.attach(&scope, &parent, NodeRef::default()); - let parent_node = parent.deref(); - assert_eq!(node_ref.get(), parent_node.first_child()); - elem.detach(&parent); - assert!(node_ref.get().is_none()); - } -} - -#[cfg(test)] -mod layout_tests { - extern crate self as yew; - - use crate::html; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; - use crate::{Children, Component, Context, Html, Properties}; - use std::marker::PhantomData; - - #[cfg(feature = "wasm_test")] - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - #[cfg(feature = "wasm_test")] - wasm_bindgen_test_configure!(run_in_browser); - - struct Comp { - _marker: PhantomData, - } - - #[derive(Properties, Clone, PartialEq)] - struct CompProps { - #[prop_or_default] - children: Children, - } - - impl Component for Comp { - type Message = (); - type Properties = CompProps; - - fn create(_: &Context) -> Self { - Comp { - _marker: PhantomData::default(), - } - } - - fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { - unimplemented!(); - } - - fn view(&self, ctx: &Context) -> Html { - html! { - <>{ ctx.props().children.clone() } - } - } - } - - struct A; - struct B; - - #[test] - fn diff() { - let layout1 = TestLayout { - name: "1", - node: html! { - > - >> - {"C"} - > - }, - expected: "C", - }; - - let layout2 = TestLayout { - name: "2", - node: html! { - > - {"A"} - > - }, - expected: "A", - }; - - let layout3 = TestLayout { - name: "3", - node: html! { - > - >> - {"B"} - > - }, - expected: "B", - }; - - let layout4 = TestLayout { - name: "4", - node: html! { - > - >{"A"}> - {"B"} - > - }, - expected: "AB", - }; - - let layout5 = TestLayout { - name: "5", - node: html! { - > - <> - > - {"A"} - > - - {"B"} - > - }, - expected: "AB", - }; - - let layout6 = TestLayout { - name: "6", - node: html! { - > - <> - > - {"A"} - > - {"B"} - - {"C"} - > - }, - expected: "ABC", - }; - - let layout7 = TestLayout { - name: "7", - node: html! { - > - <> - > - {"A"} - > - > - {"B"} - > - - {"C"} - > - }, - expected: "ABC", - }; - - let layout8 = TestLayout { - name: "8", - node: html! { - > - <> - > - {"A"} - > - > - > - {"B"} - > - > - - {"C"} - > - }, - expected: "ABC", - }; - - let layout9 = TestLayout { - name: "9", - node: html! { - > - <> - <> - {"A"} - - > - > - {"B"} - > - > - - {"C"} - > - }, - expected: "ABC", - }; - - let layout10 = TestLayout { - name: "10", - node: html! { - > - <> - > - > - {"A"} - > - > - <> - {"B"} - - - {"C"} - > - }, - expected: "ABC", - }; - - let layout11 = TestLayout { - name: "11", - node: html! { - > - <> - <> - > - > - {"A"} - > - {"B"} - > - - - {"C"} - > - }, - expected: "ABC", - }; - - let layout12 = TestLayout { - name: "12", - node: html! { - > - <> - >> - <> - > - <> - > - {"A"} - > - <> - > - >> - <> - {"B"} - <> - >> - > - - > - <> - - >> - - {"C"} - >> - <> - > - }, - expected: "ABC", - }; - - diff_layouts(vec![ - layout1, layout2, layout3, layout4, layout5, layout6, layout7, layout8, layout9, - layout10, layout11, layout12, - ]); - } - - #[test] - fn component_with_children() { - #[derive(Properties, PartialEq)] - struct Props { - children: Children, - } - - struct ComponentWithChildren; - - impl Component for ComponentWithChildren { - type Message = (); - type Properties = Props; - - fn create(_ctx: &Context) -> Self { - Self - } - - fn view(&self, ctx: &Context) -> Html { - html! { -
      - { for ctx.props().children.iter().map(|child| html! {
    • { child }
    • }) } -
    - } - } - } - - let layout = TestLayout { - name: "13", - node: html! { - - if true { - { "hello" } - { "world" } - } else { - { "goodbye" } - { "world" } - } - - }, - expected: "
    • helloworld
    ", - }; - - diff_layouts(vec![layout]); - } -} diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index 30f74a21c75..87d2e9eb41b 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -1,21 +1,15 @@ //! This module contains fragments implementation. -use super::{Key, VNode, VText}; -use crate::dom_bundle::{DomBundle, VDiff}; -use crate::html::{AnyScope, NodeRef}; -use std::borrow::Borrow; -use std::collections::HashSet; -use std::hash::Hash; +use super::{Key, VNode}; use std::ops::{Deref, DerefMut}; -use web_sys::Element; /// This struct represents a fragment of the Virtual DOM tree. #[derive(Clone, Debug, PartialEq)] pub struct VList { /// The list of child [VNode]s - children: Vec, + pub(crate) children: Vec, /// All [VNode]s in the VList have keys - fully_keyed: bool, + pub(crate) fully_keyed: bool, pub key: Option, } @@ -44,75 +38,6 @@ impl DerefMut for VList { } } -/// Log an operation during tests for debugging purposes -/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. -macro_rules! test_log { - ($fmt:literal, $($arg:expr),* $(,)?) => { - #[cfg(all(test, feature = "wasm_test", verbose_tests))] - ::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*); - }; -} - -struct ElementWriter<'s> { - parent_scope: &'s AnyScope, - parent: &'s Element, - next_sibling: NodeRef, -} - -impl<'s> ElementWriter<'s> { - fn add(self, node: VNode) -> (Self, VNode) { - test_log!("adding: {:?}", node); - test_log!("parent={:?}", self.parent.outer_html()); - // Advance the next sibling reference (from right to left) and log it for testing purposes - // Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. - #[cfg(all(test, feature = "wasm_test", verbose_tests))] - let current = format!("{:?}", self.next_sibling); - let (next, bundle) = node.attach(self.parent_scope, self.parent, self.next_sibling); - test_log!("advance next_sibling: {:?} -> {:?}", current, next); - ( - Self { - next_sibling: next, - ..self - }, - bundle, - ) - } - - fn patch(self, node: VNode, ancestor: &mut VNode) -> Self { - test_log!("patching: {:?} -> {:?}", ancestor, node); - test_log!("parent={:?}", self.parent.outer_html()); - // Advance the next sibling reference (from right to left) and log it for testing purposes - // Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. - #[cfg(all(test, feature = "wasm_test", verbose_tests))] - let current = format!("{:?}", self.next_sibling); - ancestor.move_before(self.parent, &self.next_sibling.get()); - let next = node.apply(self.parent_scope, self.parent, self.next_sibling, ancestor); - test_log!("advance next_sibling: {:?} -> {:?}", current, next); - Self { - next_sibling: next, - ..self - } - } -} - -struct NodeEntry(VNode); -impl Borrow for NodeEntry { - fn borrow(&self) -> &Key { - self.0.key().expect("unkeyed child in fully keyed list") - } -} -impl Hash for NodeEntry { - fn hash(&self, state: &mut H) { - >::borrow(self).hash(state) - } -} -impl PartialEq for NodeEntry { - fn eq(&self, other: &Self) -> bool { - >::borrow(self) == >::borrow(other) - } -} -impl Eq for NodeEntry {} - impl VList { /// Creates a new empty [VList] instance. pub const fn new() -> Self { @@ -157,1189 +82,4 @@ impl VList { pub fn recheck_fully_keyed(&mut self) { self.fully_keyed = self.children.iter().all(|ch| ch.has_key()); } - - /// Diff and patch unkeyed child lists - fn apply_unkeyed( - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - lefts: Vec, - rights: &mut Vec, - ) -> NodeRef { - let mut writer = ElementWriter { - parent_scope, - parent, - next_sibling, - }; - - // Remove extra nodes - if lefts.len() < rights.len() { - for r in rights.drain(lefts.len()..) { - test_log!("removing: {:?}", r); - r.detach(parent); - } - } - - let mut lefts_it = lefts.into_iter().rev(); - for (r, l) in rights.iter_mut().zip(&mut lefts_it) { - writer = writer.patch(l, r); - } - - // Add missing nodes - for l in lefts_it { - let (next_writer, el) = writer.add(l); - rights.push(el); - writer = next_writer; - } - writer.next_sibling - } - - /// Diff and patch fully keyed child lists. - /// - /// Optimized for node addition or removal from either end of the list and small changes in the - /// middle. - fn apply_keyed( - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - lefts: Vec, - rights: &mut Vec, - ) -> NodeRef { - /// Find the first differing key in 2 iterators - fn matching_len<'a, 'b>( - a: impl Iterator, - b: impl Iterator, - ) -> usize { - a.zip(b).take_while(|(a, b)| a == b).count() - } - - // Find first key mismatch from the back - let matching_len_end = matching_len( - lefts - .iter() - .map(|v| v.key().expect("unkeyed child in fully keyed list")) - .rev(), - rights - .iter() - .map(|v| v.key().expect("unkeyed child in fully keyed list")), - ); - - if matching_len_end == std::cmp::min(lefts.len(), rights.len()) { - // No key changes - return Self::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights); - } - // We partially deconstruct the new vector in several steps. - let mut lefts = lefts; - let mut writer = ElementWriter { - parent_scope, - parent, - next_sibling, - }; - // Diff matching children at the end - let lefts_to = lefts.len() - matching_len_end; - for (l, r) in lefts - .drain(lefts_to..) - .rev() - .zip(rights[..matching_len_end].iter_mut()) - { - writer = writer.patch(l, r); - } - // Find first key mismatch from the front - let matching_len_start = matching_len( - lefts - .iter() - .map(|v| v.key().expect("unkeyed child in fully keyed list")), - rights - .iter() - .map(|v| v.key().expect("unkeyed child in fully keyed list")) - .rev(), - ); - - // Diff mismatched children in the middle - let rights_to = rights.len() - matching_len_start; - let mut spliced_middle = rights.splice(matching_len_end..rights_to, std::iter::empty()); - let mut rights_diff: HashSet = - HashSet::with_capacity((matching_len_end..rights_to).len()); - for r in &mut spliced_middle { - rights_diff.insert(NodeEntry(r)); - } - let mut replacements: Vec = Vec::with_capacity((matching_len_start..lefts_to).len()); - for l in lefts - .drain(matching_len_start..) // lefts_to.. has been drained - .rev() - { - let l_key = l.key().expect("unkeyed child in fully keyed list"); - let bundle = match rights_diff.take(l_key) { - Some(NodeEntry(mut r_bundle)) => { - writer = writer.patch(l, &mut r_bundle); - r_bundle - } - None => { - let (next_writer, bundle) = writer.add(l); - writer = next_writer; - bundle - } - }; - replacements.push(bundle); - } - // now drop the splice iterator - std::mem::drop(spliced_middle); - rights.splice(matching_len_end..matching_len_end, replacements); - - // Remove any extra rights - for NodeEntry(r) in rights_diff.drain() { - test_log!("removing: {:?}", r); - r.detach(parent); - } - - // Diff matching children at the start - let rights_to = rights.len() - matching_len_start; - for (l, r) in lefts - .drain(..) // matching_len_start.. has been drained already - .rev() - .zip(rights[rights_to..].iter_mut()) - { - writer = writer.patch(l, r); - } - - writer.next_sibling - } -} - -impl DomBundle for VList { - fn detach(self, parent: &Element) { - for child in self.children.into_iter() { - child.detach(parent); - } - } - - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - for node in self.children.iter() { - node.shift(next_parent, next_sibling.clone()); - } - } -} - -impl VDiff for VList { - type Bundle = VList; - - fn attach( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { - let mut self_ = VNode::VList(VList::new()); - let node_ref = self.apply(parent_scope, parent, next_sibling, &mut self_); - let self_ = match self_ { - VNode::VList(self_) => self_, - _ => unreachable!("applying list should leave a VList in bundle ref"), - }; - (node_ref, self_) - } - - fn apply( - mut self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ancestor: &mut VNode, - ) -> NodeRef { - // Here, we will try to diff the previous list elements with the new - // ones we want to insert. For that, we will use two lists: - // - lefts: new elements to render in the DOM - // - rights: previously rendered elements. - // - // The left items are known since we want to insert them - // (self.children). For the right ones, we will look at the ancestor, - // i.e. the current DOM list element that we want to replace with self. - - if self.children.is_empty() { - // Without a placeholder the next element becomes first - // and corrupts the order of rendering - // We use empty text element to stake out a place - self.add_child(VText::new("").into()); - } - - let lefts = self.children; - let (rights, rights_fully_keyed) = match ancestor { - // If the ancestor is also a VList, then the "right" list is the previously - // rendered items. - VNode::VList(ref mut v) => { - v.key = self.key; - (&mut v.children, &mut v.fully_keyed) - } - // If the ancestor was not a VList, then the "right" list is a single node - _ => { - let v = std::mem::replace(ancestor, VNode::VList(VList::new())); - match ancestor { - VNode::VList(ref mut vlist) => { - vlist.key = v.key().cloned(); - vlist.add_child(v); - (&mut vlist.children, &mut vlist.fully_keyed) - } - _ => unreachable!(""), - } - } - }; - test_log!("lefts: {:?}", lefts); - test_log!("rights: {:?}", rights); - - if let Some(additional) = rights.len().checked_sub(lefts.len()) { - rights.reserve_exact(additional); - } - #[allow(clippy::let_and_return)] - let first = if self.fully_keyed && *rights_fully_keyed { - Self::apply_keyed(parent_scope, parent, next_sibling, lefts, rights) - } else { - Self::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights) - }; - *rights_fully_keyed = self.fully_keyed; - test_log!("result: {:?}", rights); - first - } -} - -#[cfg(test)] -mod layout_tests { - extern crate self as yew; - - use crate::html; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; - - #[cfg(feature = "wasm_test")] - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - #[cfg(feature = "wasm_test")] - wasm_bindgen_test_configure!(run_in_browser); - - #[test] - fn diff() { - let layout1 = TestLayout { - name: "1", - node: html! { - <> - {"a"} - {"b"} - <> - {"c"} - {"d"} - - {"e"} - - }, - expected: "abcde", - }; - - let layout2 = TestLayout { - name: "2", - node: html! { - <> - {"a"} - {"b"} - <> - {"e"} - {"f"} - - }, - expected: "abef", - }; - - let layout3 = TestLayout { - name: "3", - node: html! { - <> - {"a"} - <> - {"b"} - {"e"} - - }, - expected: "abe", - }; - - let layout4 = TestLayout { - name: "4", - node: html! { - <> - {"a"} - <> - {"c"} - {"d"} - - {"b"} - {"e"} - - }, - expected: "acdbe", - }; - - diff_layouts(vec![layout1, layout2, layout3, layout4]); - } -} - -#[cfg(test)] -mod layout_tests_keys { - extern crate self as yew; - - use crate::html; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; - use crate::virtual_dom::VNode; - use crate::{Children, Component, Context, Html, Properties}; - use web_sys::Node; - - #[cfg(feature = "wasm_test")] - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - #[cfg(feature = "wasm_test")] - wasm_bindgen_test_configure!(run_in_browser); - - struct Comp {} - - #[derive(Properties, Clone, PartialEq)] - struct CountingCompProps { - id: usize, - #[prop_or(false)] - can_change: bool, - } - - impl Component for Comp { - type Message = (); - type Properties = CountingCompProps; - - fn create(_: &Context) -> Self { - Comp {} - } - - fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { - unimplemented!(); - } - - fn view(&self, ctx: &Context) -> Html { - html! {

    { ctx.props().id }

    } - } - } - - #[derive(Clone, Properties, PartialEq)] - pub struct ListProps { - pub children: Children, - } - - pub struct List(); - - impl Component for List { - type Message = (); - type Properties = ListProps; - - fn create(_: &Context) -> Self { - Self() - } - - fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { - unimplemented!(); - } - - fn view(&self, ctx: &Context) -> Html { - html! { <>{ for ctx.props().children.iter() } } - } - } - - #[test] - fn diff() { - let mut layouts = vec![]; - - let vref_node: Node = gloo_utils::document().create_element("i").unwrap().into(); - layouts.push(TestLayout { - name: "All VNode types as children", - node: html! { - <> - {"a"} - - {"c"} - {"d"} - - - {"foo"} - {"bar"} - - {VNode::VRef(vref_node)} - - }, - expected: "acd

    0

    foobar", - }); - - layouts.extend(vec![ - TestLayout { - name: "Inserting into VList first child - before", - node: html! { - <> - - - -

    - - }, - expected: "

    ", - }, - TestLayout { - name: "Inserting into VList first child - after", - node: html! { - <> - - - - -

    - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "No matches - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "No matches - after", - node: html! { - <> - -

    - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Append - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Append - after", - node: html! { - <> - - -

    - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Prepend - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Prepend - after", - node: html! { - <> -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete first - before", - node: html! { - <> - - -

    - - }, - expected: "

    ", - }, - TestLayout { - name: "Delete first - after", - node: html! { - <> - -

    - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete last - before", - node: html! { - <> - - -

    - - }, - expected: "

    ", - }, - TestLayout { - name: "Delete last - after", - node: html! { - <> - - - - }, - expected: "", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete last and change node type - before", - node: html! { - <> - - -

    - - }, - expected: "

    ", - }, - TestLayout { - name: "Delete last - after", - node: html! { - <> - - - - - }, - expected: "", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete middle - before", - node: html! { - <> - - -

    - - - }, - expected: "

    ", - }, - TestLayout { - name: "Delete middle - after", - node: html! { - <> - - -

    - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete middle and change node type - before", - node: html! { - <> - - -

    - - - }, - expected: "

    ", - }, - TestLayout { - name: "Delete middle and change node type- after", - node: html! { - <> - - -

    - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse - before", - node: html! { - <> - - -

    - - - }, - expected: "

    ", - }, - TestLayout { - name: "Reverse - after", - node: html! { - <> - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse and change node type - before", - node: html! { - <> - - - - - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Reverse and change node type - after", - node: html! { - <> - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 1&2 - before", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap 1&2 - after", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 1&2 and change node type - before", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap 1&2 and change node type - after", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "test - before", - node: html! { - <> - - -

    - - - - - -

    - - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap 4&5 - after", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 4&5 - before", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap 4&5 - after", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 1&5 - before", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap 1&5 - after", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Move 2 after 4 - before", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Move 2 after 4 - after", - node: html! { - <> - -

    - - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 1,2 <-> 3,4 - before", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap 1,2 <-> 3,4 - after", - node: html! { - <> -

    - - - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap lists - before", - node: html! { - <> - - - - - - - - - - }, - expected: "", - }, - TestLayout { - name: "Swap lists - after", - node: html! { - <> - - - - - - - - - - }, - expected: "", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap lists with in-between - before", - node: html! { - <> - - - - -

    - - - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap lists with in-between - after", - node: html! { - <> - - - - -

    - - - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Insert VComp front - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Insert VComp front - after", - node: html! { - <> - - - - - }, - expected: "

    0

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Insert VComp middle - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Insert VComp middle - after", - node: html! { - <> - - - - - }, - expected: "

    0

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Insert VComp back - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Insert VComp back - after", - node: html! { - <> - - - - - }, - expected: "

    0

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse VComp children - before", - node: html! { - <> - - - - - }, - expected: "

    1

    2

    3

    ", - }, - TestLayout { - name: "Reverse VComp children - after", - node: html! { - <> - - - - - }, - expected: "

    3

    2

    1

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse VComp children with children - before", - node: html! { - <> -

    {"11"}

    {"12"}

    -

    {"21"}

    {"22"}

    -

    {"31"}

    {"32"}

    - - }, - expected: "

    11

    12

    21

    22

    31

    32

    ", - }, - TestLayout { - name: "Reverse VComp children with children - after", - node: html! { - <> -

    {"31"}

    {"32"}

    -

    {"21"}

    {"22"}

    -

    {"11"}

    {"12"}

    - - }, - expected: "

    31

    32

    21

    22

    11

    12

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Complex component update - before", - node: html! { - - - - - }, - expected: "

    1

    2

    ", - }, - TestLayout { - name: "Complex component update - after", - node: html! { - - - - - -

    {"2"}

    -
    -
    - }, - expected: "

    1

    2

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reorder VComp children with children - before", - node: html! { - <> -

    {"1"}

    -

    {"3"}

    -

    {"5"}

    -

    {"2"}

    -

    {"4"}

    -

    {"6"}

    - - }, - expected: "

    1

    3

    5

    2

    4

    6

    ", - }, - TestLayout { - name: "Reorder VComp children with children - after", - node: html! { - <> - - - - - - - - }, - expected: "

    6

    5

    4

    3

    2

    1

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Replace and reorder components - before", - node: html! { - -

    {"1"}

    -

    {"2"}

    -

    {"3"}

    -
    - }, - expected: "

    1

    2

    3

    ", - }, - TestLayout { - name: "Replace and reorder components - after", - node: html! { - - - - - - }, - expected: "

    3

    2

    1

    ", - }, - ]); - - diff_layouts(layouts); - } } diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 3fee2ed2141..5e1541a531b 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -1,14 +1,11 @@ //! This module contains the implementation of abstract virtual node. use super::{Key, VChild, VComp, VList, VPortal, VSuspense, VTag, VText}; -use crate::dom_bundle::{DomBundle, VDiff}; -use crate::html::{AnyScope, BaseComponent, NodeRef}; -use gloo::console; +use crate::html::BaseComponent; use std::cmp::PartialEq; use std::fmt; use std::iter::FromIterator; - -use web_sys::{Element, Node}; +use web_sys::Node; /// Bind virtual element to a DOM reference. #[derive(Clone)] @@ -53,164 +50,30 @@ impl VNode { VNode::VSuspense(vsuspense) => vsuspense.key.is_some(), } } - - /// Returns the first DOM node that is used to designate the position of the virtual DOM node. - pub(crate) fn unchecked_first_node(&self) -> Node { - match self { - VNode::VTag(vtag) => vtag - .reference() - .expect("VTag is not mounted") - .clone() - .into(), - VNode::VText(vtext) => { - let text_node = vtext.reference.as_ref().expect("VText is not mounted"); - text_node.clone().into() - } - VNode::VComp(vcomp) => vcomp.node_ref.get().unwrap_or_else(|| { - #[cfg(not(debug_assertions))] - panic!("no node_ref; VComp should be mounted"); - - #[cfg(debug_assertions)] - panic!( - "no node_ref; VComp should be mounted after: {:?}", - crate::virtual_dom::vcomp::get_event_log(vcomp.id), - ); - }), - VNode::VList(vlist) => vlist - .get(0) - .expect("VList is not mounted") - .unchecked_first_node(), - VNode::VRef(node) => node.clone(), - VNode::VPortal(_) => panic!("portals have no first node, they are empty inside"), - VNode::VSuspense(_) => unreachable!("no need to get the first node of a suspense"), - } - } - - pub(crate) fn move_before(&self, parent: &Element, next_sibling: &Option) { - match self { - VNode::VList(vlist) => { - for node in vlist.iter().rev() { - node.move_before(parent, next_sibling); - } - } - VNode::VComp(vcomp) => { - vcomp - .root_vnode() - .expect("VComp has no root vnode") - .move_before(parent, next_sibling); - } - VNode::VSuspense(vsuspense) => { - vsuspense.active_node().move_before(parent, next_sibling) - } - VNode::VPortal(_) => {} // no need to move portals - _ => super::insert_node(&self.unchecked_first_node(), parent, next_sibling.as_ref()), - }; - } -} - -impl DomBundle for VNode { - /// Remove VNode from parent. - fn detach(self, parent: &Element) { - match self { - VNode::VTag(vtag) => vtag.detach(parent), - VNode::VText(vtext) => vtext.detach(parent), - VNode::VComp(vcomp) => vcomp.detach(parent), - VNode::VList(vlist) => vlist.detach(parent), - VNode::VRef(ref node) => { - if parent.remove_child(node).is_err() { - console::warn!("Node not found to remove VRef"); - } - } - VNode::VPortal(vportal) => vportal.detach(parent), - VNode::VSuspense(vsuspense) => vsuspense.detach(parent), - } - } - - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - match *self { - VNode::VTag(ref vtag) => vtag.shift(next_parent, next_sibling), - VNode::VText(ref vtext) => vtext.shift(next_parent, next_sibling), - VNode::VComp(ref vcomp) => vcomp.shift(next_parent, next_sibling), - VNode::VList(ref vlist) => vlist.shift(next_parent, next_sibling), - VNode::VRef(ref node) => { - next_parent - .insert_before(node, next_sibling.get().as_ref()) - .unwrap(); - } - VNode::VPortal(ref vportal) => vportal.shift(next_parent, next_sibling), - VNode::VSuspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling), - } - } } -impl VDiff for VNode { - type Bundle = VNode; - - fn attach( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { - match self { - VNode::VTag(vtag) => { - let (node_ref, tag) = vtag.attach(parent_scope, parent, next_sibling); - (node_ref, tag.into()) - } - VNode::VText(vtext) => { - let (node_ref, text) = vtext.attach(parent_scope, parent, next_sibling); - (node_ref, text.into()) - } - VNode::VComp(vcomp) => { - let (node_ref, comp) = vcomp.attach(parent_scope, parent, next_sibling); - (node_ref, comp.into()) - } - VNode::VList(vlist) => { - let (node_ref, list) = vlist.attach(parent_scope, parent, next_sibling); - (node_ref, list.into()) - } - VNode::VRef(node) => { - super::insert_node(&node, parent, next_sibling.get().as_ref()); - (NodeRef::new(node.clone()), VNode::VRef(node)) - } - VNode::VPortal(vportal) => { - let (node_ref, portal) = vportal.attach(parent_scope, parent, next_sibling); - (node_ref, portal.into()) - } - VNode::VSuspense(vsuspense) => { - let (node_ref, suspense) = vsuspense.attach(parent_scope, parent, next_sibling); - (node_ref, suspense.into()) - } - } - } - - fn apply( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ancestor: &mut VNode, - ) -> NodeRef { - match self { - VNode::VTag(vtag) => vtag.apply(parent_scope, parent, next_sibling, ancestor), - VNode::VText(vtext) => vtext.apply(parent_scope, parent, next_sibling, ancestor), - VNode::VComp(vcomp) => vcomp.apply(parent_scope, parent, next_sibling, ancestor), - VNode::VList(vlist) => vlist.apply(parent_scope, parent, next_sibling, ancestor), - VNode::VRef(node) => { - if let VNode::VRef(ref n) = ancestor { - if &node == n { - return NodeRef::new(node); - } +#[cfg(test)] +mod test { + use crate::dom_bundle::VDiff; + use crate::{dom_bundle::BNode, html::AnyScope, NodeRef}; + use web_sys::Element; + + impl super::VNode { + pub(crate) fn apply_sequentially( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ancestor: &mut Option, + ) -> NodeRef { + match ancestor { + None => { + let (node_ref, node) = self.attach(parent_scope, parent, next_sibling); + *ancestor = Some(node); + node_ref } - let (node_ref, self_) = - VNode::VRef(node).attach(parent_scope, parent, next_sibling); - ancestor.replace(parent, self_); - node_ref + Some(ref mut ancestor) => self.apply(parent_scope, parent, next_sibling, ancestor), } - VNode::VSuspense(vsuspense) => { - vsuspense.apply(parent_scope, parent, next_sibling, ancestor) - } - VNode::VPortal(vportal) => vportal.apply(parent_scope, parent, next_sibling, ancestor), } } } @@ -314,61 +177,3 @@ impl PartialEq for VNode { } } } - -impl VNode { - pub(crate) fn replace(&mut self, parent: &Element, next_node: VNode) { - let ancestor = std::mem::replace(self, next_node); - ancestor.detach(parent); - } - - #[cfg(test)] - pub(crate) fn apply_sequentially( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ancestor: &mut Option, - ) -> NodeRef { - match ancestor { - None => { - let (node_ref, node) = self.attach(parent_scope, parent, next_sibling); - *ancestor = Some(node); - node_ref - } - Some(ref mut ancestor) => self.apply(parent_scope, parent, next_sibling, ancestor), - } - } -} - -#[cfg(test)] -mod layout_tests { - use super::*; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; - - #[cfg(feature = "wasm_test")] - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - #[cfg(feature = "wasm_test")] - wasm_bindgen_test_configure!(run_in_browser); - - #[test] - fn diff() { - let document = gloo_utils::document(); - let vref_node_1 = VNode::VRef(document.create_element("i").unwrap().into()); - let vref_node_2 = VNode::VRef(document.create_element("b").unwrap().into()); - - let layout1 = TestLayout { - name: "1", - node: vref_node_1, - expected: "", - }; - - let layout2 = TestLayout { - name: "2", - node: vref_node_2, - expected: "", - }; - - diff_layouts(vec![layout1, layout2]); - } -} diff --git a/packages/yew/src/virtual_dom/vportal.rs b/packages/yew/src/virtual_dom/vportal.rs index 023fabdac4e..593ab5cb4a3 100644 --- a/packages/yew/src/virtual_dom/vportal.rs +++ b/packages/yew/src/virtual_dom/vportal.rs @@ -1,21 +1,9 @@ //! This module contains the implementation of a portal `VPortal`. -use std::borrow::BorrowMut; - use super::VNode; -use crate::dom_bundle::{DomBundle, VDiff}; -use crate::html::{AnyScope, NodeRef}; +use crate::html::NodeRef; use web_sys::{Element, Node}; -/// Log an operation during tests for debugging purposes -/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. -macro_rules! test_log { - ($fmt:literal, $($arg:expr),* $(,)?) => { - #[cfg(all(test, feature = "wasm_test", verbose_tests))] - ::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*); - }; -} - #[derive(Debug, Clone)] pub struct VPortal { /// The element under which the content is inserted. @@ -24,66 +12,6 @@ pub struct VPortal { pub next_sibling: NodeRef, /// The inserted node pub node: Box, - /// The next sibling after the portal. Set when rendered - sibling_ref: NodeRef, -} - -impl DomBundle for VPortal { - fn detach(self, _: &Element) { - test_log!("Detaching portal from host{:?}", self.host.outer_html()); - self.node.detach(&self.host); - test_log!("Detached portal from host{:?}", self.host.outer_html()); - self.sibling_ref.set(None); - } - - fn shift(&self, _next_parent: &Element, _next_sibling: NodeRef) { - // portals have nothing in it's original place of DOM, we also do nothing. - } -} - -impl VDiff for VPortal { - type Bundle = VPortal; - - fn attach( - mut self, - parent_scope: &AnyScope, - _: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { - let (_, inner) = self - .node - .attach(parent_scope, &self.host, self.next_sibling.clone()); - self.node = Box::new(inner); - self.sibling_ref = next_sibling.clone(); - (next_sibling, self) - } - - fn apply( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ancestor: &mut VNode, - ) -> NodeRef { - if let VNode::VPortal(portal) = ancestor { - let old_host = std::mem::replace(&mut portal.host, self.host); - let old_sibling = std::mem::replace(&mut portal.next_sibling, self.next_sibling); - let node = &mut portal.node; - if old_host != portal.host || old_sibling != portal.next_sibling { - // Remount the inner node somewhere else instead of diffing - // Move the node, but keep the state - node.move_before(&portal.host, &portal.next_sibling.get()); - } - let inner_ancestor = node.borrow_mut(); - self.node - .apply(parent_scope, parent, next_sibling.clone(), inner_ancestor); - return next_sibling; - } - - let (_, self_) = self.attach(parent_scope, parent, next_sibling.clone()); - ancestor.replace(parent, self_.into()); - next_sibling - } } impl VPortal { @@ -93,7 +21,6 @@ impl VPortal { host, next_sibling: NodeRef::default(), node: Box::new(content), - sibling_ref: NodeRef::default(), } } /// Creates a [VPortal] rendering `content` in the DOM hierarchy under `host`. @@ -108,97 +35,6 @@ impl VPortal { sib_ref }, node: Box::new(content), - sibling_ref: NodeRef::default(), } } - /// Returns the [Node] following this [VPortal], if this [VPortal] - /// has already been mounted in the DOM. - pub fn next_sibling(&self) -> Option { - self.sibling_ref.get() - } -} - -#[cfg(test)] -mod layout_tests { - extern crate self as yew; - - use crate::html; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; - use crate::virtual_dom::VNode; - use yew::virtual_dom::VPortal; - - #[cfg(feature = "wasm_test")] - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - #[cfg(feature = "wasm_test")] - wasm_bindgen_test_configure!(run_in_browser); - - #[test] - fn diff() { - let mut layouts = vec![]; - let first_target = gloo_utils::document().create_element("i").unwrap(); - let second_target = gloo_utils::document().create_element("o").unwrap(); - let target_with_child = gloo_utils::document().create_element("i").unwrap(); - let target_child = gloo_utils::document().create_element("s").unwrap(); - target_with_child.append_child(&target_child).unwrap(); - - layouts.push(TestLayout { - name: "Portal - first target", - node: html! { -
    - {VNode::VRef(first_target.clone().into())} - {VNode::VRef(second_target.clone().into())} - {VNode::VPortal(VPortal::new( - html! { {"PORTAL"} }, - first_target.clone(), - ))} - {"AFTER"} -
    - }, - expected: "
    PORTALAFTER
    ", - }); - layouts.push(TestLayout { - name: "Portal - second target", - node: html! { -
    - {VNode::VRef(first_target.clone().into())} - {VNode::VRef(second_target.clone().into())} - {VNode::VPortal(VPortal::new( - html! { {"PORTAL"} }, - second_target.clone(), - ))} - {"AFTER"} -
    - }, - expected: "
    PORTALAFTER
    ", - }); - layouts.push(TestLayout { - name: "Portal - replaced by text", - node: html! { -
    - {VNode::VRef(first_target.clone().into())} - {VNode::VRef(second_target.clone().into())} - {"FOO"} - {"AFTER"} -
    - }, - expected: "
    FOOAFTER
    ", - }); - layouts.push(TestLayout { - name: "Portal - next sibling", - node: html! { -
    - {VNode::VRef(target_with_child.clone().into())} - {VNode::VPortal(VPortal::new_before( - html! { {"PORTAL"} }, - target_with_child.clone(), - Some(target_child.clone().into()), - ))} -
    - }, - expected: "
    PORTAL
    ", - }); - - diff_layouts(layouts) - } } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index f965000ba58..3c120d5a3e6 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -1,22 +1,17 @@ use super::{Key, VNode}; -use crate::{ - dom_bundle::{DomBundle, VDiff}, - html::{AnyScope, NodeRef}, -}; -use std::borrow::BorrowMut; use web_sys::Element; /// This struct represents a suspendable DOM fragment. #[derive(Clone, Debug, PartialEq)] pub struct VSuspense { /// Child nodes. - children: Box, + pub(crate) children: Box, /// Fallback nodes when suspended. - fallback: Box, + pub(crate) fallback: Box, /// The element to attach to when children is not attached to DOM - detached_parent: Element, + pub(crate) detached_parent: Element, /// Whether the current status is suspended. - suspended: bool, + pub(crate) suspended: bool, /// The Key. pub(crate) key: Option, } @@ -37,128 +32,4 @@ impl VSuspense { key, } } - - pub(crate) fn active_node(&self) -> &VNode { - if self.suspended { - &self.fallback - } else { - &self.children - } - } -} - -impl DomBundle for VSuspense { - fn detach(self, parent: &Element) { - if self.suspended { - self.fallback.detach(parent); - self.children.detach(&self.detached_parent); - } else { - self.children.detach(parent); - } - } - - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - self.active_node().shift(next_parent, next_sibling) - } -} - -impl VDiff for VSuspense { - type Bundle = VSuspense; - - fn attach( - mut self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { - // When it's suspended, we render children into an element that is detached from the dom - // tree while rendering fallback UI into the original place where children resides in. - let node_ref = if self.suspended { - let (_child_ref, children) = - self.children - .attach(parent_scope, &self.detached_parent, NodeRef::default()); - self.children = children.into(); - let (fallback_ref, fallback) = self.fallback.attach(parent_scope, parent, next_sibling); - self.fallback = fallback.into(); - fallback_ref - } else { - let (child_ref, children) = self.children.attach(parent_scope, parent, next_sibling); - self.children = children.into(); - child_ref - }; - (node_ref, self) - } - - fn apply( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ancestor: &mut VNode, - ) -> NodeRef { - let suspense = match ancestor { - // We only preserve the child state if they are the same suspense. - VNode::VSuspense(m) - if m.key == self.key && self.detached_parent == m.detached_parent => - { - m - } - _ => { - let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); - ancestor.replace(parent, self_.into()); - return self_ref; - } - }; - let was_suspended = suspense.suspended; - let children_ancestor = suspense.children.borrow_mut(); - let fallback_ancestor = suspense.fallback.borrow_mut(); - - suspense.suspended = self.suspended; - // no need to update key & detached_parent - - // When it's suspended, we render children into an element that is detached from the dom - // tree while rendering fallback UI into the original place where children resides in. - match (self.suspended, was_suspended) { - (true, true) => { - self.children.apply( - parent_scope, - &self.detached_parent, - NodeRef::default(), - children_ancestor, - ); - - self.fallback - .apply(parent_scope, parent, next_sibling, fallback_ancestor) - } - - (false, false) => { - self.children - .apply(parent_scope, parent, next_sibling, children_ancestor) - } - - (true, false) => { - children_ancestor.shift(&self.detached_parent, NodeRef::default()); - - self.children.apply( - parent_scope, - &self.detached_parent, - NodeRef::default(), - children_ancestor, - ); - // first render of fallback - let (fallback_ref, fallback) = - self.fallback.attach(parent_scope, parent, next_sibling); - *fallback_ancestor = fallback; - fallback_ref - } - - (false, true) => { - fallback_ancestor.replace(parent, VNode::default()); - - children_ancestor.shift(parent, next_sibling.clone()); - self.children - .apply(parent_scope, parent, next_sibling, children_ancestor) - } - } - } } diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 6e1838048ee..b4cc8e5f315 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1,19 +1,15 @@ //! This module contains the implementation of a virtual element node [VTag]. -use super::{Apply, AttrValue, Attributes, Key, Listener, Listeners, VList, VNode}; -use crate::dom_bundle::{DomBundle, VDiff}; -use crate::html::{AnyScope, IntoPropValue, NodeRef}; -use gloo::console; -use gloo_utils::document; +use super::{AttrValue, Attributes, Key, Listener, Listeners, VList, VNode}; +use crate::{ + dom_bundle::{InputFields, Value}, + html::{IntoPropValue, NodeRef}, +}; use std::borrow::Cow; use std::cmp::PartialEq; -use std::hint::unreachable_unchecked; -use std::marker::PhantomData; use std::mem; -use std::ops::{Deref, DerefMut}; use std::rc::Rc; -use wasm_bindgen::JsCast; -use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; +use web_sys::HtmlTextAreaElement as TextAreaElement; /// SVG namespace string used for creating svg elements pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; @@ -21,110 +17,14 @@ pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; /// Default namespace for html elements pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; -// Value field corresponding to an [Element]'s `value` property -#[derive(Clone, Debug, Eq, PartialEq)] -struct Value(Option, PhantomData); - -impl Default for Value { - fn default() -> Self { - Value(None, PhantomData) - } -} - -impl Apply for Value { - type Element = T; - - fn apply(&mut self, el: &Self::Element) { - if let Some(v) = &self.0 { - el.set_value(v); - } - } - - fn apply_diff(self, el: &Self::Element, ancestor: &mut Self) { - match (&self.0, &ancestor.0) { - (Some(new), Some(_)) => { - // Refresh value from the DOM. It might have changed. - if new.as_ref() != el.value() { - el.set_value(new); - } - } - (Some(new), None) => el.set_value(new), - (None, Some(_)) => el.set_value(""), - (None, None) => (), - } - } -} - -/// Able to have its value read or set -trait AccessValue { - fn value(&self) -> String; - fn set_value(&self, v: &str); -} - -macro_rules! impl_access_value { - ($( $type:ty )*) => { - $( - impl AccessValue for $type { - #[inline] - fn value(&self) -> String { - <$type>::value(&self) - } - - #[inline] - fn set_value(&self, v: &str) { - <$type>::set_value(&self, v) - } - } - )* - }; -} -impl_access_value! {InputElement TextAreaElement} - -/// Fields specific to -/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag]s -#[derive(Debug, Clone, Default, Eq, PartialEq)] -struct InputFields { - /// Contains a value of an - /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). - value: Value, - - /// Represents `checked` attribute of - /// [input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-checked). - /// It exists to override standard behavior of `checked` attribute, because - /// in original HTML it sets `defaultChecked` value of `InputElement`, but for reactive - /// frameworks it's more useful to control `checked` value of an `InputElement`. - checked: bool, -} - -impl Apply for InputFields { - type Element = InputElement; - - fn apply(&mut self, el: &Self::Element) { - // IMPORTANT! This parameter has to be set every time - // to prevent strange behaviour in the browser when the DOM changes - el.set_checked(self.checked); - - self.value.apply(el); - } - - fn apply_diff(self, el: &Self::Element, ancestor: &mut Self) { - // IMPORTANT! This parameter has to be set every time - // to prevent strange behaviour in the browser when the DOM changes - el.set_checked(self.checked); - - self.value.apply_diff(el, &mut ancestor.value); - } -} - /// [VTag] fields that are specific to different [VTag] kinds. /// Decreases the memory footprint of [VTag] by avoiding impossible field and value combinations. #[derive(Debug, Clone)] -enum VTagInner { +pub(crate) enum VTagInner { /// Fields specific to /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) /// [VTag]s Input(InputFields), - /// Fields specific to /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) /// [VTag]s @@ -133,53 +33,31 @@ enum VTagInner { /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) value: Value, }, - /// Fields for all other kinds of [VTag]s Other { /// A tag of the element. tag: Cow<'static, str>, - /// List of child nodes - children: VNode, + children: VList, }, } /// A type for a virtual /// [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) /// representation. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct VTag { /// [VTag] fields that are specific to different [VTag] kinds. - inner: VTagInner, - + pub(crate) inner: VTagInner, /// List of attached listeners. - listeners: Listeners, - - /// A reference to the DOM [`Element`]. - reference: Option, - + pub(crate) listeners: Listeners, /// A node reference used for DOM access in Component lifecycle methods pub node_ref: NodeRef, - /// List of attributes. pub attributes: Attributes, - pub key: Option, } -impl Clone for VTag { - fn clone(&self) -> Self { - VTag { - inner: self.inner.clone(), - reference: None, - listeners: self.listeners.clone(), - attributes: self.attributes.clone(), - node_ref: self.node_ref.clone(), - key: self.key.clone(), - } - } -} - impl VTag { /// Creates a new [VTag] instance with `tag` name (cannot be changed later in DOM). pub fn new(tag: impl Into>) -> Self { @@ -222,12 +100,12 @@ impl VTag { listeners: Listeners, ) -> Self { VTag::new_base( - VTagInner::Input(InputFields { - value: Value(value, PhantomData), + VTagInner::Input(InputFields::new( + value, // In HTML node `checked` attribute sets `defaultChecked` parameter, // but we use own field to control real `checked` parameter checked, - }), + )), node_ref, key, attributes, @@ -255,7 +133,7 @@ impl VTag { ) -> Self { VTag::new_base( VTagInner::Textarea { - value: Value(value, PhantomData), + value: Value::new(value), }, node_ref, key, @@ -281,7 +159,6 @@ impl VTag { listeners: Listeners, children: VList, ) -> Self { - let children = children.into(); VTag::new_base( VTagInner::Other { tag, children }, node_ref, @@ -303,7 +180,6 @@ impl VTag { ) -> Self { VTag { inner, - reference: None, attributes, listeners, node_ref, @@ -323,30 +199,21 @@ impl VTag { /// Add [VNode] child. pub fn add_child(&mut self, child: VNode) { if let VTagInner::Other { children, .. } = &mut self.inner { - match children { - VNode::VList(children) => children.add_child(child), - _ => unreachable!(), - } + children.add_child(child) } } /// Add multiple [VNode] children. pub fn add_children(&mut self, children: impl IntoIterator) { if let VTagInner::Other { children: dst, .. } = &mut self.inner { - match dst { - VNode::VList(dst) => dst.add_children(children), - _ => unreachable!(), - } + dst.add_children(children) } } /// Returns a reference to the children of this [VTag] pub fn children(&self) -> &VList { match &self.inner { - VTagInner::Other { children, .. } => match children { - VNode::VList(children) => children, - _ => unreachable!(), - }, + VTagInner::Other { children, .. } => children, _ => { // This is mutable because the VList is not Sync static mut EMPTY: VList = VList::new(); @@ -361,10 +228,7 @@ impl VTag { // children pub fn children_mut(&mut self) -> Option<&mut VList> { match &mut self.inner { - VTagInner::Other { children, .. } => match children { - VNode::VList(children) => Some(children), - _ => unreachable!(), - }, + VTagInner::Other { children, .. } => Some(children), _ => None, } } @@ -374,8 +238,8 @@ impl VTag { /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) pub fn value(&self) -> Option<&AttrValue> { match &self.inner { - VTagInner::Input(f) => f.value.0.as_ref(), - VTagInner::Textarea { value } => value.0.as_ref(), + VTagInner::Input(f) => f.as_ref(), + VTagInner::Textarea { value } => value.as_ref(), VTagInner::Other { .. } => None, } } @@ -386,10 +250,10 @@ impl VTag { pub fn set_value(&mut self, value: impl IntoPropValue>) { match &mut self.inner { VTagInner::Input(f) => { - f.value.0 = value.into_prop_value(); + f.set(value.into_prop_value()); } VTagInner::Textarea { value: dst } => { - dst.0 = value.into_prop_value(); + dst.set(value.into_prop_value()); } VTagInner::Other { .. } => (), } @@ -400,7 +264,7 @@ impl VTag { /// (Not a value of node's attribute). pub fn checked(&mut self) -> bool { match &mut self.inner { - VTagInner::Input(f) => f.checked, + VTagInner::Input(f) => f.checked(), _ => false, } } @@ -410,16 +274,10 @@ impl VTag { /// (Not a value of node's attribute). pub fn set_checked(&mut self, value: bool) { if let VTagInner::Input(f) = &mut self.inner { - f.checked = value; + f.set_checked(value); } } - /// Returns reference to the [Element] associated with this [VTag], if this [VTag] has already - /// been mounted in the DOM - pub fn reference(&self) -> Option<&Element> { - self.reference.as_ref() - } - /// Adds a key-value pair to attributes /// /// Not every attribute works when it set as an attribute. We use workarounds for: @@ -463,166 +321,6 @@ impl VTag { pub fn set_listeners(&mut self, listeners: Box<[Option>]>) { self.listeners = Listeners::Pending(listeners); } - - fn create_element(&self, parent: &Element) -> Element { - let tag = self.tag(); - if tag == "svg" - || parent - .namespace_uri() - .map_or(false, |ns| ns == SVG_NAMESPACE) - { - let namespace = Some(SVG_NAMESPACE); - document() - .create_element_ns(namespace, tag) - .expect("can't create namespaced element for vtag") - } else { - document() - .create_element(tag) - .expect("can't create element for vtag") - } - } -} - -impl DomBundle for VTag { - /// Remove VTag from parent. - fn detach(mut self, parent: &Element) { - let node = self - .reference - .take() - .expect("tried to remove not rendered VTag from DOM"); - - self.listeners.unregister(); - - // recursively remove its children - if let VTagInner::Other { children, .. } = self.inner { - children.detach(&node); - } - if parent.remove_child(&node).is_err() { - console::warn!("Node not found to remove VTag"); - } - // It could be that the ref was already reused when rendering another element. - // Only unset the ref it still belongs to our node - if self.node_ref.get().as_ref() == Some(&node) { - self.node_ref.set(None); - } - } - - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - let node = self - .reference - .as_ref() - .expect("tried to shift not rendered VTag from DOM"); - - next_parent - .insert_before(node, next_sibling.get().as_ref()) - .unwrap(); - } -} - -impl VDiff for VTag { - type Bundle = VTag; - - fn attach( - mut self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { - let el = self.create_element(parent); - super::insert_node(&el, parent, next_sibling.get().as_ref()); - - self.attributes.apply(&el); - self.listeners.apply(&el); - - self.inner = match self.inner { - VTagInner::Input(mut f) => { - f.apply(el.unchecked_ref()); - VTagInner::Input(f) - } - VTagInner::Textarea { mut value } => { - value.apply(el.unchecked_ref()); - VTagInner::Textarea { value } - } - VTagInner::Other { children, tag } => { - let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default()); - VTagInner::Other { - children: child_bundle, - tag, - } - } - }; - self.node_ref.set(Some(el.deref().clone())); - self.reference = el.into(); - (self.node_ref.clone(), self) - } - /// Renders virtual tag over DOM [Element], but it also compares this with an ancestor [VTag] - /// to compute what to patch in the actual DOM nodes. - fn apply( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - node_bundle: &mut VNode, - ) -> NodeRef { - // This kind of branching patching routine reduces branch predictor misses and the need to - // unpack the enums (including `Option`s) all the time, resulting in a more streamlined - // patching flow - let is_matching_tag = match node_bundle { - VNode::VTag(ex) if self.key == ex.key => match (&self.inner, &ex.inner) { - (VTagInner::Input(_), VTagInner::Input(_)) => true, - (VTagInner::Textarea { .. }, VTagInner::Textarea { .. }) => true, - (VTagInner::Other { tag: l, .. }, VTagInner::Other { tag: r, .. }) if l == r => { - true - } - _ => false, - }, - _ => false, - }; - // If the ancestor is a tag of the same type, don't recreate, keep the - // old tag and update its attributes and children. - let tag = if is_matching_tag { - match node_bundle { - VNode::VTag(a) => { - // Preserve the reference that already exists - a.deref_mut() - } - _ => unsafe { unreachable_unchecked() }, - } - } else { - let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); - node_bundle.replace(parent, self_.into()); - return self_ref; - }; - let el = tag.reference.take().unwrap(); - - self.attributes.apply_diff(&el, &mut tag.attributes); - self.listeners.apply_diff(&el, &mut tag.listeners); - - match (self.inner, &mut tag.inner) { - (VTagInner::Input(new), VTagInner::Input(old)) => { - new.apply_diff(el.unchecked_ref(), old); - } - (VTagInner::Textarea { value: new }, VTagInner::Textarea { value: old }) => { - new.apply_diff(el.unchecked_ref(), old); - } - (VTagInner::Other { children: new, .. }, VTagInner::Other { children: old, .. }) => { - new.apply(parent_scope, &el, NodeRef::default(), old); - } - // Can not happen, because we checked for tag equability above - _ => unsafe { unreachable_unchecked() }, - } - - tag.key = self.key; - - if self.node_ref != tag.node_ref && tag.node_ref.get().as_ref() == Some(&el) { - tag.node_ref.set(None); - } - tag.node_ref = self.node_ref; - tag.node_ref.set(Some(el.deref().clone())); - - tag.reference = el.into(); - tag.node_ref.clone() - } } impl PartialEq for VTag { @@ -643,795 +341,3 @@ impl PartialEq for VTag { } } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{html, Html}; - - #[cfg(feature = "wasm_test")] - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - #[cfg(feature = "wasm_test")] - wasm_bindgen_test_configure!(run_in_browser); - - fn test_scope() -> AnyScope { - AnyScope::test() - } - - #[test] - fn it_compares_tags() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -

    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_text() { - let a = html! { -
    { "correct" }
    - }; - - let b = html! { -
    { "correct" }
    - }; - - let c = html! { -
    { "incorrect" }
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_attributes_static() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_attributes_dynamic() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_children() { - let a = html! { -
    -

    -
    - }; - - let b = html! { -
    -

    -
    - }; - - let c = html! { -
    - -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_classes_static() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -
    - }; - - let d = html! { -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - assert_ne!(a, d); - } - - #[test] - fn it_compares_classes_dynamic() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -
    - }; - - let d = html! { -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - assert_ne!(a, d); - } - - fn assert_vtag(node: VNode) -> VTag { - if let VNode::VTag(vtag) = node { - return *vtag; - } - panic!("should be vtag"); - } - - fn assert_vtag_ref(node: &VNode) -> &VTag { - if let VNode::VTag(vtag) = node { - return vtag; - } - panic!("should be vtag"); - } - - fn assert_vtag_mut(node: &mut VNode) -> &mut VTag { - if let VNode::VTag(vtag) = node { - return vtag; - } - panic!("should be vtag"); - } - - fn assert_namespace(vtag: &VTag, namespace: &'static str) { - assert_eq!( - vtag.reference.as_ref().unwrap().namespace_uri().unwrap(), - namespace - ); - } - - #[test] - fn supports_svg() { - let document = web_sys::window().unwrap().document().unwrap(); - - let scope = test_scope(); - let div_el = document.create_element("div").unwrap(); - let namespace = SVG_NAMESPACE; - let namespace = Some(namespace); - let svg_el = document.create_element_ns(namespace, "svg").unwrap(); - - let g_node = html! { }; - let path_node = html! { }; - let svg_node = html! { {path_node} }; - - let svg_tag = assert_vtag(svg_node); - let (_, svg_tag) = svg_tag.attach(&scope, &div_el, NodeRef::default()); - assert_namespace(&svg_tag, SVG_NAMESPACE); - let path_tag = assert_vtag_ref(svg_tag.children().get(0).unwrap()); - assert_namespace(path_tag, SVG_NAMESPACE); - - let g_tag = assert_vtag(g_node); - let (_, mut g_tag) = g_tag.attach(&scope, &div_el, NodeRef::default()); - assert_namespace(&g_tag, HTML_NAMESPACE); - g_tag.reference = None; - - let (_, g_tag) = g_tag.attach(&scope, &svg_el, NodeRef::default()); - assert_namespace(&g_tag, SVG_NAMESPACE); - } - - #[test] - fn it_compares_values() { - let a = html! { - - }; - - let b = html! { - - }; - - let c = html! { - - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_kinds() { - let a = html! { - - }; - - let b = html! { - - }; - - let c = html! { - - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_checked() { - let a = html! { - - }; - - let b = html! { - - }; - - let c = html! { - - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_allows_aria_attributes() { - let a = html! { -

    - - -

    -

    - }; - if let VNode::VTag(vtag) = a { - assert_eq!( - vtag.attributes - .iter() - .find(|(k, _)| k == &"aria-controls") - .map(|(_, v)| v), - Some("it-works") - ); - } else { - panic!("vtag expected"); - } - } - - #[test] - fn it_does_not_set_missing_class_name() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - let elem = html! {
    }; - let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); - let vtag = assert_vtag_mut(&mut elem); - // test if the className has not been set - assert!(!vtag.reference.as_ref().unwrap().has_attribute("class")); - } - - fn test_set_class_name(gen_html: impl FnOnce() -> Html) { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - let elem = gen_html(); - let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); - let vtag = assert_vtag_mut(&mut elem); - // test if the className has been set - assert!(vtag.reference.as_ref().unwrap().has_attribute("class")); - } - - #[test] - fn it_sets_class_name_static() { - test_set_class_name(|| html! {
    }); - } - - #[test] - fn it_sets_class_name_dynamic() { - test_set_class_name(|| html! {
    }); - } - - #[test] - fn controlled_input_synced() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - let expected = "not_changed_value"; - - // Initial state - let elem = html! { }; - let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); - let vtag = assert_vtag_ref(&elem); - - // User input - let input_ref = vtag.reference.as_ref().unwrap(); - let input = input_ref.dyn_ref::(); - input.unwrap().set_value("User input"); - - let next_elem = html! { }; - let elem_vtag = assert_vtag(next_elem); - - // Sync happens here - elem_vtag.apply(&scope, &parent, NodeRef::default(), &mut elem); - let vtag = assert_vtag_ref(&elem); - - // Get new current value of the input element - let input_ref = vtag.reference.as_ref().unwrap(); - let input = input_ref.dyn_ref::().unwrap(); - - let current_value = input.value(); - - // check whether not changed virtual dom value has been set to the input element - assert_eq!(current_value, expected); - } - - #[test] - fn uncontrolled_input_unsynced() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - // Initial state - let elem = html! { }; - let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); - let vtag = assert_vtag_ref(&elem); - - // User input - let input_ref = vtag.reference.as_ref().unwrap(); - let input = input_ref.dyn_ref::(); - input.unwrap().set_value("User input"); - - let next_elem = html! { }; - let elem_vtag = assert_vtag(next_elem); - - // Value should not be refreshed - elem_vtag.apply(&scope, &parent, NodeRef::default(), &mut elem); - let vtag = assert_vtag_ref(&elem); - - // Get user value of the input element - let input_ref = vtag.reference.as_ref().unwrap(); - let input = input_ref.dyn_ref::().unwrap(); - - let current_value = input.value(); - - // check whether not changed virtual dom value has been set to the input element - assert_eq!(current_value, "User input"); - - // Need to remove the element to clean up the dirty state of the DOM. Failing this causes - // event listener tests to fail. - parent.remove(); - } - - #[test] - fn dynamic_tags_work() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - let elem = html! { <@{ - let mut builder = String::new(); - builder.push('a'); - builder - }/> }; - - let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); - let vtag = assert_vtag_mut(&mut elem); - // make sure the new tag name is used internally - assert_eq!(vtag.tag(), "a"); - - // Element.tagName is always in the canonical upper-case form. - assert_eq!(vtag.reference.as_ref().unwrap().tag_name(), "A"); - } - - #[test] - fn dynamic_tags_handle_value_attribute() { - let mut div_el = html! { - <@{"div"} value="Hello"/> - }; - let div_vtag = assert_vtag_mut(&mut div_el); - assert!(div_vtag.value().is_none()); - let v: Option<&str> = div_vtag - .attributes - .iter() - .find(|(k, _)| k == &"value") - .map(|(_, v)| AsRef::as_ref(v)); - assert_eq!(v, Some("Hello")); - - let mut input_el = html! { - <@{"input"} value="World"/> - }; - let input_vtag = assert_vtag_mut(&mut input_el); - assert_eq!(input_vtag.value(), Some(&AttrValue::Static("World"))); - assert!(!input_vtag.attributes.iter().any(|(k, _)| k == "value")); - } - - #[test] - fn dynamic_tags_handle_weird_capitalization() { - let mut el = html! { - <@{"tExTAREa"}/> - }; - let vtag = assert_vtag_mut(&mut el); - assert_eq!(vtag.tag(), "textarea"); - } - - #[test] - fn reset_node_ref() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - let node_ref = NodeRef::default(); - let mut elem: VNode = html! {
    }; - assert_vtag_mut(&mut elem); - let (_, elem) = elem.attach(&scope, &parent, NodeRef::default()); - let parent_node = parent.deref(); - assert_eq!(node_ref.get(), parent_node.first_child()); - elem.detach(&parent); - assert!(node_ref.get().is_none()); - } - - #[test] - fn vtag_reuse_should_reset_ancestors_node_ref() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - document().body().unwrap().append_child(&parent).unwrap(); - - let node_ref_a = NodeRef::default(); - let elem_a = html! {
    }; - let (_, mut elem) = elem_a.attach(&scope, &parent, NodeRef::default()); - - // save the Node to check later that it has been reused. - let node_a = node_ref_a.get().unwrap(); - - let node_ref_b = NodeRef::default(); - let elem_b = html! {
    }; - elem_b.apply(&scope, &parent, NodeRef::default(), &mut elem); - - let node_b = node_ref_b.get().unwrap(); - - assert_eq!(node_a, node_b, "VTag should have reused the element"); - assert!( - node_ref_a.get().is_none(), - "node_ref_a should have been reset when the element was reused." - ); - } - - #[test] - fn vtag_should_not_touch_newly_bound_refs() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - document().body().unwrap().append_child(&parent).unwrap(); - - let test_ref = NodeRef::default(); - let before = html! { - <> -
    - - }; - let after = html! { - <> -
    -
    - - }; - // The point of this diff is to first render the "after" div and then detach the "before" div, - // while both should be bound to the same node ref - - let (_, mut elem) = before.attach(&scope, &parent, NodeRef::default()); - after.apply(&scope, &parent, NodeRef::default(), &mut elem); - - assert_eq!( - test_ref - .get() - .unwrap() - .dyn_ref::() - .unwrap() - .outer_html(), - "
    " - ); - } -} - -#[cfg(test)] -mod layout_tests { - extern crate self as yew; - - use crate::html; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; - - #[cfg(feature = "wasm_test")] - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - #[cfg(feature = "wasm_test")] - wasm_bindgen_test_configure!(run_in_browser); - - #[test] - fn diff() { - let layout1 = TestLayout { - name: "1", - node: html! { -
      -
    • - {"a"} -
    • -
    • - {"b"} -
    • -
    - }, - expected: "
    • a
    • b
    ", - }; - - let layout2 = TestLayout { - name: "2", - node: html! { -
      -
    • - {"a"} -
    • -
    • - {"b"} -
    • -
    • - {"d"} -
    • -
    - }, - expected: "
    • a
    • b
    • d
    ", - }; - - let layout3 = TestLayout { - name: "3", - node: html! { -
      -
    • - {"a"} -
    • -
    • - {"b"} -
    • -
    • - {"c"} -
    • -
    • - {"d"} -
    • -
    - }, - expected: "
    • a
    • b
    • c
    • d
    ", - }; - - let layout4 = TestLayout { - name: "4", - node: html! { -
      -
    • - <> - {"a"} - -
    • -
    • - {"b"} -
    • - {"c"} -
    • -
    • - {"d"} -
    • - -
    - }, - expected: "
    • a
    • b
    • c
    • d
    ", - }; - - diff_layouts(vec![layout1, layout2, layout3, layout4]); - } -} - -#[cfg(test)] -mod tests_without_browser { - use crate::html; - - #[test] - fn html_if_bool() { - assert_eq!( - html! { - if true { -
    - } - }, - html! {
    }, - ); - assert_eq!( - html! { - if false { -
    - } else { -
    - } - }, - html! { -
    - }, - ); - assert_eq!( - html! { - if false { -
    - } - }, - html! {}, - ); - - // non-root tests - assert_eq!( - html! { -
    - if true { -
    - } -
    - }, - html! { -
    -
    -
    - }, - ); - assert_eq!( - html! { -
    - if false { -
    - } else { -
    - } -
    - }, - html! { -
    -
    -
    - }, - ); - assert_eq!( - html! { -
    - if false { -
    - } -
    - }, - html! { -
    - <> -
    - }, - ); - } - - #[test] - fn html_if_option() { - let option_foo = Some("foo"); - let none: Option<&'static str> = None; - assert_eq!( - html! { - if let Some(class) = option_foo { -
    - } - }, - html! {
    }, - ); - assert_eq!( - html! { - if let Some(class) = none { -
    - } else { -
    - } - }, - html! {
    }, - ); - assert_eq!( - html! { - if let Some(class) = none { -
    - } - }, - html! {}, - ); - - // non-root tests - assert_eq!( - html! { -
    - if let Some(class) = option_foo { -
    - } -
    - }, - html! {
    }, - ); - assert_eq!( - html! { -
    - if let Some(class) = none { -
    - } else { -
    - } -
    - }, - html! {
    }, - ); - assert_eq!( - html! { -
    - if let Some(class) = none { -
    - } -
    - }, - html! {
    <>
    }, - ); - } -} diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index 7e8decf7ffc..519d73a53e8 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -1,7 +1,7 @@ //! This module contains the implementation of a virtual text node `VText`. -use super::{AttrValue, VNode}; -use crate::dom_bundle::{DomBundle, VDiff}; +use super::AttrValue; +use crate::dom_bundle::{insert_node, BNode, DomBundle, VDiff}; use crate::html::{AnyScope, NodeRef}; use gloo::console; use gloo_utils::document; @@ -77,7 +77,7 @@ impl VDiff for VText { next_sibling: NodeRef, ) -> (NodeRef, Self::Bundle) { let text_node = document().create_text_node(&self.text); - super::insert_node(&text_node, parent, next_sibling.get().as_ref()); + insert_node(&text_node, parent, next_sibling.get().as_ref()); self.reference = Some(text_node.clone()); let node_ref = NodeRef::new(text_node.into()); (node_ref, self) @@ -89,9 +89,9 @@ impl VDiff for VText { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: &mut VNode, + ancestor: &mut BNode, ) -> NodeRef { - if let VNode::VText(ref mut vtext) = ancestor { + if let BNode::BText(ref mut vtext) = ancestor { let ancestor = std::mem::replace(vtext, self); vtext.reference = ancestor.reference; let text_node = vtext From 355e3280289d71abfccf0434647099ca9847366f Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 5 Jan 2022 23:36:56 +0100 Subject: [PATCH 05/26] mass rename: apply -> reconcile --- packages/yew/src/dom_bundle/bcomp.rs | 8 +++--- packages/yew/src/dom_bundle/blist.rs | 22 +++++++++++------ packages/yew/src/dom_bundle/bnode.rs | 20 ++++++++------- packages/yew/src/dom_bundle/bportal.rs | 8 +++--- packages/yew/src/dom_bundle/bsuspense.rs | 16 ++++++------ packages/yew/src/dom_bundle/btag.rs | 26 ++++++++++---------- packages/yew/src/dom_bundle/mod.rs | 20 +++++---------- packages/yew/src/html/component/lifecycle.rs | 4 +-- packages/yew/src/tests/layout_tests.rs | 12 ++++----- packages/yew/src/virtual_dom/vnode.rs | 8 +++--- packages/yew/src/virtual_dom/vtext.rs | 6 ++--- 11 files changed, 77 insertions(+), 73 deletions(-) diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index d1356b9acfd..17500748a02 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -1,6 +1,6 @@ //! This module contains the bundle implementation of a virtual component `BComp`. -use super::{BNode, DomBundle, VDiff}; +use super::{BNode, DomBundle, Reconcilable}; use crate::{ html::{AnyScope, BaseComponent, Scope, Scoped}, virtual_dom::{Key, VComp}, @@ -127,7 +127,7 @@ impl DomBundle for BComp { } } -impl VDiff for VComp { +impl Reconcilable for VComp { type Bundle = BComp; fn attach( @@ -173,7 +173,7 @@ impl VDiff for VComp { ) } - fn apply( + fn reconcile( self, parent_scope: &AnyScope, parent: &Element, @@ -262,7 +262,7 @@ mod tests { for _ in 0..10000 { let node = html! { }; - node.apply( + node.reconcile( &parent_scope, &parent_element, NodeRef::default(), diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index c813075e334..b730103f69f 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -1,7 +1,7 @@ //! This module contains fragments bundles. use super::test_log; use super::BNode; -use crate::dom_bundle::{DomBundle, VDiff}; +use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::{Key, VList, VNode, VText}; use std::borrow::Borrow; @@ -37,7 +37,11 @@ struct ElementWriter<'s> { impl<'s> ElementWriter<'s> { fn add(self, node: VNode) -> (Self, BNode) { test_log!("adding: {:?}", node); - test_log!(" parent={:?}, next_sibling={:?}", self.parent.outer_html(), self.next_sibling); + test_log!( + " parent={:?}, next_sibling={:?}", + self.parent.outer_html(), + self.next_sibling + ); let (next, bundle) = node.attach(self.parent_scope, self.parent, self.next_sibling); test_log!(" next_position: {:?}", next); ( @@ -51,10 +55,14 @@ impl<'s> ElementWriter<'s> { fn patch(self, node: VNode, ancestor: &mut BNode) -> Self { test_log!("patching: {:?} -> {:?}", ancestor, node); - test_log!(" parent={:?}, next_sibling={:?}", self.parent.outer_html(), self.next_sibling); + test_log!( + " parent={:?}, next_sibling={:?}", + self.parent.outer_html(), + self.next_sibling + ); // Advance the next sibling reference (from right to left) ancestor.move_before(self.parent, &self.next_sibling.get()); - let next = node.apply(self.parent_scope, self.parent, self.next_sibling, ancestor); + let next = node.reconcile(self.parent_scope, self.parent, self.next_sibling, ancestor); test_log!(" next_position: {:?}", next); Self { next_sibling: next, @@ -275,7 +283,7 @@ impl DomBundle for BList { } } -impl VDiff for VList { +impl Reconcilable for VList { type Bundle = BList; fn attach( @@ -285,7 +293,7 @@ impl VDiff for VList { next_sibling: NodeRef, ) -> (NodeRef, Self::Bundle) { let mut self_ = BNode::BList(BList::new()); - let node_ref = self.apply(parent_scope, parent, next_sibling, &mut self_); + let node_ref = self.reconcile(parent_scope, parent, next_sibling, &mut self_); let self_ = match self_ { BNode::BList(self_) => self_, _ => unreachable!("applying list should leave a VList in bundle ref"), @@ -293,7 +301,7 @@ impl VDiff for VList { (node_ref, self_) } - fn apply( + fn reconcile( mut self, parent_scope: &AnyScope, parent: &Element, diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 05a6d97e97e..60d5832a197 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -1,7 +1,7 @@ //! This module contains the bundle version of an abstract node. use super::{BComp, BList, BPortal, BSuspense, BTag}; -use crate::dom_bundle::{DomBundle, VDiff}; +use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::{Key, VNode, VText}; use gloo::console; @@ -122,7 +122,7 @@ impl DomBundle for BNode { } } -impl VDiff for VNode { +impl Reconcilable for VNode { type Bundle = BNode; fn attach( @@ -163,7 +163,7 @@ impl VDiff for VNode { } } - fn apply( + fn reconcile( self, parent_scope: &AnyScope, parent: &Element, @@ -171,10 +171,10 @@ impl VDiff for VNode { ancestor: &mut BNode, ) -> NodeRef { match self { - VNode::VTag(vtag) => vtag.apply(parent_scope, parent, next_sibling, ancestor), - VNode::VText(vtext) => vtext.apply(parent_scope, parent, next_sibling, ancestor), - VNode::VComp(vcomp) => vcomp.apply(parent_scope, parent, next_sibling, ancestor), - VNode::VList(vlist) => vlist.apply(parent_scope, parent, next_sibling, ancestor), + VNode::VTag(vtag) => vtag.reconcile(parent_scope, parent, next_sibling, ancestor), + VNode::VText(vtext) => vtext.reconcile(parent_scope, parent, next_sibling, ancestor), + VNode::VComp(vcomp) => vcomp.reconcile(parent_scope, parent, next_sibling, ancestor), + VNode::VList(vlist) => vlist.reconcile(parent_scope, parent, next_sibling, ancestor), VNode::VRef(node) => { if let BNode::BRef(ref n) = ancestor { if &node == n { @@ -186,9 +186,11 @@ impl VDiff for VNode { ancestor.replace(parent, self_); node_ref } - VNode::VPortal(vportal) => vportal.apply(parent_scope, parent, next_sibling, ancestor), + VNode::VPortal(vportal) => { + vportal.reconcile(parent_scope, parent, next_sibling, ancestor) + } VNode::VSuspense(vsuspsense) => { - vsuspsense.apply(parent_scope, parent, next_sibling, ancestor) + vsuspsense.reconcile(parent_scope, parent, next_sibling, ancestor) } } } diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index 01e92613983..32376b6912f 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -2,7 +2,7 @@ use super::test_log; use super::BNode; -use crate::dom_bundle::{DomBundle, VDiff}; +use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::Key; use crate::virtual_dom::VPortal; @@ -32,7 +32,7 @@ impl DomBundle for BPortal { } } -impl VDiff for VPortal { +impl Reconcilable for VPortal { type Bundle = BPortal; fn attach( @@ -57,7 +57,7 @@ impl VDiff for VPortal { ) } - fn apply( + fn reconcile( self, parent_scope: &AnyScope, parent: &Element, @@ -75,7 +75,7 @@ impl VDiff for VPortal { } let inner_ancestor = node.borrow_mut(); self.node - .apply(parent_scope, parent, next_sibling.clone(), inner_ancestor); + .reconcile(parent_scope, parent, next_sibling.clone(), inner_ancestor); return next_sibling; } diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 960a47665e0..f0120100e68 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -1,4 +1,4 @@ -use super::{BNode, DomBundle, VDiff}; +use super::{BNode, DomBundle, Reconcilable}; use crate::html::AnyScope; use crate::virtual_dom::{Key, VSuspense}; use crate::NodeRef; @@ -39,7 +39,7 @@ impl DomBundle for BSuspense { } } -impl VDiff for VSuspense { +impl Reconcilable for VSuspense { type Bundle = BSuspense; fn attach( @@ -85,7 +85,7 @@ impl VDiff for VSuspense { } } - fn apply( + fn reconcile( self, parent_scope: &AnyScope, parent: &Element, @@ -112,7 +112,7 @@ impl VDiff for VSuspense { // tree while rendering fallback UI into the original place where children resides in. match (self.suspended, &mut suspense.fallback) { (true, Some(fallback_ancestor)) => { - self.children.apply( + self.children.reconcile( parent_scope, &self.detached_parent, NodeRef::default(), @@ -120,18 +120,18 @@ impl VDiff for VSuspense { ); self.fallback - .apply(parent_scope, parent, next_sibling, fallback_ancestor) + .reconcile(parent_scope, parent, next_sibling, fallback_ancestor) } (false, None) => { self.children - .apply(parent_scope, parent, next_sibling, children_ancestor) + .reconcile(parent_scope, parent, next_sibling, children_ancestor) } (true, None) => { children_ancestor.shift(&self.detached_parent, NodeRef::default()); - self.children.apply( + self.children.reconcile( parent_scope, &self.detached_parent, NodeRef::default(), @@ -149,7 +149,7 @@ impl VDiff for VSuspense { children_ancestor.shift(parent, next_sibling.clone()); self.children - .apply(parent_scope, parent, next_sibling, children_ancestor) + .reconcile(parent_scope, parent, next_sibling, children_ancestor) } } } diff --git a/packages/yew/src/dom_bundle/btag.rs b/packages/yew/src/dom_bundle/btag.rs index 0400be90f2e..9d3bc0e89d8 100644 --- a/packages/yew/src/dom_bundle/btag.rs +++ b/packages/yew/src/dom_bundle/btag.rs @@ -1,7 +1,7 @@ //! This module contains the bundle implementation of a tag `BTag`. use super::listeners::ListenerRegistration; -use super::{BNode, DomBundle, InputFields, VDiff, Value}; +use super::{BNode, DomBundle, InputFields, Reconcilable, Value}; use crate::dom_bundle::attributes::Apply; use crate::html::AnyScope; use crate::virtual_dom::{vtag::VTagInner, vtag::SVG_NAMESPACE, Attributes, Key, VTag}; @@ -77,7 +77,7 @@ impl DomBundle for BTag { } } -impl VDiff for VTag { +impl Reconcilable for VTag { type Bundle = BTag; fn attach( @@ -131,7 +131,7 @@ impl VDiff for VTag { } /// Renders virtual tag over DOM [Element], but it also compares this with an ancestor [VTag] /// to compute what to patch in the actual DOM nodes. - fn apply( + fn reconcile( self, parent_scope: &AnyScope, parent: &Element, @@ -185,7 +185,7 @@ impl VDiff for VTag { child_bundle: old, .. }, ) => { - new.apply(parent_scope, el, NodeRef::default(), old); + new.reconcile(parent_scope, el, NodeRef::default(), old); } // Can not happen, because we checked for tag equability above _ => unsafe { unreachable_unchecked() }, @@ -577,7 +577,7 @@ mod tests { document().body().unwrap().append_child(&parent).unwrap(); let elem = html! {
    }; - let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); let vtag = assert_btag_mut(&mut elem); // test if the className has not been set assert!(!vtag.reference.has_attribute("class")); @@ -590,7 +590,7 @@ mod tests { document().body().unwrap().append_child(&parent).unwrap(); let elem = gen_html(); - let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); let vtag = assert_btag_mut(&mut elem); // test if the className has been set assert!(vtag.reference.has_attribute("class")); @@ -617,7 +617,7 @@ mod tests { // Initial state let elem = html! { }; - let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); let vtag = assert_btag_ref(&elem); // User input @@ -629,7 +629,7 @@ mod tests { let elem_vtag = assert_vtag(next_elem); // Sync happens here - elem_vtag.apply(&scope, &parent, NodeRef::default(), &mut elem); + elem_vtag.reconcile(&scope, &parent, NodeRef::default(), &mut elem); let vtag = assert_btag_ref(&elem); // Get new current value of the input element @@ -651,7 +651,7 @@ mod tests { // Initial state let elem = html! { }; - let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); let vtag = assert_btag_ref(&elem); // User input @@ -663,7 +663,7 @@ mod tests { let elem_vtag = assert_vtag(next_elem); // Value should not be refreshed - elem_vtag.apply(&scope, &parent, NodeRef::default(), &mut elem); + elem_vtag.reconcile(&scope, &parent, NodeRef::default(), &mut elem); let vtag = assert_btag_ref(&elem); // Get user value of the input element @@ -693,7 +693,7 @@ mod tests { builder }/> }; - let (_, mut elem) = VDiff::attach(elem, &scope, &parent, NodeRef::default()); + let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); let vtag = assert_btag_mut(&mut elem); // make sure the new tag name is used internally assert_eq!(vtag.tag(), "a"); @@ -764,7 +764,7 @@ mod tests { let node_ref_b = NodeRef::default(); let elem_b = html! {
    }; - elem_b.apply(&scope, &parent, NodeRef::default(), &mut elem); + elem_b.reconcile(&scope, &parent, NodeRef::default(), &mut elem); let node_b = node_ref_b.get().unwrap(); @@ -797,7 +797,7 @@ mod tests { // while both should be bound to the same node ref let (_, mut elem) = before.attach(&scope, &parent, NodeRef::default()); - after.apply(&scope, &parent, NodeRef::default(), &mut elem); + after.reconcile(&scope, &parent, NodeRef::default(), &mut elem); assert_eq!( test_ref diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 5ae6a9b478c..de779151a38 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -47,7 +47,7 @@ pub(crate) trait DomBundle { // `Ace` editor embedding for example? /// This trait provides features to update a tree by calculating a difference against another tree. -pub(crate) trait VDiff { +pub(crate) trait Reconcilable { type Bundle: DomBundle; fn attach( @@ -69,25 +69,17 @@ pub(crate) trait VDiff { /// - `parent`: the parent node in the DOM. /// - `next_sibling`: the next sibling, used to efficiently find where to /// put the node. - /// - `ancestor`: the node that this node will be replacing in the DOM. This - /// method will _always_ remove the `ancestor` from the `parent`. + /// - `bundle`: the node that this node will be replacing in the DOM. This + /// method will remove the `bundle` from the `parent` if it is of the wrong + /// kind, and otherwise reuse it. /// /// Returns a reference to the newly inserted element. - /// - /// ### Internal Behavior Notice: - /// - /// Note that these modify the DOM by modifying the reference that _already_ - /// exists on the `ancestor`. If `self.reference` exists (which it - /// _shouldn't_) this method will panic. - /// - /// The exception to this is obviously `VRef` which simply uses the inner - /// `Node` directly (always removes the `Node` that exists). - fn apply( + fn reconcile( self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: &mut BNode, + bundle: &mut BNode, ) -> NodeRef; } diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index e2bc433a72b..e78cab31504 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -1,7 +1,7 @@ //! Component lifecycle module use super::{AnyScope, BaseComponent, Scope}; -use crate::dom_bundle::{BNode, DomBundle, VDiff}; +use crate::dom_bundle::{BNode, DomBundle, Reconcilable}; use crate::html::RenderError; use crate::scheduler::{self, Runnable, Shared}; use crate::suspense::{Suspense, Suspension}; @@ -204,7 +204,7 @@ impl Runnable for RenderRunner { let next_sibling = state.next_sibling.clone(); let node = - root.apply(&scope, &state.parent, next_sibling, &mut state.root_node); + root.reconcile(&scope, &state.parent, next_sibling, &mut state.root_node); state.node_ref.link(node); } diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/tests/layout_tests.rs index 9511745bbef..a8241b2b14e 100644 --- a/packages/yew/src/tests/layout_tests.rs +++ b/packages/yew/src/tests/layout_tests.rs @@ -1,4 +1,4 @@ -use crate::dom_bundle::{BNode, VDiff}; +use crate::dom_bundle::{BNode, Reconcilable}; use crate::html::AnyScope; use crate::virtual_dom::{VNode, VText}; use crate::{Component, Context, Html}; @@ -64,7 +64,7 @@ pub fn diff_layouts(layouts: Vec>) { log!("Independently reapply layout '{}'", layout.name); - vnode.apply( + vnode.reconcile( &parent_scope, &parent_element, next_sibling.clone(), @@ -78,7 +78,7 @@ pub fn diff_layouts(layouts: Vec>) { ); // Detach - empty_node.clone().apply( + empty_node.clone().reconcile( &parent_scope, &parent_element, next_sibling.clone(), @@ -98,7 +98,7 @@ pub fn diff_layouts(layouts: Vec>) { let next_vnode = layout.node.clone(); log!("Sequentially apply layout '{}'", layout.name); - next_vnode.apply_sequentially( + next_vnode.reconcile_sequentially( &parent_scope, &parent_element, next_sibling.clone(), @@ -117,7 +117,7 @@ pub fn diff_layouts(layouts: Vec>) { let next_vnode = layout.node.clone(); log!("Sequentially detach layout '{}'", layout.name); - next_vnode.apply_sequentially( + next_vnode.reconcile_sequentially( &parent_scope, &parent_element, next_sibling.clone(), @@ -132,7 +132,7 @@ pub fn diff_layouts(layouts: Vec>) { } // Detach last layout - empty_node.apply_sequentially(&parent_scope, &parent_element, next_sibling, &mut ancestor); + empty_node.reconcile_sequentially(&parent_scope, &parent_element, next_sibling, &mut ancestor); assert_eq!( parent_element.inner_html(), "END", diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 5e1541a531b..6adfaa19f1f 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -54,12 +54,12 @@ impl VNode { #[cfg(test)] mod test { - use crate::dom_bundle::VDiff; + use crate::dom_bundle::Reconcilable; use crate::{dom_bundle::BNode, html::AnyScope, NodeRef}; use web_sys::Element; impl super::VNode { - pub(crate) fn apply_sequentially( + pub(crate) fn reconcile_sequentially( self, parent_scope: &AnyScope, parent: &Element, @@ -72,7 +72,9 @@ mod test { *ancestor = Some(node); node_ref } - Some(ref mut ancestor) => self.apply(parent_scope, parent, next_sibling, ancestor), + Some(ref mut ancestor) => { + self.reconcile(parent_scope, parent, next_sibling, ancestor) + } } } } diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index 519d73a53e8..c23a503f3d5 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -1,7 +1,7 @@ //! This module contains the implementation of a virtual text node `VText`. use super::AttrValue; -use crate::dom_bundle::{insert_node, BNode, DomBundle, VDiff}; +use crate::dom_bundle::{insert_node, BNode, DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use gloo::console; use gloo_utils::document; @@ -67,7 +67,7 @@ impl DomBundle for VText { } } -impl VDiff for VText { +impl Reconcilable for VText { type Bundle = VText; fn attach( @@ -84,7 +84,7 @@ impl VDiff for VText { } /// Renders virtual node over existing `TextNode`, but only if value of text has changed. - fn apply( + fn reconcile( self, parent_scope: &AnyScope, parent: &Element, From 35d7c2e05ee644ed6d7e3c194ce8da8169e337e0 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 5 Jan 2022 23:40:08 +0100 Subject: [PATCH 06/26] get rid of move_before in favor of shift --- packages/yew/src/dom_bundle/blist.rs | 2 +- packages/yew/src/dom_bundle/bnode.rs | 35 -------------------------- packages/yew/src/dom_bundle/bportal.rs | 2 +- packages/yew/src/dom_bundle/btag.rs | 4 --- 4 files changed, 2 insertions(+), 41 deletions(-) diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index b730103f69f..25e3039cc11 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -61,7 +61,7 @@ impl<'s> ElementWriter<'s> { self.next_sibling ); // Advance the next sibling reference (from right to left) - ancestor.move_before(self.parent, &self.next_sibling.get()); + ancestor.shift(self.parent, self.next_sibling.clone()); let next = node.reconcile(self.parent_scope, self.parent, self.next_sibling, ancestor); test_log!(" next_position: {:?}", next); Self { diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 60d5832a197..790424cdaab 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -50,41 +50,6 @@ impl BNode { Self::BSuspense(bsusp) => bsusp.key().is_some(), } } - - /// Returns the first DOM node that is used to designate the position of the virtual DOM node. - fn unchecked_first_node(&self) -> Node { - match self { - Self::BTag(btag) => btag.reference().clone().into(), - Self::BText(vtext) => { - let text_node = vtext.reference.as_ref().expect("VText is not mounted"); - text_node.clone().into() - } - Self::BRef(node) => node.clone(), - Self::BList(_) => unreachable!("no need to get first node of blist"), - Self::BComp(_) => unreachable!("no need to get first node of bcomp"), - Self::BSuspense(_) => unreachable!("no need to get first node of bsuspense"), - Self::BPortal(_) => unreachable!("portals have no first node, they are empty inside"), - } - } - - pub(crate) fn move_before(&self, parent: &Element, next_sibling: &Option) { - match self { - Self::BList(blist) => { - for node in blist.iter().rev() { - node.move_before(parent, next_sibling); - } - } - Self::BComp(bcomp) => { - bcomp - .root_bnode() - .expect("VComp has no root vnode") - .move_before(parent, next_sibling); - } - Self::BPortal(_) => {} // no need to move portals - Self::BSuspense(bsusp) => bsusp.active_node().move_before(parent, next_sibling), - _ => super::insert_node(&self.unchecked_first_node(), parent, next_sibling.as_ref()), - }; - } } impl DomBundle for BNode { diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index 32376b6912f..4356fe39787 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -71,7 +71,7 @@ impl Reconcilable for VPortal { if old_host != portal.host || old_sibling != portal.next_sibling { // Remount the inner node somewhere else instead of diffing // Move the node, but keep the state - node.move_before(&portal.host, &portal.next_sibling.get()); + node.shift(&portal.host, portal.next_sibling.clone()); } let inner_ancestor = node.borrow_mut(); self.node diff --git a/packages/yew/src/dom_bundle/btag.rs b/packages/yew/src/dom_bundle/btag.rs index 9d3bc0e89d8..8ea31f5983e 100644 --- a/packages/yew/src/dom_bundle/btag.rs +++ b/packages/yew/src/dom_bundle/btag.rs @@ -230,10 +230,6 @@ impl BTag { self.key.as_ref() } - pub(crate) fn reference(&self) -> &Element { - &self.reference - } - #[cfg(test)] fn children(&self) -> &[BNode] { match &self.inner { From d9bd30ae1109728c5c1344192929465f12160582 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 6 Jan 2022 00:01:24 +0100 Subject: [PATCH 07/26] generate id directly when creating a new scope --- packages/yew/src/app_handle.rs | 8 ++---- packages/yew/src/dom_bundle/bcomp.rs | 24 +++-------------- packages/yew/src/html/component/lifecycle.rs | 6 +---- packages/yew/src/html/component/scope.rs | 27 +++++++++++++++----- 4 files changed, 26 insertions(+), 39 deletions(-) diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs index 94e1c916338..9f37bfa79e8 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/app_handle.rs @@ -25,11 +25,7 @@ where pub(crate) fn mount_with_props(element: Element, props: Rc) -> Self { clear_element(&element); let app = Self { - scope: Scope::new( - None, - #[cfg(debug_assertions)] - u64::MAX, - ), + scope: Scope::new(None), }; app.scope .mount_in_place(element, NodeRef::default(), NodeRef::default(), props); @@ -38,7 +34,7 @@ where } /// Schedule the app for destruction - pub fn destroy(mut self) { + pub fn destroy(self) { self.scope.destroy() } } diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index 17500748a02..dcceb83406a 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -70,7 +70,6 @@ pub(crate) trait Mountable { parent_scope: &AnyScope, parent: Element, next_sibling: NodeRef, - #[cfg(debug_assertions)] id: u64, ) -> Box; fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); } @@ -99,13 +98,8 @@ impl Mountable for PropsWrapper { parent_scope: &AnyScope, parent: Element, next_sibling: NodeRef, - #[cfg(debug_assertions)] id: u64, ) -> Box { - let scope: Scope = Scope::new( - Some(parent_scope.clone()), - #[cfg(debug_assertions)] - id, - ); + let scope: Scope = Scope::new(Some(parent_scope.clone())); scope.mount_in_place(parent, next_sibling, node_ref, self.props); Box::new(scope) @@ -118,8 +112,8 @@ impl Mountable for PropsWrapper { } impl DomBundle for BComp { - fn detach(mut self, _parent: &Element) { - self.scope.destroy(); + fn detach(self, _parent: &Element) { + self.scope.destroy_boxed(); } fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { @@ -148,18 +142,6 @@ impl Reconcilable for VComp { parent_scope, parent.to_owned(), next_sibling, - #[cfg(debug_assertions)] - { - thread_local! { - static ID_COUNTER: std::cell::RefCell = Default::default(); - } - - ID_COUNTER.with(|c| { - let c = &mut *c.borrow_mut(); - *c += 1; - *c - }) - }, ); ( diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index e78cab31504..020e45f19e2 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -401,11 +401,7 @@ mod tests { fn test_lifecycle(props: Props, expected: &[&str]) { let document = gloo_utils::document(); - let scope = Scope::::new( - None, - #[cfg(debug_assertions)] - 0, - ); + let scope = Scope::::new(None); let el = document.create_element("div").unwrap(); let lifecycle = props.lifecycle.clone(); diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index f0fb565a8a7..7c002cc793c 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -118,8 +118,9 @@ impl AnyScope { pub(crate) trait Scoped { fn to_any(&self) -> AnyScope; fn root_bnode(&self) -> Option>; - fn destroy(&mut self); fn shift_node(&self, parent: Element, next_sibling: NodeRef); + fn destroy(self); + fn destroy_boxed(self: Box); } impl Scoped for Scope { @@ -139,14 +140,16 @@ impl Scoped for Scope { } /// Process an event to destroy a component - fn destroy(&mut self) { - scheduler::push_component_destroy(DestroyRunner { - state: self.state.clone(), - }); + fn destroy(self) { + scheduler::push_component_destroy(DestroyRunner { state: self.state }); // Not guaranteed to already have the scheduler started scheduler::start(); } + fn destroy_boxed(self: Box) { + self.destroy() + } + fn shift_node(&self, parent: Element, next_sibling: NodeRef) { scheduler::push_component_update(UpdateRunner { state: self.state.clone(), @@ -199,7 +202,7 @@ impl Scope { }) } - pub(crate) fn new(parent: Option, #[cfg(debug_assertions)] id: u64) -> Self { + pub(crate) fn new(parent: Option) -> Self { let parent = parent.map(Rc::new); let state = Rc::new(RefCell::new(None)); @@ -208,7 +211,17 @@ impl Scope { parent, #[cfg(debug_assertions)] - vcomp_id: id, + vcomp_id: { + thread_local! { + static ID_COUNTER: std::cell::RefCell = Default::default(); + } + + ID_COUNTER.with(|c| { + let c = &mut *c.borrow_mut(); + *c += 1; + *c + }) + }, } } From c843a2bae676161f81a751e7b3af1f88ab643baf Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 6 Jan 2022 00:20:10 +0100 Subject: [PATCH 08/26] bundle for text nodes --- packages/yew/src/dom_bundle/bnode.rs | 18 +-- packages/yew/src/dom_bundle/btext.rs | 158 ++++++++++++++++++++++++ packages/yew/src/dom_bundle/mod.rs | 2 + packages/yew/src/virtual_dom/vtext.rs | 166 +------------------------- 4 files changed, 171 insertions(+), 173 deletions(-) create mode 100644 packages/yew/src/dom_bundle/btext.rs diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 790424cdaab..04c41a155a9 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -1,9 +1,9 @@ //! This module contains the bundle version of an abstract node. -use super::{BComp, BList, BPortal, BSuspense, BTag}; +use super::{BComp, BList, BPortal, BSuspense, BTag, BText}; use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; -use crate::virtual_dom::{Key, VNode, VText}; +use crate::virtual_dom::{Key, VNode}; use gloo::console; use std::fmt; use web_sys::{Element, Node}; @@ -13,7 +13,7 @@ pub enum BNode { /// A bind between `VTag` and `Element`. BTag(Box), /// A bind between `VText` and `TextNode`. - BText(VText), + BText(BText), /// A bind between `VComp` and `Element`. BComp(BComp), /// A holder for a list of other nodes. @@ -57,7 +57,7 @@ impl DomBundle for BNode { fn detach(self, parent: &Element) { match self { Self::BTag(vtag) => vtag.detach(parent), - Self::BText(vtext) => vtext.detach(parent), + Self::BText(btext) => btext.detach(parent), Self::BComp(bsusp) => bsusp.detach(parent), Self::BList(blist) => blist.detach(parent), Self::BRef(ref node) => { @@ -73,7 +73,7 @@ impl DomBundle for BNode { fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { match self { Self::BTag(ref vtag) => vtag.shift(next_parent, next_sibling), - Self::BText(ref vtext) => vtext.shift(next_parent, next_sibling), + Self::BText(ref btext) => btext.shift(next_parent, next_sibling), Self::BComp(ref bsusp) => bsusp.shift(next_parent, next_sibling), Self::BList(ref vlist) => vlist.shift(next_parent, next_sibling), Self::BRef(ref node) => { @@ -161,10 +161,10 @@ impl Reconcilable for VNode { } } -impl From for BNode { +impl From for BNode { #[inline] - fn from(vtext: VText) -> Self { - Self::BText(vtext) + fn from(btext: BText) -> Self { + Self::BText(btext) } } @@ -207,7 +207,7 @@ impl fmt::Debug for BNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { Self::BTag(ref vtag) => vtag.fmt(f), - Self::BText(ref vtext) => vtext.fmt(f), + Self::BText(ref btext) => btext.fmt(f), Self::BComp(ref bsusp) => bsusp.fmt(f), Self::BList(ref vlist) => vlist.fmt(f), Self::BRef(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)), diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs new file mode 100644 index 00000000000..ceba5225f7d --- /dev/null +++ b/packages/yew/src/dom_bundle/btext.rs @@ -0,0 +1,158 @@ +//! This module contains the bundle implementation of text [BText]. + +use super::{insert_node, BNode, DomBundle, Reconcilable}; +use crate::html::AnyScope; +use crate::virtual_dom::{AttrValue, VText}; +use crate::NodeRef; +use gloo::console; +use gloo_utils::document; +use web_sys::{Element, Text as TextNode}; + +/// Bind text to a dom element. +/// Reuses the virtual dom structure of text. +pub struct BText { + text: AttrValue, + text_node: TextNode, +} + +impl DomBundle for BText { + /// Remove VText from parent. + fn detach(self, parent: &Element) { + let node = &self.text_node; + if parent.remove_child(node).is_err() { + console::warn!("Node not found to remove VText"); + } + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + let node = &self.text_node; + + next_parent + .insert_before(node, next_sibling.get().as_ref()) + .unwrap(); + } +} + +impl Reconcilable for VText { + type Bundle = BText; + + fn attach( + self, + _parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let Self { text } = self; + let text_node = document().create_text_node(&text); + insert_node(&text_node, parent, next_sibling.get().as_ref()); + let node_ref = NodeRef::new(text_node.clone().into()); + (node_ref, BText { text, text_node }) + } + + /// Renders virtual node over existing `TextNode`, but only if value of text has changed. + fn reconcile( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ancestor: &mut BNode, + ) -> NodeRef { + let btext = match ancestor { + BNode::BText(btext) => btext, + _ => { + let (node_ref, self_) = self.attach(parent_scope, parent, next_sibling); + ancestor.replace(parent, self_.into()); + return node_ref; + } + }; + let Self { text } = self; + let ancestor_text = std::mem::replace(&mut btext.text, text); + if btext.text != ancestor_text { + btext.text_node.set_node_value(Some(&btext.text)); + } + NodeRef::new(btext.text_node.clone().into()) + } +} + +impl std::fmt::Debug for BText { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "BText {{ text: \"{}\" }}", self.text) + } +} + +#[cfg(test)] +mod test { + extern crate self as yew; + + use crate::html; + + #[cfg(feature = "wasm_test")] + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + #[cfg(feature = "wasm_test")] + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn text_as_root() { + html! { + "Text Node As Root" + }; + + html! { + { "Text Node As Root" } + }; + } +} + +#[cfg(test)] +mod layout_tests { + extern crate self as yew; + + use crate::html; + use crate::tests::layout_tests::{diff_layouts, TestLayout}; + + #[cfg(feature = "wasm_test")] + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + #[cfg(feature = "wasm_test")] + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn diff() { + let layout1 = TestLayout { + name: "1", + node: html! { "a" }, + expected: "a", + }; + + let layout2 = TestLayout { + name: "2", + node: html! { "b" }, + expected: "b", + }; + + let layout3 = TestLayout { + name: "3", + node: html! { + <> + {"a"} + {"b"} + + }, + expected: "ab", + }; + + let layout4 = TestLayout { + name: "4", + node: html! { + <> + {"b"} + {"a"} + + }, + expected: "ba", + }; + + diff_layouts(vec![layout1, layout2, layout3, layout4]); + } +} diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index de779151a38..64fe17f6cbb 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -12,6 +12,7 @@ mod bnode; mod bportal; mod bsuspense; mod btag; +mod btext; mod listeners; #[cfg(debug_assertions)] @@ -23,6 +24,7 @@ pub use self::bnode::BNode; pub use self::bportal::BPortal; pub use self::bsuspense::BSuspense; pub use self::btag::BTag; +pub use self::btext::BText; pub(crate) use self::attributes::{Apply, InputFields, Value}; pub(crate) use self::bcomp::{Mountable, PropsWrapper}; diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index c23a503f3d5..a33e6e035b2 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -1,12 +1,7 @@ //! This module contains the implementation of a virtual text node `VText`. use super::AttrValue; -use crate::dom_bundle::{insert_node, BNode, DomBundle, Reconcilable}; -use crate::html::{AnyScope, NodeRef}; -use gloo::console; -use gloo_utils::document; use std::cmp::PartialEq; -use web_sys::{Element, Text as TextNode}; /// A type for a virtual /// [`TextNode`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode) @@ -15,98 +10,18 @@ use web_sys::{Element, Text as TextNode}; pub struct VText { /// Contains a text of the node. pub text: AttrValue, - /// A reference to the `TextNode`. - pub reference: Option, } impl VText { /// Creates new virtual text node with a content. pub fn new(text: impl Into) -> Self { - VText { - text: text.into(), - reference: None, - } + VText { text: text.into() } } } impl std::fmt::Debug for VText { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "VText {{ text: \"{}\", reference: {} }}", - self.text, - match &self.reference { - Some(_) => "Some(...)", - None => "None", - } - ) - } -} - -impl DomBundle for VText { - /// Remove VText from parent. - fn detach(mut self, parent: &Element) { - let node = self - .reference - .take() - .expect("tried to remove not rendered VText from DOM"); - if parent.remove_child(&node).is_err() { - console::warn!("Node not found to remove VText"); - } - } - - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - let node = self - .reference - .as_ref() - .expect("tried to shift not rendered VTag from DOM"); - - next_parent - .insert_before(node, next_sibling.get().as_ref()) - .unwrap(); - } -} - -impl Reconcilable for VText { - type Bundle = VText; - - fn attach( - mut self, - _parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { - let text_node = document().create_text_node(&self.text); - insert_node(&text_node, parent, next_sibling.get().as_ref()); - self.reference = Some(text_node.clone()); - let node_ref = NodeRef::new(text_node.into()); - (node_ref, self) - } - - /// Renders virtual node over existing `TextNode`, but only if value of text has changed. - fn reconcile( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ancestor: &mut BNode, - ) -> NodeRef { - if let BNode::BText(ref mut vtext) = ancestor { - let ancestor = std::mem::replace(vtext, self); - vtext.reference = ancestor.reference; - let text_node = vtext - .reference - .clone() - .expect("Rendered VText nodes should have a ref"); - if vtext.text != ancestor.text { - text_node.set_node_value(Some(&vtext.text)); - } - - return NodeRef::new(text_node.into()); - } - let (node_ref, self_) = self.attach(parent_scope, parent, next_sibling); - ancestor.replace(parent, self_.into()); - node_ref + write!(f, "VText {{ text: \"{}\" }}", self.text) } } @@ -115,80 +30,3 @@ impl PartialEq for VText { self.text == other.text } } - -#[cfg(test)] -mod test { - extern crate self as yew; - - use crate::html; - - #[cfg(feature = "wasm_test")] - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - #[cfg(feature = "wasm_test")] - wasm_bindgen_test_configure!(run_in_browser); - - #[test] - fn text_as_root() { - html! { - "Text Node As Root" - }; - - html! { - { "Text Node As Root" } - }; - } -} - -#[cfg(test)] -mod layout_tests { - extern crate self as yew; - - use crate::html; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; - - #[cfg(feature = "wasm_test")] - use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - - #[cfg(feature = "wasm_test")] - wasm_bindgen_test_configure!(run_in_browser); - - #[test] - fn diff() { - let layout1 = TestLayout { - name: "1", - node: html! { "a" }, - expected: "a", - }; - - let layout2 = TestLayout { - name: "2", - node: html! { "b" }, - expected: "b", - }; - - let layout3 = TestLayout { - name: "3", - node: html! { - <> - {"a"} - {"b"} - - }, - expected: "ab", - }; - - let layout4 = TestLayout { - name: "4", - node: html! { - <> - {"b"} - {"a"} - - }, - expected: "ba", - }; - - diff_layouts(vec![layout1, layout2, layout3, layout4]); - } -} From f576c361a654aa84b47fcd3c3e5955f50ba555cb Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 6 Jan 2022 00:49:50 +0100 Subject: [PATCH 09/26] work on naming: ancestor -> bundle --- packages/yew/src/dom_bundle/attributes.rs | 12 +++--- packages/yew/src/dom_bundle/bcomp.rs | 14 +++---- packages/yew/src/dom_bundle/blist.rs | 16 +++---- packages/yew/src/dom_bundle/bnode.rs | 31 +++++++------- packages/yew/src/dom_bundle/bportal.rs | 51 +++++++++++++---------- packages/yew/src/dom_bundle/bsuspense.rs | 24 +++++------ packages/yew/src/dom_bundle/btag.rs | 8 ++-- packages/yew/src/dom_bundle/btext.rs | 10 ++--- packages/yew/src/tests/layout_tests.rs | 26 +++++------- packages/yew/src/virtual_dom/vnode.rs | 14 +++---- packages/yew/src/virtual_dom/vportal.rs | 12 +++--- 11 files changed, 111 insertions(+), 107 deletions(-) diff --git a/packages/yew/src/dom_bundle/attributes.rs b/packages/yew/src/dom_bundle/attributes.rs index 912329eace5..8a60aad9f49 100644 --- a/packages/yew/src/dom_bundle/attributes.rs +++ b/packages/yew/src/dom_bundle/attributes.rs @@ -47,8 +47,8 @@ impl Apply for Value { self } - fn apply_diff(self, el: &Self::Element, ancestor: &mut Self) { - match (&self.0, &ancestor.0) { + fn apply_diff(self, el: &Self::Element, bundle: &mut Self) { + match (&self.0, &bundle.0) { (Some(new), Some(_)) => { // Refresh value from the DOM. It might have changed. if new.as_ref() != el.value() { @@ -96,8 +96,8 @@ pub(crate) trait Apply { /// Apply contained values to [Element] with no ancestor fn apply(self, el: &Self::Element) -> Self::Bundle; - /// Apply diff between [self] and `ancestor` to [Element]. - fn apply_diff(self, el: &Self::Element, ancestor: &mut Self::Bundle); + /// Apply diff between [self] and `bundle` to [Element]. + fn apply_diff(self, el: &Self::Element, bundle: &mut Self::Bundle); } /// Fields specific to @@ -159,12 +159,12 @@ impl Apply for InputFields { self } - fn apply_diff(self, el: &Self::Element, ancestor: &mut Self) { + fn apply_diff(self, el: &Self::Element, bundle: &mut Self) { // IMPORTANT! This parameter has to be set every time // to prevent strange behaviour in the browser when the DOM changes el.set_checked(self.checked); - self.value.apply_diff(el, &mut ancestor.value); + self.value.apply_diff(el, &mut bundle.value); } } diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index dcceb83406a..8542194d0b9 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -160,10 +160,10 @@ impl Reconcilable for VComp { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: &mut BNode, + bundle: &mut BNode, ) -> NodeRef { - let bcomp = match ancestor { - // If the ancestor is the same type, reuse it and update its properties + let bcomp = match bundle { + // If the existing bundle is the same type, reuse it and update its properties BNode::BComp(ref mut bcomp) if self.type_id == bcomp.type_id && self.key == bcomp.key => { @@ -171,7 +171,7 @@ impl Reconcilable for VComp { } _ => { let (node_ref, self_) = self.attach(parent_scope, parent, next_sibling); - ancestor.replace(parent, self_.into()); + bundle.replace(parent, self_.into()); return node_ref; } }; @@ -239,8 +239,8 @@ mod tests { let parent_scope: AnyScope = AnyScope::test(); let parent_element = document.create_element("div").unwrap(); - let ancestor = html! { }; - let (_, mut comp) = ancestor.attach(&parent_scope, &parent_element, NodeRef::default()); + let comp = html! { }; + let (_, mut bundle) = comp.attach(&parent_scope, &parent_element, NodeRef::default()); for _ in 0..10000 { let node = html! { }; @@ -248,7 +248,7 @@ mod tests { &parent_scope, &parent_element, NodeRef::default(), - &mut comp, + &mut bundle, ); } } diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 25e3039cc11..19c65d82e9d 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -53,16 +53,16 @@ impl<'s> ElementWriter<'s> { ) } - fn patch(self, node: VNode, ancestor: &mut BNode) -> Self { - test_log!("patching: {:?} -> {:?}", ancestor, node); + fn patch(self, node: VNode, bundle: &mut BNode) -> Self { + test_log!("patching: {:?} -> {:?}", bundle, node); test_log!( " parent={:?}, next_sibling={:?}", self.parent.outer_html(), self.next_sibling ); // Advance the next sibling reference (from right to left) - ancestor.shift(self.parent, self.next_sibling.clone()); - let next = node.reconcile(self.parent_scope, self.parent, self.next_sibling, ancestor); + bundle.shift(self.parent, self.next_sibling.clone()); + let next = node.reconcile(self.parent_scope, self.parent, self.next_sibling, bundle); test_log!(" next_position: {:?}", next); Self { next_sibling: next, @@ -306,7 +306,7 @@ impl Reconcilable for VList { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: &mut BNode, + bundle: &mut BNode, ) -> NodeRef { // Here, we will try to diff the previous list elements with the new // ones we want to insert. For that, we will use two lists: @@ -314,7 +314,7 @@ impl Reconcilable for VList { // - rights: previously rendered elements. // // The left items are known since we want to insert them - // (self.children). For the right ones, we will look at the ancestor, + // (self.children). For the right ones, we will look at the bundle, // i.e. the current DOM list element that we want to replace with self. if self.children.is_empty() { @@ -325,7 +325,9 @@ impl Reconcilable for VList { } let lefts = self.children; - let blist = ancestor.make_list(); + // 'Forcefully' create a pretend the existing node is a list. Creates a + // singleton list if it isn't already. + let blist = bundle.make_list(); let rights = &mut blist.rev_children; test_log!("lefts: {:?}", lefts); test_log!("rights: {:?}", rights); diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 04c41a155a9..903079fa6e5 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -133,29 +133,30 @@ impl Reconcilable for VNode { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: &mut BNode, + bundle: &mut BNode, ) -> NodeRef { match self { - VNode::VTag(vtag) => vtag.reconcile(parent_scope, parent, next_sibling, ancestor), - VNode::VText(vtext) => vtext.reconcile(parent_scope, parent, next_sibling, ancestor), - VNode::VComp(vcomp) => vcomp.reconcile(parent_scope, parent, next_sibling, ancestor), - VNode::VList(vlist) => vlist.reconcile(parent_scope, parent, next_sibling, ancestor), + VNode::VTag(vtag) => vtag.reconcile(parent_scope, parent, next_sibling, bundle), + VNode::VText(vtext) => vtext.reconcile(parent_scope, parent, next_sibling, bundle), + VNode::VComp(vcomp) => vcomp.reconcile(parent_scope, parent, next_sibling, bundle), + VNode::VList(vlist) => vlist.reconcile(parent_scope, parent, next_sibling, bundle), VNode::VRef(node) => { - if let BNode::BRef(ref n) = ancestor { - if &node == n { - return NodeRef::new(node); + let _existing = match bundle { + BNode::BRef(ref n) if &node == n => n, + _ => { + let (node_ref, self_) = + VNode::VRef(node).attach(parent_scope, parent, next_sibling); + bundle.replace(parent, self_); + return node_ref; } - } - let (node_ref, self_) = - VNode::VRef(node).attach(parent_scope, parent, next_sibling); - ancestor.replace(parent, self_); - node_ref + }; + NodeRef::new(node) } VNode::VPortal(vportal) => { - vportal.reconcile(parent_scope, parent, next_sibling, ancestor) + vportal.reconcile(parent_scope, parent, next_sibling, bundle) } VNode::VSuspense(vsuspsense) => { - vsuspsense.reconcile(parent_scope, parent, next_sibling, ancestor) + vsuspsense.reconcile(parent_scope, parent, next_sibling, bundle) } } } diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index 4356fe39787..0217cbebe69 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -6,7 +6,6 @@ use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::Key; use crate::virtual_dom::VPortal; -use std::borrow::BorrowMut; use web_sys::Element; /// The bundle implementation to [VPortal]. @@ -15,7 +14,7 @@ pub struct BPortal { /// The element under which the content is inserted. host: Element, /// The next sibling after the inserted content - next_sibling: NodeRef, + inner_sibling: NodeRef, /// The inserted node node: Box, } @@ -41,18 +40,18 @@ impl Reconcilable for VPortal { _parent: &Element, host_next_sibling: NodeRef, ) -> (NodeRef, Self::Bundle) { - let VPortal { + let Self { host, - next_sibling, + inner_sibling, node, } = self; - let (_, inner) = node.attach(parent_scope, &host, next_sibling.clone()); + let (_, inner) = node.attach(parent_scope, &host, inner_sibling.clone()); ( host_next_sibling, BPortal { host, node: Box::new(inner), - next_sibling, + inner_sibling, }, ) } @@ -62,25 +61,33 @@ impl Reconcilable for VPortal { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: &mut BNode, + bundle: &mut BNode, ) -> NodeRef { - if let BNode::BPortal(portal) = ancestor { - let old_host = std::mem::replace(&mut portal.host, self.host); - let old_sibling = std::mem::replace(&mut portal.next_sibling, self.next_sibling); - let node = &mut portal.node; - if old_host != portal.host || old_sibling != portal.next_sibling { - // Remount the inner node somewhere else instead of diffing - // Move the node, but keep the state - node.shift(&portal.host, portal.next_sibling.clone()); + let portal = match bundle { + BNode::BPortal(portal) => portal, + _ => { + let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); + bundle.replace(parent, self_.into()); + return self_ref; } - let inner_ancestor = node.borrow_mut(); - self.node - .reconcile(parent_scope, parent, next_sibling.clone(), inner_ancestor); - return next_sibling; - } + }; + let Self { + host, + inner_sibling, + node, + } = self; + + let old_host = std::mem::replace(&mut portal.host, host); + let old_inner_sibling = std::mem::replace(&mut portal.inner_sibling, inner_sibling); - let (_, self_) = self.attach(parent_scope, parent, next_sibling.clone()); - ancestor.replace(parent, self_.into()); + if old_host != portal.host || old_inner_sibling != portal.inner_sibling { + // Remount the inner node somewhere else instead of diffing + // Move the node, but keep the state + portal + .node + .shift(&portal.host, portal.inner_sibling.clone()); + } + node.reconcile(parent_scope, parent, next_sibling.clone(), &mut portal.node); next_sibling } } diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index f0120100e68..0430d2f9735 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -90,9 +90,9 @@ impl Reconcilable for VSuspense { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: &mut BNode, + bundle: &mut BNode, ) -> NodeRef { - let suspense = match ancestor { + let suspense = match bundle { // We only preserve the child state if they are the same suspense. BNode::BSuspense(m) if m.key == self.key && self.detached_parent == m.detached_parent => @@ -101,41 +101,41 @@ impl Reconcilable for VSuspense { } _ => { let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); - ancestor.replace(parent, self_.into()); + bundle.replace(parent, self_.into()); return self_ref; } }; - let children_ancestor = &mut suspense.children; + let children_bundle = &mut suspense.children; // no need to update key & detached_parent // When it's suspended, we render children into an element that is detached from the dom // tree while rendering fallback UI into the original place where children resides in. match (self.suspended, &mut suspense.fallback) { - (true, Some(fallback_ancestor)) => { + (true, Some(fallback_bundle)) => { self.children.reconcile( parent_scope, &self.detached_parent, NodeRef::default(), - children_ancestor, + children_bundle, ); self.fallback - .reconcile(parent_scope, parent, next_sibling, fallback_ancestor) + .reconcile(parent_scope, parent, next_sibling, fallback_bundle) } (false, None) => { self.children - .reconcile(parent_scope, parent, next_sibling, children_ancestor) + .reconcile(parent_scope, parent, next_sibling, children_bundle) } (true, None) => { - children_ancestor.shift(&self.detached_parent, NodeRef::default()); + children_bundle.shift(&self.detached_parent, NodeRef::default()); self.children.reconcile( parent_scope, &self.detached_parent, NodeRef::default(), - children_ancestor, + children_bundle, ); // first render of fallback let (fallback_ref, fallback) = @@ -147,9 +147,9 @@ impl Reconcilable for VSuspense { (false, Some(_)) => { suspense.fallback.take().unwrap().detach(parent); - children_ancestor.shift(parent, next_sibling.clone()); + children_bundle.shift(parent, next_sibling.clone()); self.children - .reconcile(parent_scope, parent, next_sibling, children_ancestor) + .reconcile(parent_scope, parent, next_sibling, children_bundle) } } } diff --git a/packages/yew/src/dom_bundle/btag.rs b/packages/yew/src/dom_bundle/btag.rs index 8ea31f5983e..30d88c1edb7 100644 --- a/packages/yew/src/dom_bundle/btag.rs +++ b/packages/yew/src/dom_bundle/btag.rs @@ -136,12 +136,12 @@ impl Reconcilable for VTag { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - node_bundle: &mut BNode, + bundle: &mut BNode, ) -> NodeRef { // This kind of branching patching routine reduces branch predictor misses and the need to // unpack the enums (including `Option`s) all the time, resulting in a more streamlined // patching flow - let is_matching_tag = match node_bundle { + let is_matching_tag = match bundle { BNode::BTag(ex) if self.key == ex.key => match (&self.inner, &ex.inner) { (VTagInner::Input(_), BTagInner::Input(_)) => true, (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true, @@ -155,7 +155,7 @@ impl Reconcilable for VTag { // If the ancestor is a tag of the same type, don't recreate, keep the // old tag and update its attributes and children. let tag = if is_matching_tag { - match node_bundle { + match bundle { BNode::BTag(a) => { // Preserve the reference that already exists a.deref_mut() @@ -164,7 +164,7 @@ impl Reconcilable for VTag { } } else { let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); - node_bundle.replace(parent, self_.into()); + bundle.replace(parent, self_.into()); return self_ref; }; diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index ceba5225f7d..40004c681c2 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -55,14 +55,14 @@ impl Reconcilable for VText { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: &mut BNode, + bundle: &mut BNode, ) -> NodeRef { - let btext = match ancestor { + let btext = match bundle { BNode::BText(btext) => btext, _ => { - let (node_ref, self_) = self.attach(parent_scope, parent, next_sibling); - ancestor.replace(parent, self_.into()); - return node_ref; + let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); + bundle.replace(parent, self_.into()); + return self_ref; } }; let Self { text } = self; diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/tests/layout_tests.rs index a8241b2b14e..38bb2f09bf4 100644 --- a/packages/yew/src/tests/layout_tests.rs +++ b/packages/yew/src/tests/layout_tests.rs @@ -1,6 +1,6 @@ -use crate::dom_bundle::{BNode, Reconcilable}; +use crate::dom_bundle::{BNode, DomBundle, Reconcilable}; use crate::html::AnyScope; -use crate::virtual_dom::{VNode, VText}; +use crate::virtual_dom::VNode; use crate::{Component, Context, Html}; use gloo::console::log; use web_sys::Node; @@ -42,7 +42,6 @@ pub fn diff_layouts(layouts: Vec>) { let parent_node: Node = parent_element.clone().into(); let end_node = document.create_text_node("END"); parent_node.append_child(&end_node).unwrap(); - let empty_node: VNode = VText::new("").into(); // Tests each layout independently let next_sibling = NodeRef::new(end_node.into()); @@ -51,7 +50,7 @@ pub fn diff_layouts(layouts: Vec>) { let vnode = layout.node.clone(); log!("Independently apply layout '{}'", layout.name); - let (_, mut node) = vnode.attach(&parent_scope, &parent_element, next_sibling.clone()); + let (_, mut bundle) = vnode.attach(&parent_scope, &parent_element, next_sibling.clone()); assert_eq!( parent_element.inner_html(), format!("{}END", layout.expected), @@ -68,7 +67,7 @@ pub fn diff_layouts(layouts: Vec>) { &parent_scope, &parent_element, next_sibling.clone(), - &mut node, + &mut bundle, ); assert_eq!( parent_element.inner_html(), @@ -78,12 +77,7 @@ pub fn diff_layouts(layouts: Vec>) { ); // Detach - empty_node.clone().reconcile( - &parent_scope, - &parent_element, - next_sibling.clone(), - &mut node, - ); + bundle.detach(&parent_element); assert_eq!( parent_element.inner_html(), "END", @@ -93,7 +87,7 @@ pub fn diff_layouts(layouts: Vec>) { } // Sequentially apply each layout - let mut ancestor: Option = None; + let mut bundle: Option = None; for layout in layouts.iter() { let next_vnode = layout.node.clone(); @@ -102,7 +96,7 @@ pub fn diff_layouts(layouts: Vec>) { &parent_scope, &parent_element, next_sibling.clone(), - &mut ancestor, + &mut bundle, ); assert_eq!( parent_element.inner_html(), @@ -121,7 +115,7 @@ pub fn diff_layouts(layouts: Vec>) { &parent_scope, &parent_element, next_sibling.clone(), - &mut ancestor, + &mut bundle, ); assert_eq!( parent_element.inner_html(), @@ -132,7 +126,9 @@ pub fn diff_layouts(layouts: Vec>) { } // Detach last layout - empty_node.reconcile_sequentially(&parent_scope, &parent_element, next_sibling, &mut ancestor); + if let Some(bundle) = bundle { + bundle.detach(&parent_element); + } assert_eq!( parent_element.inner_html(), "END", diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 6adfaa19f1f..4b03eadd89d 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -64,17 +64,15 @@ mod test { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - ancestor: &mut Option, + bundle: &mut Option, ) -> NodeRef { - match ancestor { + match bundle { None => { - let (node_ref, node) = self.attach(parent_scope, parent, next_sibling); - *ancestor = Some(node); - node_ref - } - Some(ref mut ancestor) => { - self.reconcile(parent_scope, parent, next_sibling, ancestor) + let (self_ref, node) = self.attach(parent_scope, parent, next_sibling); + *bundle = Some(node); + self_ref } + Some(bundle) => self.reconcile(parent_scope, parent, next_sibling, bundle), } } } diff --git a/packages/yew/src/virtual_dom/vportal.rs b/packages/yew/src/virtual_dom/vportal.rs index 593ab5cb4a3..abcb4f1a19b 100644 --- a/packages/yew/src/virtual_dom/vportal.rs +++ b/packages/yew/src/virtual_dom/vportal.rs @@ -8,8 +8,8 @@ use web_sys::{Element, Node}; pub struct VPortal { /// The element under which the content is inserted. pub host: Element, - /// The next sibling after the inserted content - pub next_sibling: NodeRef, + /// The next sibling after the inserted content. Most be a child of `host`. + pub inner_sibling: NodeRef, /// The inserted node pub node: Box, } @@ -19,19 +19,19 @@ impl VPortal { pub fn new(content: VNode, host: Element) -> Self { Self { host, - next_sibling: NodeRef::default(), + inner_sibling: NodeRef::default(), node: Box::new(content), } } /// Creates a [VPortal] rendering `content` in the DOM hierarchy under `host`. /// If `next_sibling` is given, the content is inserted before that [Node]. /// The parent of `next_sibling`, if given, must be `host`. - pub fn new_before(content: VNode, host: Element, next_sibling: Option) -> Self { + pub fn new_before(content: VNode, host: Element, inner_sibling: Option) -> Self { Self { host, - next_sibling: { + inner_sibling: { let sib_ref = NodeRef::default(); - sib_ref.set(next_sibling); + sib_ref.set(inner_sibling); sib_ref }, node: Box::new(content), From 751fed232968d349dcd00bcd1bbd0322544678a7 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 6 Jan 2022 18:15:06 +0100 Subject: [PATCH 10/26] slightly optimize list reconciler, add doccomments --- packages/yew/src/dom_bundle/blist.rs | 107 ++++++++++++++++----------- 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 19c65d82e9d..31a0df4d9b8 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -53,6 +53,10 @@ impl<'s> ElementWriter<'s> { ) } + fn shift(&self, bundle: &mut BNode) { + bundle.shift(self.parent, self.next_sibling.clone()); + } + fn patch(self, node: VNode, bundle: &mut BNode) -> Self { test_log!("patching: {:?} -> {:?}", bundle, node); test_log!( @@ -61,7 +65,6 @@ impl<'s> ElementWriter<'s> { self.next_sibling ); // Advance the next sibling reference (from right to left) - bundle.shift(self.parent, self.next_sibling.clone()); let next = node.reconcile(self.parent_scope, self.parent, self.next_sibling, bundle); test_log!(" next_position: {:?}", next); Self { @@ -71,23 +74,23 @@ impl<'s> ElementWriter<'s> { } } -struct NodeEntry(BNode); -impl Borrow for NodeEntry { +struct KeyedEntry(BNode, usize); +impl Borrow for KeyedEntry { fn borrow(&self) -> &Key { self.0.key().expect("unkeyed child in fully keyed list") } } -impl Hash for NodeEntry { +impl Hash for KeyedEntry { fn hash(&self, state: &mut H) { >::borrow(self).hash(state) } } -impl PartialEq for NodeEntry { +impl PartialEq for KeyedEntry { fn eq(&self, other: &Self) -> bool { >::borrow(self) == >::borrow(other) } } -impl Eq for NodeEntry {} +impl Eq for KeyedEntry {} impl BNode { fn make_list(&mut self) -> &mut BList { @@ -165,9 +168,14 @@ impl BList { parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - lefts: Vec, - rights: &mut Vec, + left_vdoms: Vec, + rev_bundles: &mut Vec, ) -> NodeRef { + macro_rules! key { + ($v:expr) => { + $v.key().expect("unkeyed child in fully keyed list") + }; + } /// Find the first differing key in 2 iterators fn matching_len<'a, 'b>( a: impl Iterator, @@ -178,62 +186,72 @@ impl BList { // Find first key mismatch from the back let matching_len_end = matching_len( - lefts - .iter() - .map(|v| v.key().expect("unkeyed child in fully keyed list")) - .rev(), - rights - .iter() - .map(|v| v.key().expect("unkeyed child in fully keyed list")), + left_vdoms.iter().map(|v| key!(v)).rev(), + rev_bundles.iter().map(|v| key!(v)), ); - if matching_len_end == std::cmp::min(lefts.len(), rights.len()) { + // If there is no key mismatch, apply the unkeyed approach + // Corresponds to adding or removing items from the back of the list + if matching_len_end == std::cmp::min(left_vdoms.len(), rev_bundles.len()) { // No key changes - return Self::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights); + return Self::apply_unkeyed( + parent_scope, + parent, + next_sibling, + left_vdoms, + rev_bundles, + ); } - // We partially deconstruct the new vector in several steps. - let mut lefts = lefts; + + // We partially drain the new vnodes in several steps. + let mut lefts = left_vdoms; let mut writer = ElementWriter { parent_scope, parent, next_sibling, }; - // Diff matching children at the end + // Step 1. Diff matching children at the end let lefts_to = lefts.len() - matching_len_end; for (l, r) in lefts .drain(lefts_to..) .rev() - .zip(rights[..matching_len_end].iter_mut()) + .zip(rev_bundles[..matching_len_end].iter_mut()) { writer = writer.patch(l, r); } + + // Step 2. Diff matching children in the middle, that is between the first and last key mismatch // Find first key mismatch from the front let matching_len_start = matching_len( - lefts - .iter() - .map(|v| v.key().expect("unkeyed child in fully keyed list")), - rights - .iter() - .map(|v| v.key().expect("unkeyed child in fully keyed list")) - .rev(), + lefts.iter().map(|v| key!(v)), + rev_bundles.iter().map(|v| key!(v)).rev(), ); - // Diff mismatched children in the middle - let rights_to = rights.len() - matching_len_start; - let mut spliced_middle = rights.splice(matching_len_end..rights_to, std::iter::empty()); - let mut rights_diff: HashSet = + // Step 2.1. Splice out the existing middle part and build a lookup by key + let rights_to = rev_bundles.len() - matching_len_start; + let mut spliced_middle = + rev_bundles.splice(matching_len_end..rights_to, std::iter::empty()); + let mut spare_bundles: HashSet = HashSet::with_capacity((matching_len_end..rights_to).len()); - for r in &mut spliced_middle { - rights_diff.insert(NodeEntry(r)); + for (idx, r) in (&mut spliced_middle).enumerate() { + spare_bundles.insert(KeyedEntry(r, idx)); } + + // Step 2.2. Put the middle part back together in the new key order let mut replacements: Vec = Vec::with_capacity((matching_len_start..lefts_to).len()); + // Roughly keep track of the order in which elements appear. If one appears out-of-order + // we (over approximately) have to shift the element, otherwise it is guaranteed to be in place. + let mut max_seen_idx = 0; for l in lefts .drain(matching_len_start..) // lefts_to.. has been drained .rev() { - let l_key = l.key().expect("unkeyed child in fully keyed list"); - let bundle = match rights_diff.take(l_key) { - Some(NodeEntry(mut r_bundle)) => { + let bundle = match spare_bundles.take(key!(l)) { + Some(KeyedEntry(mut r_bundle, idx)) => { + if idx < max_seen_idx { + writer.shift(&mut r_bundle); + } + max_seen_idx = usize::max(max_seen_idx, idx); writer = writer.patch(l, &mut r_bundle); r_bundle } @@ -245,22 +263,22 @@ impl BList { }; replacements.push(bundle); } - // now drop the splice iterator + // drop the splice iterator and immediately replace the range with the reordered elements std::mem::drop(spliced_middle); - rights.splice(matching_len_end..matching_len_end, replacements); + rev_bundles.splice(matching_len_end..matching_len_end, replacements); - // Remove any extra rights - for NodeEntry(r) in rights_diff.drain() { + // Step 2.3. Remove any extra rights + for KeyedEntry(r, _) in spare_bundles.drain() { test_log!("removing: {:?}", r); r.detach(parent); } - // Diff matching children at the start - let rights_to = rights.len() - matching_len_start; + // Step 3. Diff matching children at the start + let rights_to = rev_bundles.len() - matching_len_start; for (l, r) in lefts .drain(..) // matching_len_start.. has been drained already .rev() - .zip(rights[rights_to..].iter_mut()) + .zip(rev_bundles[rights_to..].iter_mut()) { writer = writer.patch(l, r); } @@ -335,7 +353,6 @@ impl Reconcilable for VList { if let Some(additional) = rights.len().checked_sub(lefts.len()) { rights.reserve_exact(additional); } - #[allow(clippy::let_and_return)] let first = if self.fully_keyed && blist.fully_keyed { BList::apply_keyed(parent_scope, parent, next_sibling, lefts, rights) } else { From 749c078bc06f317a9d00d76367851d60cf9d152e Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 12 Jan 2022 20:58:51 +0100 Subject: [PATCH 11/26] address review --- packages/yew/src/dom_bundle/blist.rs | 5 +++-- packages/yew/src/dom_bundle/bnode.rs | 12 ------------ packages/yew/src/virtual_dom/vnode.rs | 11 ++--------- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 31a0df4d9b8..fd2ab36c832 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -102,9 +102,10 @@ impl BNode { BNode::BList(blist) => blist, _ => unreachable!("just been set to the variant"), }; - self_list.fully_keyed = b.has_key(); - self_list.key = b.key().cloned(); + let key = b.key().cloned(); self_list.rev_children.push(b); + self_list.fully_keyed = key.is_some(); + self_list.key = key; self_list } } diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 903079fa6e5..6a9e75041db 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -38,18 +38,6 @@ impl BNode { Self::BSuspense(bsusp) => bsusp.key(), } } - - /// Returns true if the [VNode] has a key without needlessly cloning the key. - pub(crate) fn has_key(&self) -> bool { - match self { - Self::BComp(bsusp) => bsusp.key().is_some(), - Self::BList(blist) => blist.key().is_some(), - Self::BRef(_) | Self::BText(_) => false, - Self::BTag(vtag) => vtag.key().is_some(), - Self::BPortal(bportal) => bportal.key().is_some(), - Self::BSuspense(bsusp) => bsusp.key().is_some(), - } - } } impl DomBundle for BNode { diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 4b03eadd89d..70d77591229 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -39,16 +39,9 @@ impl VNode { } } - /// Returns true if the [VNode] has a key without needlessly cloning the key. + /// Returns true if the [VNode] has a key. pub fn has_key(&self) -> bool { - match self { - VNode::VComp(vcomp) => vcomp.key.is_some(), - VNode::VList(vlist) => vlist.key.is_some(), - VNode::VRef(_) | VNode::VText(_) => false, - VNode::VTag(vtag) => vtag.key.is_some(), - VNode::VPortal(vportal) => vportal.node.has_key(), - VNode::VSuspense(vsuspense) => vsuspense.key.is_some(), - } + self.key().is_some() } } From c3570456893c93badf2c9ce5fe2061a232fe9d2f Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Sat, 15 Jan 2022 12:12:35 +0100 Subject: [PATCH 12/26] add internal documentation --- packages/yew/src/dom_bundle/attributes.rs | 15 +++++++------ packages/yew/src/dom_bundle/bcomp.rs | 25 +++++++++++----------- packages/yew/src/dom_bundle/blist.rs | 26 ++++++++++++++--------- packages/yew/src/dom_bundle/bnode.rs | 6 ++++-- packages/yew/src/dom_bundle/bportal.rs | 3 ++- packages/yew/src/dom_bundle/bsuspense.rs | 11 ++++++---- packages/yew/src/dom_bundle/btag.rs | 7 +++--- packages/yew/src/dom_bundle/btext.rs | 4 +--- packages/yew/src/dom_bundle/listeners.rs | 3 ++- packages/yew/src/dom_bundle/mod.rs | 17 ++++++++++++--- packages/yew/src/html/component/scope.rs | 8 ++++--- 11 files changed, 75 insertions(+), 50 deletions(-) diff --git a/packages/yew/src/dom_bundle/attributes.rs b/packages/yew/src/dom_bundle/attributes.rs index 8a60aad9f49..18fc6cf1d23 100644 --- a/packages/yew/src/dom_bundle/attributes.rs +++ b/packages/yew/src/dom_bundle/attributes.rs @@ -8,7 +8,7 @@ use std::{ }; use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; -// Value field corresponding to an [Element]'s `value` property +/// Value field corresponding to an [Element]'s `value` property #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct Value(Option, PhantomData); @@ -88,7 +88,7 @@ pub(crate) trait AccessValue { } /// Applies contained changes to DOM [Element] -pub(crate) trait Apply { +pub(super) trait Apply { /// [Element] type to apply the changes to type Element; type Bundle; @@ -300,10 +300,11 @@ impl Apply for Attributes { fn apply_diff(self, el: &Element, bundle: &mut Self) { #[inline] fn ptr_eq(a: &[T], b: &[T]) -> bool { - a.as_ptr() == b.as_ptr() + std::ptr::eq(a, b) } let ancestor = std::mem::replace(bundle, self); + let bundle = &*bundle; // reborrow it immutably from here match (bundle, ancestor) { // Hot path (Self::Static(new), Self::Static(old)) if ptr_eq(new, old) => (), @@ -348,14 +349,14 @@ impl Apply for Attributes { } } // For VTag's constructed outside the html! macro - (Self::IndexMap(new), Self::IndexMap(old)) => { + (Self::IndexMap(new), Self::IndexMap(ref old)) => { let new_iter = new.iter().map(|(k, v)| (*k, v.as_ref())); - Self::apply_diff_index_maps(el, new_iter, new, &old); + Self::apply_diff_index_maps(el, new_iter, new, old); } // Cold path. Happens only with conditional swapping and reordering of `VTag`s with the // same tag and no keys. - (new, ancestor) => { - Self::apply_diff_as_maps(el, new, &ancestor); + (new, ref ancestor) => { + Self::apply_diff_as_maps(el, new, ancestor); } } } diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index 91527d6f312..5ae0277dfce 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -1,14 +1,12 @@ -//! This module contains the bundle implementation of a virtual component `BComp`. +//! This module contains the bundle implementation of a virtual component [BComp]. use super::{BNode, DomBundle, Reconcilable}; -use crate::{ - html::{AnyScope, BaseComponent, Scope, Scoped}, - virtual_dom::{Key, VComp}, - NodeRef, -}; +use crate::html::{AnyScope, BaseComponent, Scope, Scoped}; +use crate::virtual_dom::{Key, VComp}; +use crate::NodeRef; #[cfg(feature = "ssr")] use futures::future::{FutureExt, LocalBoxFuture}; -use std::{any::TypeId, borrow::Borrow, ops::Deref}; +use std::{any::TypeId, borrow::Borrow}; use std::{fmt, rc::Rc}; use web_sys::Element; @@ -41,7 +39,7 @@ pub(crate) fn get_event_log(vcomp_id: u64) -> Vec { }) } -/// A virtual component. +/// A virtual component. Compare with [VComp]. pub struct BComp { type_id: TypeId, scope: Box, @@ -50,9 +48,7 @@ pub struct BComp { } impl BComp { - pub(crate) fn root_bnode(&self) -> Option + '_> { - self.scope.root_bnode() - } + /// Get the key of the underlying component pub(crate) fn key(&self) -> Option<&Key> { self.key.as_ref() } @@ -60,7 +56,11 @@ impl BComp { impl fmt::Debug for BComp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "BComp {{ root: {:?} }}", self.root_bnode().as_deref()) + write!( + f, + "BComp {{ root: {:?} }}", + self.scope.root_bnode().as_deref() + ) } } @@ -220,6 +220,7 @@ mod tests { Children, Component, Context, Html, NodeRef, Properties, }; use gloo_utils::document; + use std::ops::Deref; use web_sys::Node; #[cfg(feature = "wasm_test")] diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index fd2ab36c832..ea218000cf8 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -1,6 +1,5 @@ -//! This module contains fragments bundles. -use super::test_log; -use super::BNode; +//! This module contains fragments bundles, a [BList] +use super::{test_log, BNode}; use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::{Key, VList, VNode, VText}; @@ -10,7 +9,7 @@ use std::hash::Hash; use std::ops::Deref; use web_sys::Element; -/// This struct represents a fragment of the Virtual DOM tree. +/// This struct represents a mounted [VList] #[derive(Debug)] pub struct BList { /// The reverse (render order) list of child [BNode]s @@ -28,13 +27,15 @@ impl Deref for BList { } } -struct ElementWriter<'s> { +/// Helper struct, that keeps the position where the next element is to be placed at +struct NodeWriter<'s> { parent_scope: &'s AnyScope, parent: &'s Element, next_sibling: NodeRef, } -impl<'s> ElementWriter<'s> { +impl<'s> NodeWriter<'s> { + /// Write a new node that has no ancestor fn add(self, node: VNode) -> (Self, BNode) { test_log!("adding: {:?}", node); test_log!( @@ -53,10 +54,12 @@ impl<'s> ElementWriter<'s> { ) } + /// Shift a bundle into place without patching it fn shift(&self, bundle: &mut BNode) { bundle.shift(self.parent, self.next_sibling.clone()); } + /// Patch a bundle with a new node fn patch(self, node: VNode, bundle: &mut BNode) -> Self { test_log!("patching: {:?} -> {:?}", bundle, node); test_log!( @@ -73,7 +76,7 @@ impl<'s> ElementWriter<'s> { } } } - +/// Helper struct implementing [Eq] and [Hash] by only looking at a node's key struct KeyedEntry(BNode, usize); impl Borrow for KeyedEntry { fn borrow(&self) -> &Key { @@ -93,6 +96,7 @@ impl PartialEq for KeyedEntry { impl Eq for KeyedEntry {} impl BNode { + /// Assert that a bundle node is a list, or convert it to a list with a single child fn make_list(&mut self) -> &mut BList { match self { Self::BList(blist) => blist, @@ -113,6 +117,7 @@ impl BNode { } impl BList { + /// Create a new empty [BList] pub(crate) const fn new() -> BList { BList { rev_children: vec![], @@ -121,6 +126,7 @@ impl BList { } } + /// Get the key of the underlying fragment pub(crate) fn key(&self) -> Option<&Key> { self.key.as_ref() } @@ -133,7 +139,7 @@ impl BList { lefts: Vec, rights: &mut Vec, ) -> NodeRef { - let mut writer = ElementWriter { + let mut writer = NodeWriter { parent_scope, parent, next_sibling, @@ -206,7 +212,7 @@ impl BList { // We partially drain the new vnodes in several steps. let mut lefts = left_vdoms; - let mut writer = ElementWriter { + let mut writer = NodeWriter { parent_scope, parent, next_sibling, @@ -265,7 +271,7 @@ impl BList { replacements.push(bundle); } // drop the splice iterator and immediately replace the range with the reordered elements - std::mem::drop(spliced_middle); + drop(spliced_middle); rev_bundles.splice(matching_len_end..matching_len_end, replacements); // Step 2.3. Remove any extra rights diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 6a9e75041db..58a3d318326 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -1,4 +1,4 @@ -//! This module contains the bundle version of an abstract node. +//! This module contains the bundle version of an abstract node [BNode] use super::{BComp, BList, BPortal, BSuspense, BTag, BText}; use crate::dom_bundle::{DomBundle, Reconcilable}; @@ -8,7 +8,7 @@ use gloo::console; use std::fmt; use web_sys::{Element, Node}; -/// Bind virtual element to a DOM reference. +/// The bundle implementation to [VNode]. pub enum BNode { /// A bind between `VTag` and `Element`. BTag(Box), @@ -27,6 +27,7 @@ pub enum BNode { } impl BNode { + /// Get the key of the underlying node pub(crate) fn key(&self) -> Option<&Key> { match self { Self::BComp(bsusp) => bsusp.key(), @@ -207,6 +208,7 @@ impl fmt::Debug for BNode { } impl BNode { + /// Replace this node with a new node, making sure to detach the ancestor pub(crate) fn replace(&mut self, parent: &Element, next_node: BNode) { let ancestor = std::mem::replace(self, next_node); ancestor.detach(parent); diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index 0217cbebe69..f0eb2438802 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -1,4 +1,4 @@ -//! This module contains the bundle implementation of a portal `BPortal`. +//! This module contains the bundle implementation of a portal [BPortal]. use super::test_log; use super::BNode; @@ -93,6 +93,7 @@ impl Reconcilable for VPortal { } impl BPortal { + /// Get the key of the underlying portal pub(crate) fn key(&self) -> Option<&Key> { self.node.key() } diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 1020a39a7e2..f10c8db1bcc 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -1,25 +1,28 @@ +//! This module contains the bundle version of a supsense [BSuspense] + use super::{BNode, DomBundle, Reconcilable}; use crate::html::AnyScope; use crate::virtual_dom::{Key, VSuspense}; use crate::NodeRef; use web_sys::Element; -/// This struct represents a suspendable DOM fragment. +/// The bundle implementation to [VSuspense] #[derive(Debug)] pub struct BSuspense { children: BNode, - // suspended if fallback is Some + /// The supsense is suspended if fallback contains [Some] bundle fallback: Option, detached_parent: Element, key: Option, } impl BSuspense { + /// Get the key of the underlying suspense pub(crate) fn key(&self) -> Option<&Key> { self.key.as_ref() } - - pub(crate) fn active_node(&self) -> &BNode { + /// Get the bundle node that actually shows up in the dom + fn active_node(&self) -> &BNode { self.fallback.as_ref().unwrap_or(&self.children) } } diff --git a/packages/yew/src/dom_bundle/btag.rs b/packages/yew/src/dom_bundle/btag.rs index 30d88c1edb7..40e3f69fe06 100644 --- a/packages/yew/src/dom_bundle/btag.rs +++ b/packages/yew/src/dom_bundle/btag.rs @@ -1,4 +1,4 @@ -//! This module contains the bundle implementation of a tag `BTag`. +//! This module contains the bundle implementation of a tag [BTag] use super::listeners::ListenerRegistration; use super::{BNode, DomBundle, InputFields, Reconcilable, Value}; @@ -51,7 +51,6 @@ pub struct BTag { } impl DomBundle for BTag { - /// Remove VTag from parent. fn detach(self, parent: &Element) { self.listeners.unregister(); @@ -129,8 +128,7 @@ impl Reconcilable for VTag { }, ) } - /// Renders virtual tag over DOM [Element], but it also compares this with an ancestor [VTag] - /// to compute what to patch in the actual DOM nodes. + fn reconcile( self, parent_scope: &AnyScope, @@ -226,6 +224,7 @@ impl VTag { } impl BTag { + /// Get the key of the underlying tag pub(crate) fn key(&self) -> Option<&Key> { self.key.as_ref() } diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index 40004c681c2..23187470d8a 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -8,15 +8,13 @@ use gloo::console; use gloo_utils::document; use web_sys::{Element, Text as TextNode}; -/// Bind text to a dom element. -/// Reuses the virtual dom structure of text. +/// The bundle implementation to [VText] pub struct BText { text: AttrValue, text_node: TextNode, } impl DomBundle for BText { - /// Remove VText from parent. fn detach(self, parent: &Element) { let node = &self.text_node; if parent.remove_child(node).is_err() { diff --git a/packages/yew/src/dom_bundle/listeners.rs b/packages/yew/src/dom_bundle/listeners.rs index fde7171cb10..81adc56b6dd 100644 --- a/packages/yew/src/dom_bundle/listeners.rs +++ b/packages/yew/src/dom_bundle/listeners.rs @@ -36,8 +36,9 @@ pub fn set_event_bubbling(bubble: bool) { BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); } +/// An active set of listeners on an element #[derive(Debug)] -pub(crate) enum ListenerRegistration { +pub(super) enum ListenerRegistration { /// No listeners registered. NoReg, /// Added to global registry by ID diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 64fe17f6cbb..40764af7e09 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -26,7 +26,8 @@ pub use self::bsuspense::BSuspense; pub use self::btag::BTag; pub use self::btext::BText; -pub(crate) use self::attributes::{Apply, InputFields, Value}; +pub(self) use self::attributes::Apply; +pub(crate) use self::attributes::{InputFields, Value}; pub(crate) use self::bcomp::{Mountable, PropsWrapper}; #[doc(hidden)] pub use self::listeners::set_event_bubbling; @@ -52,6 +53,15 @@ pub(crate) trait DomBundle { pub(crate) trait Reconcilable { type Bundle: DomBundle; + /// Attach a virtual node to the DOM tree. + /// + /// Parameters: + /// - `parent_scope`: the parent `Scope` used for passing messages to the + /// parent `Component`. + /// - `parent`: the parent node in the DOM. + /// - `next_sibling`: to find where to put the node. + /// + /// Returns a reference to the newly inserted element. fn attach( self, parent_scope: &AnyScope, @@ -85,6 +95,7 @@ pub(crate) trait Reconcilable { ) -> NodeRef; } +/// Insert a concrete [Node] into the DOM pub(crate) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) { match next_sibling { Some(next_sibling) => parent @@ -94,8 +105,6 @@ pub(crate) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&N }; } -/// Log an operation during tests for debugging purposes -/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. #[cfg(all(test, feature = "wasm_test", verbose_tests))] macro_rules! test_log { ($fmt:literal, $($arg:expr),* $(,)?) => { @@ -108,4 +117,6 @@ macro_rules! test_log { let _ = std::format_args!(concat!("\t ", $fmt), $($arg),*); }; } +/// Log an operation during tests for debugging purposes +/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. pub(self) use test_log; diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 4589c59bfc9..5714cce5f8c 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -115,8 +115,11 @@ impl AnyScope { pub(crate) trait Scoped { fn to_any(&self) -> AnyScope; + /// Got the root node if it hasn't already been destroyed fn root_bnode(&self) -> Option>; + /// Shift the node associated with this scope to a new place fn shift_node(&self, parent: Element, next_sibling: NodeRef); + /// Process an event to destroy a component fn destroy(self); fn destroy_boxed(self: Box); } @@ -137,7 +140,6 @@ impl Scoped for Scope { })) } - /// Process an event to destroy a component fn destroy(self) { scheduler::push_component_destroy(DestroyRunner { state: self.state }); // Not guaranteed to already have the scheduler started @@ -159,11 +161,11 @@ impl Scoped for Scope { /// A context which allows sending messages to a component. pub struct Scope { parent: Option>, - pub(crate) state: Shared>>, + pub(super) state: Shared>>, // Used for debug logging #[cfg(debug_assertions)] - pub(crate) vcomp_id: u64, + pub(super) vcomp_id: u64, } impl fmt::Debug for Scope { From a95bf13debbba5494e9672d59cb040d15fd42bbe Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Sat, 15 Jan 2022 13:04:36 +0100 Subject: [PATCH 13/26] address review comments rename fields in bsuspense convert to gloo::events --- packages/yew/src/dom_bundle/bsuspense.rs | 41 +++++++++--------- packages/yew/src/dom_bundle/listeners.rs | 51 ++++++----------------- packages/yew/src/virtual_dom/listeners.rs | 13 ++++-- 3 files changed, 45 insertions(+), 60 deletions(-) diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index f10c8db1bcc..93503922f19 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -9,9 +9,9 @@ use web_sys::Element; /// The bundle implementation to [VSuspense] #[derive(Debug)] pub struct BSuspense { - children: BNode, + children_bundle: BNode, /// The supsense is suspended if fallback contains [Some] bundle - fallback: Option, + fallback_bundle: Option, detached_parent: Element, key: Option, } @@ -23,17 +23,19 @@ impl BSuspense { } /// Get the bundle node that actually shows up in the dom fn active_node(&self) -> &BNode { - self.fallback.as_ref().unwrap_or(&self.children) + self.fallback_bundle + .as_ref() + .unwrap_or(&self.children_bundle) } } impl DomBundle for BSuspense { fn detach(self, parent: &Element) { - if let Some(fallback) = self.fallback { + if let Some(fallback) = self.fallback_bundle { fallback.detach(parent); - self.children.detach(&self.detached_parent); + self.children_bundle.detach(&self.detached_parent); } else { - self.children.detach(parent); + self.children_bundle.detach(parent); } } @@ -63,25 +65,25 @@ impl Reconcilable for VSuspense { // When it's suspended, we render children into an element that is detached from the dom // tree while rendering fallback UI into the original place where children resides in. if suspended { - let (_child_ref, children) = + let (_child_ref, children_bundle) = children.attach(parent_scope, &detached_parent, NodeRef::default()); let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling); ( fallback_ref, BSuspense { - children, - fallback: Some(fallback), + children_bundle, + fallback_bundle: Some(fallback), detached_parent, key, }, ) } else { - let (child_ref, children) = children.attach(parent_scope, parent, next_sibling); + let (child_ref, children_bundle) = children.attach(parent_scope, parent, next_sibling); ( child_ref, BSuspense { - children, - fallback: None, + children_bundle, + fallback_bundle: None, detached_parent, key, }, @@ -119,12 +121,13 @@ impl Reconcilable for VSuspense { } = self; let detached_parent = detached_parent.expect("no detached parent?"); - let children_bundle = &mut suspense.children; + let children_bundle = &mut suspense.children_bundle; // no need to update key & detached_parent // When it's suspended, we render children into an element that is detached from the dom // tree while rendering fallback UI into the original place where children resides in. - match (suspended, &mut suspense.fallback) { + match (suspended, &mut suspense.fallback_bundle) { + // Both suspended, reconcile children into detached_parent, fallback into the DOM (true, Some(fallback_bundle)) => { children.reconcile( parent_scope, @@ -135,11 +138,11 @@ impl Reconcilable for VSuspense { fallback.reconcile(parent_scope, parent, next_sibling, fallback_bundle) } - + // Not suspended, just reconcile the children into the DOM (false, None) => { children.reconcile(parent_scope, parent, next_sibling, children_bundle) } - + // Freshly suspended. Shift children into the detached parent, then add fallback to the DOM (true, None) => { children_bundle.shift(&detached_parent, NodeRef::default()); @@ -151,12 +154,12 @@ impl Reconcilable for VSuspense { ); // first render of fallback let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling); - suspense.fallback = Some(fallback); + suspense.fallback_bundle = Some(fallback); fallback_ref } - + // Freshly unsuspended. Detach fallback from the DOM, then shift children into it. (false, Some(_)) => { - suspense.fallback.take().unwrap().detach(parent); + suspense.fallback_bundle.take().unwrap().detach(parent); children_bundle.shift(parent, next_sibling.clone()); children.reconcile(parent_scope, parent, next_sibling, children_bundle) diff --git a/packages/yew/src/dom_bundle/listeners.rs b/packages/yew/src/dom_bundle/listeners.rs index 81adc56b6dd..adf2183acc6 100644 --- a/packages/yew/src/dom_bundle/listeners.rs +++ b/packages/yew/src/dom_bundle/listeners.rs @@ -1,11 +1,12 @@ use crate::dom_bundle::test_log; use crate::virtual_dom::{Listener, ListenerKind, Listeners}; +use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::ops::Deref; use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; -use wasm_bindgen::{prelude::Closure, JsCast}; +use wasm_bindgen::JsCast; use web_sys::{Element, Event}; thread_local! { @@ -136,8 +137,7 @@ struct GlobalHandlers { /// Keep track of all listeners to drop them on registry drop. /// The registry is never dropped in production. #[cfg(test)] - #[allow(clippy::type_complexity)] - registered: Vec<(ListenerKind, Closure)>, + registered: Vec<(ListenerKind, EventListener)>, } impl GlobalHandlers { @@ -145,24 +145,16 @@ impl GlobalHandlers { fn ensure_handled(&mut self, desc: EventDescriptor) { if !self.handling.contains(&desc) { let cl = BODY.with(|body| { - let cl = Closure::wrap( - Box::new(move |e: Event| Registry::handle(desc, e)) as Box - ); - AsRef::::as_ref(body) - .add_event_listener_with_callback_and_add_event_listener_options( - &desc.kind.as_ref()[2..], - cl.as_ref().unchecked_ref(), - &{ - let mut opts = web_sys::AddEventListenerOptions::new(); - opts.capture(true); - // We need to explicitly set passive to override any browser defaults - opts.passive(desc.passive); - opts - }, - ) - .map_err(|e| format!("could not register global listener: {:?}", e)) - .unwrap(); - cl + let options = EventListenerOptions { + phase: EventListenerPhase::Capture, + passive: desc.passive, + }; + EventListener::new_with_options( + body, + &desc.kind.event_type()[2..], + options, + move |e: &Event| Registry::handle(desc, e.clone()), + ) }); // Never drop the closure as this event handler is static @@ -176,23 +168,6 @@ impl GlobalHandlers { } } -// Enable resetting between tests -#[cfg(test)] -impl Drop for GlobalHandlers { - fn drop(&mut self) { - BODY.with(|body| { - for (kind, cl) in std::mem::take(&mut self.registered) { - AsRef::::as_ref(body) - .remove_event_listener_with_callback( - &kind.as_ref()[2..], - cl.as_ref().unchecked_ref(), - ) - .unwrap(); - } - }); - } -} - /// Global multiplexing event handler registry #[derive(Default, Debug)] struct Registry { diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs index ed27ba86903..a5241f0c42d 100644 --- a/packages/yew/src/virtual_dom/listeners.rs +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -37,13 +37,20 @@ macro_rules! gen_listener_kinds { $( $kind, )* } - impl AsRef for ListenerKind { - fn as_ref(&self) -> &str { + impl ListenerKind { + /// Get the case-sensitive string representing the event type + pub fn event_type(&self) -> &'static str { match self { - $( Self::$kind => stringify!($kind), )* + $( ListenerKind::$kind => stringify!($kind), )* } } } + + impl AsRef for ListenerKind { + fn as_ref(&self) -> &str { + self.event_type() + } + } }; } From 3563f396ffc9b9be7bf953db5be86ac864985b15 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Sat, 15 Jan 2022 17:30:17 +0100 Subject: [PATCH 14/26] move even more stuff into dom_bundle to scope exports - app_handle and layout_tests are now in there - items are publically re-exported in crate::dom_bundle - dom_bundle itself is private - btag and bcomp get their own submodules - bcomp now contains the lifecycle and scope impls --- .../yew/src/{ => dom_bundle}/app_handle.rs | 13 +- .../yew/src/dom_bundle/bcomp/bcomp_impl.rs | 213 +++++++++++++ .../bcomp}/lifecycle.rs | 64 ++-- .../src/dom_bundle/{bcomp.rs => bcomp/mod.rs} | 221 +------------- .../component => dom_bundle/bcomp}/scope.rs | 32 +- packages/yew/src/dom_bundle/blist.rs | 4 +- packages/yew/src/dom_bundle/bnode.rs | 15 +- packages/yew/src/dom_bundle/bportal.rs | 2 +- packages/yew/src/dom_bundle/bsuspense.rs | 2 +- .../src/dom_bundle/{ => btag}/attributes.rs | 37 +-- packages/yew/src/dom_bundle/btag/btag_impl.rs | 257 ++++++++++++++++ .../src/dom_bundle/{ => btag}/listeners.rs | 3 +- .../src/dom_bundle/{btag.rs => btag/mod.rs} | 289 ++---------------- packages/yew/src/dom_bundle/mod.rs | 44 +-- .../{ => dom_bundle}/tests/layout_tests.rs | 0 packages/yew/src/dom_bundle/tests/mod.rs | 26 ++ packages/yew/src/html/component/mod.rs | 5 +- packages/yew/src/lib.rs | 14 +- packages/yew/src/tests/mod.rs | 1 - packages/yew/src/virtual_dom/vnode.rs | 26 -- packages/yew/src/virtual_dom/vtag.rs | 6 +- 21 files changed, 651 insertions(+), 623 deletions(-) rename packages/yew/src/{ => dom_bundle}/app_handle.rs (84%) create mode 100644 packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs rename packages/yew/src/{html/component => dom_bundle/bcomp}/lifecycle.rs (91%) rename packages/yew/src/dom_bundle/{bcomp.rs => bcomp/mod.rs} (73%) rename packages/yew/src/{html/component => dom_bundle/bcomp}/scope.rs (95%) rename packages/yew/src/dom_bundle/{ => btag}/attributes.rs (92%) create mode 100644 packages/yew/src/dom_bundle/btag/btag_impl.rs rename packages/yew/src/dom_bundle/{ => btag}/listeners.rs (99%) rename packages/yew/src/dom_bundle/{btag.rs => btag/mod.rs} (70%) rename packages/yew/src/{ => dom_bundle}/tests/layout_tests.rs (100%) create mode 100644 packages/yew/src/dom_bundle/tests/mod.rs delete mode 100644 packages/yew/src/tests/mod.rs diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/dom_bundle/app_handle.rs similarity index 84% rename from packages/yew/src/app_handle.rs rename to packages/yew/src/dom_bundle/app_handle.rs index 9f37bfa79e8..4b73f438ce4 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/dom_bundle/app_handle.rs @@ -1,17 +1,16 @@ -//! This module contains the `App` struct, which is used to bootstrap -//! a component in an isolated scope. +//! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope. -use std::ops::Deref; - -use crate::html::{BaseComponent, NodeRef, Scope, Scoped}; -use std::rc::Rc; +use super::Scope; +use super::Scoped; +use crate::{html::BaseComponent, NodeRef}; +use std::{ops::Deref, rc::Rc}; use web_sys::Element; /// An instance of an application. #[derive(Debug)] pub struct AppHandle { /// `Scope` holder - pub(crate) scope: Scope, + scope: Scope, } impl AppHandle diff --git a/packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs b/packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs new file mode 100644 index 00000000000..6eaacf08e07 --- /dev/null +++ b/packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs @@ -0,0 +1,213 @@ +//! This module contains the bundle implementation of a virtual component [BComp]. + +use super::Scoped; +use crate::dom_bundle::{BNode, DomBundle, Reconcilable}; +use crate::html::{AnyScope, BaseComponent, Scope}; +use crate::virtual_dom::{Key, VComp}; +use crate::NodeRef; +#[cfg(feature = "ssr")] +use futures::future::{FutureExt, LocalBoxFuture}; +use std::{any::TypeId, borrow::Borrow}; +use std::{fmt, rc::Rc}; +use web_sys::Element; + +thread_local! { + #[cfg(debug_assertions)] + static EVENT_HISTORY: std::cell::RefCell>> + = Default::default(); +} + +/// Push [VComp] event to lifecycle debugging registry +#[cfg(debug_assertions)] +pub fn log_event(vcomp_id: u64, event: impl ToString) { + EVENT_HISTORY.with(|h| { + h.borrow_mut() + .entry(vcomp_id) + .or_default() + .push(event.to_string()) + }); +} + +/// Get [VComp] event log from lifecycle debugging registry +#[cfg(debug_assertions)] +#[allow(dead_code)] +pub fn get_event_log(vcomp_id: u64) -> Vec { + EVENT_HISTORY.with(|h| { + h.borrow() + .get(&vcomp_id) + .map(|l| (*l).clone()) + .unwrap_or_default() + }) +} + +/// A virtual component. Compare with [VComp]. +pub struct BComp { + type_id: TypeId, + scope: Box, + node_ref: NodeRef, + key: Option, +} + +impl BComp { + /// Get the key of the underlying component + pub(in crate::dom_bundle) fn key(&self) -> Option<&Key> { + self.key.as_ref() + } +} + +impl fmt::Debug for BComp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "BComp {{ root: {:?} }}", + self.scope.root_bnode().as_deref() + ) + } +} + +pub trait Mountable { + fn copy(&self) -> Box; + fn mount( + self: Box, + node_ref: NodeRef, + parent_scope: &AnyScope, + parent: Element, + next_sibling: NodeRef, + ) -> Box; + fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); + + #[cfg(feature = "ssr")] + fn render_to_string<'a>( + &'a self, + w: &'a mut String, + parent_scope: &'a AnyScope, + ) -> LocalBoxFuture<'a, ()>; +} + +pub struct PropsWrapper { + props: Rc, +} + +impl PropsWrapper { + pub fn new(props: Rc) -> Self { + Self { props } + } +} + +impl Mountable for PropsWrapper { + fn copy(&self) -> Box { + let wrapper: PropsWrapper = PropsWrapper { + props: Rc::clone(&self.props), + }; + Box::new(wrapper) + } + + fn mount( + self: Box, + node_ref: NodeRef, + parent_scope: &AnyScope, + parent: Element, + next_sibling: NodeRef, + ) -> Box { + let scope: Scope = Scope::new(Some(parent_scope.clone())); + scope.mount_in_place(parent, next_sibling, node_ref, self.props); + + Box::new(scope) + } + + fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { + let scope: Scope = scope.to_any().downcast(); + scope.reuse(self.props, node_ref, next_sibling); + } + + #[cfg(feature = "ssr")] + fn render_to_string<'a>( + &'a self, + w: &'a mut String, + parent_scope: &'a AnyScope, + ) -> LocalBoxFuture<'a, ()> { + async move { + let scope: Scope = Scope::new(Some(parent_scope.clone())); + scope.render_to_string(w, self.props.clone()).await; + } + .boxed_local() + } +} + +impl DomBundle for BComp { + fn detach(self, _parent: &Element) { + self.scope.destroy_boxed(); + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + self.scope.shift_node(next_parent.clone(), next_sibling); + } +} + +impl Reconcilable for VComp { + type Bundle = BComp; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let VComp { + type_id, + mountable, + node_ref, + key, + } = self; + + let scope = mountable.mount( + node_ref.clone(), + parent_scope, + parent.to_owned(), + next_sibling, + ); + + ( + node_ref.clone(), + BComp { + type_id, + node_ref, + key, + scope, + }, + ) + } + + fn reconcile( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut BNode, + ) -> NodeRef { + let bcomp = match bundle { + // If the existing bundle is the same type, reuse it and update its properties + BNode::BComp(ref mut bcomp) + if self.type_id == bcomp.type_id && self.key == bcomp.key => + { + bcomp + } + _ => { + let (node_ref, self_) = self.attach(parent_scope, parent, next_sibling); + bundle.replace(parent, self_.into()); + return node_ref; + } + }; + let VComp { + mountable, + node_ref, + key, + type_id: _, + } = self; + bcomp.key = key; + let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref.clone()); + bcomp.node_ref.reuse(old_ref); + mountable.reuse(node_ref.clone(), bcomp.scope.borrow(), next_sibling); + node_ref + } +} diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/dom_bundle/bcomp/lifecycle.rs similarity index 91% rename from packages/yew/src/html/component/lifecycle.rs rename to packages/yew/src/dom_bundle/bcomp/lifecycle.rs index c443394fb71..da664eaa819 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/dom_bundle/bcomp/lifecycle.rs @@ -1,8 +1,8 @@ //! Component lifecycle module -use super::{AnyScope, BaseComponent, Scope}; +use super::scope::{AnyScope, Scope}; use crate::dom_bundle::{BNode, DomBundle, Reconcilable}; -use crate::html::RenderError; +use crate::html::{BaseComponent, RenderError}; use crate::scheduler::{self, Runnable, Shared}; use crate::suspense::{Suspense, Suspension}; #[cfg(feature = "ssr")] @@ -14,9 +14,9 @@ use futures::channel::oneshot; use std::rc::Rc; use web_sys::Element; -pub(crate) struct ComponentState { - pub(crate) component: Box, - pub(crate) root_node: BNode, +pub struct ComponentState { + pub(super) component: Box, + pub(super) root_node: BNode, context: Context, @@ -34,11 +34,11 @@ pub(crate) struct ComponentState { // Used for debug logging #[cfg(debug_assertions)] - pub(crate) vcomp_id: u64, + pub(super) vcomp_id: u64, } impl ComponentState { - pub(crate) fn new( + fn new( parent: Option, next_sibling: NodeRef, root_node: BNode, @@ -49,7 +49,7 @@ impl ComponentState { ) -> Self { #[cfg(debug_assertions)] let vcomp_id = { - use super::Scoped; + use super::scope::Scoped; scope.to_any().vcomp_id }; @@ -75,15 +75,15 @@ impl ComponentState { } } -pub(crate) struct CreateRunner { - pub(crate) parent: Option, - pub(crate) next_sibling: NodeRef, - pub(crate) placeholder: BNode, - pub(crate) node_ref: NodeRef, - pub(crate) props: Rc, - pub(crate) scope: Scope, +pub struct CreateRunner { + pub(super) parent: Option, + pub(super) next_sibling: NodeRef, + pub(super) placeholder: BNode, + pub(super) node_ref: NodeRef, + pub(super) props: Rc, + pub(super) scope: Scope, #[cfg(feature = "ssr")] - pub(crate) html_sender: Option>, + pub(super) html_sender: Option>, } impl Runnable for CreateRunner { @@ -91,7 +91,7 @@ impl Runnable for CreateRunner { let mut current_state = self.scope.state.borrow_mut(); if current_state.is_none() { #[cfg(debug_assertions)] - crate::dom_bundle::log_event(self.scope.vcomp_id, "create"); + super::log_event(self.scope.vcomp_id, "create"); *current_state = Some(ComponentState::new( self.parent, @@ -107,7 +107,7 @@ impl Runnable for CreateRunner { } } -pub(crate) enum UpdateEvent { +pub enum UpdateEvent { /// Wraps messages for a component. Message(COMP::Message), /// Wraps batch of messages for a component. @@ -118,9 +118,9 @@ pub(crate) enum UpdateEvent { Shift(Element, NodeRef), } -pub(crate) struct UpdateRunner { - pub(crate) state: Shared>>, - pub(crate) event: UpdateEvent, +pub struct UpdateRunner { + pub(super) state: Shared>>, + pub(super) event: UpdateEvent, } impl Runnable for UpdateRunner { @@ -157,7 +157,7 @@ impl Runnable for UpdateRunner { }; #[cfg(debug_assertions)] - crate::dom_bundle::log_event( + super::log_event( state.vcomp_id, format!("update(schedule_render={})", schedule_render), ); @@ -178,15 +178,15 @@ impl Runnable for UpdateRunner { } } -pub(crate) struct DestroyRunner { - pub(crate) state: Shared>>, +pub struct DestroyRunner { + pub(super) state: Shared>>, } impl Runnable for DestroyRunner { fn run(self: Box) { if let Some(mut state) = self.state.borrow_mut().take() { #[cfg(debug_assertions)] - crate::dom_bundle::log_event(state.vcomp_id, "destroy"); + super::log_event(state.vcomp_id, "destroy"); state.component.destroy(&state.context); @@ -198,15 +198,15 @@ impl Runnable for DestroyRunner { } } -pub(crate) struct RenderRunner { - pub(crate) state: Shared>>, +pub struct RenderRunner { + pub(super) state: Shared>>, } impl Runnable for RenderRunner { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { #[cfg(debug_assertions)] - crate::dom_bundle::log_event(state.vcomp_id, "render"); + super::log_event(state.vcomp_id, "render"); match state.component.view(&state.context) { Ok(root) => { @@ -291,15 +291,15 @@ impl Runnable for RenderRunner { } } -pub(crate) struct RenderedRunner { - pub(crate) state: Shared>>, +pub struct RenderedRunner { + pub(super) state: Shared>>, } impl Runnable for RenderedRunner { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { #[cfg(debug_assertions)] - crate::dom_bundle::log_event(state.vcomp_id, "rendered"); + super::log_event(state.vcomp_id, "rendered"); if state.suspension.is_none() && state.parent.is_some() { let first_render = !state.has_rendered; @@ -317,7 +317,9 @@ mod tests { use crate::html; use crate::html::*; use crate::Properties; + use std::cell::RefCell; use std::ops::Deref; + use std::rc::Rc; #[cfg(feature = "wasm_test")] use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp/mod.rs similarity index 73% rename from packages/yew/src/dom_bundle/bcomp.rs rename to packages/yew/src/dom_bundle/bcomp/mod.rs index 5ae0277dfce..d8434311811 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp/mod.rs @@ -1,226 +1,25 @@ -//! This module contains the bundle implementation of a virtual component [BComp]. - -use super::{BNode, DomBundle, Reconcilable}; -use crate::html::{AnyScope, BaseComponent, Scope, Scoped}; -use crate::virtual_dom::{Key, VComp}; -use crate::NodeRef; -#[cfg(feature = "ssr")] -use futures::future::{FutureExt, LocalBoxFuture}; -use std::{any::TypeId, borrow::Borrow}; -use std::{fmt, rc::Rc}; -use web_sys::Element; - -thread_local! { - #[cfg(debug_assertions)] - static EVENT_HISTORY: std::cell::RefCell>> - = Default::default(); -} - -/// Push [VComp] event to lifecycle debugging registry -#[cfg(debug_assertions)] -pub(crate) fn log_event(vcomp_id: u64, event: impl ToString) { - EVENT_HISTORY.with(|h| { - h.borrow_mut() - .entry(vcomp_id) - .or_default() - .push(event.to_string()) - }); -} +mod bcomp_impl; +mod lifecycle; +mod scope; -/// Get [VComp] event log from lifecycle debugging registry #[cfg(debug_assertions)] -#[allow(dead_code)] -pub(crate) fn get_event_log(vcomp_id: u64) -> Vec { - EVENT_HISTORY.with(|h| { - h.borrow() - .get(&vcomp_id) - .map(|l| (*l).clone()) - .unwrap_or_default() - }) -} - -/// A virtual component. Compare with [VComp]. -pub struct BComp { - type_id: TypeId, - scope: Box, - node_ref: NodeRef, - key: Option, -} - -impl BComp { - /// Get the key of the underlying component - pub(crate) fn key(&self) -> Option<&Key> { - self.key.as_ref() - } -} - -impl fmt::Debug for BComp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "BComp {{ root: {:?} }}", - self.scope.root_bnode().as_deref() - ) - } -} - -pub(crate) trait Mountable { - fn copy(&self) -> Box; - fn mount( - self: Box, - node_ref: NodeRef, - parent_scope: &AnyScope, - parent: Element, - next_sibling: NodeRef, - ) -> Box; - fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); - - #[cfg(feature = "ssr")] - fn render_to_string<'a>( - &'a self, - w: &'a mut String, - parent_scope: &'a AnyScope, - ) -> LocalBoxFuture<'a, ()>; -} - -pub(crate) struct PropsWrapper { - props: Rc, -} - -impl PropsWrapper { - pub fn new(props: Rc) -> Self { - Self { props } - } -} - -impl Mountable for PropsWrapper { - fn copy(&self) -> Box { - let wrapper: PropsWrapper = PropsWrapper { - props: Rc::clone(&self.props), - }; - Box::new(wrapper) - } +pub(self) use bcomp_impl::log_event; - fn mount( - self: Box, - node_ref: NodeRef, - parent_scope: &AnyScope, - parent: Element, - next_sibling: NodeRef, - ) -> Box { - let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.mount_in_place(parent, next_sibling, node_ref, self.props); - - Box::new(scope) - } - - fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { - let scope: Scope = scope.to_any().downcast(); - scope.reuse(self.props, node_ref, next_sibling); - } - - #[cfg(feature = "ssr")] - fn render_to_string<'a>( - &'a self, - w: &'a mut String, - parent_scope: &'a AnyScope, - ) -> LocalBoxFuture<'a, ()> { - async move { - let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.render_to_string(w, self.props.clone()).await; - } - .boxed_local() - } -} - -impl DomBundle for BComp { - fn detach(self, _parent: &Element) { - self.scope.destroy_boxed(); - } - - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - self.scope.shift_node(next_parent.clone(), next_sibling); - } -} - -impl Reconcilable for VComp { - type Bundle = BComp; - - fn attach( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { - let VComp { - type_id, - mountable, - node_ref, - key, - } = self; - - let scope = mountable.mount( - node_ref.clone(), - parent_scope, - parent.to_owned(), - next_sibling, - ); - - ( - node_ref.clone(), - BComp { - type_id, - node_ref, - key, - scope, - }, - ) - } - - fn reconcile( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - bundle: &mut BNode, - ) -> NodeRef { - let bcomp = match bundle { - // If the existing bundle is the same type, reuse it and update its properties - BNode::BComp(ref mut bcomp) - if self.type_id == bcomp.type_id && self.key == bcomp.key => - { - bcomp - } - _ => { - let (node_ref, self_) = self.attach(parent_scope, parent, next_sibling); - bundle.replace(parent, self_.into()); - return node_ref; - } - }; - let VComp { - mountable, - node_ref, - key, - type_id: _, - } = self; - bcomp.key = key; - let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref.clone()); - bcomp.node_ref.reuse(old_ref); - mountable.reuse(node_ref.clone(), bcomp.scope.borrow(), next_sibling); - node_ref - } -} +pub use bcomp_impl::{BComp, Mountable, PropsWrapper}; +pub use scope::{AnyScope, Scope, Scoped, SendAsMessage}; #[cfg(test)] mod tests { use super::*; + use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::{ html, - virtual_dom::{VChild, VNode}, + virtual_dom::{Key, VChild, VNode}, Children, Component, Context, Html, NodeRef, Properties, }; use gloo_utils::document; use std::ops::Deref; + use web_sys::Element; use web_sys::Node; #[cfg(feature = "wasm_test")] @@ -413,8 +212,6 @@ mod tests { } } - use super::{AnyScope, Element}; - fn setup_parent() -> (AnyScope, Element) { let scope = AnyScope::test(); let parent = document().create_element("div").unwrap(); diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/dom_bundle/bcomp/scope.rs similarity index 95% rename from packages/yew/src/html/component/scope.rs rename to packages/yew/src/dom_bundle/bcomp/scope.rs index 5714cce5f8c..9015b447205 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/dom_bundle/bcomp/scope.rs @@ -1,15 +1,12 @@ //! Component scope module -use super::{ - lifecycle::{ - ComponentState, CreateRunner, DestroyRunner, RenderRunner, RenderedRunner, UpdateEvent, - UpdateRunner, - }, - BaseComponent, +use super::lifecycle::{ + ComponentState, CreateRunner, DestroyRunner, RenderRunner, RenderedRunner, UpdateEvent, + UpdateRunner, }; use crate::context::{ContextHandle, ContextProvider}; use crate::dom_bundle::insert_node; -use crate::html::NodeRef; +use crate::html::{BaseComponent, NodeRef}; use crate::scheduler::{self, Shared}; use crate::{callback::Callback, dom_bundle::BNode}; use gloo_utils::document; @@ -29,7 +26,7 @@ pub struct AnyScope { // Used for debug logging #[cfg(debug_assertions)] - pub(crate) vcomp_id: u64, + pub(super) vcomp_id: u64, } impl From> for AnyScope { @@ -47,7 +44,7 @@ impl From> for AnyScope { impl AnyScope { #[cfg(test)] - pub(crate) fn test() -> Self { + pub(in crate::dom_bundle) fn test() -> Self { Self { type_id: TypeId::of::<()>(), parent: None, @@ -91,7 +88,7 @@ impl AnyScope { } } - pub(crate) fn find_parent_scope(&self) -> Option> { + pub(super) fn find_parent_scope(&self) -> Option> { let expected_type_id = TypeId::of::(); iter::successors(Some(self), |scope| scope.get_parent()) .filter(|scope| scope.get_type_id() == &expected_type_id) @@ -113,7 +110,7 @@ impl AnyScope { } } -pub(crate) trait Scoped { +pub trait Scoped { fn to_any(&self) -> AnyScope; /// Got the root node if it hasn't already been destroyed fn root_bnode(&self) -> Option>; @@ -202,6 +199,7 @@ impl Scope { }) } + /// Crate a scope with an optional parent scope pub(crate) fn new(parent: Option) -> Self { let parent = parent.map(Rc::new); let state = Rc::new(RefCell::new(None)); @@ -226,7 +224,7 @@ impl Scope { } /// Mounts a component with `props` to the specified `element` in the DOM. - pub(crate) fn mount_in_place( + pub(in crate::dom_bundle) fn mount_in_place( &self, parent: Element, next_sibling: NodeRef, @@ -234,7 +232,7 @@ impl Scope { props: Rc, ) { #[cfg(debug_assertions)] - crate::dom_bundle::log_event(self.vcomp_id, "create placeholder"); + super::log_event(self.vcomp_id, "create placeholder"); let placeholder = { let placeholder: Node = document().create_text_node("").into(); insert_node(&placeholder, &parent, next_sibling.get().as_ref()); @@ -264,14 +262,14 @@ impl Scope { scheduler::start(); } - pub(crate) fn reuse( + pub(super) fn reuse( &self, props: Rc, node_ref: NodeRef, next_sibling: NodeRef, ) { #[cfg(debug_assertions)] - crate::dom_bundle::log_event(self.vcomp_id, "reuse"); + super::log_event(self.vcomp_id, "reuse"); self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling)); } @@ -318,7 +316,7 @@ impl Scope { /// component's update method when invoked. /// /// Please be aware that currently the result of this callback - /// synchronously schedules a call to the [Component](Component) + /// synchronously schedules a call to the [Component](crate::html::Component) /// interface. pub fn callback(&self, function: F) -> Callback where @@ -347,7 +345,7 @@ impl Scope { /// /// Please be aware that currently the results of these callbacks /// will synchronously schedule calls to the - /// [Component](Component) interface. + /// [Component](crate::html::Component) interface. pub fn batch_callback(&self, function: F) -> Callback where F: Fn(IN) -> OUT + 'static, diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index ea218000cf8..7cc2c898716 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -118,7 +118,7 @@ impl BNode { impl BList { /// Create a new empty [BList] - pub(crate) const fn new() -> BList { + pub(super) const fn new() -> BList { BList { rev_children: vec![], fully_keyed: true, @@ -127,7 +127,7 @@ impl BList { } /// Get the key of the underlying fragment - pub(crate) fn key(&self) -> Option<&Key> { + pub(super) fn key(&self) -> Option<&Key> { self.key.as_ref() } diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 58a3d318326..fb13696309c 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -28,7 +28,7 @@ pub enum BNode { impl BNode { /// Get the key of the underlying node - pub(crate) fn key(&self) -> Option<&Key> { + pub(super) fn key(&self) -> Option<&Key> { match self { Self::BComp(bsusp) => bsusp.key(), Self::BList(blist) => blist.key(), @@ -39,6 +39,11 @@ impl BNode { Self::BSuspense(bsusp) => bsusp.key(), } } + /// Replace this node with a new node, making sure to detach the ancestor + pub(super) fn replace(&mut self, parent: &Element, next_node: BNode) { + let ancestor = std::mem::replace(self, next_node); + ancestor.detach(parent); + } } impl DomBundle for BNode { @@ -207,14 +212,6 @@ impl fmt::Debug for BNode { } } -impl BNode { - /// Replace this node with a new node, making sure to detach the ancestor - pub(crate) fn replace(&mut self, parent: &Element, next_node: BNode) { - let ancestor = std::mem::replace(self, next_node); - ancestor.detach(parent); - } -} - #[cfg(test)] mod layout_tests { use super::*; diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index f0eb2438802..58c8dd1575c 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -94,7 +94,7 @@ impl Reconcilable for VPortal { impl BPortal { /// Get the key of the underlying portal - pub(crate) fn key(&self) -> Option<&Key> { + pub(super) fn key(&self) -> Option<&Key> { self.node.key() } } diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 93503922f19..b8218f39f64 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -18,7 +18,7 @@ pub struct BSuspense { impl BSuspense { /// Get the key of the underlying suspense - pub(crate) fn key(&self) -> Option<&Key> { + pub(super) fn key(&self) -> Option<&Key> { self.key.as_ref() } /// Get the bundle node that actually shows up in the dom diff --git a/packages/yew/src/dom_bundle/attributes.rs b/packages/yew/src/dom_bundle/btag/attributes.rs similarity index 92% rename from packages/yew/src/dom_bundle/attributes.rs rename to packages/yew/src/dom_bundle/btag/attributes.rs index 18fc6cf1d23..8f7d9683bc4 100644 --- a/packages/yew/src/dom_bundle/attributes.rs +++ b/packages/yew/src/dom_bundle/btag/attributes.rs @@ -1,3 +1,4 @@ +use super::Apply; use crate::virtual_dom::{AttrValue, Attributes}; use indexmap::IndexMap; use std::{ @@ -10,7 +11,7 @@ use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as /// Value field corresponding to an [Element]'s `value` property #[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct Value(Option, PhantomData); +pub struct Value(Option, PhantomData); impl Default for Value { fn default() -> Self { @@ -20,11 +21,11 @@ impl Default for Value { impl Value { /// Create a new value. The caller should take care that the value is valid for the element's `value` property - pub(crate) fn new(value: Option) -> Self { + pub fn new(value: Option) -> Self { Value(value, PhantomData) } /// Set a new value. The caller should take care that the value is valid for the element's `value` property - pub(crate) fn set(&mut self, value: Option) { + pub fn set(&mut self, value: Option) { self.0 = value; } } @@ -82,28 +83,15 @@ macro_rules! impl_access_value { impl_access_value! {InputElement TextAreaElement} /// Able to have its value read or set -pub(crate) trait AccessValue { +pub trait AccessValue { fn value(&self) -> String; fn set_value(&self, v: &str); } -/// Applies contained changes to DOM [Element] -pub(super) trait Apply { - /// [Element] type to apply the changes to - type Element; - type Bundle; - - /// Apply contained values to [Element] with no ancestor - fn apply(self, el: &Self::Element) -> Self::Bundle; - - /// Apply diff between [self] and `bundle` to [Element]. - fn apply_diff(self, el: &Self::Element, bundle: &mut Self::Bundle); -} - /// Fields specific to -/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag]s +/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag](crate::virtual_dom::VTag)s #[derive(Debug, Clone, Default, Eq, PartialEq)] -pub(crate) struct InputFields { +pub struct InputFields { /// Contains a value of an /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). value: Value, @@ -130,18 +118,19 @@ impl DerefMut for InputFields { } impl InputFields { - pub(crate) fn new(value: Option, checked: bool) -> Self { + /// Crate new attributes for an [InputElement] element + pub fn new(value: Option, checked: bool) -> Self { Self { value: Value::new(value), checked, } } - - pub(crate) fn checked(&self) -> bool { + /// Get the 'checked' attribute on the [InputElement] + pub fn checked(&self) -> bool { self.checked } - - pub(crate) fn set_checked(&mut self, checked: bool) { + /// Set the 'checked' attribute on the [InputElement] + pub fn set_checked(&mut self, checked: bool) { self.checked = checked; } } diff --git a/packages/yew/src/dom_bundle/btag/btag_impl.rs b/packages/yew/src/dom_bundle/btag/btag_impl.rs new file mode 100644 index 00000000000..9983842eadb --- /dev/null +++ b/packages/yew/src/dom_bundle/btag/btag_impl.rs @@ -0,0 +1,257 @@ +//! This module contains the bundle implementation of a tag [BTag] + +use super::attributes::{InputFields, Value}; +use super::listeners::ListenerRegistration; +use super::Apply; +use crate::dom_bundle::{insert_node, BNode, DomBundle, Reconcilable}; +use crate::html::AnyScope; +use crate::virtual_dom::{vtag::VTagInner, vtag::SVG_NAMESPACE, Attributes, Key, VTag}; +use crate::NodeRef; +use gloo::console; +use gloo_utils::document; +use std::ops::DerefMut; +use std::{borrow::Cow, hint::unreachable_unchecked}; +use wasm_bindgen::JsCast; +use web_sys::{Element, HtmlTextAreaElement as TextAreaElement}; + +/// [BTag] fields that are specific to different [BTag] kinds. +/// Decreases the memory footprint of [BTag] by avoiding impossible field and value combinations. +#[derive(Debug)] +enum BTagInner { + /// Fields specific to + /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) + Input(InputFields), + /// Fields specific to + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + Textarea { + /// Contains a value of an + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + value: Value, + }, + /// Fields for all other kinds of [VTag]s + Other { + /// A tag of the element. + tag: Cow<'static, str>, + /// List of child nodes + child_bundle: BNode, + }, +} + +/// The bundle implementation to [VTag] +#[derive(Debug)] +pub struct BTag { + /// [BTag] fields that are specific to different [BTag] kinds. + inner: BTagInner, + listeners: ListenerRegistration, + /// A reference to the DOM [`Element`]. + reference: Element, + /// A node reference used for DOM access in Component lifecycle methods + node_ref: NodeRef, + attributes: Attributes, + key: Option, +} + +impl DomBundle for BTag { + fn detach(self, parent: &Element) { + self.listeners.unregister(); + + let node = self.reference; + // recursively remove its children + if let BTagInner::Other { child_bundle, .. } = self.inner { + child_bundle.detach(&node); + } + if parent.remove_child(&node).is_err() { + console::warn!("Node not found to remove VTag"); + } + // It could be that the ref was already reused when rendering another element. + // Only unset the ref it still belongs to our node + if self.node_ref.get().as_ref() == Some(&node) { + self.node_ref.set(None); + } + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + next_parent + .insert_before(&self.reference, next_sibling.get().as_ref()) + .unwrap(); + } +} + +impl Reconcilable for VTag { + type Bundle = BTag; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let el = self.create_element(parent); + let Self { + listeners, + attributes, + node_ref, + key, + .. + } = self; + insert_node(&el, parent, next_sibling.get().as_ref()); + + let attributes = attributes.apply(&el); + let listeners = listeners.apply(&el); + + let inner = match self.inner { + VTagInner::Input(f) => { + let f = f.apply(el.unchecked_ref()); + BTagInner::Input(f) + } + VTagInner::Textarea { value } => { + let value = value.apply(el.unchecked_ref()); + BTagInner::Textarea { value } + } + VTagInner::Other { children, tag } => { + let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default()); + BTagInner::Other { + child_bundle: child_bundle.into(), + tag, + } + } + }; + node_ref.set(Some(el.clone().into())); + ( + node_ref.clone(), + BTag { + inner, + listeners, + reference: el, + attributes, + key, + node_ref, + }, + ) + } + + fn reconcile( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut BNode, + ) -> NodeRef { + // This kind of branching patching routine reduces branch predictor misses and the need to + // unpack the enums (including `Option`s) all the time, resulting in a more streamlined + // patching flow + let is_matching_tag = match bundle { + BNode::BTag(ex) if self.key == ex.key => match (&self.inner, &ex.inner) { + (VTagInner::Input(_), BTagInner::Input(_)) => true, + (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true, + (VTagInner::Other { tag: l, .. }, BTagInner::Other { tag: r, .. }) if l == r => { + true + } + _ => false, + }, + _ => false, + }; + // If the ancestor is a tag of the same type, don't recreate, keep the + // old tag and update its attributes and children. + let tag = if is_matching_tag { + match bundle { + BNode::BTag(a) => { + // Preserve the reference that already exists + a.deref_mut() + } + _ => unsafe { unreachable_unchecked() }, + } + } else { + let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); + bundle.replace(parent, self_.into()); + return self_ref; + }; + + let el = &tag.reference; + self.attributes.apply_diff(el, &mut tag.attributes); + self.listeners.apply_diff(el, &mut tag.listeners); + + match (self.inner, &mut tag.inner) { + (VTagInner::Input(new), BTagInner::Input(old)) => { + new.apply_diff(el.unchecked_ref(), old); + } + (VTagInner::Textarea { value: new }, BTagInner::Textarea { value: old }) => { + new.apply_diff(el.unchecked_ref(), old); + } + ( + VTagInner::Other { children: new, .. }, + BTagInner::Other { + child_bundle: old, .. + }, + ) => { + new.reconcile(parent_scope, el, NodeRef::default(), old); + } + // Can not happen, because we checked for tag equability above + _ => unsafe { unreachable_unchecked() }, + } + + tag.key = self.key; + + if self.node_ref != tag.node_ref && tag.node_ref.get().as_ref() == Some(el) { + tag.node_ref.set(None); + } + if self.node_ref != tag.node_ref { + tag.node_ref = self.node_ref; + tag.node_ref.set(Some(el.clone().into())); + } + + tag.node_ref.clone() + } +} + +impl VTag { + fn create_element(&self, parent: &Element) -> Element { + let tag = self.tag(); + if tag == "svg" + || parent + .namespace_uri() + .map_or(false, |ns| ns == SVG_NAMESPACE) + { + let namespace = Some(SVG_NAMESPACE); + document() + .create_element_ns(namespace, tag) + .expect("can't create namespaced element for vtag") + } else { + document() + .create_element(tag) + .expect("can't create element for vtag") + } + } +} + +impl BTag { + /// Get the key of the underlying tag + pub(in crate::dom_bundle) fn key(&self) -> Option<&Key> { + self.key.as_ref() + } + + #[cfg(test)] + pub(super) fn reference(&self) -> &Element { + &self.reference + } + + #[cfg(test)] + pub(super) fn children(&self) -> &[BNode] { + match &self.inner { + BTagInner::Other { child_bundle, .. } => match child_bundle { + BNode::BList(blist) => blist, + _ => unreachable!("should be blist"), + }, + _ => &[], + } + } + + #[cfg(test)] + pub(super) fn tag(&self) -> &str { + match &self.inner { + BTagInner::Input { .. } => "input", + BTagInner::Textarea { .. } => "textarea", + BTagInner::Other { tag, .. } => tag.as_ref(), + } + } +} diff --git a/packages/yew/src/dom_bundle/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs similarity index 99% rename from packages/yew/src/dom_bundle/listeners.rs rename to packages/yew/src/dom_bundle/btag/listeners.rs index adf2183acc6..c3c4382da71 100644 --- a/packages/yew/src/dom_bundle/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -1,3 +1,4 @@ +use super::Apply; use crate::dom_bundle::test_log; use crate::virtual_dom::{Listener, ListenerKind, Listeners}; use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase}; @@ -46,7 +47,7 @@ pub(super) enum ListenerRegistration { Registered(u32), } -impl super::Apply for Listeners { +impl Apply for Listeners { type Element = Element; type Bundle = ListenerRegistration; diff --git a/packages/yew/src/dom_bundle/btag.rs b/packages/yew/src/dom_bundle/btag/mod.rs similarity index 70% rename from packages/yew/src/dom_bundle/btag.rs rename to packages/yew/src/dom_bundle/btag/mod.rs index 40e3f69fe06..83cd0208e1f 100644 --- a/packages/yew/src/dom_bundle/btag.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -1,263 +1,34 @@ -//! This module contains the bundle implementation of a tag [BTag] - -use super::listeners::ListenerRegistration; -use super::{BNode, DomBundle, InputFields, Reconcilable, Value}; -use crate::dom_bundle::attributes::Apply; -use crate::html::AnyScope; -use crate::virtual_dom::{vtag::VTagInner, vtag::SVG_NAMESPACE, Attributes, Key, VTag}; -use crate::NodeRef; -use gloo::console; -use gloo_utils::document; -use std::ops::DerefMut; -use std::{borrow::Cow, hint::unreachable_unchecked}; -use wasm_bindgen::JsCast; -use web_sys::{Element, HtmlTextAreaElement as TextAreaElement}; - -/// [BTag] fields that are specific to different [BTag] kinds. -/// Decreases the memory footprint of [BTag] by avoiding impossible field and value combinations. -#[derive(Debug)] -enum BTagInner { - /// Fields specific to - /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) - Input(InputFields), - /// Fields specific to - /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) - Textarea { - /// Contains a value of an - /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) - value: Value, - }, - /// Fields for all other kinds of [VTag]s - Other { - /// A tag of the element. - tag: Cow<'static, str>, - /// List of child nodes - child_bundle: BNode, - }, -} - -/// The bundle implementation to [VTag] -#[derive(Debug)] -pub struct BTag { - /// [BTag] fields that are specific to different [BTag] kinds. - inner: BTagInner, - listeners: ListenerRegistration, - /// A reference to the DOM [`Element`]. - reference: Element, - /// A node reference used for DOM access in Component lifecycle methods - node_ref: NodeRef, - attributes: Attributes, - key: Option, -} - -impl DomBundle for BTag { - fn detach(self, parent: &Element) { - self.listeners.unregister(); - - let node = self.reference; - // recursively remove its children - if let BTagInner::Other { child_bundle, .. } = self.inner { - child_bundle.detach(&node); - } - if parent.remove_child(&node).is_err() { - console::warn!("Node not found to remove VTag"); - } - // It could be that the ref was already reused when rendering another element. - // Only unset the ref it still belongs to our node - if self.node_ref.get().as_ref() == Some(&node) { - self.node_ref.set(None); - } - } - - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - next_parent - .insert_before(&self.reference, next_sibling.get().as_ref()) - .unwrap(); - } -} - -impl Reconcilable for VTag { - type Bundle = BTag; - - fn attach( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { - let el = self.create_element(parent); - let Self { - listeners, - attributes, - node_ref, - key, - .. - } = self; - super::insert_node(&el, parent, next_sibling.get().as_ref()); - - let attributes = attributes.apply(&el); - let listeners = listeners.apply(&el); - - let inner = match self.inner { - VTagInner::Input(f) => { - let f = f.apply(el.unchecked_ref()); - BTagInner::Input(f) - } - VTagInner::Textarea { value } => { - let value = value.apply(el.unchecked_ref()); - BTagInner::Textarea { value } - } - VTagInner::Other { children, tag } => { - let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default()); - BTagInner::Other { - child_bundle: child_bundle.into(), - tag, - } - } - }; - node_ref.set(Some(el.clone().into())); - ( - node_ref.clone(), - BTag { - inner, - listeners, - reference: el, - attributes, - key, - node_ref, - }, - ) - } - - fn reconcile( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - bundle: &mut BNode, - ) -> NodeRef { - // This kind of branching patching routine reduces branch predictor misses and the need to - // unpack the enums (including `Option`s) all the time, resulting in a more streamlined - // patching flow - let is_matching_tag = match bundle { - BNode::BTag(ex) if self.key == ex.key => match (&self.inner, &ex.inner) { - (VTagInner::Input(_), BTagInner::Input(_)) => true, - (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true, - (VTagInner::Other { tag: l, .. }, BTagInner::Other { tag: r, .. }) if l == r => { - true - } - _ => false, - }, - _ => false, - }; - // If the ancestor is a tag of the same type, don't recreate, keep the - // old tag and update its attributes and children. - let tag = if is_matching_tag { - match bundle { - BNode::BTag(a) => { - // Preserve the reference that already exists - a.deref_mut() - } - _ => unsafe { unreachable_unchecked() }, - } - } else { - let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); - bundle.replace(parent, self_.into()); - return self_ref; - }; - - let el = &tag.reference; - self.attributes.apply_diff(el, &mut tag.attributes); - self.listeners.apply_diff(el, &mut tag.listeners); - - match (self.inner, &mut tag.inner) { - (VTagInner::Input(new), BTagInner::Input(old)) => { - new.apply_diff(el.unchecked_ref(), old); - } - (VTagInner::Textarea { value: new }, BTagInner::Textarea { value: old }) => { - new.apply_diff(el.unchecked_ref(), old); - } - ( - VTagInner::Other { children: new, .. }, - BTagInner::Other { - child_bundle: old, .. - }, - ) => { - new.reconcile(parent_scope, el, NodeRef::default(), old); - } - // Can not happen, because we checked for tag equability above - _ => unsafe { unreachable_unchecked() }, - } +mod attributes; +mod btag_impl; +mod listeners; - tag.key = self.key; +/// Applies contained changes to DOM [Element] +trait Apply { + /// [Element] type to apply the changes to + type Element; + type Bundle; - if self.node_ref != tag.node_ref && tag.node_ref.get().as_ref() == Some(el) { - tag.node_ref.set(None); - } - if self.node_ref != tag.node_ref { - tag.node_ref = self.node_ref; - tag.node_ref.set(Some(el.clone().into())); - } + /// Apply contained values to [Element] with no ancestor + fn apply(self, el: &Self::Element) -> Self::Bundle; - tag.node_ref.clone() - } -} - -impl VTag { - fn create_element(&self, parent: &Element) -> Element { - let tag = self.tag(); - if tag == "svg" - || parent - .namespace_uri() - .map_or(false, |ns| ns == SVG_NAMESPACE) - { - let namespace = Some(SVG_NAMESPACE); - document() - .create_element_ns(namespace, tag) - .expect("can't create namespaced element for vtag") - } else { - document() - .create_element(tag) - .expect("can't create element for vtag") - } - } + /// Apply diff between [self] and `bundle` to [Element]. + fn apply_diff(self, el: &Self::Element, bundle: &mut Self::Bundle); } -impl BTag { - /// Get the key of the underlying tag - pub(crate) fn key(&self) -> Option<&Key> { - self.key.as_ref() - } - - #[cfg(test)] - fn children(&self) -> &[BNode] { - match &self.inner { - BTagInner::Other { child_bundle, .. } => match child_bundle { - BNode::BList(blist) => blist, - _ => unreachable!("should be blist"), - }, - _ => &[], - } - } - - #[cfg(test)] - fn tag(&self) -> &str { - match &self.inner { - BTagInner::Input { .. } => "input", - BTagInner::Textarea { .. } => "textarea", - BTagInner::Other { tag, .. } => tag.as_ref(), - } - } -} +pub use attributes::{InputFields, Value}; +pub use btag_impl::BTag; +pub use listeners::set_event_bubbling; #[cfg(test)] mod tests { use super::*; - use crate::{ - html, - virtual_dom::{vtag::HTML_NAMESPACE, AttrValue, VNode}, - Html, - }; + use crate::dom_bundle::{BNode, DomBundle, Reconcilable}; + use crate::html; + use crate::html::AnyScope; + use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE}; + use crate::virtual_dom::{AttrValue, VNode, VTag}; + use crate::{Html, NodeRef}; + use gloo_utils::document; use wasm_bindgen::JsCast; use web_sys::HtmlInputElement as InputElement; @@ -442,7 +213,7 @@ mod tests { } fn assert_namespace(vtag: &BTag, namespace: &'static str) { - assert_eq!(vtag.reference.namespace_uri().unwrap(), namespace); + assert_eq!(vtag.reference().namespace_uri().unwrap(), namespace); } #[test] @@ -575,7 +346,7 @@ mod tests { let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); let vtag = assert_btag_mut(&mut elem); // test if the className has not been set - assert!(!vtag.reference.has_attribute("class")); + assert!(!vtag.reference().has_attribute("class")); } fn test_set_class_name(gen_html: impl FnOnce() -> Html) { @@ -588,7 +359,7 @@ mod tests { let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); let vtag = assert_btag_mut(&mut elem); // test if the className has been set - assert!(vtag.reference.has_attribute("class")); + assert!(vtag.reference().has_attribute("class")); } #[test] @@ -616,7 +387,7 @@ mod tests { let vtag = assert_btag_ref(&elem); // User input - let input_ref = &vtag.reference; + let input_ref = &vtag.reference(); let input = input_ref.dyn_ref::(); input.unwrap().set_value("User input"); @@ -628,7 +399,7 @@ mod tests { let vtag = assert_btag_ref(&elem); // Get new current value of the input element - let input_ref = &vtag.reference; + let input_ref = &vtag.reference(); let input = input_ref.dyn_ref::().unwrap(); let current_value = input.value(); @@ -650,7 +421,7 @@ mod tests { let vtag = assert_btag_ref(&elem); // User input - let input_ref = &vtag.reference; + let input_ref = &vtag.reference(); let input = input_ref.dyn_ref::(); input.unwrap().set_value("User input"); @@ -662,7 +433,7 @@ mod tests { let vtag = assert_btag_ref(&elem); // Get user value of the input element - let input_ref = &vtag.reference; + let input_ref = &vtag.reference(); let input = input_ref.dyn_ref::().unwrap(); let current_value = input.value(); @@ -694,7 +465,7 @@ mod tests { assert_eq!(vtag.tag(), "a"); // Element.tagName is always in the canonical upper-case form. - assert_eq!(vtag.reference.tag_name(), "A"); + assert_eq!(vtag.reference().tag_name(), "A"); } #[test] diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 40764af7e09..a5e483d37ce 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -5,7 +5,7 @@ //! In order to efficiently implement updates, and diffing, additional information has to be //! kept around. This information is carried in the bundle. -mod attributes; +mod app_handle; mod bcomp; mod blist; mod bnode; @@ -13,29 +13,35 @@ mod bportal; mod bsuspense; mod btag; mod btext; -mod listeners; -#[cfg(debug_assertions)] -pub(crate) use self::bcomp::log_event; +#[cfg(test)] +mod tests; -pub use self::bcomp::BComp; -pub use self::blist::BList; -pub use self::bnode::BNode; -pub use self::bportal::BPortal; -pub use self::bsuspense::BSuspense; -pub use self::btag::BTag; -pub use self::btext::BText; +use self::bcomp::{BComp, Scoped}; +use self::blist::BList; +use self::bnode::BNode; +use self::bportal::BPortal; +use self::bsuspense::BSuspense; +use self::btag::BTag; +use self::btext::BText; -pub(self) use self::attributes::Apply; -pub(crate) use self::attributes::{InputFields, Value}; pub(crate) use self::bcomp::{Mountable, PropsWrapper}; -#[doc(hidden)] -pub use self::listeners::set_event_bubbling; +pub(crate) use self::btag::{InputFields, Value}; -use crate::{html::AnyScope, NodeRef}; +#[doc(hidden)] // Publically exported from crate::app_handle +pub use self::app_handle::AppHandle; +#[doc(hidden)] // Publically exported from crate::html +pub use self::bcomp::{AnyScope, Scope, SendAsMessage}; +#[doc(hidden)] // Publically exported from crate::events +pub use self::btag::set_event_bubbling; +#[cfg(test)] +#[doc(hidden)] // Publically exported from crate::tests +pub use self::tests::layout_tests; + +use crate::NodeRef; use web_sys::{Element, Node}; -pub(crate) trait DomBundle { +trait DomBundle { /// Remove self from parent. fn detach(self, parent: &Element); @@ -50,7 +56,7 @@ pub(crate) trait DomBundle { // `Ace` editor embedding for example? /// This trait provides features to update a tree by calculating a difference against another tree. -pub(crate) trait Reconcilable { +trait Reconcilable { type Bundle: DomBundle; /// Attach a virtual node to the DOM tree. @@ -96,7 +102,7 @@ pub(crate) trait Reconcilable { } /// Insert a concrete [Node] into the DOM -pub(crate) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) { +fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) { match next_sibling { Some(next_sibling) => parent .insert_before(node, Some(next_sibling)) diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/dom_bundle/tests/layout_tests.rs similarity index 100% rename from packages/yew/src/tests/layout_tests.rs rename to packages/yew/src/dom_bundle/tests/layout_tests.rs diff --git a/packages/yew/src/dom_bundle/tests/mod.rs b/packages/yew/src/dom_bundle/tests/mod.rs new file mode 100644 index 00000000000..60d8d164651 --- /dev/null +++ b/packages/yew/src/dom_bundle/tests/mod.rs @@ -0,0 +1,26 @@ +pub mod layout_tests; + +use super::Reconcilable; + +use crate::virtual_dom::VNode; +use crate::{dom_bundle::BNode, html::AnyScope, NodeRef}; +use web_sys::Element; + +impl VNode { + fn reconcile_sequentially( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut Option, + ) -> NodeRef { + match bundle { + None => { + let (self_ref, node) = self.attach(parent_scope, parent, next_sibling); + *bundle = Some(node); + self_ref + } + Some(bundle) => self.reconcile(parent_scope, parent, next_sibling, bundle), + } + } +} diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index 3528c7c9470..aaaa63f7ca0 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -1,15 +1,12 @@ //! Components wrapped with context including properties, state, and link mod children; -mod lifecycle; mod properties; -mod scope; use super::{Html, HtmlResult, IntoHtmlResult}; +pub use crate::dom_bundle::{AnyScope, Scope, SendAsMessage}; pub use children::*; pub use properties::*; -pub(crate) use scope::Scoped; -pub use scope::{AnyScope, Scope, SendAsMessage}; use std::rc::Rc; /// The [`Component`]'s context. This contains component's [`Scope`] and and props and diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index dd41f84249f..b649341c5d9 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -263,10 +263,9 @@ pub mod macros { pub use crate::props; } -mod app_handle; pub mod callback; pub mod context; -pub mod dom_bundle; +mod dom_bundle; pub mod functional; pub mod html; mod io_coop; @@ -275,13 +274,16 @@ mod sealed; #[cfg(feature = "ssr")] mod server_renderer; pub mod suspense; -#[cfg(test)] -pub mod tests; pub mod utils; pub mod virtual_dom; #[cfg(feature = "ssr")] pub use server_renderer::*; +#[cfg(test)] +pub mod tests { + pub use crate::dom_bundle::layout_tests; +} + /// The module that contains all events available in the framework. pub mod events { pub use crate::html::TargetCast; @@ -295,7 +297,7 @@ pub mod events { }; } -pub use crate::app_handle::AppHandle; +pub use crate::dom_bundle::AppHandle; use web_sys::Element; use crate::html::BaseComponent; @@ -375,9 +377,9 @@ where /// use yew::prelude::*; /// ``` pub mod prelude { - pub use crate::app_handle::AppHandle; pub use crate::callback::Callback; pub use crate::context::ContextProvider; + pub use crate::dom_bundle::AppHandle; pub use crate::events::*; pub use crate::html::{ create_portal, BaseComponent, Children, ChildrenWithProps, Classes, Component, Context, diff --git a/packages/yew/src/tests/mod.rs b/packages/yew/src/tests/mod.rs deleted file mode 100644 index 7c8881b072f..00000000000 --- a/packages/yew/src/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod layout_tests; diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index b4913f382d1..3b98a0e7ae4 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -45,32 +45,6 @@ impl VNode { } } -#[cfg(test)] -mod test { - use crate::dom_bundle::Reconcilable; - use crate::{dom_bundle::BNode, html::AnyScope, NodeRef}; - use web_sys::Element; - - impl super::VNode { - pub(crate) fn reconcile_sequentially( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - bundle: &mut Option, - ) -> NodeRef { - match bundle { - None => { - let (self_ref, node) = self.attach(parent_scope, parent, next_sibling); - *bundle = Some(node); - self_ref - } - Some(bundle) => self.reconcile(parent_scope, parent, next_sibling, bundle), - } - } - } -} - impl Default for VNode { fn default() -> Self { VNode::VList(VList::default()) diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index aaaf127bb8a..1964711aecd 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -187,7 +187,7 @@ impl VTag { } } - /// Returns tag of an [Element]. In HTML tags are always uppercase. + /// Returns tag of an [Element](web_sys::Element). In HTML tags are always uppercase. pub fn tag(&self) -> &str { match &self.inner { VTagInner::Input { .. } => "input", @@ -303,7 +303,7 @@ impl VTag { .insert(key, value.into_prop_value()); } - /// Add event listener on the [VTag]'s [Element]. + /// Add event listener on the [VTag]'s [Element](web_sys::Element). /// Returns `true` if the listener has been added, `false` otherwise. pub fn add_listener(&mut self, listener: Rc) -> bool { if let Listeners::Pending(listeners) = &mut self.listeners { @@ -317,7 +317,7 @@ impl VTag { } } - /// Set event listeners on the [VTag]'s [Element] + /// Set event listeners on the [VTag]'s [Element](web_sys::Element) pub fn set_listeners(&mut self, listeners: Box<[Option>]>) { self.listeners = Listeners::Pending(listeners); } From c1a0b4976bcf10408c523e13f5850d17374136a3 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Sat, 15 Jan 2022 17:42:16 +0100 Subject: [PATCH 15/26] move replace into Reconcilable --- .../yew/src/dom_bundle/bcomp/bcomp_impl.rs | 4 +--- packages/yew/src/dom_bundle/bnode.rs | 15 ++++++--------- packages/yew/src/dom_bundle/bportal.rs | 4 +--- packages/yew/src/dom_bundle/bsuspense.rs | 4 +--- packages/yew/src/dom_bundle/btag/btag_impl.rs | 4 +--- packages/yew/src/dom_bundle/btext.rs | 4 +--- packages/yew/src/dom_bundle/mod.rs | 18 ++++++++++++++++++ 7 files changed, 29 insertions(+), 24 deletions(-) diff --git a/packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs b/packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs index 6eaacf08e07..b4c359d4c6e 100644 --- a/packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs +++ b/packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs @@ -193,9 +193,7 @@ impl Reconcilable for VComp { bcomp } _ => { - let (node_ref, self_) = self.attach(parent_scope, parent, next_sibling); - bundle.replace(parent, self_.into()); - return node_ref; + return self.replace(parent_scope, parent, next_sibling, bundle); } }; let VComp { diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index fb13696309c..ef04d872d82 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -39,11 +39,6 @@ impl BNode { Self::BSuspense(bsusp) => bsusp.key(), } } - /// Replace this node with a new node, making sure to detach the ancestor - pub(super) fn replace(&mut self, parent: &Element, next_node: BNode) { - let ancestor = std::mem::replace(self, next_node); - ancestor.detach(parent); - } } impl DomBundle for BNode { @@ -138,10 +133,12 @@ impl Reconcilable for VNode { let _existing = match bundle { BNode::BRef(ref n) if &node == n => n, _ => { - let (node_ref, self_) = - VNode::VRef(node).attach(parent_scope, parent, next_sibling); - bundle.replace(parent, self_); - return node_ref; + return VNode::VRef(node).replace( + parent_scope, + parent, + next_sibling, + bundle, + ); } }; NodeRef::new(node) diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index 58c8dd1575c..f624857d374 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -66,9 +66,7 @@ impl Reconcilable for VPortal { let portal = match bundle { BNode::BPortal(portal) => portal, _ => { - let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); - bundle.replace(parent, self_.into()); - return self_ref; + return self.replace(parent_scope, parent, next_sibling, bundle); } }; let Self { diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index b8218f39f64..fdb97134666 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -107,9 +107,7 @@ impl Reconcilable for VSuspense { m } _ => { - let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); - bundle.replace(parent, self_.into()); - return self_ref; + return self.replace(parent_scope, parent, next_sibling, bundle); } }; let VSuspense { diff --git a/packages/yew/src/dom_bundle/btag/btag_impl.rs b/packages/yew/src/dom_bundle/btag/btag_impl.rs index 9983842eadb..6db340d1f00 100644 --- a/packages/yew/src/dom_bundle/btag/btag_impl.rs +++ b/packages/yew/src/dom_bundle/btag/btag_impl.rs @@ -162,9 +162,7 @@ impl Reconcilable for VTag { _ => unsafe { unreachable_unchecked() }, } } else { - let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); - bundle.replace(parent, self_.into()); - return self_ref; + return self.replace(parent_scope, parent, next_sibling, bundle); }; let el = &tag.reference; diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index 23187470d8a..7ee71916cf9 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -58,9 +58,7 @@ impl Reconcilable for VText { let btext = match bundle { BNode::BText(btext) => btext, _ => { - let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); - bundle.replace(parent, self_.into()); - return self_ref; + return self.replace(parent_scope, parent, next_sibling, bundle); } }; let Self { text } = self; diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index a5e483d37ce..31337d0431f 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -99,6 +99,24 @@ trait Reconcilable { next_sibling: NodeRef, bundle: &mut BNode, ) -> NodeRef; + + /// Replace an existing bundle by attaching self and detaching the existing one + fn replace( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut BNode, + ) -> NodeRef + where + Self: Sized, + Self::Bundle: Into, + { + let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); + let ancestor = std::mem::replace(bundle, self_.into()); + ancestor.detach(parent); + self_ref + } } /// Insert a concrete [Node] into the DOM From 25a760cc69c9a908fff99520d60918c67cca4e40 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 19 Jan 2022 13:26:58 +0100 Subject: [PATCH 16/26] move lifecycle and scope back into html as per review --- packages/yew/src/dom_bundle/app_handle.rs | 11 +- .../src/dom_bundle/{bcomp/mod.rs => bcomp.rs} | 284 +++++++++++++++++- .../yew/src/dom_bundle/bcomp/bcomp_impl.rs | 211 ------------- packages/yew/src/dom_bundle/mod.rs | 7 +- .../bcomp => html/component}/lifecycle.rs | 104 ++----- packages/yew/src/html/component/mod.rs | 37 ++- .../bcomp => html/component}/scope.rs | 124 +++----- packages/yew/src/server_renderer.rs | 2 +- 8 files changed, 388 insertions(+), 392 deletions(-) rename packages/yew/src/dom_bundle/{bcomp/mod.rs => bcomp.rs} (66%) delete mode 100644 packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs rename packages/yew/src/{dom_bundle/bcomp => html/component}/lifecycle.rs (84%) rename packages/yew/src/{dom_bundle/bcomp => html/component}/scope.rs (83%) diff --git a/packages/yew/src/dom_bundle/app_handle.rs b/packages/yew/src/dom_bundle/app_handle.rs index 4b73f438ce4..9820481a8a4 100644 --- a/packages/yew/src/dom_bundle/app_handle.rs +++ b/packages/yew/src/dom_bundle/app_handle.rs @@ -1,8 +1,8 @@ //! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope. -use super::Scope; -use super::Scoped; -use crate::{html::BaseComponent, NodeRef}; +use super::{ComponentRenderState, Scoped}; +use crate::html::{BaseComponent, Scope}; +use crate::NodeRef; use std::{ops::Deref, rc::Rc}; use web_sys::Element; @@ -26,8 +26,11 @@ where let app = Self { scope: Scope::new(None), }; + let node_ref = NodeRef::default(); + let initial_render_state = + ComponentRenderState::new(element, NodeRef::default(), &node_ref); app.scope - .mount_in_place(element, NodeRef::default(), NodeRef::default(), props); + .mount_in_place(initial_render_state, node_ref, props); app } diff --git a/packages/yew/src/dom_bundle/bcomp/mod.rs b/packages/yew/src/dom_bundle/bcomp.rs similarity index 66% rename from packages/yew/src/dom_bundle/bcomp/mod.rs rename to packages/yew/src/dom_bundle/bcomp.rs index d8434311811..aa9501f67bb 100644 --- a/packages/yew/src/dom_bundle/bcomp/mod.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -1,12 +1,282 @@ -mod bcomp_impl; -mod lifecycle; -mod scope; +//! This module contains the bundle implementation of a virtual component [BComp]. + +use super::{insert_node, BNode, DomBundle, Reconcilable}; +use crate::html::{AnyScope, BaseComponent, Scope}; +use crate::virtual_dom::{Key, VComp, VNode}; +use crate::NodeRef; +#[cfg(feature = "ssr")] +use futures::channel::oneshot; +#[cfg(feature = "ssr")] +use futures::future::{FutureExt, LocalBoxFuture}; +use gloo_utils::document; +use std::cell::Ref; +use std::{any::TypeId, borrow::Borrow}; +use std::{fmt, rc::Rc}; +use web_sys::{Element, Node}; + +/// A virtual component. Compare with [VComp]. +pub struct BComp { + type_id: TypeId, + scope: Box, + node_ref: NodeRef, + key: Option, +} + +impl BComp { + /// Get the key of the underlying component + pub(super) fn key(&self) -> Option<&Key> { + self.key.as_ref() + } +} + +impl fmt::Debug for BComp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "BComp {{ root: {:?} }}", + self.scope.as_ref().render_state(), + ) + } +} + +impl DomBundle for BComp { + fn detach(self, _parent: &Element) { + self.scope.destroy_boxed(); + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + self.scope.shift_node(next_parent.clone(), next_sibling); + } +} + +impl Reconcilable for VComp { + type Bundle = BComp; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let VComp { + type_id, + mountable, + node_ref, + key, + } = self; + + let scope = mountable.mount( + node_ref.clone(), + parent_scope, + parent.to_owned(), + next_sibling, + ); + + ( + node_ref.clone(), + BComp { + type_id, + node_ref, + key, + scope, + }, + ) + } + + fn reconcile( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut BNode, + ) -> NodeRef { + let bcomp = match bundle { + // If the existing bundle is the same type, reuse it and update its properties + BNode::BComp(ref mut bcomp) + if self.type_id == bcomp.type_id && self.key == bcomp.key => + { + bcomp + } + _ => { + return self.replace(parent_scope, parent, next_sibling, bundle); + } + }; + let VComp { + mountable, + node_ref, + key, + type_id: _, + } = self; + bcomp.key = key; + let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref.clone()); + bcomp.node_ref.reuse(old_ref); + mountable.reuse(node_ref.clone(), bcomp.scope.borrow(), next_sibling); + node_ref + } +} + +pub trait Mountable { + fn copy(&self) -> Box; + fn mount( + self: Box, + node_ref: NodeRef, + parent_scope: &AnyScope, + parent: Element, + next_sibling: NodeRef, + ) -> Box; + fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); + + #[cfg(feature = "ssr")] + fn render_to_string<'a>( + &'a self, + w: &'a mut String, + parent_scope: &'a AnyScope, + ) -> LocalBoxFuture<'a, ()>; +} + +pub struct PropsWrapper { + props: Rc, +} + +impl PropsWrapper { + pub fn new(props: Rc) -> Self { + Self { props } + } +} + +impl Mountable for PropsWrapper { + fn copy(&self) -> Box { + let wrapper: PropsWrapper = PropsWrapper { + props: Rc::clone(&self.props), + }; + Box::new(wrapper) + } + + fn mount( + self: Box, + node_ref: NodeRef, + parent_scope: &AnyScope, + parent: Element, + next_sibling: NodeRef, + ) -> Box { + let scope: Scope = Scope::new(Some(parent_scope.clone())); + let initial_render_state = ComponentRenderState::new(parent, next_sibling, &node_ref); + scope.mount_in_place(initial_render_state, node_ref, self.props); + + Box::new(scope) + } + + fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { + let scope: Scope = scope.to_any().downcast(); + scope.reuse(self.props, node_ref, next_sibling); + } + + #[cfg(feature = "ssr")] + fn render_to_string<'a>( + &'a self, + w: &'a mut String, + parent_scope: &'a AnyScope, + ) -> LocalBoxFuture<'a, ()> { + async move { + let scope: Scope = Scope::new(Some(parent_scope.clone())); + scope.render_to_string(w, self.props.clone()).await; + } + .boxed_local() + } +} + +pub struct ComponentRenderState { + root_node: BNode, + /// When a component has no parent, it means that it should not be rendered. + parent: Option, + next_sibling: NodeRef, + + #[cfg(feature = "ssr")] + html_sender: Option>, +} + +impl std::fmt::Debug for ComponentRenderState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.root_node.fmt(f) + } +} + +impl ComponentRenderState { + /// Prepare a place in the DOM to hold the eventual [VNode] from rendering a component + pub(crate) fn new(parent: Element, next_sibling: NodeRef, node_ref: &NodeRef) -> Self { + let placeholder = { + let placeholder: Node = document().create_text_node("").into(); + insert_node(&placeholder, &parent, next_sibling.get().as_ref()); + node_ref.set(Some(placeholder.clone())); + BNode::BRef(placeholder) + }; + Self { + root_node: placeholder, + parent: Some(parent), + next_sibling, + #[cfg(feature = "ssr")] + html_sender: None, + } + } + /// Set up server-side rendering of a component + #[cfg(feature = "ssr")] + pub(crate) fn new_ssr(tx: oneshot::Sender) -> Self { + use super::blist::BList; + + Self { + root_node: BNode::BList(BList::new()), + parent: None, + next_sibling: NodeRef::default(), + html_sender: Some(tx), + } + } + /// Reuse the render state, asserting a new next_sibling + pub(crate) fn reuse(&mut self, next_sibling: NodeRef) { + self.next_sibling = next_sibling; + } + /// Shift the rendered content to a new DOM position + pub(crate) fn shift(&mut self, new_parent: Element, next_sibling: NodeRef) { + self.root_node.shift(&new_parent, next_sibling.clone()); + + self.parent = Some(new_parent); + self.next_sibling = next_sibling; + } + /// Reconcile the rendered content with a new [VNode] + pub(crate) fn reconcile(&mut self, root: VNode, scope: &AnyScope) -> NodeRef { + if let Some(ref parent) = self.parent { + let next_sibling = self.next_sibling.clone(); + + root.reconcile(scope, parent, next_sibling, &mut self.root_node) + } else { + #[cfg(feature = "ssr")] + if let Some(tx) = self.html_sender.take() { + tx.send(root).unwrap(); + } + NodeRef::default() + } + } + /// Detach the rendered content from the DOM + pub(crate) fn detach(self) { + if let Some(ref m) = self.parent { + self.root_node.detach(m); + } + } -#[cfg(debug_assertions)] -pub(self) use bcomp_impl::log_event; + pub(crate) fn should_trigger_rendered(&self) -> bool { + self.parent.is_some() + } +} -pub use bcomp_impl::{BComp, Mountable, PropsWrapper}; -pub use scope::{AnyScope, Scope, Scoped, SendAsMessage}; +pub trait Scoped { + fn to_any(&self) -> AnyScope; + /// Get the render state if it hasn't already been destroyed + fn render_state(&self) -> Option>; + /// Shift the node associated with this scope to a new place + fn shift_node(&self, parent: Element, next_sibling: NodeRef); + /// Process an event to destroy a component + fn destroy(self); + fn destroy_boxed(self: Box); +} #[cfg(test)] mod tests { diff --git a/packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs b/packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs deleted file mode 100644 index b4c359d4c6e..00000000000 --- a/packages/yew/src/dom_bundle/bcomp/bcomp_impl.rs +++ /dev/null @@ -1,211 +0,0 @@ -//! This module contains the bundle implementation of a virtual component [BComp]. - -use super::Scoped; -use crate::dom_bundle::{BNode, DomBundle, Reconcilable}; -use crate::html::{AnyScope, BaseComponent, Scope}; -use crate::virtual_dom::{Key, VComp}; -use crate::NodeRef; -#[cfg(feature = "ssr")] -use futures::future::{FutureExt, LocalBoxFuture}; -use std::{any::TypeId, borrow::Borrow}; -use std::{fmt, rc::Rc}; -use web_sys::Element; - -thread_local! { - #[cfg(debug_assertions)] - static EVENT_HISTORY: std::cell::RefCell>> - = Default::default(); -} - -/// Push [VComp] event to lifecycle debugging registry -#[cfg(debug_assertions)] -pub fn log_event(vcomp_id: u64, event: impl ToString) { - EVENT_HISTORY.with(|h| { - h.borrow_mut() - .entry(vcomp_id) - .or_default() - .push(event.to_string()) - }); -} - -/// Get [VComp] event log from lifecycle debugging registry -#[cfg(debug_assertions)] -#[allow(dead_code)] -pub fn get_event_log(vcomp_id: u64) -> Vec { - EVENT_HISTORY.with(|h| { - h.borrow() - .get(&vcomp_id) - .map(|l| (*l).clone()) - .unwrap_or_default() - }) -} - -/// A virtual component. Compare with [VComp]. -pub struct BComp { - type_id: TypeId, - scope: Box, - node_ref: NodeRef, - key: Option, -} - -impl BComp { - /// Get the key of the underlying component - pub(in crate::dom_bundle) fn key(&self) -> Option<&Key> { - self.key.as_ref() - } -} - -impl fmt::Debug for BComp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "BComp {{ root: {:?} }}", - self.scope.root_bnode().as_deref() - ) - } -} - -pub trait Mountable { - fn copy(&self) -> Box; - fn mount( - self: Box, - node_ref: NodeRef, - parent_scope: &AnyScope, - parent: Element, - next_sibling: NodeRef, - ) -> Box; - fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef); - - #[cfg(feature = "ssr")] - fn render_to_string<'a>( - &'a self, - w: &'a mut String, - parent_scope: &'a AnyScope, - ) -> LocalBoxFuture<'a, ()>; -} - -pub struct PropsWrapper { - props: Rc, -} - -impl PropsWrapper { - pub fn new(props: Rc) -> Self { - Self { props } - } -} - -impl Mountable for PropsWrapper { - fn copy(&self) -> Box { - let wrapper: PropsWrapper = PropsWrapper { - props: Rc::clone(&self.props), - }; - Box::new(wrapper) - } - - fn mount( - self: Box, - node_ref: NodeRef, - parent_scope: &AnyScope, - parent: Element, - next_sibling: NodeRef, - ) -> Box { - let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.mount_in_place(parent, next_sibling, node_ref, self.props); - - Box::new(scope) - } - - fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { - let scope: Scope = scope.to_any().downcast(); - scope.reuse(self.props, node_ref, next_sibling); - } - - #[cfg(feature = "ssr")] - fn render_to_string<'a>( - &'a self, - w: &'a mut String, - parent_scope: &'a AnyScope, - ) -> LocalBoxFuture<'a, ()> { - async move { - let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.render_to_string(w, self.props.clone()).await; - } - .boxed_local() - } -} - -impl DomBundle for BComp { - fn detach(self, _parent: &Element) { - self.scope.destroy_boxed(); - } - - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - self.scope.shift_node(next_parent.clone(), next_sibling); - } -} - -impl Reconcilable for VComp { - type Bundle = BComp; - - fn attach( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { - let VComp { - type_id, - mountable, - node_ref, - key, - } = self; - - let scope = mountable.mount( - node_ref.clone(), - parent_scope, - parent.to_owned(), - next_sibling, - ); - - ( - node_ref.clone(), - BComp { - type_id, - node_ref, - key, - scope, - }, - ) - } - - fn reconcile( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - bundle: &mut BNode, - ) -> NodeRef { - let bcomp = match bundle { - // If the existing bundle is the same type, reuse it and update its properties - BNode::BComp(ref mut bcomp) - if self.type_id == bcomp.type_id && self.key == bcomp.key => - { - bcomp - } - _ => { - return self.replace(parent_scope, parent, next_sibling, bundle); - } - }; - let VComp { - mountable, - node_ref, - key, - type_id: _, - } = self; - bcomp.key = key; - let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref.clone()); - bcomp.node_ref.reuse(old_ref); - mountable.reuse(node_ref.clone(), bcomp.scope.borrow(), next_sibling); - node_ref - } -} diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 31337d0431f..6c78b64e27e 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -17,7 +17,7 @@ mod btext; #[cfg(test)] mod tests; -use self::bcomp::{BComp, Scoped}; +use self::bcomp::BComp; use self::blist::BList; use self::bnode::BNode; use self::bportal::BPortal; @@ -25,19 +25,18 @@ use self::bsuspense::BSuspense; use self::btag::BTag; use self::btext::BText; -pub(crate) use self::bcomp::{Mountable, PropsWrapper}; +pub(crate) use self::bcomp::{ComponentRenderState, Mountable, PropsWrapper, Scoped}; pub(crate) use self::btag::{InputFields, Value}; #[doc(hidden)] // Publically exported from crate::app_handle pub use self::app_handle::AppHandle; -#[doc(hidden)] // Publically exported from crate::html -pub use self::bcomp::{AnyScope, Scope, SendAsMessage}; #[doc(hidden)] // Publically exported from crate::events pub use self::btag::set_event_bubbling; #[cfg(test)] #[doc(hidden)] // Publically exported from crate::tests pub use self::tests::layout_tests; +use crate::html::AnyScope; use crate::NodeRef; use web_sys::{Element, Node}; diff --git a/packages/yew/src/dom_bundle/bcomp/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs similarity index 84% rename from packages/yew/src/dom_bundle/bcomp/lifecycle.rs rename to packages/yew/src/html/component/lifecycle.rs index da664eaa819..b421922bcf4 100644 --- a/packages/yew/src/dom_bundle/bcomp/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -1,74 +1,50 @@ //! Component lifecycle module use super::scope::{AnyScope, Scope}; -use crate::dom_bundle::{BNode, DomBundle, Reconcilable}; +use crate::dom_bundle::ComponentRenderState; use crate::html::{BaseComponent, RenderError}; use crate::scheduler::{self, Runnable, Shared}; use crate::suspense::{Suspense, Suspension}; -#[cfg(feature = "ssr")] -use crate::virtual_dom::VNode; use crate::Callback; use crate::{Context, NodeRef}; -#[cfg(feature = "ssr")] -use futures::channel::oneshot; use std::rc::Rc; use web_sys::Element; pub struct ComponentState { pub(super) component: Box, - pub(super) root_node: BNode, + pub(super) render_state: ComponentRenderState, context: Context, - - /// When a component has no parent, it means that it should not be rendered. - parent: Option, - - next_sibling: NodeRef, node_ref: NodeRef, has_rendered: bool, suspension: Option, - #[cfg(feature = "ssr")] - html_sender: Option>, - // Used for debug logging #[cfg(debug_assertions)] - pub(super) vcomp_id: u64, + vcomp_id: u64, } impl ComponentState { fn new( - parent: Option, - next_sibling: NodeRef, - root_node: BNode, + initial_render_state: ComponentRenderState, node_ref: NodeRef, scope: Scope, props: Rc, - #[cfg(feature = "ssr")] html_sender: Option>, ) -> Self { #[cfg(debug_assertions)] - let vcomp_id = { - use super::scope::Scoped; - - scope.to_any().vcomp_id - }; + let vcomp_id = { scope.vcomp_id }; let context = Context { scope, props }; let component = Box::new(COMP::create(&context)); Self { component, - root_node, + render_state: initial_render_state, context, - parent, - next_sibling, node_ref, suspension: None, has_rendered: false, - #[cfg(feature = "ssr")] - html_sender, - #[cfg(debug_assertions)] vcomp_id, } @@ -76,14 +52,10 @@ impl ComponentState { } pub struct CreateRunner { - pub(super) parent: Option, - pub(super) next_sibling: NodeRef, - pub(super) placeholder: BNode, - pub(super) node_ref: NodeRef, - pub(super) props: Rc, - pub(super) scope: Scope, - #[cfg(feature = "ssr")] - pub(super) html_sender: Option>, + pub initial_render_state: ComponentRenderState, + pub node_ref: NodeRef, + pub props: Rc, + pub scope: Scope, } impl Runnable for CreateRunner { @@ -94,14 +66,10 @@ impl Runnable for CreateRunner { super::log_event(self.scope.vcomp_id, "create"); *current_state = Some(ComponentState::new( - self.parent, - self.next_sibling, - self.placeholder, + self.initial_render_state, self.node_ref, self.scope.clone(), self.props, - #[cfg(feature = "ssr")] - self.html_sender, )); } } @@ -119,8 +87,8 @@ pub enum UpdateEvent { } pub struct UpdateRunner { - pub(super) state: Shared>>, - pub(super) event: UpdateEvent, + pub state: Shared>>, + pub event: UpdateEvent, } impl Runnable for UpdateRunner { @@ -137,7 +105,7 @@ impl Runnable for UpdateRunner { // When components are updated, a new node ref could have been passed in state.node_ref = node_ref; // When components are updated, their siblings were likely also updated - state.next_sibling = next_sibling; + state.render_state.reuse(next_sibling); // Only trigger changed if props were changed if state.context.props != props { state.context.props = Rc::clone(&props); @@ -146,11 +114,8 @@ impl Runnable for UpdateRunner { false } } - UpdateEvent::Shift(parent, next_sibling) => { - state.root_node.shift(&parent, next_sibling.clone()); - - state.parent = Some(parent); - state.next_sibling = next_sibling; + UpdateEvent::Shift(new_parent, next_sibling) => { + state.render_state.shift(new_parent, next_sibling); false } @@ -179,7 +144,7 @@ impl Runnable for UpdateRunner { } pub struct DestroyRunner { - pub(super) state: Shared>>, + pub state: Shared>>, } impl Runnable for DestroyRunner { @@ -189,17 +154,14 @@ impl Runnable for DestroyRunner { super::log_event(state.vcomp_id, "destroy"); state.component.destroy(&state.context); - - if let Some(ref m) = state.parent { - state.root_node.detach(m); - state.node_ref.set(None); - } + state.render_state.detach(); + state.node_ref.set(None); } } } pub struct RenderRunner { - pub(super) state: Shared>>, + pub state: Shared>>, } impl Runnable for RenderRunner { @@ -220,20 +182,9 @@ impl Runnable for RenderRunner { suspense.resume(m); } - - if let Some(ref parent) = state.parent { - let scope = state.context.scope.clone().into(); - let next_sibling = state.next_sibling.clone(); - - let node = - root.reconcile(&scope, parent, next_sibling, &mut state.root_node); - state.node_ref.link(node); - } else { - #[cfg(feature = "ssr")] - if let Some(tx) = state.html_sender.take() { - tx.send(root).unwrap(); - } - } + let scope = state.context.scope.clone().into(); + let node = state.render_state.reconcile(root, &scope); + state.node_ref.link(node); } Err(RenderError::Suspended(m)) => { @@ -292,7 +243,7 @@ impl Runnable for RenderRunner { } pub struct RenderedRunner { - pub(super) state: Shared>>, + pub state: Shared>>, } impl Runnable for RenderedRunner { @@ -301,7 +252,7 @@ impl Runnable for RenderedRunner { #[cfg(debug_assertions)] super::log_event(state.vcomp_id, "rendered"); - if state.suspension.is_none() && state.parent.is_some() { + if state.suspension.is_none() && state.render_state.should_trigger_rendered() { let first_render = !state.has_rendered; state.component.rendered(&state.context, first_render); state.has_rendered = true; @@ -314,6 +265,7 @@ impl Runnable for RenderedRunner { mod tests { extern crate self as yew; + use crate::dom_bundle::ComponentRenderState; use crate::html; use crate::html::*; use crate::Properties; @@ -437,10 +389,12 @@ mod tests { let document = gloo_utils::document(); let scope = Scope::::new(None); let el = document.create_element("div").unwrap(); + let node_ref = NodeRef::default(); + let render_state = ComponentRenderState::new(el, NodeRef::default(), &node_ref); let lifecycle = props.lifecycle.clone(); lifecycle.borrow_mut().clear(); - scope.mount_in_place(el, NodeRef::default(), NodeRef::default(), Rc::new(props)); + scope.mount_in_place(render_state, node_ref, Rc::new(props)); assert_eq!(&lifecycle.borrow_mut().deref()[..], expected); } diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index aaaa63f7ca0..5e7048451f3 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -1,20 +1,51 @@ //! Components wrapped with context including properties, state, and link mod children; +mod lifecycle; mod properties; +mod scope; use super::{Html, HtmlResult, IntoHtmlResult}; -pub use crate::dom_bundle::{AnyScope, Scope, SendAsMessage}; pub use children::*; pub use properties::*; +pub use scope::{AnyScope, Scope, SendAsMessage}; use std::rc::Rc; +thread_local! { + #[cfg(debug_assertions)] + static EVENT_HISTORY: std::cell::RefCell>> + = Default::default(); +} + +/// Push [VComp] event to lifecycle debugging registry +#[cfg(debug_assertions)] +pub fn log_event(vcomp_id: u64, event: impl ToString) { + EVENT_HISTORY.with(|h| { + h.borrow_mut() + .entry(vcomp_id) + .or_default() + .push(event.to_string()) + }); +} + +/// Get [VComp] event log from lifecycle debugging registry +#[cfg(debug_assertions)] +#[allow(dead_code)] +pub fn get_event_log(vcomp_id: u64) -> Vec { + EVENT_HISTORY.with(|h| { + h.borrow() + .get(&vcomp_id) + .map(|l| (*l).clone()) + .unwrap_or_default() + }) +} + /// The [`Component`]'s context. This contains component's [`Scope`] and and props and /// is passed to every lifecycle method. #[derive(Debug)] pub struct Context { - pub(crate) scope: Scope, - pub(crate) props: Rc, + scope: Scope, + props: Rc, } impl Context { diff --git a/packages/yew/src/dom_bundle/bcomp/scope.rs b/packages/yew/src/html/component/scope.rs similarity index 83% rename from packages/yew/src/dom_bundle/bcomp/scope.rs rename to packages/yew/src/html/component/scope.rs index 0feb600afa4..20d9b1ccb50 100644 --- a/packages/yew/src/dom_bundle/bcomp/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -4,18 +4,17 @@ use super::lifecycle::{ ComponentState, CreateRunner, DestroyRunner, RenderRunner, RenderedRunner, UpdateEvent, UpdateRunner, }; +use crate::callback::Callback; use crate::context::{ContextHandle, ContextProvider}; -use crate::dom_bundle::insert_node; +use crate::dom_bundle::{ComponentRenderState, Scoped}; use crate::html::{BaseComponent, NodeRef}; use crate::scheduler::{self, Shared}; -use crate::{callback::Callback, dom_bundle::BNode}; -use gloo_utils::document; use std::any::{Any, TypeId}; use std::cell::{Ref, RefCell}; use std::ops::Deref; use std::rc::Rc; use std::{fmt, iter}; -use web_sys::{Element, Node}; +use web_sys::Element; /// Untyped scope used for accessing parent scope #[derive(Debug, Clone)] @@ -29,6 +28,20 @@ pub struct AnyScope { pub(super) vcomp_id: u64, } +#[cfg(test)] +impl AnyScope { + pub(crate) fn test() -> Self { + Self { + type_id: TypeId::of::<()>(), + parent: None, + state: Rc::new(()), + + #[cfg(debug_assertions)] + vcomp_id: 0, + } + } +} + impl From> for AnyScope { fn from(scope: Scope) -> Self { AnyScope { @@ -43,18 +56,6 @@ impl From> for AnyScope { } impl AnyScope { - #[cfg(test)] - pub(in crate::dom_bundle) fn test() -> Self { - Self { - type_id: TypeId::of::<()>(), - parent: None, - state: Rc::new(()), - - #[cfg(debug_assertions)] - vcomp_id: 0, - } - } - /// Returns the parent scope pub fn get_parent(&self) -> Option<&AnyScope> { self.parent.as_deref() @@ -72,19 +73,12 @@ impl AnyScope { .downcast::>>>() .expect("unexpected component type"); - #[cfg(debug_assertions)] - let vcomp_id = state - .borrow() - .as_ref() - .map(|s| s.vcomp_id) - .unwrap_or_default(); - Scope { parent: self.parent, state, #[cfg(debug_assertions)] - vcomp_id, + vcomp_id: self.vcomp_id, } } @@ -110,15 +104,14 @@ impl AnyScope { } } -pub trait Scoped { - fn to_any(&self) -> AnyScope; - /// Got the root node if it hasn't already been destroyed - fn root_bnode(&self) -> Option>; - /// Shift the node associated with this scope to a new place - fn shift_node(&self, parent: Element, next_sibling: NodeRef); - /// Process an event to destroy a component - fn destroy(self); - fn destroy_boxed(self: Box); +/// A context which allows sending messages to a component. +pub struct Scope { + parent: Option>, + pub(super) state: Shared>>, + + // Used for debug logging + #[cfg(debug_assertions)] + pub(super) vcomp_id: u64, } impl Scoped for Scope { @@ -126,14 +119,14 @@ impl Scoped for Scope { self.clone().into() } - fn root_bnode(&self) -> Option> { + fn render_state(&self) -> Option> { let state_ref = self.state.borrow(); // check that component hasn't been destroyed state_ref.as_ref()?; Some(Ref::map(state_ref, |state_ref| { - &state_ref.as_ref().unwrap().root_node + &state_ref.as_ref().unwrap().render_state })) } @@ -155,16 +148,6 @@ impl Scoped for Scope { } } -/// A context which allows sending messages to a component. -pub struct Scope { - parent: Option>, - pub(super) state: Shared>>, - - // Used for debug logging - #[cfg(debug_assertions)] - pub(super) vcomp_id: u64, -} - impl fmt::Debug for Scope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("Scope<_>") @@ -224,32 +207,18 @@ impl Scope { } /// Mounts a component with `props` to the specified `element` in the DOM. - pub(in crate::dom_bundle) fn mount_in_place( + pub(crate) fn mount_in_place( &self, - parent: Element, - next_sibling: NodeRef, + initial_render_state: ComponentRenderState, node_ref: NodeRef, props: Rc, ) { - #[cfg(debug_assertions)] - super::log_event(self.vcomp_id, "create placeholder"); - let placeholder = { - let placeholder: Node = document().create_text_node("").into(); - insert_node(&placeholder, &parent, next_sibling.get().as_ref()); - node_ref.set(Some(placeholder.clone())); - BNode::BRef(placeholder) - }; - scheduler::push_component_create( CreateRunner { - parent: Some(parent), - next_sibling, - placeholder, + initial_render_state, node_ref, props, scope: self.clone(), - #[cfg(feature = "ssr")] - html_sender: None, }, RenderRunner { state: self.state.clone(), @@ -262,7 +231,7 @@ impl Scope { scheduler::start(); } - pub(super) fn reuse( + pub(crate) fn reuse( &self, props: Rc, node_ref: NodeRef, @@ -372,44 +341,25 @@ impl Scope { #[cfg(feature = "ssr")] mod feat_ssr { use super::*; - use crate::dom_bundle::BList; use futures::channel::oneshot; impl Scope { - pub(crate) async fn render_to_string(&self, w: &mut String, props: Rc) { + pub(crate) async fn render_to_string(self, w: &mut String, props: Rc) { let (tx, rx) = oneshot::channel(); + let initial_render_state = ComponentRenderState::new_ssr(tx); - scheduler::push_component_create( - CreateRunner { - parent: None, - next_sibling: NodeRef::default(), - placeholder: BNode::BList(BList::new()), - node_ref: NodeRef::default(), - props, - scope: self.clone(), - html_sender: Some(tx), - }, - RenderRunner { - state: self.state.clone(), - }, - RenderedRunner { - state: self.state.clone(), - }, - ); - scheduler::start(); + self.mount_in_place(initial_render_state, NodeRef::default(), props); let html = rx.await.unwrap(); let self_any_scope = self.to_any(); html.render_to_string(w, &self_any_scope).await; - scheduler::push_component_destroy(DestroyRunner { - state: self.state.clone(), - }); - scheduler::start(); + self.destroy(); } } } + #[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] #[cfg(any(target_arch = "wasm32", feature = "tokio"))] mod feat_io { diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index 9e5cd5fe1cb..91bf4c95212 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -3,8 +3,8 @@ use super::*; use crate::html::Scope; /// A Yew Server-side Renderer. -#[cfg_attr(documenting, doc(cfg(feature = "ssr")))] #[derive(Debug)] +#[cfg_attr(documenting, doc(cfg(feature = "ssr")))] pub struct ServerRenderer where COMP: BaseComponent, From 87048812cc9e5d08d5bc385f51b7c702c96c98e9 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 19 Jan 2022 14:40:59 +0100 Subject: [PATCH 17/26] move back Value and InputFields into html --- .../yew/src/dom_bundle/btag/attributes.rs | 91 ++----------------- packages/yew/src/dom_bundle/btag/btag_impl.rs | 4 +- packages/yew/src/dom_bundle/btag/mod.rs | 1 - packages/yew/src/dom_bundle/mod.rs | 6 +- packages/yew/src/virtual_dom/vtag.rs | 82 +++++++++++++++-- 5 files changed, 84 insertions(+), 100 deletions(-) diff --git a/packages/yew/src/dom_bundle/btag/attributes.rs b/packages/yew/src/dom_bundle/btag/attributes.rs index 8f7d9683bc4..cdec0630e6b 100644 --- a/packages/yew/src/dom_bundle/btag/attributes.rs +++ b/packages/yew/src/dom_bundle/btag/attributes.rs @@ -1,55 +1,25 @@ use super::Apply; -use crate::virtual_dom::{AttrValue, Attributes}; +use crate::virtual_dom::vtag::{InputFields, Value}; +use crate::virtual_dom::Attributes; use indexmap::IndexMap; -use std::{ - collections::HashMap, - iter, - marker::PhantomData, - ops::{Deref, DerefMut}, -}; +use std::collections::HashMap; +use std::iter; +use std::ops::Deref; use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; -/// Value field corresponding to an [Element]'s `value` property -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Value(Option, PhantomData); - -impl Default for Value { - fn default() -> Self { - Value(None, PhantomData) - } -} - -impl Value { - /// Create a new value. The caller should take care that the value is valid for the element's `value` property - pub fn new(value: Option) -> Self { - Value(value, PhantomData) - } - /// Set a new value. The caller should take care that the value is valid for the element's `value` property - pub fn set(&mut self, value: Option) { - self.0 = value; - } -} - -impl Deref for Value { - type Target = Option; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - impl Apply for Value { type Element = T; type Bundle = Self; fn apply(self, el: &Self::Element) -> Self { - if let Some(v) = &self.0 { + if let Some(v) = self.deref() { el.set_value(v); } self } fn apply_diff(self, el: &Self::Element, bundle: &mut Self) { - match (&self.0, &bundle.0) { + match (self.deref(), (*bundle).deref()) { (Some(new), Some(_)) => { // Refresh value from the DOM. It might have changed. if new.as_ref() != el.value() { @@ -88,53 +58,6 @@ pub trait AccessValue { fn set_value(&self, v: &str); } -/// Fields specific to -/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag](crate::virtual_dom::VTag)s -#[derive(Debug, Clone, Default, Eq, PartialEq)] -pub struct InputFields { - /// Contains a value of an - /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). - value: Value, - /// Represents `checked` attribute of - /// [input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-checked). - /// It exists to override standard behavior of `checked` attribute, because - /// in original HTML it sets `defaultChecked` value of `InputElement`, but for reactive - /// frameworks it's more useful to control `checked` value of an `InputElement`. - checked: bool, -} - -impl Deref for InputFields { - type Target = Value; - - fn deref(&self) -> &Self::Target { - &self.value - } -} - -impl DerefMut for InputFields { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.value - } -} - -impl InputFields { - /// Crate new attributes for an [InputElement] element - pub fn new(value: Option, checked: bool) -> Self { - Self { - value: Value::new(value), - checked, - } - } - /// Get the 'checked' attribute on the [InputElement] - pub fn checked(&self) -> bool { - self.checked - } - /// Set the 'checked' attribute on the [InputElement] - pub fn set_checked(&mut self, checked: bool) { - self.checked = checked; - } -} - impl Apply for InputFields { type Element = InputElement; type Bundle = Self; diff --git a/packages/yew/src/dom_bundle/btag/btag_impl.rs b/packages/yew/src/dom_bundle/btag/btag_impl.rs index 6db340d1f00..c9b4a40b454 100644 --- a/packages/yew/src/dom_bundle/btag/btag_impl.rs +++ b/packages/yew/src/dom_bundle/btag/btag_impl.rs @@ -1,11 +1,11 @@ //! This module contains the bundle implementation of a tag [BTag] -use super::attributes::{InputFields, Value}; use super::listeners::ListenerRegistration; use super::Apply; use crate::dom_bundle::{insert_node, BNode, DomBundle, Reconcilable}; use crate::html::AnyScope; -use crate::virtual_dom::{vtag::VTagInner, vtag::SVG_NAMESPACE, Attributes, Key, VTag}; +use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE}; +use crate::virtual_dom::{Attributes, Key, VTag}; use crate::NodeRef; use gloo::console; use gloo_utils::document; diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index cf6870c44f5..fcb8291da30 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -15,7 +15,6 @@ trait Apply { fn apply_diff(self, el: &Self::Element, bundle: &mut Self::Bundle); } -pub use attributes::{InputFields, Value}; pub use btag_impl::BTag; pub use listeners::set_event_bubbling; diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 6c78b64e27e..30f87e0f4be 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -26,7 +26,6 @@ use self::btag::BTag; use self::btext::BText; pub(crate) use self::bcomp::{ComponentRenderState, Mountable, PropsWrapper, Scoped}; -pub(crate) use self::btag::{InputFields, Value}; #[doc(hidden)] // Publically exported from crate::app_handle pub use self::app_handle::AppHandle; @@ -50,10 +49,6 @@ trait DomBundle { fn shift(&self, next_parent: &Element, next_sibling: NodeRef); } -// TODO(#938): What about implementing `VDiff` for `Element`? -// It would make it possible to include ANY element into the tree. -// `Ace` editor embedding for example? - /// This trait provides features to update a tree by calculating a difference against another tree. trait Reconcilable { type Bundle: DomBundle; @@ -137,6 +132,7 @@ macro_rules! test_log { #[cfg(not(all(test, feature = "wasm_test", verbose_tests)))] macro_rules! test_log { ($fmt:literal, $($arg:expr),* $(,)?) => { + // Only type-check the format expression, do not run any side effects let _ = std::format_args!(concat!("\t ", $fmt), $($arg),*); }; } diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 1964711aecd..b01d5062a41 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1,15 +1,14 @@ //! This module contains the implementation of a virtual element node [VTag]. use super::{AttrValue, Attributes, Key, Listener, Listeners, VList, VNode}; -use crate::{ - dom_bundle::{InputFields, Value}, - html::{IntoPropValue, NodeRef}, -}; -use std::borrow::Cow; +use crate::html::{IntoPropValue, NodeRef}; use std::cmp::PartialEq; +use std::marker::PhantomData; use std::mem; +use std::ops::Deref; use std::rc::Rc; -use web_sys::HtmlTextAreaElement as TextAreaElement; +use std::{borrow::Cow, ops::DerefMut}; +use web_sys::{HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; /// SVG namespace string used for creating svg elements pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; @@ -17,6 +16,73 @@ pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; /// Default namespace for html elements pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; +/// Value field corresponding to an [Element]'s `value` property +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct Value(Option, PhantomData); + +impl Default for Value { + fn default() -> Self { + Self::new(None) + } +} + +impl Value { + /// Create a new value. The caller should take care that the value is valid for the element's `value` property + fn new(value: Option) -> Self { + Value(value, PhantomData) + } + /// Set a new value. The caller should take care that the value is valid for the element's `value` property + fn set(&mut self, value: Option) { + self.0 = value; + } +} + +impl Deref for Value { + type Target = Option; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// Fields specific to +/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag](crate::virtual_dom::VTag)s +#[derive(Debug, Clone, Default, Eq, PartialEq)] +pub(crate) struct InputFields { + /// Contains a value of an + /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). + pub(crate) value: Value, + /// Represents `checked` attribute of + /// [input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-checked). + /// It exists to override standard behavior of `checked` attribute, because + /// in original HTML it sets `defaultChecked` value of `InputElement`, but for reactive + /// frameworks it's more useful to control `checked` value of an `InputElement`. + pub(crate) checked: bool, +} + +impl Deref for InputFields { + type Target = Value; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl DerefMut for InputFields { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.value + } +} + +impl InputFields { + /// Crate new attributes for an [InputElement] element + fn new(value: Option, checked: bool) -> Self { + Self { + value: Value::new(value), + checked, + } + } +} + /// [VTag] fields that are specific to different [VTag] kinds. /// Decreases the memory footprint of [VTag] by avoiding impossible field and value combinations. #[derive(Debug, Clone)] @@ -264,7 +330,7 @@ impl VTag { /// (Not a value of node's attribute). pub fn checked(&self) -> bool { match &self.inner { - VTagInner::Input(f) => f.checked(), + VTagInner::Input(f) => f.checked, _ => false, } } @@ -274,7 +340,7 @@ impl VTag { /// (Not a value of node's attribute). pub fn set_checked(&mut self, value: bool) { if let VTagInner::Input(f) = &mut self.inner { - f.set_checked(value); + f.checked = value; } } From 02800d38b1018bcabc316f0cf4ce9966d7e59176 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 19 Jan 2022 16:45:54 +0100 Subject: [PATCH 18/26] actually only type-check format args in production --- packages/yew/src/dom_bundle/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 30f87e0f4be..c56fe6f0fcf 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -133,7 +133,7 @@ macro_rules! test_log { macro_rules! test_log { ($fmt:literal, $($arg:expr),* $(,)?) => { // Only type-check the format expression, do not run any side effects - let _ = std::format_args!(concat!("\t ", $fmt), $($arg),*); + let _ = || { std::format_args!(concat!("\t ", $fmt), $($arg),*); }; }; } /// Log an operation during tests for debugging purposes From f28ebfe16f2ec29d90ba4a1bce05d40cd222fc02 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 19 Jan 2022 18:03:46 +0100 Subject: [PATCH 19/26] fix documentation link --- packages/yew/src/html/component/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index 5e7048451f3..200704647f8 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -17,9 +17,9 @@ thread_local! { = Default::default(); } -/// Push [VComp] event to lifecycle debugging registry +/// Push [Component] event to lifecycle debugging registry #[cfg(debug_assertions)] -pub fn log_event(vcomp_id: u64, event: impl ToString) { +pub(crate) fn log_event(vcomp_id: u64, event: impl ToString) { EVENT_HISTORY.with(|h| { h.borrow_mut() .entry(vcomp_id) @@ -28,10 +28,10 @@ pub fn log_event(vcomp_id: u64, event: impl ToString) { }); } -/// Get [VComp] event log from lifecycle debugging registry +/// Get [Component] event log from lifecycle debugging registry #[cfg(debug_assertions)] #[allow(dead_code)] -pub fn get_event_log(vcomp_id: u64) -> Vec { +pub(crate) fn get_event_log(vcomp_id: u64) -> Vec { EVENT_HISTORY.with(|h| { h.borrow() .get(&vcomp_id) From a2a6b928734625965edb27c3a399b0b1e9a44c25 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 19 Jan 2022 20:47:03 +0100 Subject: [PATCH 20/26] move btag_impl up into containing module --- packages/yew/src/dom_bundle/btag/btag_impl.rs | 255 ----------------- packages/yew/src/dom_bundle/btag/mod.rs | 259 +++++++++++++++++- 2 files changed, 256 insertions(+), 258 deletions(-) delete mode 100644 packages/yew/src/dom_bundle/btag/btag_impl.rs diff --git a/packages/yew/src/dom_bundle/btag/btag_impl.rs b/packages/yew/src/dom_bundle/btag/btag_impl.rs deleted file mode 100644 index c9b4a40b454..00000000000 --- a/packages/yew/src/dom_bundle/btag/btag_impl.rs +++ /dev/null @@ -1,255 +0,0 @@ -//! This module contains the bundle implementation of a tag [BTag] - -use super::listeners::ListenerRegistration; -use super::Apply; -use crate::dom_bundle::{insert_node, BNode, DomBundle, Reconcilable}; -use crate::html::AnyScope; -use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE}; -use crate::virtual_dom::{Attributes, Key, VTag}; -use crate::NodeRef; -use gloo::console; -use gloo_utils::document; -use std::ops::DerefMut; -use std::{borrow::Cow, hint::unreachable_unchecked}; -use wasm_bindgen::JsCast; -use web_sys::{Element, HtmlTextAreaElement as TextAreaElement}; - -/// [BTag] fields that are specific to different [BTag] kinds. -/// Decreases the memory footprint of [BTag] by avoiding impossible field and value combinations. -#[derive(Debug)] -enum BTagInner { - /// Fields specific to - /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) - Input(InputFields), - /// Fields specific to - /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) - Textarea { - /// Contains a value of an - /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) - value: Value, - }, - /// Fields for all other kinds of [VTag]s - Other { - /// A tag of the element. - tag: Cow<'static, str>, - /// List of child nodes - child_bundle: BNode, - }, -} - -/// The bundle implementation to [VTag] -#[derive(Debug)] -pub struct BTag { - /// [BTag] fields that are specific to different [BTag] kinds. - inner: BTagInner, - listeners: ListenerRegistration, - /// A reference to the DOM [`Element`]. - reference: Element, - /// A node reference used for DOM access in Component lifecycle methods - node_ref: NodeRef, - attributes: Attributes, - key: Option, -} - -impl DomBundle for BTag { - fn detach(self, parent: &Element) { - self.listeners.unregister(); - - let node = self.reference; - // recursively remove its children - if let BTagInner::Other { child_bundle, .. } = self.inner { - child_bundle.detach(&node); - } - if parent.remove_child(&node).is_err() { - console::warn!("Node not found to remove VTag"); - } - // It could be that the ref was already reused when rendering another element. - // Only unset the ref it still belongs to our node - if self.node_ref.get().as_ref() == Some(&node) { - self.node_ref.set(None); - } - } - - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - next_parent - .insert_before(&self.reference, next_sibling.get().as_ref()) - .unwrap(); - } -} - -impl Reconcilable for VTag { - type Bundle = BTag; - - fn attach( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ) -> (NodeRef, Self::Bundle) { - let el = self.create_element(parent); - let Self { - listeners, - attributes, - node_ref, - key, - .. - } = self; - insert_node(&el, parent, next_sibling.get().as_ref()); - - let attributes = attributes.apply(&el); - let listeners = listeners.apply(&el); - - let inner = match self.inner { - VTagInner::Input(f) => { - let f = f.apply(el.unchecked_ref()); - BTagInner::Input(f) - } - VTagInner::Textarea { value } => { - let value = value.apply(el.unchecked_ref()); - BTagInner::Textarea { value } - } - VTagInner::Other { children, tag } => { - let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default()); - BTagInner::Other { - child_bundle: child_bundle.into(), - tag, - } - } - }; - node_ref.set(Some(el.clone().into())); - ( - node_ref.clone(), - BTag { - inner, - listeners, - reference: el, - attributes, - key, - node_ref, - }, - ) - } - - fn reconcile( - self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - bundle: &mut BNode, - ) -> NodeRef { - // This kind of branching patching routine reduces branch predictor misses and the need to - // unpack the enums (including `Option`s) all the time, resulting in a more streamlined - // patching flow - let is_matching_tag = match bundle { - BNode::BTag(ex) if self.key == ex.key => match (&self.inner, &ex.inner) { - (VTagInner::Input(_), BTagInner::Input(_)) => true, - (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true, - (VTagInner::Other { tag: l, .. }, BTagInner::Other { tag: r, .. }) if l == r => { - true - } - _ => false, - }, - _ => false, - }; - // If the ancestor is a tag of the same type, don't recreate, keep the - // old tag and update its attributes and children. - let tag = if is_matching_tag { - match bundle { - BNode::BTag(a) => { - // Preserve the reference that already exists - a.deref_mut() - } - _ => unsafe { unreachable_unchecked() }, - } - } else { - return self.replace(parent_scope, parent, next_sibling, bundle); - }; - - let el = &tag.reference; - self.attributes.apply_diff(el, &mut tag.attributes); - self.listeners.apply_diff(el, &mut tag.listeners); - - match (self.inner, &mut tag.inner) { - (VTagInner::Input(new), BTagInner::Input(old)) => { - new.apply_diff(el.unchecked_ref(), old); - } - (VTagInner::Textarea { value: new }, BTagInner::Textarea { value: old }) => { - new.apply_diff(el.unchecked_ref(), old); - } - ( - VTagInner::Other { children: new, .. }, - BTagInner::Other { - child_bundle: old, .. - }, - ) => { - new.reconcile(parent_scope, el, NodeRef::default(), old); - } - // Can not happen, because we checked for tag equability above - _ => unsafe { unreachable_unchecked() }, - } - - tag.key = self.key; - - if self.node_ref != tag.node_ref && tag.node_ref.get().as_ref() == Some(el) { - tag.node_ref.set(None); - } - if self.node_ref != tag.node_ref { - tag.node_ref = self.node_ref; - tag.node_ref.set(Some(el.clone().into())); - } - - tag.node_ref.clone() - } -} - -impl VTag { - fn create_element(&self, parent: &Element) -> Element { - let tag = self.tag(); - if tag == "svg" - || parent - .namespace_uri() - .map_or(false, |ns| ns == SVG_NAMESPACE) - { - let namespace = Some(SVG_NAMESPACE); - document() - .create_element_ns(namespace, tag) - .expect("can't create namespaced element for vtag") - } else { - document() - .create_element(tag) - .expect("can't create element for vtag") - } - } -} - -impl BTag { - /// Get the key of the underlying tag - pub(in crate::dom_bundle) fn key(&self) -> Option<&Key> { - self.key.as_ref() - } - - #[cfg(test)] - pub(super) fn reference(&self) -> &Element { - &self.reference - } - - #[cfg(test)] - pub(super) fn children(&self) -> &[BNode] { - match &self.inner { - BTagInner::Other { child_bundle, .. } => match child_bundle { - BNode::BList(blist) => blist, - _ => unreachable!("should be blist"), - }, - _ => &[], - } - } - - #[cfg(test)] - pub(super) fn tag(&self) -> &str { - match &self.inner { - BTagInner::Input { .. } => "input", - BTagInner::Textarea { .. } => "textarea", - BTagInner::Other { tag, .. } => tag.as_ref(), - } - } -} diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index fcb8291da30..e53d9dfe318 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -1,7 +1,23 @@ +//! This module contains the bundle implementation of a tag [BTag] + mod attributes; -mod btag_impl; mod listeners; +pub use listeners::set_event_bubbling; + +use super::{insert_node, BNode, DomBundle, Reconcilable}; +use crate::html::AnyScope; +use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE}; +use crate::virtual_dom::{Attributes, Key, VTag}; +use crate::NodeRef; +use gloo::console; +use gloo_utils::document; +use listeners::ListenerRegistration; +use std::ops::DerefMut; +use std::{borrow::Cow, hint::unreachable_unchecked}; +use wasm_bindgen::JsCast; +use web_sys::{Element, HtmlTextAreaElement as TextAreaElement}; + /// Applies contained changes to DOM [web_sys::Element] trait Apply { /// [web_sys::Element] subtype to apply the changes to @@ -15,8 +31,245 @@ trait Apply { fn apply_diff(self, el: &Self::Element, bundle: &mut Self::Bundle); } -pub use btag_impl::BTag; -pub use listeners::set_event_bubbling; +/// [BTag] fields that are specific to different [BTag] kinds. +/// Decreases the memory footprint of [BTag] by avoiding impossible field and value combinations. +#[derive(Debug)] +enum BTagInner { + /// Fields specific to + /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) + Input(InputFields), + /// Fields specific to + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + Textarea { + /// Contains a value of an + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + value: Value, + }, + /// Fields for all other kinds of [VTag]s + Other { + /// A tag of the element. + tag: Cow<'static, str>, + /// List of child nodes + child_bundle: BNode, + }, +} + +/// The bundle implementation to [VTag] +#[derive(Debug)] +pub struct BTag { + /// [BTag] fields that are specific to different [BTag] kinds. + inner: BTagInner, + listeners: ListenerRegistration, + /// A reference to the DOM [`Element`]. + reference: Element, + /// A node reference used for DOM access in Component lifecycle methods + node_ref: NodeRef, + attributes: Attributes, + key: Option, +} + +impl DomBundle for BTag { + fn detach(self, parent: &Element) { + self.listeners.unregister(); + + let node = self.reference; + // recursively remove its children + if let BTagInner::Other { child_bundle, .. } = self.inner { + child_bundle.detach(&node); + } + if parent.remove_child(&node).is_err() { + console::warn!("Node not found to remove VTag"); + } + // It could be that the ref was already reused when rendering another element. + // Only unset the ref it still belongs to our node + if self.node_ref.get().as_ref() == Some(&node) { + self.node_ref.set(None); + } + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + next_parent + .insert_before(&self.reference, next_sibling.get().as_ref()) + .unwrap(); + } +} + +impl Reconcilable for VTag { + type Bundle = BTag; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let el = self.create_element(parent); + let Self { + listeners, + attributes, + node_ref, + key, + .. + } = self; + insert_node(&el, parent, next_sibling.get().as_ref()); + + let attributes = attributes.apply(&el); + let listeners = listeners.apply(&el); + + let inner = match self.inner { + VTagInner::Input(f) => { + let f = f.apply(el.unchecked_ref()); + BTagInner::Input(f) + } + VTagInner::Textarea { value } => { + let value = value.apply(el.unchecked_ref()); + BTagInner::Textarea { value } + } + VTagInner::Other { children, tag } => { + let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default()); + BTagInner::Other { + child_bundle: child_bundle.into(), + tag, + } + } + }; + node_ref.set(Some(el.clone().into())); + ( + node_ref.clone(), + BTag { + inner, + listeners, + reference: el, + attributes, + key, + node_ref, + }, + ) + } + + fn reconcile( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut BNode, + ) -> NodeRef { + // This kind of branching patching routine reduces branch predictor misses and the need to + // unpack the enums (including `Option`s) all the time, resulting in a more streamlined + // patching flow + let is_matching_tag = match bundle { + BNode::BTag(ex) if self.key == ex.key => match (&self.inner, &ex.inner) { + (VTagInner::Input(_), BTagInner::Input(_)) => true, + (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true, + (VTagInner::Other { tag: l, .. }, BTagInner::Other { tag: r, .. }) if l == r => { + true + } + _ => false, + }, + _ => false, + }; + // If the ancestor is a tag of the same type, don't recreate, keep the + // old tag and update its attributes and children. + let tag = if is_matching_tag { + match bundle { + BNode::BTag(a) => { + // Preserve the reference that already exists + a.deref_mut() + } + _ => unsafe { unreachable_unchecked() }, + } + } else { + return self.replace(parent_scope, parent, next_sibling, bundle); + }; + + let el = &tag.reference; + self.attributes.apply_diff(el, &mut tag.attributes); + self.listeners.apply_diff(el, &mut tag.listeners); + + match (self.inner, &mut tag.inner) { + (VTagInner::Input(new), BTagInner::Input(old)) => { + new.apply_diff(el.unchecked_ref(), old); + } + (VTagInner::Textarea { value: new }, BTagInner::Textarea { value: old }) => { + new.apply_diff(el.unchecked_ref(), old); + } + ( + VTagInner::Other { children: new, .. }, + BTagInner::Other { + child_bundle: old, .. + }, + ) => { + new.reconcile(parent_scope, el, NodeRef::default(), old); + } + // Can not happen, because we checked for tag equability above + _ => unsafe { unreachable_unchecked() }, + } + + tag.key = self.key; + + if self.node_ref != tag.node_ref && tag.node_ref.get().as_ref() == Some(el) { + tag.node_ref.set(None); + } + if self.node_ref != tag.node_ref { + tag.node_ref = self.node_ref; + tag.node_ref.set(Some(el.clone().into())); + } + + tag.node_ref.clone() + } +} + +impl VTag { + fn create_element(&self, parent: &Element) -> Element { + let tag = self.tag(); + if tag == "svg" + || parent + .namespace_uri() + .map_or(false, |ns| ns == SVG_NAMESPACE) + { + let namespace = Some(SVG_NAMESPACE); + document() + .create_element_ns(namespace, tag) + .expect("can't create namespaced element for vtag") + } else { + document() + .create_element(tag) + .expect("can't create element for vtag") + } + } +} + +impl BTag { + /// Get the key of the underlying tag + pub(super) fn key(&self) -> Option<&Key> { + self.key.as_ref() + } + + #[cfg(test)] + fn reference(&self) -> &Element { + &self.reference + } + + #[cfg(test)] + fn children(&self) -> &[BNode] { + match &self.inner { + BTagInner::Other { child_bundle, .. } => match child_bundle { + BNode::BList(blist) => blist, + _ => unreachable!("should be blist"), + }, + _ => &[], + } + } + + #[cfg(test)] + fn tag(&self) -> &str { + match &self.inner { + BTagInner::Input { .. } => "input", + BTagInner::Textarea { .. } => "textarea", + BTagInner::Other { tag, .. } => tag.as_ref(), + } + } +} #[cfg(test)] mod tests { From 1b446a630cc5104607bdeb745ebc168f6caa0a65 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 20 Jan 2022 16:02:14 +0100 Subject: [PATCH 21/26] shift comps immediately shifting the rendered Nodes does not tie into the lifecycle, as such it can happen immediately --- packages/yew/src/dom_bundle/blist.rs | 10 +++++----- packages/yew/src/html/component/lifecycle.rs | 8 -------- packages/yew/src/html/component/scope.rs | 19 +++++++++---------- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 7cc2c898716..a978acb8873 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -77,10 +77,10 @@ impl<'s> NodeWriter<'s> { } } /// Helper struct implementing [Eq] and [Hash] by only looking at a node's key -struct KeyedEntry(BNode, usize); +struct KeyedEntry(usize, BNode); impl Borrow for KeyedEntry { fn borrow(&self) -> &Key { - self.0.key().expect("unkeyed child in fully keyed list") + self.1.key().expect("unkeyed child in fully keyed list") } } impl Hash for KeyedEntry { @@ -241,7 +241,7 @@ impl BList { let mut spare_bundles: HashSet = HashSet::with_capacity((matching_len_end..rights_to).len()); for (idx, r) in (&mut spliced_middle).enumerate() { - spare_bundles.insert(KeyedEntry(r, idx)); + spare_bundles.insert(KeyedEntry(idx, r)); } // Step 2.2. Put the middle part back together in the new key order @@ -254,7 +254,7 @@ impl BList { .rev() { let bundle = match spare_bundles.take(key!(l)) { - Some(KeyedEntry(mut r_bundle, idx)) => { + Some(KeyedEntry(idx, mut r_bundle)) => { if idx < max_seen_idx { writer.shift(&mut r_bundle); } @@ -275,7 +275,7 @@ impl BList { rev_bundles.splice(matching_len_end..matching_len_end, replacements); // Step 2.3. Remove any extra rights - for KeyedEntry(r, _) in spare_bundles.drain() { + for KeyedEntry(_, r) in spare_bundles.drain() { test_log!("removing: {:?}", r); r.detach(parent); } diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index b421922bcf4..18a46004647 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -8,7 +8,6 @@ use crate::suspense::{Suspense, Suspension}; use crate::Callback; use crate::{Context, NodeRef}; use std::rc::Rc; -use web_sys::Element; pub struct ComponentState { pub(super) component: Box, @@ -82,8 +81,6 @@ pub enum UpdateEvent { MessageBatch(Vec), /// Wraps properties, node ref, and next sibling for a component. Properties(Rc, NodeRef, NodeRef), - /// Shift Scope. - Shift(Element, NodeRef), } pub struct UpdateRunner { @@ -114,11 +111,6 @@ impl Runnable for UpdateRunner { false } } - UpdateEvent::Shift(new_parent, next_sibling) => { - state.render_state.shift(new_parent, next_sibling); - - false - } }; #[cfg(debug_assertions)] diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 20d9b1ccb50..8b42a3ef97e 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -141,10 +141,10 @@ impl Scoped for Scope { } fn shift_node(&self, parent: Element, next_sibling: NodeRef) { - scheduler::push_component_update(UpdateRunner { - state: self.state.clone(), - event: UpdateEvent::Shift(parent, next_sibling), - }); + let mut state_ref = self.state.borrow_mut(); + if let Some(render_state) = state_ref.as_mut() { + render_state.render_state.shift(parent, next_sibling) + } } } @@ -174,12 +174,11 @@ impl Scope { /// Returns the linked component if available pub fn get_component(&self) -> Option + '_> { - self.state.try_borrow().ok().and_then(|state_ref| { - state_ref.as_ref()?; - Some(Ref::map(state_ref, |state| { - state.as_ref().unwrap().component.as_ref() - })) - }) + let state_ref = self.state.try_borrow().ok()?; + state_ref.as_ref()?; + Some(Ref::map(state_ref, |state| { + state.as_ref().unwrap().component.as_ref() + })) } /// Crate a scope with an optional parent scope From e96bceca1fe01c0bbdd83fb071efdaadbe498148 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Sat, 22 Jan 2022 13:55:20 +0100 Subject: [PATCH 22/26] use list-bundle in tag-bundle --- packages/yew/src/dom_bundle/bcomp.rs | 26 ++++--- packages/yew/src/dom_bundle/blist.rs | 26 ++++--- packages/yew/src/dom_bundle/bnode.rs | 22 ++++-- packages/yew/src/dom_bundle/bportal.rs | 23 +++--- packages/yew/src/dom_bundle/bsuspense.rs | 31 ++++---- packages/yew/src/dom_bundle/btag/mod.rs | 72 +++++++++---------- packages/yew/src/dom_bundle/btext.rs | 20 ++++-- packages/yew/src/dom_bundle/mod.rs | 10 ++- .../yew/src/dom_bundle/tests/layout_tests.rs | 2 +- packages/yew/src/dom_bundle/tests/mod.rs | 2 +- 10 files changed, 142 insertions(+), 92 deletions(-) diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index aa9501f67bb..5b35ba8070f 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -83,30 +83,38 @@ impl Reconcilable for VComp { ) } - fn reconcile( + fn reconcile_node( self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, bundle: &mut BNode, ) -> NodeRef { - let bcomp = match bundle { + match bundle { // If the existing bundle is the same type, reuse it and update its properties BNode::BComp(ref mut bcomp) if self.type_id == bcomp.type_id && self.key == bcomp.key => { - bcomp - } - _ => { - return self.replace(parent_scope, parent, next_sibling, bundle); + self.reconcile(parent_scope, parent, next_sibling, bcomp) } - }; + _ => self.replace(parent_scope, parent, next_sibling, bundle), + } + } + + fn reconcile( + self, + _parent_scope: &AnyScope, + _parent: &Element, + next_sibling: NodeRef, + bcomp: &mut Self::Bundle, + ) -> NodeRef { let VComp { mountable, node_ref, key, type_id: _, } = self; + bcomp.key = key; let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref.clone()); bcomp.node_ref.reuse(old_ref); @@ -246,7 +254,7 @@ impl ComponentRenderState { if let Some(ref parent) = self.parent { let next_sibling = self.next_sibling.clone(); - root.reconcile(scope, parent, next_sibling, &mut self.root_node) + root.reconcile_node(scope, parent, next_sibling, &mut self.root_node) } else { #[cfg(feature = "ssr")] if let Some(tx) = self.html_sender.take() { @@ -336,7 +344,7 @@ mod tests { for _ in 0..10000 { let node = html! { }; - node.reconcile( + node.reconcile_node( &parent_scope, &parent_element, NodeRef::default(), diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index a978acb8873..103890ba918 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -68,7 +68,7 @@ impl<'s> NodeWriter<'s> { self.next_sibling ); // Advance the next sibling reference (from right to left) - let next = node.reconcile(self.parent_scope, self.parent, self.next_sibling, bundle); + let next = node.reconcile_node(self.parent_scope, self.parent, self.next_sibling, bundle); test_log!(" next_position: {:?}", next); Self { next_sibling: next, @@ -317,21 +317,30 @@ impl Reconcilable for VList { parent: &Element, next_sibling: NodeRef, ) -> (NodeRef, Self::Bundle) { - let mut self_ = BNode::BList(BList::new()); + let mut self_ = BList::new(); let node_ref = self.reconcile(parent_scope, parent, next_sibling, &mut self_); - let self_ = match self_ { - BNode::BList(self_) => self_, - _ => unreachable!("applying list should leave a VList in bundle ref"), - }; (node_ref, self_) } + fn reconcile_node( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut BNode, + ) -> NodeRef { + // 'Forcefully' create a pretend the existing node is a list. Creates a + // singleton list if it isn't already. + let blist = bundle.make_list(); + self.reconcile(parent_scope, parent, next_sibling, blist) + } + fn reconcile( mut self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, - bundle: &mut BNode, + blist: &mut BList, ) -> NodeRef { // Here, we will try to diff the previous list elements with the new // ones we want to insert. For that, we will use two lists: @@ -350,9 +359,6 @@ impl Reconcilable for VList { } let lefts = self.children; - // 'Forcefully' create a pretend the existing node is a list. Creates a - // singleton list if it isn't already. - let blist = bundle.make_list(); let rights = &mut blist.rev_children; test_log!("lefts: {:?}", lefts); test_log!("rights: {:?}", rights); diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index ef04d872d82..169081dbcdc 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -117,6 +117,16 @@ impl Reconcilable for VNode { } } + fn reconcile_node( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut BNode, + ) -> NodeRef { + self.reconcile(parent_scope, parent, next_sibling, bundle) + } + fn reconcile( self, parent_scope: &AnyScope, @@ -125,10 +135,10 @@ impl Reconcilable for VNode { bundle: &mut BNode, ) -> NodeRef { match self { - VNode::VTag(vtag) => vtag.reconcile(parent_scope, parent, next_sibling, bundle), - VNode::VText(vtext) => vtext.reconcile(parent_scope, parent, next_sibling, bundle), - VNode::VComp(vcomp) => vcomp.reconcile(parent_scope, parent, next_sibling, bundle), - VNode::VList(vlist) => vlist.reconcile(parent_scope, parent, next_sibling, bundle), + VNode::VTag(vtag) => vtag.reconcile_node(parent_scope, parent, next_sibling, bundle), + VNode::VText(vtext) => vtext.reconcile_node(parent_scope, parent, next_sibling, bundle), + VNode::VComp(vcomp) => vcomp.reconcile_node(parent_scope, parent, next_sibling, bundle), + VNode::VList(vlist) => vlist.reconcile_node(parent_scope, parent, next_sibling, bundle), VNode::VRef(node) => { let _existing = match bundle { BNode::BRef(ref n) if &node == n => n, @@ -144,10 +154,10 @@ impl Reconcilable for VNode { NodeRef::new(node) } VNode::VPortal(vportal) => { - vportal.reconcile(parent_scope, parent, next_sibling, bundle) + vportal.reconcile_node(parent_scope, parent, next_sibling, bundle) } VNode::VSuspense(vsuspsense) => { - vsuspsense.reconcile(parent_scope, parent, next_sibling, bundle) + vsuspsense.reconcile_node(parent_scope, parent, next_sibling, bundle) } } } diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index f624857d374..cf88176dc38 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -56,19 +56,26 @@ impl Reconcilable for VPortal { ) } - fn reconcile( + fn reconcile_node( self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, bundle: &mut BNode, ) -> NodeRef { - let portal = match bundle { - BNode::BPortal(portal) => portal, - _ => { - return self.replace(parent_scope, parent, next_sibling, bundle); - } - }; + match bundle { + BNode::BPortal(portal) => self.reconcile(parent_scope, parent, next_sibling, portal), + _ => self.replace(parent_scope, parent, next_sibling, bundle), + } + } + + fn reconcile( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + portal: &mut Self::Bundle, + ) -> NodeRef { let Self { host, inner_sibling, @@ -85,7 +92,7 @@ impl Reconcilable for VPortal { .node .shift(&portal.host, portal.inner_sibling.clone()); } - node.reconcile(parent_scope, parent, next_sibling.clone(), &mut portal.node); + node.reconcile_node(parent_scope, parent, next_sibling.clone(), &mut portal.node); next_sibling } } diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index fdb97134666..77afe039450 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -91,25 +91,32 @@ impl Reconcilable for VSuspense { } } - fn reconcile( + fn reconcile_node( self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, bundle: &mut BNode, ) -> NodeRef { - let suspense = match bundle { + match bundle { // We only preserve the child state if they are the same suspense. BNode::BSuspense(m) if m.key == self.key && self.detached_parent.as_ref() == Some(&m.detached_parent) => { - m - } - _ => { - return self.replace(parent_scope, parent, next_sibling, bundle); + self.reconcile(parent_scope, parent, next_sibling, m) } - }; + _ => self.replace(parent_scope, parent, next_sibling, bundle), + } + } + + fn reconcile( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + suspense: &mut Self::Bundle, + ) -> NodeRef { let VSuspense { children, fallback, @@ -127,24 +134,24 @@ impl Reconcilable for VSuspense { match (suspended, &mut suspense.fallback_bundle) { // Both suspended, reconcile children into detached_parent, fallback into the DOM (true, Some(fallback_bundle)) => { - children.reconcile( + children.reconcile_node( parent_scope, &detached_parent, NodeRef::default(), children_bundle, ); - fallback.reconcile(parent_scope, parent, next_sibling, fallback_bundle) + fallback.reconcile_node(parent_scope, parent, next_sibling, fallback_bundle) } // Not suspended, just reconcile the children into the DOM (false, None) => { - children.reconcile(parent_scope, parent, next_sibling, children_bundle) + children.reconcile_node(parent_scope, parent, next_sibling, children_bundle) } // Freshly suspended. Shift children into the detached parent, then add fallback to the DOM (true, None) => { children_bundle.shift(&detached_parent, NodeRef::default()); - children.reconcile( + children.reconcile_node( parent_scope, &detached_parent, NodeRef::default(), @@ -160,7 +167,7 @@ impl Reconcilable for VSuspense { suspense.fallback_bundle.take().unwrap().detach(parent); children_bundle.shift(parent, next_sibling.clone()); - children.reconcile(parent_scope, parent, next_sibling, children_bundle) + children.reconcile_node(parent_scope, parent, next_sibling, children_bundle) } } } diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index e53d9dfe318..761bfd0a850 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -5,7 +5,7 @@ mod listeners; pub use listeners::set_event_bubbling; -use super::{insert_node, BNode, DomBundle, Reconcilable}; +use super::{insert_node, BList, BNode, DomBundle, Reconcilable}; use crate::html::AnyScope; use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE}; use crate::virtual_dom::{Attributes, Key, VTag}; @@ -50,7 +50,7 @@ enum BTagInner { /// A tag of the element. tag: Cow<'static, str>, /// List of child nodes - child_bundle: BNode, + child_bundle: BList, }, } @@ -60,11 +60,11 @@ pub struct BTag { /// [BTag] fields that are specific to different [BTag] kinds. inner: BTagInner, listeners: ListenerRegistration, + attributes: Attributes, /// A reference to the DOM [`Element`]. reference: Element, /// A node reference used for DOM access in Component lifecycle methods node_ref: NodeRef, - attributes: Attributes, key: Option, } @@ -127,10 +127,7 @@ impl Reconcilable for VTag { } VTagInner::Other { children, tag } => { let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default()); - BTagInner::Other { - child_bundle: child_bundle.into(), - tag, - } + BTagInner::Other { child_bundle, tag } } }; node_ref.set(Some(el.clone().into())); @@ -147,7 +144,7 @@ impl Reconcilable for VTag { ) } - fn reconcile( + fn reconcile_node( self, parent_scope: &AnyScope, parent: &Element, @@ -157,31 +154,35 @@ impl Reconcilable for VTag { // This kind of branching patching routine reduces branch predictor misses and the need to // unpack the enums (including `Option`s) all the time, resulting in a more streamlined // patching flow - let is_matching_tag = match bundle { - BNode::BTag(ex) if self.key == ex.key => match (&self.inner, &ex.inner) { - (VTagInner::Input(_), BTagInner::Input(_)) => true, - (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true, - (VTagInner::Other { tag: l, .. }, BTagInner::Other { tag: r, .. }) if l == r => { - true - } - _ => false, - }, - _ => false, - }; - // If the ancestor is a tag of the same type, don't recreate, keep the - // old tag and update its attributes and children. - let tag = if is_matching_tag { - match bundle { - BNode::BTag(a) => { - // Preserve the reference that already exists - a.deref_mut() + match bundle { + // If the ancestor is a tag of the same type, don't recreate, keep the + // old tag and update its attributes and children. + BNode::BTag(ex) if self.key == ex.key => { + if match (&self.inner, &ex.inner) { + (VTagInner::Input(_), BTagInner::Input(_)) => true, + (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true, + (VTagInner::Other { tag: l, .. }, BTagInner::Other { tag: r, .. }) + if l == r => + { + true + } + _ => false, + } { + return self.reconcile(parent_scope, parent, next_sibling, ex.deref_mut()); } - _ => unsafe { unreachable_unchecked() }, } - } else { - return self.replace(parent_scope, parent, next_sibling, bundle); + _ => {} }; + self.replace(parent_scope, parent, next_sibling, bundle) + } + fn reconcile( + self, + parent_scope: &AnyScope, + _parent: &Element, + _next_sibling: NodeRef, + tag: &mut Self::Bundle, + ) -> NodeRef { let el = &tag.reference; self.attributes.apply_diff(el, &mut tag.attributes); self.listeners.apply_diff(el, &mut tag.listeners); @@ -253,10 +254,7 @@ impl BTag { #[cfg(test)] fn children(&self) -> &[BNode] { match &self.inner { - BTagInner::Other { child_bundle, .. } => match child_bundle { - BNode::BList(blist) => blist, - _ => unreachable!("should be blist"), - }, + BTagInner::Other { child_bundle, .. } => child_bundle, _ => &[], } } @@ -647,7 +645,7 @@ mod tests { let elem_vtag = assert_vtag(next_elem); // Sync happens here - elem_vtag.reconcile(&scope, &parent, NodeRef::default(), &mut elem); + elem_vtag.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); let vtag = assert_btag_ref(&elem); // Get new current value of the input element @@ -681,7 +679,7 @@ mod tests { let elem_vtag = assert_vtag(next_elem); // Value should not be refreshed - elem_vtag.reconcile(&scope, &parent, NodeRef::default(), &mut elem); + elem_vtag.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); let vtag = assert_btag_ref(&elem); // Get user value of the input element @@ -782,7 +780,7 @@ mod tests { let node_ref_b = NodeRef::default(); let elem_b = html! {
    }; - elem_b.reconcile(&scope, &parent, NodeRef::default(), &mut elem); + elem_b.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); let node_b = node_ref_b.get().unwrap(); @@ -815,7 +813,7 @@ mod tests { // while both should be bound to the same node ref let (_, mut elem) = before.attach(&scope, &parent, NodeRef::default()); - after.reconcile(&scope, &parent, NodeRef::default(), &mut elem); + after.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); assert_eq!( test_ref diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index 7ee71916cf9..e427483de01 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -48,19 +48,25 @@ impl Reconcilable for VText { } /// Renders virtual node over existing `TextNode`, but only if value of text has changed. - fn reconcile( + fn reconcile_node( self, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, bundle: &mut BNode, ) -> NodeRef { - let btext = match bundle { - BNode::BText(btext) => btext, - _ => { - return self.replace(parent_scope, parent, next_sibling, bundle); - } - }; + match bundle { + BNode::BText(btext) => self.reconcile(parent_scope, parent, next_sibling, btext), + _ => self.replace(parent_scope, parent, next_sibling, bundle), + } + } + fn reconcile( + self, + _parent_scope: &AnyScope, + _parent: &Element, + _next_sibling: NodeRef, + btext: &mut Self::Bundle, + ) -> NodeRef { let Self { text } = self; let ancestor_text = std::mem::replace(&mut btext.text, text); if btext.text != ancestor_text { diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index c56fe6f0fcf..bee8165a4d9 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -86,7 +86,7 @@ trait Reconcilable { /// kind, and otherwise reuse it. /// /// Returns a reference to the newly inserted element. - fn reconcile( + fn reconcile_node( self, parent_scope: &AnyScope, parent: &Element, @@ -94,6 +94,14 @@ trait Reconcilable { bundle: &mut BNode, ) -> NodeRef; + fn reconcile( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut Self::Bundle, + ) -> NodeRef; + /// Replace an existing bundle by attaching self and detaching the existing one fn replace( self, diff --git a/packages/yew/src/dom_bundle/tests/layout_tests.rs b/packages/yew/src/dom_bundle/tests/layout_tests.rs index 38bb2f09bf4..ef843d3949a 100644 --- a/packages/yew/src/dom_bundle/tests/layout_tests.rs +++ b/packages/yew/src/dom_bundle/tests/layout_tests.rs @@ -63,7 +63,7 @@ pub fn diff_layouts(layouts: Vec>) { log!("Independently reapply layout '{}'", layout.name); - vnode.reconcile( + vnode.reconcile_node( &parent_scope, &parent_element, next_sibling.clone(), diff --git a/packages/yew/src/dom_bundle/tests/mod.rs b/packages/yew/src/dom_bundle/tests/mod.rs index 60d8d164651..1208f4409c5 100644 --- a/packages/yew/src/dom_bundle/tests/mod.rs +++ b/packages/yew/src/dom_bundle/tests/mod.rs @@ -20,7 +20,7 @@ impl VNode { *bundle = Some(node); self_ref } - Some(bundle) => self.reconcile(parent_scope, parent, next_sibling, bundle), + Some(bundle) => self.reconcile_node(parent_scope, parent, next_sibling, bundle), } } } From 3dbc93287b4531a45bc5cf439d1af29331dcc8ee Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Sat, 22 Jan 2022 15:16:02 +0100 Subject: [PATCH 23/26] fix cargo make tests --- Makefile.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.toml b/Makefile.toml index 3e62f00fcb8..56d8503140f 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -82,7 +82,7 @@ dependencies = ["test"] [tasks.test] private = true command = "cargo" -args = ["test", "--all-targets", "--workspace", "--exclude", "website-test"] +args = ["test", "--all-targets"] [tasks.doc-test-flow] private = true From 9ee4561bff0ad73643293f44ea2016087a273d99 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 26 Jan 2022 22:15:58 +0100 Subject: [PATCH 24/26] improve 05_swap benchmark --- packages/yew/src/dom_bundle/blist.rs | 89 +++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 103890ba918..b0b00b57c90 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -4,6 +4,7 @@ use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::{Key, VList, VNode, VText}; use std::borrow::Borrow; +use std::cmp::Ordering; use std::collections::HashSet; use std::hash::Hash; use std::ops::Deref; @@ -28,6 +29,7 @@ impl Deref for BList { } /// Helper struct, that keeps the position where the next element is to be placed at +#[derive(Clone)] struct NodeWriter<'s> { parent_scope: &'s AnyScope, parent: &'s Element, @@ -246,27 +248,86 @@ impl BList { // Step 2.2. Put the middle part back together in the new key order let mut replacements: Vec = Vec::with_capacity((matching_len_start..lefts_to).len()); - // Roughly keep track of the order in which elements appear. If one appears out-of-order - // we (over approximately) have to shift the element, otherwise it is guaranteed to be in place. - let mut max_seen_idx = 0; + // The goal is to shift as few nodes as possible. + + // We handle runs of in-order nodes. When we encounter one out-of-order, we decide whether: + // - to shift all nodes in the current run to the position after the node before of the run, or to + // - "commit" to the current run, shift all nodes before the end of the run that we might + // encounter in the future, and then start a new run. + // Example of a run: + // barrier_idx --v v-- end_idx + // spliced_middle [ ... , M , N , C , D , E , F , G , ... ] (original element order) + // ^---^-----------^ the nodes that are part of the current run + // v start_writer + // replacements [ ... , M , C , D , G ] (new element order) + // ^-- start_idx + let mut barrier_idx = 0; // nodes from spliced_middle[..barrier_idx] are shifted unconditionally + struct RunInformation<'a> { + start_writer: NodeWriter<'a>, + start_idx: usize, + end_idx: usize, + } + let mut current_run: Option> = None; + for l in lefts .drain(matching_len_start..) // lefts_to.. has been drained .rev() { - let bundle = match spare_bundles.take(key!(l)) { - Some(KeyedEntry(idx, mut r_bundle)) => { - if idx < max_seen_idx { - writer.shift(&mut r_bundle); + let ancestor = spare_bundles.take(key!(l)); + // Check if we need to shift or commit a run + if let Some(run) = current_run.as_mut() { + if let Some(KeyedEntry(idx, _)) = ancestor { + // If there are only few runs, this is a cold path + if idx < run.end_idx { + // Have to decide whether to shift or commit the current run. A few calculations: + // A perfect estimate of the amount of nodes we have to shift if we move this run: + let run_length = replacements.len() - run.start_idx; + // A very crude estimate of the amount of nodes we will have to shift if we commit the run: + // Note nodes of the current run should not be counted here! + let estimated_skipped_nodes = run.end_idx - idx.max(barrier_idx); + // double run_length to counteract that the run is part of the estimated_skipped_nodes + if 2 * run_length > estimated_skipped_nodes { + // less work to commit to this run + barrier_idx = 1 + run.end_idx; + } else { + // Less work to shift this run + for r in replacements[run.start_idx..].iter_mut().rev() { + run.start_writer.shift(r); + } + } + current_run = None; } - max_seen_idx = usize::max(max_seen_idx, idx); - writer = writer.patch(l, &mut r_bundle); - r_bundle } - None => { - let (next_writer, bundle) = writer.add(l); - writer = next_writer; - bundle + } + let bundle = if let Some(KeyedEntry(idx, mut r_bundle)) = ancestor { + match current_run.as_mut() { + // hot path + // We know that idx >= run.end_idx, so this node doesn't need to shift + Some(run) => run.end_idx = idx, + None => { + match idx.cmp(&barrier_idx) { + // peep hole optimization, don't start a run as the element is already where it should be + Ordering::Equal => barrier_idx += 1, + // shift the node unconditionally, don't start a run + Ordering::Less => writer.shift(&mut r_bundle), + // start a run + Ordering::Greater => { + current_run = Some(RunInformation { + start_writer: writer.clone(), + start_idx: replacements.len(), + end_idx: idx, + }) + } + } + } } + writer = writer.patch(l, &mut r_bundle); + r_bundle + } else { + // Even if there is an active run, we don't have to modify it + let (next_writer, bundle) = writer.add(l); + writer = next_writer; + bundle }; replacements.push(bundle); } From c7130271327ec20c18ef2388e411ec50a1f1dcd7 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Sat, 29 Jan 2022 04:47:34 +0100 Subject: [PATCH 25/26] fix a blunder where I swapped operands --- packages/yew/src/dom_bundle/blist.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index c6ce4d07a1a..25d2c35c47f 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -422,7 +422,7 @@ impl Reconcilable for VList { test_log!("lefts: {:?}", lefts); test_log!("rights: {:?}", rights); - if let Some(additional) = rights.len().checked_sub(lefts.len()) { + if let Some(additional) = lefts.len().checked_sub(rights.len()) { rights.reserve_exact(additional); } let first = if self.fully_keyed && blist.fully_keyed { From bea070899aab9dbc4b60fe352d72b00b15845e01 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Sat, 5 Mar 2022 22:22:34 +0100 Subject: [PATCH 26/26] fix naming of BNode variants --- packages/yew/src/dom_bundle/bcomp.rs | 6 +- packages/yew/src/dom_bundle/blist.rs | 6 +- packages/yew/src/dom_bundle/bnode.rs | 86 ++++++++++++------------ packages/yew/src/dom_bundle/bportal.rs | 2 +- packages/yew/src/dom_bundle/bsuspense.rs | 2 +- packages/yew/src/dom_bundle/btag/mod.rs | 6 +- packages/yew/src/dom_bundle/btext.rs | 2 +- 7 files changed, 55 insertions(+), 55 deletions(-) diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index 51d018d402f..fe2d1ccabf0 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -92,7 +92,7 @@ impl Reconcilable for VComp { ) -> NodeRef { match bundle { // If the existing bundle is the same type, reuse it and update its properties - BNode::BComp(ref mut bcomp) + BNode::Comp(ref mut bcomp) if self.type_id == bcomp.type_id && self.key == bcomp.key => { self.reconcile(parent_scope, parent, next_sibling, bcomp) @@ -216,7 +216,7 @@ impl ComponentRenderState { let placeholder: Node = document().create_text_node("").into(); insert_node(&placeholder, &parent, next_sibling.get().as_ref()); node_ref.set(Some(placeholder.clone())); - BNode::BRef(placeholder) + BNode::Ref(placeholder) }; Self { root_node: placeholder, @@ -232,7 +232,7 @@ impl ComponentRenderState { use super::blist::BList; Self { - root_node: BNode::BList(BList::new()), + root_node: BNode::List(BList::new()), parent: None, next_sibling: NodeRef::default(), html_sender: Some(tx), diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 6a4f0541e66..21e66fc60ec 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -101,11 +101,11 @@ impl BNode { /// Assert that a bundle node is a list, or convert it to a list with a single child fn make_list(&mut self) -> &mut BList { match self { - Self::BList(blist) => blist, + Self::List(blist) => blist, self_ => { - let b = std::mem::replace(self_, BNode::BList(BList::new())); + let b = std::mem::replace(self_, BNode::List(BList::new())); let self_list = match self_ { - BNode::BList(blist) => blist, + BNode::List(blist) => blist, _ => unreachable!("just been set to the variant"), }; let key = b.key().cloned(); diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 22ed9d0b723..0e80563fd30 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -11,32 +11,32 @@ use web_sys::{Element, Node}; /// The bundle implementation to [VNode]. pub enum BNode { /// A bind between `VTag` and `Element`. - BTag(Box), + Tag(Box), /// A bind between `VText` and `TextNode`. - BText(BText), + Text(BText), /// A bind between `VComp` and `Element`. - BComp(BComp), + Comp(BComp), /// A holder for a list of other nodes. - BList(BList), + List(BList), /// A portal to another part of the document - BPortal(BPortal), + Portal(BPortal), /// A holder for any `Node` (necessary for replacing node). - BRef(Node), + Ref(Node), /// A suspendible document fragment. - BSuspense(Box), + Suspense(Box), } impl BNode { /// Get the key of the underlying node pub(super) fn key(&self) -> Option<&Key> { match self { - Self::BComp(bsusp) => bsusp.key(), - Self::BList(blist) => blist.key(), - Self::BRef(_) => None, - Self::BTag(btag) => btag.key(), - Self::BText(_) => None, - Self::BPortal(bportal) => bportal.key(), - Self::BSuspense(bsusp) => bsusp.key(), + Self::Comp(bsusp) => bsusp.key(), + Self::List(blist) => blist.key(), + Self::Ref(_) => None, + Self::Tag(btag) => btag.key(), + Self::Text(_) => None, + Self::Portal(bportal) => bportal.key(), + Self::Suspense(bsusp) => bsusp.key(), } } } @@ -45,34 +45,34 @@ impl DomBundle for BNode { /// Remove VNode from parent. fn detach(self, parent: &Element, parent_to_detach: bool) { match self { - Self::BTag(vtag) => vtag.detach(parent, parent_to_detach), - Self::BText(btext) => btext.detach(parent, parent_to_detach), - Self::BComp(bsusp) => bsusp.detach(parent, parent_to_detach), - Self::BList(blist) => blist.detach(parent, parent_to_detach), - Self::BRef(ref node) => { + Self::Tag(vtag) => vtag.detach(parent, parent_to_detach), + Self::Text(btext) => btext.detach(parent, parent_to_detach), + Self::Comp(bsusp) => bsusp.detach(parent, parent_to_detach), + Self::List(blist) => blist.detach(parent, parent_to_detach), + Self::Ref(ref node) => { // Always remove user-defined nodes to clear possible parent references of them if parent.remove_child(node).is_err() { console::warn!("Node not found to remove VRef"); } } - Self::BPortal(bportal) => bportal.detach(parent, parent_to_detach), - Self::BSuspense(bsusp) => bsusp.detach(parent, parent_to_detach), + Self::Portal(bportal) => bportal.detach(parent, parent_to_detach), + Self::Suspense(bsusp) => bsusp.detach(parent, parent_to_detach), } } fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { match self { - Self::BTag(ref vtag) => vtag.shift(next_parent, next_sibling), - Self::BText(ref btext) => btext.shift(next_parent, next_sibling), - Self::BComp(ref bsusp) => bsusp.shift(next_parent, next_sibling), - Self::BList(ref vlist) => vlist.shift(next_parent, next_sibling), - Self::BRef(ref node) => { + Self::Tag(ref vtag) => vtag.shift(next_parent, next_sibling), + Self::Text(ref btext) => btext.shift(next_parent, next_sibling), + Self::Comp(ref bsusp) => bsusp.shift(next_parent, next_sibling), + Self::List(ref vlist) => vlist.shift(next_parent, next_sibling), + Self::Ref(ref node) => { next_parent .insert_before(node, next_sibling.get().as_ref()) .unwrap(); } - Self::BPortal(ref vportal) => vportal.shift(next_parent, next_sibling), - Self::BSuspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling), + Self::Portal(ref vportal) => vportal.shift(next_parent, next_sibling), + Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling), } } } @@ -105,7 +105,7 @@ impl Reconcilable for VNode { } VNode::VRef(node) => { super::insert_node(&node, parent, next_sibling.get().as_ref()); - (NodeRef::new(node.clone()), BNode::BRef(node)) + (NodeRef::new(node.clone()), BNode::Ref(node)) } VNode::VPortal(vportal) => { let (node_ref, portal) = vportal.attach(parent_scope, parent, next_sibling); @@ -142,7 +142,7 @@ impl Reconcilable for VNode { VNode::VList(vlist) => vlist.reconcile_node(parent_scope, parent, next_sibling, bundle), VNode::VRef(node) => { let _existing = match bundle { - BNode::BRef(ref n) if &node == n => n, + BNode::Ref(ref n) if &node == n => n, _ => { return VNode::VRef(node).replace( parent_scope, @@ -167,55 +167,55 @@ impl Reconcilable for VNode { impl From for BNode { #[inline] fn from(btext: BText) -> Self { - Self::BText(btext) + Self::Text(btext) } } impl From for BNode { #[inline] fn from(blist: BList) -> Self { - Self::BList(blist) + Self::List(blist) } } impl From for BNode { #[inline] fn from(btag: BTag) -> Self { - Self::BTag(Box::new(btag)) + Self::Tag(Box::new(btag)) } } impl From for BNode { #[inline] fn from(bcomp: BComp) -> Self { - Self::BComp(bcomp) + Self::Comp(bcomp) } } impl From for BNode { #[inline] fn from(bportal: BPortal) -> Self { - Self::BPortal(bportal) + Self::Portal(bportal) } } impl From for BNode { #[inline] fn from(bsusp: BSuspense) -> Self { - Self::BSuspense(Box::new(bsusp)) + Self::Suspense(Box::new(bsusp)) } } impl fmt::Debug for BNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { - Self::BTag(ref vtag) => vtag.fmt(f), - Self::BText(ref btext) => btext.fmt(f), - Self::BComp(ref bsusp) => bsusp.fmt(f), - Self::BList(ref vlist) => vlist.fmt(f), - Self::BRef(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)), - Self::BPortal(ref vportal) => vportal.fmt(f), - Self::BSuspense(ref bsusp) => bsusp.fmt(f), + Self::Tag(ref vtag) => vtag.fmt(f), + Self::Text(ref btext) => btext.fmt(f), + Self::Comp(ref bsusp) => bsusp.fmt(f), + Self::List(ref vlist) => vlist.fmt(f), + Self::Ref(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)), + Self::Portal(ref vportal) => vportal.fmt(f), + Self::Suspense(ref bsusp) => bsusp.fmt(f), } } } diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index 0ddb324fdfa..a5c6d769181 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -64,7 +64,7 @@ impl Reconcilable for VPortal { bundle: &mut BNode, ) -> NodeRef { match bundle { - BNode::BPortal(portal) => self.reconcile(parent_scope, parent, next_sibling, portal), + BNode::Portal(portal) => self.reconcile(parent_scope, parent, next_sibling, portal), _ => self.replace(parent_scope, parent, next_sibling, bundle), } } diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 4660ec95704..0781b512e7d 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -100,7 +100,7 @@ impl Reconcilable for VSuspense { ) -> NodeRef { match bundle { // We only preserve the child state if they are the same suspense. - BNode::BSuspense(m) + BNode::Suspense(m) if m.key == self.key && self.detached_parent.as_ref() == Some(&m.detached_parent) => { diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index 1153ab13a01..1d172c1f87d 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -162,7 +162,7 @@ impl Reconcilable for VTag { match bundle { // If the ancestor is a tag of the same type, don't recreate, keep the // old tag and update its attributes and children. - BNode::BTag(ex) if self.key == ex.key => { + BNode::Tag(ex) if self.key == ex.key => { if match (&self.inner, &ex.inner) { (VTagInner::Input(_), BTagInner::Input(_)) => true, (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true, @@ -447,7 +447,7 @@ mod tests { } fn assert_btag_ref(node: &BNode) -> &BTag { - if let BNode::BTag(vtag) = node { + if let BNode::Tag(vtag) = node { return vtag; } panic!("should be btag"); @@ -461,7 +461,7 @@ mod tests { } fn assert_btag_mut(node: &mut BNode) -> &mut BTag { - if let BNode::BTag(btag) = node { + if let BNode::Tag(btag) = node { return btag; } panic!("should be btag"); diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index 49cead246d5..af152955daf 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -59,7 +59,7 @@ impl Reconcilable for VText { bundle: &mut BNode, ) -> NodeRef { match bundle { - BNode::BText(btext) => self.reconcile(parent_scope, parent, next_sibling, btext), + BNode::Text(btext) => self.reconcile(parent_scope, parent, next_sibling, btext), _ => self.replace(parent_scope, parent, next_sibling, bundle), } }