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! {
+
+ }
+ }
+ }
+
+ 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 = "\
+ ";
+
+ 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: "",
+ };
+
+ 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: "a cd0
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: "PORTAL AFTER
",
+ });
+ 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: "PORTAL AFTER
",
+ });
+ 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! {
+
+
+ { "Link with href" }
+
+
+ { "Button with data-target" }
+
+
+
+ };
+ 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: "",
+ };
+
+ let layout2 = TestLayout {
+ name: "2",
+ node: html! {
+
+
+ {"a"}
+
+
+ {"b"}
+
+
+ {"d"}
+
+
+ },
+ expected: "",
+ };
+
+ let layout3 = TestLayout {
+ name: "3",
+ node: html! {
+
+
+ {"a"}
+
+
+ {"b"}
+
+
+ {"c"}
+
+
+ {"d"}
+
+
+ },
+ expected: "",
+ };
+
+ let layout4 = TestLayout {
+ name: "4",
+ node: html! {
+
+
+ <>
+ {"a"}
+ >
+
+
+ {"b"}
+
+ {"c"}
+
+
+ {"d"}
+
+
+
+ },
+ expected: "",
+ };
+
+ 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! {
+
+ },
+ html! {
+
+ },
+ );
+ assert_eq!(
+ html! {
+
+ if false {
+
+ } else {
+
+ }
+
+ },
+ html! {
+
+ },
+ );
+ assert_eq!(
+ html! {
+
+ },
+ 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! {
-
- }
- }
- }
-
- 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 = "\
- ";
-
- 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: "",
- };
-
- 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: "a cd0
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! {
-
-
-