diff --git a/Makefile.toml b/Makefile.toml
index 3e62f00fcb8..56d8503140f 100644
--- a/Makefile.toml
+++ b/Makefile.toml
@@ -82,7 +82,7 @@ dependencies = ["test"]
[tasks.test]
private = true
command = "cargo"
-args = ["test", "--all-targets", "--workspace", "--exclude", "website-test"]
+args = ["test", "--all-targets"]
[tasks.doc-test-flow]
private = true
diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/dom_bundle/app_handle.rs
similarity index 71%
rename from packages/yew/src/app_handle.rs
rename to packages/yew/src/dom_bundle/app_handle.rs
index 5badcd8c750..771eb1b9d8e 100644
--- a/packages/yew/src/app_handle.rs
+++ b/packages/yew/src/dom_bundle/app_handle.rs
@@ -1,17 +1,16 @@
-//! This module contains the `App` struct, which is used to bootstrap
-//! a component in an isolated scope.
+//! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope.
-use std::ops::Deref;
-
-use crate::html::{BaseComponent, NodeRef, Scope, Scoped};
-use std::rc::Rc;
+use super::{ComponentRenderState, Scoped};
+use crate::html::{BaseComponent, Scope};
+use crate::NodeRef;
+use std::{ops::Deref, rc::Rc};
use web_sys::Element;
/// An instance of an application.
#[derive(Debug)]
pub struct AppHandle {
/// `Scope` holder
- pub(crate) scope: Scope,
+ scope: Scope,
}
impl AppHandle
@@ -27,14 +26,17 @@ where
let app = Self {
scope: Scope::new(None),
};
+ let node_ref = NodeRef::default();
+ let initial_render_state =
+ ComponentRenderState::new(element, NodeRef::default(), &node_ref);
app.scope
- .mount_in_place(element, NodeRef::default(), NodeRef::default(), props);
+ .mount_in_place(initial_render_state, node_ref, props);
app
}
/// Schedule the app for destruction
- pub fn destroy(mut self) {
+ pub fn destroy(self) {
self.scope.destroy(false)
}
}
diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs
new file mode 100644
index 00000000000..fe2d1ccabf0
--- /dev/null
+++ b/packages/yew/src/dom_bundle/bcomp.rs
@@ -0,0 +1,886 @@
+//! This module contains the bundle implementation of a virtual component [BComp].
+
+use super::{insert_node, BNode, DomBundle, Reconcilable};
+use crate::html::{AnyScope, BaseComponent, Scope};
+use crate::virtual_dom::{Key, VComp, VNode};
+use crate::NodeRef;
+#[cfg(feature = "ssr")]
+use futures::channel::oneshot;
+#[cfg(feature = "ssr")]
+use futures::future::{FutureExt, LocalBoxFuture};
+use gloo_utils::document;
+use std::cell::Ref;
+use std::{any::TypeId, borrow::Borrow};
+use std::{fmt, rc::Rc};
+use web_sys::{Element, Node};
+
+/// A virtual component. Compare with [VComp].
+pub struct BComp {
+ type_id: TypeId,
+ scope: Box,
+ node_ref: NodeRef,
+ key: Option,
+}
+
+impl BComp {
+ /// Get the key of the underlying component
+ pub(super) fn key(&self) -> Option<&Key> {
+ self.key.as_ref()
+ }
+}
+
+impl fmt::Debug for BComp {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "BComp {{ root: {:?} }}",
+ self.scope.as_ref().render_state(),
+ )
+ }
+}
+
+impl DomBundle for BComp {
+ fn detach(self, _parent: &Element, parent_to_detach: bool) {
+ self.scope.destroy_boxed(parent_to_detach);
+ }
+
+ fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
+ self.scope.shift_node(next_parent.clone(), next_sibling);
+ }
+}
+
+impl Reconcilable for VComp {
+ type Bundle = BComp;
+
+ fn attach(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ ) -> (NodeRef, Self::Bundle) {
+ let VComp {
+ type_id,
+ mountable,
+ node_ref,
+ key,
+ } = self;
+
+ let scope = mountable.mount(
+ node_ref.clone(),
+ parent_scope,
+ parent.to_owned(),
+ next_sibling,
+ );
+
+ (
+ node_ref.clone(),
+ BComp {
+ type_id,
+ node_ref,
+ key,
+ scope,
+ },
+ )
+ }
+
+ fn reconcile_node(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut BNode,
+ ) -> NodeRef {
+ match bundle {
+ // If the existing bundle is the same type, reuse it and update its properties
+ BNode::Comp(ref mut bcomp)
+ if self.type_id == bcomp.type_id && self.key == bcomp.key =>
+ {
+ self.reconcile(parent_scope, parent, next_sibling, bcomp)
+ }
+ _ => self.replace(parent_scope, parent, next_sibling, bundle),
+ }
+ }
+
+ fn reconcile(
+ self,
+ _parent_scope: &AnyScope,
+ _parent: &Element,
+ next_sibling: NodeRef,
+ bcomp: &mut Self::Bundle,
+ ) -> NodeRef {
+ let VComp {
+ mountable,
+ node_ref,
+ key,
+ type_id: _,
+ } = self;
+
+ bcomp.key = key;
+ let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref.clone());
+ bcomp.node_ref.reuse(old_ref);
+ mountable.reuse(node_ref.clone(), bcomp.scope.borrow(), next_sibling);
+ node_ref
+ }
+}
+
+pub trait Mountable {
+ fn copy(&self) -> Box;
+ fn mount(
+ self: Box,
+ node_ref: NodeRef,
+ parent_scope: &AnyScope,
+ parent: Element,
+ next_sibling: NodeRef,
+ ) -> Box;
+ fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef);
+
+ #[cfg(feature = "ssr")]
+ fn render_to_string<'a>(
+ &'a self,
+ w: &'a mut String,
+ parent_scope: &'a AnyScope,
+ ) -> LocalBoxFuture<'a, ()>;
+}
+
+pub struct PropsWrapper {
+ props: Rc,
+}
+
+impl PropsWrapper {
+ pub fn new(props: Rc) -> Self {
+ Self { props }
+ }
+}
+
+impl Mountable for PropsWrapper {
+ fn copy(&self) -> Box {
+ let wrapper: PropsWrapper = PropsWrapper {
+ props: Rc::clone(&self.props),
+ };
+ Box::new(wrapper)
+ }
+
+ fn mount(
+ self: Box,
+ node_ref: NodeRef,
+ parent_scope: &AnyScope,
+ parent: Element,
+ next_sibling: NodeRef,
+ ) -> Box {
+ let scope: Scope = Scope::new(Some(parent_scope.clone()));
+ let initial_render_state = ComponentRenderState::new(parent, next_sibling, &node_ref);
+ scope.mount_in_place(initial_render_state, node_ref, self.props);
+
+ Box::new(scope)
+ }
+
+ fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) {
+ let scope: Scope = scope.to_any().downcast();
+ scope.reuse(self.props, node_ref, next_sibling);
+ }
+
+ #[cfg(feature = "ssr")]
+ fn render_to_string<'a>(
+ &'a self,
+ w: &'a mut String,
+ parent_scope: &'a AnyScope,
+ ) -> LocalBoxFuture<'a, ()> {
+ async move {
+ let scope: Scope = Scope::new(Some(parent_scope.clone()));
+ scope.render_to_string(w, self.props.clone()).await;
+ }
+ .boxed_local()
+ }
+}
+
+pub struct ComponentRenderState {
+ root_node: BNode,
+ /// When a component has no parent, it means that it should not be rendered.
+ parent: Option,
+ next_sibling: NodeRef,
+
+ #[cfg(feature = "ssr")]
+ html_sender: Option>,
+}
+
+impl std::fmt::Debug for ComponentRenderState {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.root_node.fmt(f)
+ }
+}
+
+impl ComponentRenderState {
+ /// Prepare a place in the DOM to hold the eventual [VNode] from rendering a component
+ pub(crate) fn new(parent: Element, next_sibling: NodeRef, node_ref: &NodeRef) -> Self {
+ let placeholder = {
+ let placeholder: Node = document().create_text_node("").into();
+ insert_node(&placeholder, &parent, next_sibling.get().as_ref());
+ node_ref.set(Some(placeholder.clone()));
+ BNode::Ref(placeholder)
+ };
+ Self {
+ root_node: placeholder,
+ parent: Some(parent),
+ next_sibling,
+ #[cfg(feature = "ssr")]
+ html_sender: None,
+ }
+ }
+ /// Set up server-side rendering of a component
+ #[cfg(feature = "ssr")]
+ pub(crate) fn new_ssr(tx: oneshot::Sender) -> Self {
+ use super::blist::BList;
+
+ Self {
+ root_node: BNode::List(BList::new()),
+ parent: None,
+ next_sibling: NodeRef::default(),
+ html_sender: Some(tx),
+ }
+ }
+ /// Reuse the render state, asserting a new next_sibling
+ pub(crate) fn reuse(&mut self, next_sibling: NodeRef) {
+ self.next_sibling = next_sibling;
+ }
+ /// Shift the rendered content to a new DOM position
+ pub(crate) fn shift(&mut self, new_parent: Element, next_sibling: NodeRef) {
+ self.root_node.shift(&new_parent, next_sibling.clone());
+
+ self.parent = Some(new_parent);
+ self.next_sibling = next_sibling;
+ }
+ /// Reconcile the rendered content with a new [VNode]
+ pub(crate) fn reconcile(&mut self, root: VNode, scope: &AnyScope) -> NodeRef {
+ if let Some(ref parent) = self.parent {
+ let next_sibling = self.next_sibling.clone();
+
+ root.reconcile_node(scope, parent, next_sibling, &mut self.root_node)
+ } else {
+ #[cfg(feature = "ssr")]
+ if let Some(tx) = self.html_sender.take() {
+ tx.send(root).unwrap();
+ }
+ NodeRef::default()
+ }
+ }
+ /// Detach the rendered content from the DOM
+ pub(crate) fn detach(self, parent_to_detach: bool) {
+ if let Some(ref m) = self.parent {
+ self.root_node.detach(m, parent_to_detach);
+ }
+ }
+
+ pub(crate) fn should_trigger_rendered(&self) -> bool {
+ self.parent.is_some()
+ }
+}
+
+pub trait Scoped {
+ fn to_any(&self) -> AnyScope;
+ /// Get the render state if it hasn't already been destroyed
+ fn render_state(&self) -> Option[>;
+ /// Shift the node associated with this scope to a new place
+ fn shift_node(&self, parent: Element, next_sibling: NodeRef);
+ /// Process an event to destroy a component
+ fn destroy(self, parent_to_detach: bool);
+ fn destroy_boxed(self: Box], parent_to_detach: bool);
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::dom_bundle::{DomBundle, Reconcilable};
+ use crate::scheduler;
+ use crate::{
+ html,
+ virtual_dom::{Key, VChild, VNode},
+ Children, Component, Context, Html, NodeRef, Properties,
+ };
+ use gloo_utils::document;
+ use std::ops::Deref;
+ use web_sys::Element;
+ use web_sys::Node;
+
+ #[cfg(feature = "wasm_test")]
+ 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 comp = html! { };
+ let (_, mut bundle) = comp.attach(&parent_scope, &parent_element, NodeRef::default());
+ scheduler::start_now();
+
+ for _ in 0..10000 {
+ let node = html! { };
+ node.reconcile_node(
+ &parent_scope,
+ &parent_element,
+ NodeRef::default(),
+ &mut bundle,
+ );
+ scheduler::start_now();
+ }
+ }
+
+ #[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! {
+
+ }
+ }
+ }
+
+ 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());
+ scheduler::start_now();
+ 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());
+ scheduler::start_now();
+ let parent_node = parent.deref();
+ assert_eq!(node_ref.get(), parent_node.first_child());
+ elem.detach(&parent, false);
+ scheduler::start_now();
+ 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..21e66fc60ec
--- /dev/null
+++ b/packages/yew/src/dom_bundle/blist.rs
@@ -0,0 +1,1382 @@
+//! This module contains fragments bundles, a [BList]
+use super::{test_log, BNode};
+use crate::dom_bundle::{DomBundle, Reconcilable};
+use crate::html::{AnyScope, NodeRef};
+use crate::virtual_dom::{Key, VList, VNode, VText};
+use std::borrow::Borrow;
+use std::cmp::Ordering;
+use std::collections::HashSet;
+use std::hash::Hash;
+use std::ops::Deref;
+use web_sys::Element;
+
+/// This struct represents a mounted [VList]
+#[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
+ }
+}
+
+/// Helper struct, that keeps the position where the next element is to be placed at
+#[derive(Clone)]
+struct NodeWriter<'s> {
+ parent_scope: &'s AnyScope,
+ parent: &'s Element,
+ next_sibling: NodeRef,
+}
+
+impl<'s> NodeWriter<'s> {
+ /// Write a new node that has no ancestor
+ fn add(self, node: VNode) -> (Self, BNode) {
+ test_log!("adding: {:?}", node);
+ test_log!(
+ " 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,
+ )
+ }
+
+ /// Shift a bundle into place without patching it
+ fn shift(&self, bundle: &mut BNode) {
+ bundle.shift(self.parent, self.next_sibling.clone());
+ }
+
+ /// Patch a bundle with a new node
+ fn patch(self, node: VNode, bundle: &mut BNode) -> Self {
+ test_log!("patching: {:?} -> {:?}", bundle, node);
+ test_log!(
+ " parent={:?}, next_sibling={:?}",
+ self.parent.outer_html(),
+ self.next_sibling
+ );
+ // Advance the next sibling reference (from right to left)
+ let next = node.reconcile_node(self.parent_scope, self.parent, self.next_sibling, bundle);
+ test_log!(" next_position: {:?}", next);
+ Self {
+ next_sibling: next,
+ ..self
+ }
+ }
+}
+/// Helper struct implementing [Eq] and [Hash] by only looking at a node's key
+struct KeyedEntry(usize, BNode);
+impl Borrow for KeyedEntry {
+ fn borrow(&self) -> &Key {
+ self.1.key().expect("unkeyed child in fully keyed list")
+ }
+}
+impl Hash for KeyedEntry {
+ fn hash(&self, state: &mut H) {
+ >::borrow(self).hash(state)
+ }
+}
+impl PartialEq for KeyedEntry {
+ fn eq(&self, other: &Self) -> bool {
+ >::borrow(self) == >::borrow(other)
+ }
+}
+impl Eq for KeyedEntry {}
+
+impl BNode {
+ /// Assert that a bundle node is a list, or convert it to a list with a single child
+ fn make_list(&mut self) -> &mut BList {
+ match self {
+ Self::List(blist) => blist,
+ self_ => {
+ let b = std::mem::replace(self_, BNode::List(BList::new()));
+ let self_list = match self_ {
+ BNode::List(blist) => blist,
+ _ => unreachable!("just been set to the variant"),
+ };
+ let key = b.key().cloned();
+ self_list.rev_children.push(b);
+ self_list.fully_keyed = key.is_some();
+ self_list.key = key;
+ self_list
+ }
+ }
+ }
+}
+
+impl BList {
+ /// Create a new empty [BList]
+ pub(super) const fn new() -> BList {
+ BList {
+ rev_children: vec![],
+ fully_keyed: true,
+ key: None,
+ }
+ }
+
+ /// Get the key of the underlying fragment
+ pub(super) 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 = NodeWriter {
+ 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, false);
+ }
+ }
+
+ 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,
+ left_vdoms: Vec,
+ rev_bundles: &mut Vec,
+ ) -> NodeRef {
+ macro_rules! key {
+ ($v:expr) => {
+ $v.key().expect("unkeyed child in fully keyed list")
+ };
+ }
+ /// Find the first differing key in 2 iterators
+ fn matching_len<'a, 'b>(
+ a: impl Iterator- ,
+ 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(
+ left_vdoms.iter().map(|v| key!(v)).rev(),
+ rev_bundles.iter().map(|v| key!(v)),
+ );
+
+ // If there is no key mismatch, apply the unkeyed approach
+ // Corresponds to adding or removing items from the back of the list
+ if matching_len_end == std::cmp::min(left_vdoms.len(), rev_bundles.len()) {
+ // No key changes
+ return Self::apply_unkeyed(
+ parent_scope,
+ parent,
+ next_sibling,
+ left_vdoms,
+ rev_bundles,
+ );
+ }
+
+ // We partially drain the new vnodes in several steps.
+ let mut lefts = left_vdoms;
+ let mut writer = NodeWriter {
+ parent_scope,
+ parent,
+ next_sibling,
+ };
+ // Step 1. Diff matching children at the end
+ let lefts_to = lefts.len() - matching_len_end;
+ for (l, r) in lefts
+ .drain(lefts_to..)
+ .rev()
+ .zip(rev_bundles[..matching_len_end].iter_mut())
+ {
+ writer = writer.patch(l, r);
+ }
+
+ // Step 2. Diff matching children in the middle, that is between the first and last key mismatch
+ // Find first key mismatch from the front
+ let matching_len_start = matching_len(
+ lefts.iter().map(|v| key!(v)),
+ rev_bundles.iter().map(|v| key!(v)).rev(),
+ );
+
+ // Step 2.1. Splice out the existing middle part and build a lookup by key
+ let rights_to = rev_bundles.len() - matching_len_start;
+ let mut spliced_middle =
+ rev_bundles.splice(matching_len_end..rights_to, std::iter::empty());
+ let mut spare_bundles: HashSet
=
+ HashSet::with_capacity((matching_len_end..rights_to).len());
+ for (idx, r) in (&mut spliced_middle).enumerate() {
+ spare_bundles.insert(KeyedEntry(idx, r));
+ }
+
+ // Step 2.2. Put the middle part back together in the new key order
+ let mut replacements: Vec = Vec::with_capacity((matching_len_start..lefts_to).len());
+ // The goal is to shift as few nodes as possible.
+
+ // We handle runs of in-order nodes. When we encounter one out-of-order, we decide whether:
+ // - to shift all nodes in the current run to the position after the node before of the run, or to
+ // - "commit" to the current run, shift all nodes before the end of the run that we might
+ // encounter in the future, and then start a new run.
+ // Example of a run:
+ // barrier_idx --v v-- end_idx
+ // spliced_middle [ ... , M , N , C , D , E , F , G , ... ] (original element order)
+ // ^---^-----------^ the nodes that are part of the current run
+ // v start_writer
+ // replacements [ ... , M , C , D , G ] (new element order)
+ // ^-- start_idx
+ let mut barrier_idx = 0; // nodes from spliced_middle[..barrier_idx] are shifted unconditionally
+ struct RunInformation<'a> {
+ start_writer: NodeWriter<'a>,
+ start_idx: usize,
+ end_idx: usize,
+ }
+ let mut current_run: Option> = None;
+
+ for l in lefts
+ .drain(matching_len_start..) // lefts_to.. has been drained
+ .rev()
+ {
+ let ancestor = spare_bundles.take(key!(l));
+ // Check if we need to shift or commit a run
+ if let Some(run) = current_run.as_mut() {
+ if let Some(KeyedEntry(idx, _)) = ancestor {
+ // If there are only few runs, this is a cold path
+ if idx < run.end_idx {
+ // Have to decide whether to shift or commit the current run. A few calculations:
+ // A perfect estimate of the amount of nodes we have to shift if we move this run:
+ let run_length = replacements.len() - run.start_idx;
+ // A very crude estimate of the amount of nodes we will have to shift if we commit the run:
+ // Note nodes of the current run should not be counted here!
+ let estimated_skipped_nodes = run.end_idx - idx.max(barrier_idx);
+ // double run_length to counteract that the run is part of the estimated_skipped_nodes
+ if 2 * run_length > estimated_skipped_nodes {
+ // less work to commit to this run
+ barrier_idx = 1 + run.end_idx;
+ } else {
+ // Less work to shift this run
+ for r in replacements[run.start_idx..].iter_mut().rev() {
+ run.start_writer.shift(r);
+ }
+ }
+ current_run = None;
+ }
+ }
+ }
+ let bundle = if let Some(KeyedEntry(idx, mut r_bundle)) = ancestor {
+ match current_run.as_mut() {
+ // hot path
+ // We know that idx >= run.end_idx, so this node doesn't need to shift
+ Some(run) => run.end_idx = idx,
+ None => match idx.cmp(&barrier_idx) {
+ // peep hole optimization, don't start a run as the element is already where it should be
+ Ordering::Equal => barrier_idx += 1,
+ // shift the node unconditionally, don't start a run
+ Ordering::Less => writer.shift(&mut r_bundle),
+ // start a run
+ Ordering::Greater => {
+ current_run = Some(RunInformation {
+ start_writer: writer.clone(),
+ start_idx: replacements.len(),
+ end_idx: idx,
+ })
+ }
+ },
+ }
+ writer = writer.patch(l, &mut r_bundle);
+ r_bundle
+ } else {
+ // Even if there is an active run, we don't have to modify it
+ let (next_writer, bundle) = writer.add(l);
+ writer = next_writer;
+ bundle
+ };
+ replacements.push(bundle);
+ }
+ // drop the splice iterator and immediately replace the range with the reordered elements
+ drop(spliced_middle);
+ rev_bundles.splice(matching_len_end..matching_len_end, replacements);
+
+ // Step 2.3. Remove any extra rights
+ for KeyedEntry(_, r) in spare_bundles.drain() {
+ test_log!("removing: {:?}", r);
+ r.detach(parent, false);
+ }
+
+ // Step 3. Diff matching children at the start
+ let rights_to = rev_bundles.len() - matching_len_start;
+ for (l, r) in lefts
+ .drain(..) // matching_len_start.. has been drained already
+ .rev()
+ .zip(rev_bundles[rights_to..].iter_mut())
+ {
+ writer = writer.patch(l, r);
+ }
+
+ writer.next_sibling
+ }
+}
+
+impl DomBundle for BList {
+ fn detach(self, parent: &Element, parent_to_detach: bool) {
+ for child in self.rev_children.into_iter() {
+ child.detach(parent, parent_to_detach);
+ }
+ }
+
+ 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 Reconcilable for VList {
+ type Bundle = BList;
+
+ fn attach(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ ) -> (NodeRef, Self::Bundle) {
+ let mut self_ = BList::new();
+ let node_ref = self.reconcile(parent_scope, parent, next_sibling, &mut self_);
+ (node_ref, self_)
+ }
+
+ fn reconcile_node(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut BNode,
+ ) -> NodeRef {
+ // 'Forcefully' create a pretend the existing node is a list. Creates a
+ // singleton list if it isn't already.
+ let blist = bundle.make_list();
+ self.reconcile(parent_scope, parent, next_sibling, blist)
+ }
+
+ fn reconcile(
+ mut self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ blist: &mut BList,
+ ) -> NodeRef {
+ // Here, we will try to diff the previous list elements with the new
+ // ones we want to insert. For that, we will use two lists:
+ // - 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 bundle,
+ // 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 = &mut blist.rev_children;
+ test_log!("lefts: {:?}", lefts);
+ test_log!("rights: {:?}", rights);
+
+ if let Some(additional) = lefts.len().checked_sub(rights.len()) {
+ rights.reserve_exact(additional);
+ }
+ 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..0e80563fd30
--- /dev/null
+++ b/packages/yew/src/dom_bundle/bnode.rs
@@ -0,0 +1,254 @@
+//! This module contains the bundle version of an abstract node [BNode]
+
+use super::{BComp, BList, BPortal, BSuspense, BTag, BText};
+use crate::dom_bundle::{DomBundle, Reconcilable};
+use crate::html::{AnyScope, NodeRef};
+use crate::virtual_dom::{Key, VNode};
+use gloo::console;
+use std::fmt;
+use web_sys::{Element, Node};
+
+/// The bundle implementation to [VNode].
+pub enum BNode {
+ /// A bind between `VTag` and `Element`.
+ Tag(Box),
+ /// A bind between `VText` and `TextNode`.
+ Text(BText),
+ /// A bind between `VComp` and `Element`.
+ Comp(BComp),
+ /// A holder for a list of other nodes.
+ List(BList),
+ /// A portal to another part of the document
+ Portal(BPortal),
+ /// A holder for any `Node` (necessary for replacing node).
+ Ref(Node),
+ /// A suspendible document fragment.
+ Suspense(Box),
+}
+
+impl BNode {
+ /// Get the key of the underlying node
+ pub(super) fn key(&self) -> Option<&Key> {
+ match self {
+ Self::Comp(bsusp) => bsusp.key(),
+ Self::List(blist) => blist.key(),
+ Self::Ref(_) => None,
+ Self::Tag(btag) => btag.key(),
+ Self::Text(_) => None,
+ Self::Portal(bportal) => bportal.key(),
+ Self::Suspense(bsusp) => bsusp.key(),
+ }
+ }
+}
+
+impl DomBundle for BNode {
+ /// Remove VNode from parent.
+ fn detach(self, parent: &Element, parent_to_detach: bool) {
+ match self {
+ Self::Tag(vtag) => vtag.detach(parent, parent_to_detach),
+ Self::Text(btext) => btext.detach(parent, parent_to_detach),
+ Self::Comp(bsusp) => bsusp.detach(parent, parent_to_detach),
+ Self::List(blist) => blist.detach(parent, parent_to_detach),
+ Self::Ref(ref node) => {
+ // Always remove user-defined nodes to clear possible parent references of them
+ if parent.remove_child(node).is_err() {
+ console::warn!("Node not found to remove VRef");
+ }
+ }
+ Self::Portal(bportal) => bportal.detach(parent, parent_to_detach),
+ Self::Suspense(bsusp) => bsusp.detach(parent, parent_to_detach),
+ }
+ }
+
+ fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
+ match self {
+ Self::Tag(ref vtag) => vtag.shift(next_parent, next_sibling),
+ Self::Text(ref btext) => btext.shift(next_parent, next_sibling),
+ Self::Comp(ref bsusp) => bsusp.shift(next_parent, next_sibling),
+ Self::List(ref vlist) => vlist.shift(next_parent, next_sibling),
+ Self::Ref(ref node) => {
+ next_parent
+ .insert_before(node, next_sibling.get().as_ref())
+ .unwrap();
+ }
+ Self::Portal(ref vportal) => vportal.shift(next_parent, next_sibling),
+ Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling),
+ }
+ }
+}
+
+impl Reconcilable 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::Ref(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 reconcile_node(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut BNode,
+ ) -> NodeRef {
+ self.reconcile(parent_scope, parent, next_sibling, bundle)
+ }
+
+ fn reconcile(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut BNode,
+ ) -> NodeRef {
+ match self {
+ VNode::VTag(vtag) => vtag.reconcile_node(parent_scope, parent, next_sibling, bundle),
+ VNode::VText(vtext) => vtext.reconcile_node(parent_scope, parent, next_sibling, bundle),
+ VNode::VComp(vcomp) => vcomp.reconcile_node(parent_scope, parent, next_sibling, bundle),
+ VNode::VList(vlist) => vlist.reconcile_node(parent_scope, parent, next_sibling, bundle),
+ VNode::VRef(node) => {
+ let _existing = match bundle {
+ BNode::Ref(ref n) if &node == n => n,
+ _ => {
+ return VNode::VRef(node).replace(
+ parent_scope,
+ parent,
+ next_sibling,
+ bundle,
+ );
+ }
+ };
+ NodeRef::new(node)
+ }
+ VNode::VPortal(vportal) => {
+ vportal.reconcile_node(parent_scope, parent, next_sibling, bundle)
+ }
+ VNode::VSuspense(vsuspsense) => {
+ vsuspsense.reconcile_node(parent_scope, parent, next_sibling, bundle)
+ }
+ }
+ }
+}
+
+impl From for BNode {
+ #[inline]
+ fn from(btext: BText) -> Self {
+ Self::Text(btext)
+ }
+}
+
+impl From for BNode {
+ #[inline]
+ fn from(blist: BList) -> Self {
+ Self::List(blist)
+ }
+}
+
+impl From for BNode {
+ #[inline]
+ fn from(btag: BTag) -> Self {
+ Self::Tag(Box::new(btag))
+ }
+}
+
+impl From for BNode {
+ #[inline]
+ fn from(bcomp: BComp) -> Self {
+ Self::Comp(bcomp)
+ }
+}
+
+impl From for BNode {
+ #[inline]
+ fn from(bportal: BPortal) -> Self {
+ Self::Portal(bportal)
+ }
+}
+
+impl From for BNode {
+ #[inline]
+ fn from(bsusp: BSuspense) -> Self {
+ Self::Suspense(Box::new(bsusp))
+ }
+}
+
+impl fmt::Debug for BNode {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match *self {
+ Self::Tag(ref vtag) => vtag.fmt(f),
+ Self::Text(ref btext) => btext.fmt(f),
+ Self::Comp(ref bsusp) => bsusp.fmt(f),
+ Self::List(ref vlist) => vlist.fmt(f),
+ Self::Ref(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)),
+ Self::Portal(ref vportal) => vportal.fmt(f),
+ Self::Suspense(ref bsusp) => bsusp.fmt(f),
+ }
+ }
+}
+
+#[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..a5c6d769181
--- /dev/null
+++ b/packages/yew/src/dom_bundle/bportal.rs
@@ -0,0 +1,190 @@
+//! This module contains the bundle implementation of a portal [BPortal].
+
+use super::test_log;
+use super::BNode;
+use crate::dom_bundle::{DomBundle, Reconcilable};
+use crate::html::{AnyScope, NodeRef};
+use crate::virtual_dom::Key;
+use crate::virtual_dom::VPortal;
+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
+ inner_sibling: NodeRef,
+ /// The inserted node
+ node: Box,
+}
+
+impl DomBundle for BPortal {
+ fn detach(self, _: &Element, _parent_to_detach: bool) {
+ test_log!("Detaching portal from host{:?}", self.host.outer_html());
+ self.node.detach(&self.host, false);
+ 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 Reconcilable for VPortal {
+ type Bundle = BPortal;
+
+ fn attach(
+ self,
+ parent_scope: &AnyScope,
+ _parent: &Element,
+ host_next_sibling: NodeRef,
+ ) -> (NodeRef, Self::Bundle) {
+ let Self {
+ host,
+ inner_sibling,
+ node,
+ } = self;
+ let (_, inner) = node.attach(parent_scope, &host, inner_sibling.clone());
+ (
+ host_next_sibling,
+ BPortal {
+ host,
+ node: Box::new(inner),
+ inner_sibling,
+ },
+ )
+ }
+
+ fn reconcile_node(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut BNode,
+ ) -> NodeRef {
+ match bundle {
+ BNode::Portal(portal) => self.reconcile(parent_scope, parent, next_sibling, portal),
+ _ => self.replace(parent_scope, parent, next_sibling, bundle),
+ }
+ }
+
+ fn reconcile(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ portal: &mut Self::Bundle,
+ ) -> NodeRef {
+ let Self {
+ host,
+ inner_sibling,
+ node,
+ } = self;
+
+ let old_host = std::mem::replace(&mut portal.host, host);
+ let old_inner_sibling = std::mem::replace(&mut portal.inner_sibling, inner_sibling);
+
+ if old_host != portal.host || old_inner_sibling != portal.inner_sibling {
+ // Remount the inner node somewhere else instead of diffing
+ // Move the node, but keep the state
+ portal
+ .node
+ .shift(&portal.host, portal.inner_sibling.clone());
+ }
+ node.reconcile_node(parent_scope, parent, next_sibling.clone(), &mut portal.node);
+ next_sibling
+ }
+}
+
+impl BPortal {
+ /// Get the key of the underlying portal
+ pub(super) 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..0781b512e7d
--- /dev/null
+++ b/packages/yew/src/dom_bundle/bsuspense.rs
@@ -0,0 +1,178 @@
+//! This module contains the bundle version of a supsense [BSuspense]
+
+use super::{BNode, DomBundle, Reconcilable};
+use crate::html::AnyScope;
+use crate::virtual_dom::{Key, VSuspense};
+use crate::NodeRef;
+use web_sys::Element;
+
+/// The bundle implementation to [VSuspense]
+#[derive(Debug)]
+pub struct BSuspense {
+ children_bundle: BNode,
+ /// The supsense is suspended if fallback contains [Some] bundle
+ fallback_bundle: Option,
+ detached_parent: Element,
+ key: Option,
+}
+
+impl BSuspense {
+ /// Get the key of the underlying suspense
+ pub(super) fn key(&self) -> Option<&Key> {
+ self.key.as_ref()
+ }
+ /// Get the bundle node that actually shows up in the dom
+ fn active_node(&self) -> &BNode {
+ self.fallback_bundle
+ .as_ref()
+ .unwrap_or(&self.children_bundle)
+ }
+}
+
+impl DomBundle for BSuspense {
+ fn detach(self, parent: &Element, parent_to_detach: bool) {
+ if let Some(fallback) = self.fallback_bundle {
+ fallback.detach(parent, parent_to_detach);
+ self.children_bundle.detach(&self.detached_parent, false);
+ } else {
+ self.children_bundle.detach(parent, parent_to_detach);
+ }
+ }
+
+ fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
+ self.active_node().shift(next_parent, next_sibling)
+ }
+}
+
+impl Reconcilable 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;
+ let detached_parent = detached_parent.expect("no 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.
+ if suspended {
+ let (_child_ref, children_bundle) =
+ children.attach(parent_scope, &detached_parent, NodeRef::default());
+ let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling);
+ (
+ fallback_ref,
+ BSuspense {
+ children_bundle,
+ fallback_bundle: Some(fallback),
+ detached_parent,
+ key,
+ },
+ )
+ } else {
+ let (child_ref, children_bundle) = children.attach(parent_scope, parent, next_sibling);
+ (
+ child_ref,
+ BSuspense {
+ children_bundle,
+ fallback_bundle: None,
+ detached_parent,
+ key,
+ },
+ )
+ }
+ }
+
+ fn reconcile_node(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut BNode,
+ ) -> NodeRef {
+ match bundle {
+ // We only preserve the child state if they are the same suspense.
+ BNode::Suspense(m)
+ if m.key == self.key
+ && self.detached_parent.as_ref() == Some(&m.detached_parent) =>
+ {
+ self.reconcile(parent_scope, parent, next_sibling, m)
+ }
+ _ => self.replace(parent_scope, parent, next_sibling, bundle),
+ }
+ }
+
+ fn reconcile(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ suspense: &mut Self::Bundle,
+ ) -> NodeRef {
+ let VSuspense {
+ children,
+ fallback,
+ detached_parent,
+ suspended,
+ key: _,
+ } = self;
+ let detached_parent = detached_parent.expect("no detached parent?");
+
+ let children_bundle = &mut suspense.children_bundle;
+ // no need to update key & detached_parent
+
+ // When it's suspended, we render children into an element that is detached from the dom
+ // tree while rendering fallback UI into the original place where children resides in.
+ match (suspended, &mut suspense.fallback_bundle) {
+ // Both suspended, reconcile children into detached_parent, fallback into the DOM
+ (true, Some(fallback_bundle)) => {
+ children.reconcile_node(
+ parent_scope,
+ &detached_parent,
+ NodeRef::default(),
+ children_bundle,
+ );
+
+ fallback.reconcile_node(parent_scope, parent, next_sibling, fallback_bundle)
+ }
+ // Not suspended, just reconcile the children into the DOM
+ (false, None) => {
+ children.reconcile_node(parent_scope, parent, next_sibling, children_bundle)
+ }
+ // Freshly suspended. Shift children into the detached parent, then add fallback to the DOM
+ (true, None) => {
+ children_bundle.shift(&detached_parent, NodeRef::default());
+
+ children.reconcile_node(
+ parent_scope,
+ &detached_parent,
+ NodeRef::default(),
+ children_bundle,
+ );
+ // first render of fallback
+ let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling);
+ suspense.fallback_bundle = Some(fallback);
+ fallback_ref
+ }
+ // Freshly unsuspended. Detach fallback from the DOM, then shift children into it.
+ (false, Some(_)) => {
+ suspense
+ .fallback_bundle
+ .take()
+ .unwrap() // We just matched Some(_)
+ .detach(parent, false);
+
+ children_bundle.shift(parent, next_sibling.clone());
+ children.reconcile_node(parent_scope, parent, next_sibling, children_bundle)
+ }
+ }
+ }
+}
diff --git a/packages/yew/src/dom_bundle/btag/attributes.rs b/packages/yew/src/dom_bundle/btag/attributes.rs
new file mode 100644
index 00000000000..cdec0630e6b
--- /dev/null
+++ b/packages/yew/src/dom_bundle/btag/attributes.rs
@@ -0,0 +1,275 @@
+use super::Apply;
+use crate::virtual_dom::vtag::{InputFields, Value};
+use crate::virtual_dom::Attributes;
+use indexmap::IndexMap;
+use std::collections::HashMap;
+use std::iter;
+use std::ops::Deref;
+use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
+
+impl Apply for Value {
+ type Element = T;
+ type Bundle = Self;
+
+ fn apply(self, el: &Self::Element) -> Self {
+ if let Some(v) = self.deref() {
+ el.set_value(v);
+ }
+ self
+ }
+
+ fn apply_diff(self, el: &Self::Element, bundle: &mut Self) {
+ match (self.deref(), (*bundle).deref()) {
+ (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 trait AccessValue {
+ fn value(&self) -> String;
+ fn set_value(&self, v: &str);
+}
+
+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, bundle: &mut Self) {
+ // IMPORTANT! This parameter has to be set every time
+ // to prevent strange behaviour in the browser when the DOM changes
+ el.set_checked(self.checked);
+
+ self.value.apply_diff(el, &mut bundle.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 {
+ std::ptr::eq(a, b)
+ }
+
+ let ancestor = std::mem::replace(bundle, self);
+ let bundle = &*bundle; // reborrow it immutably from here
+ match (bundle, ancestor) {
+ // Hot path
+ (Self::Static(new), Self::Static(old)) if ptr_eq(new, old) => (),
+ // 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(ref 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, ref ancestor) => {
+ Self::apply_diff_as_maps(el, new, ancestor);
+ }
+ }
+ }
+}
diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs
new file mode 100644
index 00000000000..66f14363b3f
--- /dev/null
+++ b/packages/yew/src/dom_bundle/btag/listeners.rs
@@ -0,0 +1,709 @@
+use super::Apply;
+use crate::dom_bundle::test_log;
+use crate::virtual_dom::{Listener, ListenerKind, Listeners};
+use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase};
+use std::cell::RefCell;
+use std::collections::{HashMap, HashSet};
+use std::ops::Deref;
+use std::rc::Rc;
+use std::sync::atomic::{AtomicBool, Ordering};
+use wasm_bindgen::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);
+}
+
+/// An active set of listeners on an element
+#[derive(Debug)]
+pub(super) enum ListenerRegistration {
+ /// No listeners registered.
+ NoReg,
+ /// Added to global registry by ID
+ Registered(u32),
+}
+
+impl 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, 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)]
+ registered: Vec<(ListenerKind, EventListener)>,
+}
+
+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 = {
+ let desc = desc.clone();
+ BODY.with(move |body| {
+ let options = EventListenerOptions {
+ phase: EventListenerPhase::Capture,
+ passive: desc.passive,
+ };
+ EventListener::new_with_options(
+ body,
+ desc.kind.type_name(),
+ options,
+ move |e: &Event| Registry::handle(desc.clone(), e.clone()),
+ )
+ })
+ };
+
+ // Never drop the closure as this event handler is static
+ #[cfg(not(test))]
+ cl.forget();
+ #[cfg(test)]
+ self.registered.push((desc.kind.clone(), cl));
+
+ self.handling.insert(desc);
+ }
+ }
+}
+
+/// 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.clone());
+ 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.clone());
+ 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()
+ .and_then(|el| el.dyn_into::().ok())
+ {
+ 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())
+ .and_then(|v| v.dyn_into().ok())
+ .and_then(|num: js_sys::Number| {
+ Registry::with(|r| {
+ r.by_id
+ .get(&(num.value_of() as u32))
+ .and_then(|s| s.get(&desc))
+ .cloned()
+ })
+ })
+ {
+ 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, scheduler, AppHandle, Component, Context, Html};
+ use gloo_utils::document;
+ use wasm_bindgen::JsCast;
+ use yew::Callback;
+
+ #[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,
+ {
+ let link = ctx.link().clone();
+ let onclick = Callback::from(move |_| {
+ link.send_message(Message::Action);
+ scheduler::start_now();
+ });
+
+ 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);
+ scheduler::start_now();
+
+ (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);
+ scheduler::start_now();
+
+ 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 link = ctx.link().clone();
+ let onblur = Callback::from(move |_| {
+ link.send_message(Message::Action);
+ scheduler::start_now();
+ });
+ 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 link = ctx.link().clone();
+ let cb = Callback::from(move |_| {
+ link.send_message(Message::Action);
+ scheduler::start_now();
+ });
+ 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);
+ scheduler::start_now();
+ 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,
+ {
+ let link = ctx.link().clone();
+ let onclick = Callback::from(move |_| {
+ link.send_message(Message::Action);
+ scheduler::start_now();
+ });
+
+ let link = ctx.link().clone();
+ let onclick2 = Callback::from(move |e: MouseEvent| {
+ e.stop_propagation();
+ link.send_message(Message::Action);
+ scheduler::start_now();
+ });
+
+ 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,
+ {
+ let link = ctx.link().clone();
+ let onclick = Callback::from(move |_| {
+ link.send_message(Message::Action);
+ scheduler::start_now();
+ });
+
+ let link = ctx.link().clone();
+ let onclick2 = Callback::from(move |e: MouseEvent| {
+ e.stop_propagation();
+ link.send_message(Message::Action);
+ scheduler::start_now();
+ });
+ 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 {
+ let link = ctx.link().clone();
+ let onchange = Callback::from(move |e: web_sys::Event| {
+ let el: web_sys::HtmlInputElement = e.target_unchecked_into();
+ link.send_message(Message::SetText(el.value()));
+ scheduler::start_now();
+ });
+
+ let link = ctx.link().clone();
+ let oninput = Callback::from(move |e: web_sys::InputEvent| {
+ let el: web_sys::HtmlInputElement = e.target_unchecked_into();
+ link.send_message(Message::SetText(el.value()));
+ scheduler::start_now();
+ });
+
+ 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);
+ scheduler::start_now();
+
+ 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/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs
new file mode 100644
index 00000000000..1d172c1f87d
--- /dev/null
+++ b/packages/yew/src/dom_bundle/btag/mod.rs
@@ -0,0 +1,1078 @@
+//! This module contains the bundle implementation of a tag [BTag]
+
+mod attributes;
+mod listeners;
+
+pub use listeners::set_event_bubbling;
+
+use super::{insert_node, BList, BNode, DomBundle, Reconcilable};
+use crate::html::AnyScope;
+use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE};
+use crate::virtual_dom::{Attributes, Key, VTag};
+use crate::NodeRef;
+use gloo::console;
+use gloo_utils::document;
+use listeners::ListenerRegistration;
+use std::ops::DerefMut;
+use std::{borrow::Cow, hint::unreachable_unchecked};
+use wasm_bindgen::JsCast;
+use web_sys::{Element, HtmlTextAreaElement as TextAreaElement};
+
+/// Applies contained changes to DOM [web_sys::Element]
+trait Apply {
+ /// [web_sys::Element] subtype to apply the changes to
+ type Element;
+ type Bundle;
+
+ /// Apply contained values to [Element](Self::Element) with no ancestor
+ fn apply(self, el: &Self::Element) -> Self::Bundle;
+
+ /// Apply diff between [self] and `bundle` to [Element](Self::Element).
+ fn apply_diff(self, el: &Self::Element, bundle: &mut Self::Bundle);
+}
+
+/// [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: BList,
+ },
+}
+
+/// The bundle implementation to [VTag]
+#[derive(Debug)]
+pub struct BTag {
+ /// [BTag] fields that are specific to different [BTag] kinds.
+ inner: BTagInner,
+ listeners: ListenerRegistration,
+ attributes: Attributes,
+ /// A reference to the DOM [`Element`].
+ reference: Element,
+ /// A node reference used for DOM access in Component lifecycle methods
+ node_ref: NodeRef,
+ key: Option,
+}
+
+impl DomBundle for BTag {
+ fn detach(self, parent: &Element, parent_to_detach: bool) {
+ self.listeners.unregister();
+
+ let node = self.reference;
+ // recursively remove its children
+ if let BTagInner::Other { child_bundle, .. } = self.inner {
+ // This tag will be removed, so there's no point to remove any child.
+ child_bundle.detach(&node, true);
+ }
+ if !parent_to_detach {
+ let result = parent.remove_child(&node);
+
+ if result.is_err() {
+ console::warn!("Node not found to remove VTag");
+ }
+ }
+ // It could be that the ref was already reused when rendering another element.
+ // Only unset the ref it still belongs to our node
+ if self.node_ref.get().as_ref() == Some(&node) {
+ self.node_ref.set(None);
+ }
+ }
+
+ fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
+ next_parent
+ .insert_before(&self.reference, next_sibling.get().as_ref())
+ .unwrap();
+ }
+}
+
+impl Reconcilable for VTag {
+ type Bundle = BTag;
+
+ fn attach(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ ) -> (NodeRef, Self::Bundle) {
+ let el = self.create_element(parent);
+ let Self {
+ listeners,
+ attributes,
+ node_ref,
+ key,
+ ..
+ } = self;
+ insert_node(&el, parent, next_sibling.get().as_ref());
+
+ let attributes = attributes.apply(&el);
+ let listeners = listeners.apply(&el);
+
+ let inner = match self.inner {
+ VTagInner::Input(f) => {
+ let f = f.apply(el.unchecked_ref());
+ BTagInner::Input(f)
+ }
+ VTagInner::Textarea { value } => {
+ let value = value.apply(el.unchecked_ref());
+ BTagInner::Textarea { value }
+ }
+ VTagInner::Other { children, tag } => {
+ let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default());
+ BTagInner::Other { child_bundle, tag }
+ }
+ };
+ node_ref.set(Some(el.clone().into()));
+ (
+ node_ref.clone(),
+ BTag {
+ inner,
+ listeners,
+ reference: el,
+ attributes,
+ key,
+ node_ref,
+ },
+ )
+ }
+
+ fn reconcile_node(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut BNode,
+ ) -> NodeRef {
+ // This kind of branching patching routine reduces branch predictor misses and the need to
+ // unpack the enums (including `Option`s) all the time, resulting in a more streamlined
+ // patching flow
+ match bundle {
+ // If the ancestor is a tag of the same type, don't recreate, keep the
+ // old tag and update its attributes and children.
+ BNode::Tag(ex) if self.key == ex.key => {
+ if match (&self.inner, &ex.inner) {
+ (VTagInner::Input(_), BTagInner::Input(_)) => true,
+ (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true,
+ (VTagInner::Other { tag: l, .. }, BTagInner::Other { tag: r, .. })
+ if l == r =>
+ {
+ true
+ }
+ _ => false,
+ } {
+ return self.reconcile(parent_scope, parent, next_sibling, ex.deref_mut());
+ }
+ }
+ _ => {}
+ };
+ self.replace(parent_scope, parent, next_sibling, bundle)
+ }
+
+ fn reconcile(
+ self,
+ parent_scope: &AnyScope,
+ _parent: &Element,
+ _next_sibling: NodeRef,
+ tag: &mut Self::Bundle,
+ ) -> NodeRef {
+ let el = &tag.reference;
+ self.attributes.apply_diff(el, &mut tag.attributes);
+ self.listeners.apply_diff(el, &mut tag.listeners);
+
+ match (self.inner, &mut tag.inner) {
+ (VTagInner::Input(new), BTagInner::Input(old)) => {
+ new.apply_diff(el.unchecked_ref(), old);
+ }
+ (VTagInner::Textarea { value: new }, BTagInner::Textarea { value: old }) => {
+ new.apply_diff(el.unchecked_ref(), old);
+ }
+ (
+ VTagInner::Other { children: new, .. },
+ BTagInner::Other {
+ child_bundle: old, ..
+ },
+ ) => {
+ new.reconcile(parent_scope, el, NodeRef::default(), old);
+ }
+ // Can not happen, because we checked for tag equability above
+ _ => unsafe { unreachable_unchecked() },
+ }
+
+ tag.key = self.key;
+
+ if self.node_ref != tag.node_ref && tag.node_ref.get().as_ref() == Some(el) {
+ tag.node_ref.set(None);
+ }
+ if self.node_ref != tag.node_ref {
+ tag.node_ref = self.node_ref;
+ tag.node_ref.set(Some(el.clone().into()));
+ }
+
+ tag.node_ref.clone()
+ }
+}
+
+impl VTag {
+ fn create_element(&self, parent: &Element) -> Element {
+ let tag = self.tag();
+ if tag == "svg"
+ || parent
+ .namespace_uri()
+ .map_or(false, |ns| ns == SVG_NAMESPACE)
+ {
+ let namespace = Some(SVG_NAMESPACE);
+ document()
+ .create_element_ns(namespace, tag)
+ .expect("can't create namespaced element for vtag")
+ } else {
+ document()
+ .create_element(tag)
+ .expect("can't create element for vtag")
+ }
+ }
+}
+
+impl BTag {
+ /// Get the key of the underlying tag
+ pub(super) fn key(&self) -> Option<&Key> {
+ self.key.as_ref()
+ }
+
+ #[cfg(test)]
+ fn reference(&self) -> &Element {
+ &self.reference
+ }
+
+ #[cfg(test)]
+ fn children(&self) -> &[BNode] {
+ match &self.inner {
+ BTagInner::Other { child_bundle, .. } => child_bundle,
+ _ => &[],
+ }
+ }
+
+ #[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::dom_bundle::{BNode, DomBundle, Reconcilable};
+ use crate::html;
+ use crate::html::AnyScope;
+ use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE};
+ use crate::virtual_dom::{AttrValue, VNode, VTag};
+ use crate::{Html, NodeRef};
+ use gloo_utils::document;
+ use wasm_bindgen::JsCast;
+ use web_sys::HtmlInputElement as InputElement;
+
+ #[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::Tag(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::Tag(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) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default());
+ let vtag = assert_btag_mut(&mut elem);
+ // test if the className has not been set
+ assert!(!vtag.reference().has_attribute("class"));
+ }
+
+ 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) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default());
+ let vtag = assert_btag_mut(&mut elem);
+ // test if the className has been set
+ assert!(vtag.reference().has_attribute("class"));
+ }
+
+ #[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) = Reconcilable::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.reconcile_node(&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) = Reconcilable::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.reconcile_node(&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) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default());
+ let vtag = assert_btag_mut(&mut elem);
+ // make sure the new tag name is used internally
+ assert_eq!(vtag.tag(), "a");
+
+ // 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, false);
+ 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.reconcile_node(&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.reconcile_node(&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/btext.rs b/packages/yew/src/dom_bundle/btext.rs
new file mode 100644
index 00000000000..af152955daf
--- /dev/null
+++ b/packages/yew/src/dom_bundle/btext.rs
@@ -0,0 +1,163 @@
+//! This module contains the bundle implementation of text [BText].
+
+use super::{insert_node, BNode, DomBundle, Reconcilable};
+use crate::html::AnyScope;
+use crate::virtual_dom::{AttrValue, VText};
+use crate::NodeRef;
+use gloo::console;
+use gloo_utils::document;
+use web_sys::{Element, Text as TextNode};
+
+/// The bundle implementation to [VText]
+pub struct BText {
+ text: AttrValue,
+ text_node: TextNode,
+}
+
+impl DomBundle for BText {
+ fn detach(self, parent: &Element, parent_to_detach: bool) {
+ if !parent_to_detach {
+ let result = parent.remove_child(&self.text_node);
+
+ if result.is_err() {
+ console::warn!("Node not found to remove VText");
+ }
+ }
+ }
+
+ fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
+ let node = &self.text_node;
+
+ next_parent
+ .insert_before(node, next_sibling.get().as_ref())
+ .unwrap();
+ }
+}
+
+impl Reconcilable for VText {
+ type Bundle = BText;
+
+ fn attach(
+ self,
+ _parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ ) -> (NodeRef, Self::Bundle) {
+ let Self { text } = self;
+ let text_node = document().create_text_node(&text);
+ insert_node(&text_node, parent, next_sibling.get().as_ref());
+ let node_ref = NodeRef::new(text_node.clone().into());
+ (node_ref, BText { text, text_node })
+ }
+
+ /// Renders virtual node over existing `TextNode`, but only if value of text has changed.
+ fn reconcile_node(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut BNode,
+ ) -> NodeRef {
+ match bundle {
+ BNode::Text(btext) => self.reconcile(parent_scope, parent, next_sibling, btext),
+ _ => self.replace(parent_scope, parent, next_sibling, bundle),
+ }
+ }
+ fn reconcile(
+ self,
+ _parent_scope: &AnyScope,
+ _parent: &Element,
+ _next_sibling: NodeRef,
+ btext: &mut Self::Bundle,
+ ) -> NodeRef {
+ let Self { text } = self;
+ let ancestor_text = std::mem::replace(&mut btext.text, text);
+ if btext.text != ancestor_text {
+ btext.text_node.set_node_value(Some(&btext.text));
+ }
+ NodeRef::new(btext.text_node.clone().into())
+ }
+}
+
+impl std::fmt::Debug for BText {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "BText {{ text: \"{}\" }}", self.text)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ extern crate self as yew;
+
+ use crate::html;
+
+ #[cfg(feature = "wasm_test")]
+ use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
+
+ #[cfg(feature = "wasm_test")]
+ wasm_bindgen_test_configure!(run_in_browser);
+
+ #[test]
+ fn text_as_root() {
+ html! {
+ "Text Node As Root"
+ };
+
+ html! {
+ { "Text Node As Root" }
+ };
+ }
+}
+
+#[cfg(test)]
+mod layout_tests {
+ extern crate self as yew;
+
+ use crate::html;
+ use crate::tests::layout_tests::{diff_layouts, TestLayout};
+
+ #[cfg(feature = "wasm_test")]
+ use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
+
+ #[cfg(feature = "wasm_test")]
+ wasm_bindgen_test_configure!(run_in_browser);
+
+ #[test]
+ fn diff() {
+ let layout1 = TestLayout {
+ name: "1",
+ node: html! { "a" },
+ expected: "a",
+ };
+
+ let layout2 = TestLayout {
+ name: "2",
+ node: html! { "b" },
+ expected: "b",
+ };
+
+ let layout3 = TestLayout {
+ name: "3",
+ node: html! {
+ <>
+ {"a"}
+ {"b"}
+ >
+ },
+ expected: "ab",
+ };
+
+ let layout4 = TestLayout {
+ name: "4",
+ node: html! {
+ <>
+ {"b"}
+ {"a"}
+ >
+ },
+ expected: "ba",
+ };
+
+ diff_layouts(vec![layout1, layout2, layout3, layout4]);
+ }
+}
diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs
new file mode 100644
index 00000000000..a1f0b596a14
--- /dev/null
+++ b/packages/yew/src/dom_bundle/mod.rs
@@ -0,0 +1,151 @@
+//! 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. This information is carried in the bundle.
+
+mod app_handle;
+mod bcomp;
+mod blist;
+mod bnode;
+mod bportal;
+mod bsuspense;
+mod btag;
+mod btext;
+
+#[cfg(test)]
+mod tests;
+
+use self::bcomp::BComp;
+use self::blist::BList;
+use self::bnode::BNode;
+use self::bportal::BPortal;
+use self::bsuspense::BSuspense;
+use self::btag::BTag;
+use self::btext::BText;
+
+pub(crate) use self::bcomp::{ComponentRenderState, Mountable, PropsWrapper, Scoped};
+
+#[doc(hidden)] // Publically exported from crate::app_handle
+pub use self::app_handle::AppHandle;
+#[doc(hidden)] // Publically exported from crate::events
+pub use self::btag::set_event_bubbling;
+#[cfg(test)]
+#[doc(hidden)] // Publically exported from crate::tests
+pub use self::tests::layout_tests;
+
+use crate::html::AnyScope;
+use crate::NodeRef;
+use web_sys::{Element, Node};
+
+trait DomBundle {
+ /// Remove self from parent.
+ ///
+ /// Parent to detach is `true` if the parent element will also be detached.
+ fn detach(self, parent: &Element, parent_to_detach: bool);
+
+ /// Move elements from one parent to another parent.
+ /// This is for example used by `VSuspense` to preserve component state without detaching
+ /// (which destroys component state).
+ fn shift(&self, next_parent: &Element, next_sibling: NodeRef);
+}
+
+/// This trait provides features to update a tree by calculating a difference against another tree.
+trait Reconcilable {
+ type Bundle: DomBundle;
+
+ /// Attach a virtual node to the DOM tree.
+ ///
+ /// Parameters:
+ /// - `parent_scope`: the parent `Scope` used for passing messages to the
+ /// parent `Component`.
+ /// - `parent`: the parent node in the DOM.
+ /// - `next_sibling`: to find where to put the node.
+ ///
+ /// Returns a reference to the newly inserted element.
+ fn attach(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ ) -> (NodeRef, Self::Bundle);
+
+ /// 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.
+ /// - `bundle`: the node that this node will be replacing in the DOM. This
+ /// method will remove the `bundle` from the `parent` if it is of the wrong
+ /// kind, and otherwise reuse it.
+ ///
+ /// Returns a reference to the newly inserted element.
+ fn reconcile_node(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut BNode,
+ ) -> NodeRef;
+
+ fn reconcile(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut Self::Bundle,
+ ) -> NodeRef;
+
+ /// Replace an existing bundle by attaching self and detaching the existing one
+ fn replace(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut BNode,
+ ) -> NodeRef
+ where
+ Self: Sized,
+ Self::Bundle: Into,
+ {
+ let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling);
+ let ancestor = std::mem::replace(bundle, self_.into());
+ ancestor.detach(parent, false);
+ self_ref
+ }
+}
+
+/// Insert a concrete [Node] into the DOM
+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_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),* $(,)?) => {
+ // Only type-check the format expression, do not run any side effects
+ let _ = || { std::format_args!(concat!("\t ", $fmt), $($arg),*); };
+ };
+}
+/// Log an operation during tests for debugging purposes
+/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate.
+pub(self) use test_log;
diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/dom_bundle/tests/layout_tests.rs
similarity index 76%
rename from packages/yew/src/tests/layout_tests.rs
rename to packages/yew/src/dom_bundle/tests/layout_tests.rs
index 744651bafcc..d0ef714a5fc 100644
--- a/packages/yew/src/tests/layout_tests.rs
+++ b/packages/yew/src/dom_bundle/tests/layout_tests.rs
@@ -1,6 +1,7 @@
-use crate::html::{AnyScope, Scope};
+use crate::dom_bundle::{BNode, DomBundle, Reconcilable};
+use crate::html::AnyScope;
use crate::scheduler;
-use crate::virtual_dom::{VDiff, VNode, VText};
+use crate::virtual_dom::VNode;
use crate::{Component, Context, Html};
use gloo::console::log;
use web_sys::Node;
@@ -37,21 +38,20 @@ 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");
parent_node.append_child(&end_node).unwrap();
- let mut 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 bundle) = vnode.attach(&parent_scope, &parent_element, next_sibling.clone());
scheduler::start_now();
assert_eq!(
parent_element.inner_html(),
@@ -61,15 +61,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.reconcile_node(
&parent_scope,
&parent_element,
next_sibling.clone(),
- Some(node),
+ &mut bundle,
);
scheduler::start_now();
assert_eq!(
@@ -80,12 +80,7 @@ pub fn diff_layouts(layouts: Vec>) {
);
// Detach
- empty_node.clone().apply(
- &parent_scope,
- &parent_element,
- next_sibling.clone(),
- Some(node_clone),
- );
+ bundle.detach(&parent_element, false);
scheduler::start_now();
assert_eq!(
parent_element.inner_html(),
@@ -96,16 +91,16 @@ pub fn diff_layouts(layouts: Vec>) {
}
// Sequentially apply each layout
- let mut ancestor: Option = None;
+ let mut bundle: Option = None;
for layout in layouts.iter() {
- let mut next_node = layout.node.clone();
+ let next_vnode = layout.node.clone();
log!("Sequentially apply layout '{}'", layout.name);
- next_node.apply(
+ next_vnode.reconcile_sequentially(
&parent_scope,
&parent_element,
next_sibling.clone(),
- ancestor,
+ &mut bundle,
);
scheduler::start_now();
assert_eq!(
@@ -114,19 +109,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.reconcile_sequentially(
&parent_scope,
&parent_element,
next_sibling.clone(),
- ancestor,
+ &mut bundle,
);
scheduler::start_now();
assert_eq!(
@@ -135,11 +129,12 @@ 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);
+ if let Some(bundle) = bundle {
+ bundle.detach(&parent_element, false);
+ }
scheduler::start_now();
assert_eq!(
parent_element.inner_html(),
diff --git a/packages/yew/src/dom_bundle/tests/mod.rs b/packages/yew/src/dom_bundle/tests/mod.rs
new file mode 100644
index 00000000000..1208f4409c5
--- /dev/null
+++ b/packages/yew/src/dom_bundle/tests/mod.rs
@@ -0,0 +1,26 @@
+pub mod layout_tests;
+
+use super::Reconcilable;
+
+use crate::virtual_dom::VNode;
+use crate::{dom_bundle::BNode, html::AnyScope, NodeRef};
+use web_sys::Element;
+
+impl VNode {
+ fn reconcile_sequentially(
+ self,
+ parent_scope: &AnyScope,
+ parent: &Element,
+ next_sibling: NodeRef,
+ bundle: &mut Option,
+ ) -> NodeRef {
+ match bundle {
+ None => {
+ let (self_ref, node) = self.attach(parent_scope, parent, next_sibling);
+ *bundle = Some(node);
+ self_ref
+ }
+ Some(bundle) => self.reconcile_node(parent_scope, parent, next_sibling, bundle),
+ }
+ }
+}
diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs
index 15595b91de0..ed833c4d46e 100644
--- a/packages/yew/src/html/component/lifecycle.rs
+++ b/packages/yew/src/html/component/lifecycle.rs
@@ -1,19 +1,16 @@
//! Component lifecycle module
-use super::{AnyScope, BaseComponent, Scope};
-use crate::html::{RenderError, RenderResult};
+use super::scope::{AnyScope, Scope};
+use super::BaseComponent;
+use crate::dom_bundle::ComponentRenderState;
+use crate::html::RenderError;
use crate::scheduler::{self, Runnable, Shared};
use crate::suspense::{Suspense, Suspension};
-use crate::virtual_dom::{VDiff, VNode};
-use crate::Callback;
-use crate::{Context, NodeRef};
-#[cfg(feature = "ssr")]
-use futures::channel::oneshot;
+use crate::{Callback, Context, HtmlResult, NodeRef};
use std::any::Any;
use std::rc::Rc;
-use web_sys::Element;
-pub(crate) struct CompStateInner
+pub struct CompStateInner
where
COMP: BaseComponent,
{
@@ -26,8 +23,8 @@ where
///
/// Mostly a thin wrapper that passes the context to a component's lifecycle
/// methods.
-pub(crate) trait Stateful {
- fn view(&self) -> RenderResult;
+pub trait Stateful {
+ fn view(&self) -> HtmlResult;
fn rendered(&mut self, first_render: bool);
fn destroy(&mut self);
@@ -44,7 +41,7 @@ impl Stateful for CompStateInner
where
COMP: BaseComponent,
{
- fn view(&self) -> RenderResult {
+ fn view(&self) -> HtmlResult {
self.component.view(&self.context)
}
@@ -93,23 +90,15 @@ where
}
}
-pub(crate) struct ComponentState {
- pub(crate) inner: Box,
+pub struct ComponentState {
+ pub(super) inner: Box,
- pub(crate) root_node: VNode,
-
- /// When a component has no parent, it means that it should not be rendered.
- parent: Option,
-
- next_sibling: NodeRef,
+ pub(super) render_state: ComponentRenderState,
node_ref: NodeRef,
has_rendered: bool,
suspension: Option,
- #[cfg(feature = "ssr")]
- html_sender: Option>,
-
// Used for debug logging
#[cfg(debug_assertions)]
pub(crate) vcomp_id: usize,
@@ -117,13 +106,10 @@ pub(crate) struct ComponentState {
impl ComponentState {
pub(crate) fn new(
- parent: Option,
- next_sibling: NodeRef,
- root_node: VNode,
+ initial_render_state: ComponentRenderState,
node_ref: NodeRef,
scope: Scope,
props: Rc,
- #[cfg(feature = "ssr")] html_sender: Option>,
) -> Self {
#[cfg(debug_assertions)]
let vcomp_id = scope.vcomp_id;
@@ -136,31 +122,22 @@ impl ComponentState {
Self {
inner,
- root_node,
- parent,
- next_sibling,
+ render_state: initial_render_state,
node_ref,
suspension: None,
has_rendered: false,
- #[cfg(feature = "ssr")]
- html_sender,
-
#[cfg(debug_assertions)]
vcomp_id,
}
}
}
-pub(crate) struct CreateRunner {
- pub(crate) parent: Option,
- pub(crate) next_sibling: NodeRef,
- pub(crate) placeholder: VNode,
- pub(crate) node_ref: NodeRef,
- pub(crate) props: Rc,
- pub(crate) scope: Scope,
- #[cfg(feature = "ssr")]
- pub(crate) html_sender: Option>,
+pub struct CreateRunner {
+ pub initial_render_state: ComponentRenderState,
+ pub node_ref: NodeRef,
+ pub props: Rc,
+ pub scope: Scope,
}
impl Runnable for CreateRunner {
@@ -168,34 +145,28 @@ 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");
+ super::log_event(self.scope.vcomp_id, "create");
*current_state = Some(ComponentState::new(
- self.parent,
- self.next_sibling,
- self.placeholder,
+ self.initial_render_state,
self.node_ref,
self.scope.clone(),
self.props,
- #[cfg(feature = "ssr")]
- self.html_sender,
));
}
}
}
-pub(crate) enum UpdateEvent {
+pub enum UpdateEvent {
/// Drain messages for a component.
Message,
/// Wraps properties, node ref, and next sibling for a component.
Properties(Rc, NodeRef, NodeRef),
- /// Shift Scope.
- Shift(Element, NodeRef),
}
-pub(crate) struct UpdateRunner {
- pub(crate) state: Shared>,
- pub(crate) event: UpdateEvent,
+pub struct UpdateRunner {
+ pub state: Shared >,
+ pub event: UpdateEvent,
}
impl Runnable for UpdateRunner {
@@ -207,27 +178,15 @@ impl Runnable for UpdateRunner {
// When components are updated, a new node ref could have been passed in
state.node_ref = node_ref;
// When components are updated, their siblings were likely also updated
- state.next_sibling = next_sibling;
+ state.render_state.reuse(next_sibling);
// Only trigger changed if props were changed
state.inner.props_changed(props)
}
- UpdateEvent::Shift(parent, next_sibling) => {
- state.root_node.shift(
- state.parent.as_ref().unwrap(),
- &parent,
- next_sibling.clone(),
- );
-
- state.parent = Some(parent);
- state.next_sibling = next_sibling;
-
- false
- }
};
#[cfg(debug_assertions)]
- crate::virtual_dom::vcomp::log_event(
+ super::log_event(
state.vcomp_id,
format!("update(schedule_render={})", schedule_render),
);
@@ -245,46 +204,38 @@ impl Runnable for UpdateRunner {
}
}
-pub(crate) struct DestroyRunner {
- pub(crate) state: Shared >,
- pub(crate) parent_to_detach: bool,
+pub struct DestroyRunner {
+ pub state: Shared >,
+ pub parent_to_detach: bool,
}
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");
+ super::log_event(state.vcomp_id, "destroy");
state.inner.destroy();
-
- if let Some(ref m) = state.parent {
- state.root_node.detach(m, self.parent_to_detach);
- state.node_ref.set(None);
- }
+ state.render_state.detach(self.parent_to_detach);
+ state.node_ref.set(None);
}
}
}
-pub(crate) struct RenderRunner {
- pub(crate) state: Shared>,
+pub struct RenderRunner {
+ pub state: Shared >,
}
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");
+ super::log_event(state.vcomp_id, "render");
match state.inner.view() {
- Ok(m) => {
+ Ok(root) => {
// Currently not suspended, we remove any previous suspension and update
// normally.
- let mut root = m;
- if state.parent.is_some() {
- std::mem::swap(&mut root, &mut state.root_node);
- }
-
if let Some(m) = state.suspension.take() {
let comp_scope = state.inner.any_scope();
@@ -294,15 +245,11 @@ impl Runnable for RenderRunner {
suspense.resume(m);
}
- if let Some(ref m) = state.parent {
- let ancestor = Some(root);
- let new_root = &mut state.root_node;
- let scope = state.inner.any_scope();
- let next_sibling = state.next_sibling.clone();
-
- let node = new_root.apply(&scope, m, next_sibling, ancestor);
- state.node_ref.link(node);
+ let scope = state.inner.any_scope();
+ let node = state.render_state.reconcile(root, &scope);
+ state.node_ref.link(node);
+ if state.render_state.should_trigger_rendered() {
let first_render = !state.has_rendered;
state.has_rendered = true;
@@ -314,11 +261,6 @@ impl Runnable for RenderRunner {
},
first_render,
);
- } else {
- #[cfg(feature = "ssr")]
- if let Some(tx) = state.html_sender.take() {
- tx.send(root).unwrap();
- }
}
}
@@ -372,8 +314,8 @@ impl Runnable for RenderRunner {
}
}
-pub(crate) struct RenderedRunner {
- pub(crate) state: Shared>,
+struct RenderedRunner {
+ state: Shared >,
first_render: bool,
}
@@ -381,9 +323,9 @@ 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");
+ super::log_event(state.vcomp_id, "rendered");
- if state.suspension.is_none() && state.parent.is_some() {
+ if state.suspension.is_none() {
state.inner.rendered(self.first_render);
}
}
@@ -394,10 +336,13 @@ impl Runnable for RenderedRunner {
mod tests {
extern crate self as yew;
+ use crate::dom_bundle::ComponentRenderState;
use crate::html;
use crate::html::*;
use crate::Properties;
+ use std::cell::RefCell;
use std::ops::Deref;
+ use std::rc::Rc;
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
@@ -515,10 +460,12 @@ mod tests {
let document = gloo_utils::document();
let scope = Scope::::new(None);
let el = document.create_element("div").unwrap();
+ let node_ref = NodeRef::default();
+ let render_state = ComponentRenderState::new(el, NodeRef::default(), &node_ref);
let lifecycle = props.lifecycle.clone();
lifecycle.borrow_mut().clear();
- scope.mount_in_place(el, NodeRef::default(), NodeRef::default(), Rc::new(props));
+ scope.mount_in_place(render_state, node_ref, Rc::new(props));
crate::scheduler::start_now();
assert_eq!(&lifecycle.borrow_mut().deref()[..], expected);
diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs
index 3528c7c9470..74443781962 100644
--- a/packages/yew/src/html/component/mod.rs
+++ b/packages/yew/src/html/component/mod.rs
@@ -8,16 +8,52 @@ mod scope;
use super::{Html, HtmlResult, IntoHtmlResult};
pub use children::*;
pub use properties::*;
-pub(crate) use scope::Scoped;
pub use scope::{AnyScope, Scope, SendAsMessage};
use std::rc::Rc;
+#[cfg(debug_assertions)]
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+#[cfg(debug_assertions)]
+thread_local! {
+ static EVENT_HISTORY: std::cell::RefCell>>
+ = Default::default();
+ static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
+}
+
+/// Push [Component] event to lifecycle debugging registry
+#[cfg(debug_assertions)]
+pub(crate) fn log_event(vcomp_id: usize, event: impl ToString) {
+ EVENT_HISTORY.with(|h| {
+ h.borrow_mut()
+ .entry(vcomp_id)
+ .or_default()
+ .push(event.to_string())
+ });
+}
+
+/// Get [Component] event log from lifecycle debugging registry
+#[cfg(debug_assertions)]
+#[allow(dead_code)]
+pub(crate) fn get_event_log(vcomp_id: usize) -> Vec {
+ EVENT_HISTORY.with(|h| {
+ h.borrow()
+ .get(&vcomp_id)
+ .map(|l| (*l).clone())
+ .unwrap_or_default()
+ })
+}
+
+#[cfg(debug_assertions)]
+pub(crate) fn next_id() -> usize {
+ COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed))
+}
/// The [`Component`]'s context. This contains component's [`Scope`] and and props and
/// is passed to every lifecycle method.
#[derive(Debug)]
pub struct Context {
- pub(crate) scope: Scope,
- pub(crate) props: Rc,
+ scope: Scope,
+ props: Rc,
}
impl Context {
diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs
index 6229f3b3a57..89529c9d1e2 100644
--- a/packages/yew/src/html/component/scope.rs
+++ b/packages/yew/src/html/component/scope.rs
@@ -9,17 +9,16 @@ use super::{
};
use crate::callback::Callback;
use crate::context::{ContextHandle, ContextProvider};
+use crate::dom_bundle::{ComponentRenderState, Scoped};
use crate::html::NodeRef;
use crate::scheduler::{self, Shared};
-use crate::virtual_dom::{insert_node, VNode};
-use gloo_utils::document;
use std::any::TypeId;
use std::cell::{Ref, RefCell};
use std::marker::PhantomData;
use std::ops::Deref;
use std::rc::Rc;
use std::{fmt, iter};
-use web_sys::{Element, Node};
+use web_sys::Element;
#[derive(Debug)]
pub(crate) struct MsgQueue(Shared>);
@@ -65,9 +64,6 @@ pub struct AnyScope {
type_id: TypeId,
parent: Option>,
state: Shared>,
-
- #[cfg(debug_assertions)]
- pub(crate) vcomp_id: usize,
}
impl fmt::Debug for AnyScope {
@@ -82,9 +78,6 @@ impl From> for AnyScope {
type_id: TypeId::of::(),
parent: scope.parent,
state: scope.state,
-
- #[cfg(debug_assertions)]
- vcomp_id: scope.vcomp_id,
}
}
}
@@ -96,9 +89,6 @@ impl AnyScope {
type_id: TypeId::of::<()>(),
parent: None,
state: Rc::new(RefCell::new(None)),
-
- #[cfg(debug_assertions)]
- vcomp_id: 0,
}
}
@@ -163,44 +153,41 @@ impl AnyScope {
}
}
-pub(crate) trait Scoped {
- fn to_any(&self) -> AnyScope;
- fn root_vnode(&self) -> Option[>;
- fn destroy(&mut self, parent_to_detach: bool);
- fn shift_node(&self, parent: Element, next_sibling: NodeRef);
-}
-
impl] Scoped for Scope {
fn to_any(&self) -> AnyScope {
self.clone().into()
}
- fn root_vnode(&self) -> Option[> {
+ fn render_state(&self) -> Option][> {
let state_ref = self.state.borrow();
// check that component hasn't been destroyed
state_ref.as_ref()?;
Some(Ref::map(state_ref, |state_ref| {
- &state_ref.as_ref().unwrap().root_node
+ &state_ref.as_ref().unwrap().render_state
}))
}
/// Process an event to destroy a component
- fn destroy(&mut self, parent_to_detach: bool) {
+ fn destroy(self, parent_to_detach: bool) {
scheduler::push_component_destroy(DestroyRunner {
- state: self.state.clone(),
+ state: self.state,
parent_to_detach,
});
// Not guaranteed to already have the scheduler started
scheduler::start();
}
+ fn destroy_boxed(self: Box], parent_to_detach: bool) {
+ self.destroy(parent_to_detach)
+ }
+
fn shift_node(&self, parent: Element, next_sibling: NodeRef) {
- scheduler::push_component_update(UpdateRunner {
- state: self.state.clone(),
- event: UpdateEvent::Shift(parent, next_sibling),
- });
+ let mut state_ref = self.state.borrow_mut();
+ if let Some(render_state) = state_ref.as_mut() {
+ render_state.render_state.shift(parent, next_sibling)
+ }
}
}
@@ -258,14 +245,12 @@ impl Scope {
})
}
+ /// Crate a scope with an optional parent scope
pub(crate) fn new(parent: Option) -> Self {
let parent = parent.map(Rc::new);
let state = Rc::new(RefCell::new(None));
let pending_messages = MsgQueue::new();
- #[cfg(debug_assertions)]
- let vcomp_id = parent.as_ref().map(|p| p.vcomp_id).unwrap_or_default();
-
Scope {
_marker: PhantomData,
pending_messages,
@@ -273,37 +258,23 @@ impl Scope {
parent,
#[cfg(debug_assertions)]
- vcomp_id,
+ vcomp_id: super::next_id(),
}
}
/// Mounts a component with `props` to the specified `element` in the DOM.
pub(crate) fn mount_in_place(
&self,
- parent: Element,
- next_sibling: NodeRef,
+ initial_render_state: ComponentRenderState,
node_ref: NodeRef,
props: Rc,
) {
- #[cfg(debug_assertions)]
- crate::virtual_dom::vcomp::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)
- };
-
scheduler::push_component_create(
CreateRunner {
- parent: Some(parent),
- next_sibling,
- placeholder,
+ initial_render_state,
node_ref,
props,
scope: self.clone(),
- #[cfg(feature = "ssr")]
- html_sender: None,
},
RenderRunner {
state: self.state.clone(),
@@ -320,7 +291,7 @@ impl Scope {
next_sibling: NodeRef,
) {
#[cfg(debug_assertions)]
- crate::virtual_dom::vcomp::log_event(self.vcomp_id, "reuse");
+ super::log_event(self.vcomp_id, "reuse");
self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling));
}
@@ -346,6 +317,9 @@ impl Scope {
}
/// Send a batch of messages to the component.
+ ///
+ /// This is slightly more efficient than calling [`send_message`](Self::send_message)
+ /// in a loop.
pub fn send_message_batch(&self, mut messages: Vec) {
let msg_len = messages.len();
@@ -410,24 +384,11 @@ mod feat_ssr {
use futures::channel::oneshot;
impl Scope {
- pub(crate) async fn render_to_string(&self, w: &mut String, props: Rc) {
+ pub(crate) async fn render_to_string(self, w: &mut String, props: Rc) {
let (tx, rx) = oneshot::channel();
+ let initial_render_state = ComponentRenderState::new_ssr(tx);
- scheduler::push_component_create(
- CreateRunner {
- parent: None,
- next_sibling: NodeRef::default(),
- placeholder: VNode::default(),
- node_ref: NodeRef::default(),
- props,
- scope: self.clone(),
- html_sender: Some(tx),
- },
- RenderRunner {
- state: self.state.clone(),
- },
- );
- scheduler::start();
+ self.mount_in_place(initial_render_state, NodeRef::default(), props);
let html = rx.await.unwrap();
@@ -442,6 +403,7 @@ mod feat_ssr {
}
}
}
+
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
mod feat_io {
diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs
index e0885153ca5..e614ff2a77e 100644
--- a/packages/yew/src/lib.rs
+++ b/packages/yew/src/lib.rs
@@ -263,9 +263,9 @@ pub mod macros {
pub use crate::props;
}
-mod app_handle;
pub mod callback;
pub mod context;
+mod dom_bundle;
pub mod functional;
pub mod html;
mod io_coop;
@@ -274,18 +274,21 @@ mod sealed;
#[cfg(feature = "ssr")]
mod server_renderer;
pub mod suspense;
-#[cfg(test)]
-pub mod tests;
pub mod utils;
pub mod virtual_dom;
#[cfg(feature = "ssr")]
pub use server_renderer::*;
+#[cfg(test)]
+pub mod tests {
+ pub use crate::dom_bundle::layout_tests;
+}
+
/// The module that contains all events available in the framework.
pub mod events {
pub use crate::html::TargetCast;
- pub use crate::virtual_dom::listeners::set_event_bubbling;
+ pub use crate::dom_bundle::set_event_bubbling;
#[doc(no_inline)]
pub use web_sys::{
@@ -294,7 +297,7 @@ pub mod events {
};
}
-pub use crate::app_handle::AppHandle;
+pub use crate::dom_bundle::AppHandle;
use web_sys::Element;
use crate::html::BaseComponent;
@@ -324,7 +327,7 @@ where
COMP: BaseComponent,
COMP::Properties: Default,
{
- start_app_with_props_in_element(element, COMP::Properties::default())
+ start_app_with_props_in_element::(element, COMP::Properties::default())
}
/// Starts an yew app mounted to the body of the document.
@@ -334,7 +337,7 @@ where
COMP: BaseComponent,
COMP::Properties: Default,
{
- start_app_with_props(COMP::Properties::default())
+ start_app_with_props::(COMP::Properties::default())
}
/// The main entry point of a Yew application. This function does the
@@ -356,7 +359,7 @@ pub fn start_app_with_props(props: COMP::Properties) -> AppHandle
where
COMP: BaseComponent,
{
- start_app_with_props_in_element(
+ start_app_with_props_in_element::(
gloo_utils::document()
.body()
.expect("no body node found")
@@ -374,9 +377,9 @@ where
/// use yew::prelude::*;
/// ```
pub mod prelude {
- pub use crate::app_handle::AppHandle;
pub use crate::callback::Callback;
pub use crate::context::{ContextHandle, ContextProvider};
+ pub use crate::dom_bundle::AppHandle;
pub use crate::events::*;
pub use crate::html::{
create_portal, BaseComponent, Children, ChildrenWithProps, Classes, Component, Context,
diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs
index 9e5cd5fe1cb..91bf4c95212 100644
--- a/packages/yew/src/server_renderer.rs
+++ b/packages/yew/src/server_renderer.rs
@@ -3,8 +3,8 @@ use super::*;
use crate::html::Scope;
/// A Yew Server-side Renderer.
-#[cfg_attr(documenting, doc(cfg(feature = "ssr")))]
#[derive(Debug)]
+#[cfg_attr(documenting, doc(cfg(feature = "ssr")))]
pub struct ServerRenderer
where
COMP: BaseComponent,
diff --git a/packages/yew/src/tests/mod.rs b/packages/yew/src/tests/mod.rs
deleted file mode 100644
index 7c8881b072f..00000000000
--- a/packages/yew/src/tests/mod.rs
+++ /dev/null
@@ -1 +0,0 @@
-pub mod layout_tests;
diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs
index 29c0db7adf4..caa0943d599 100644
--- a/packages/yew/src/virtual_dom/listeners.rs
+++ b/packages/yew/src/virtual_dom/listeners.rs
@@ -1,40 +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};
-
-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).
@@ -75,10 +39,10 @@ macro_rules! gen_listener_kinds {
}
impl ListenerKind {
- pub fn type_name(&self) -> &str {
+ pub fn type_name(&self) -> std::borrow::Cow<'static, str> {
match self {
- Self::other(type_name) => type_name.as_ref(),
- kind => &kind.as_ref()[2..],
+ Self::other(type_name) => type_name.clone(),
+ $( Self::$kind => stringify!($kind)[2..].into(), )*
}
}
}
@@ -200,90 +164,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(&mut self, el: &Self::Element, ancestor: Self) {
- use Listeners::*;
-
- match (std::mem::take(self), ancestor) {
- (Pending(pending), Registered(id)) => {
- // Reuse the ID
- Registry::with(|reg| reg.patch(&id, &*pending));
- *self = Registered(id);
- }
- (Pending(pending), None) => {
- *self = Self::register(el, &pending);
- }
- (None, Registered(id)) => {
- Registry::with(|reg| reg.unregister(&id));
- }
- _ => (),
- };
- }
-}
-
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
@@ -303,7 +193,7 @@ impl PartialEq for Listeners {
})
}
}
- _ => false,
+ (None, Pending(pending)) | (Pending(pending), None) => pending.len() == 0,
}
}
}
@@ -311,7 +201,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()),
}
}
@@ -322,623 +212,3 @@ impl Default for Listeners {
Self::None
}
}
-
-#[derive(Clone, 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({
- let desc = desc.clone();
- move |e: Event| Registry::handle(desc.clone(), e)
- }) as Box);
- AsRef::::as_ref(body)
- .add_event_listener_with_callback_and_add_event_listener_options(
- desc.kind.type_name(),
- 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.clone(), 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.type_name(),
- 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.clone());
- 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.clone());
- 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()
- .and_then(|el| el.dyn_into::().ok())
- {
- 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())
- .and_then(|v| v.dyn_into().ok())
- .and_then(|num: js_sys::Number| {
- Registry::with(|r| {
- r.by_id
- .get(&(num.value_of() as u32))
- .and_then(|s| s.get(&desc))
- .cloned()
- })
- })
- {
- 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, scheduler, AppHandle, Component, Context, Html};
- use gloo_utils::document;
- use wasm_bindgen::JsCast;
- use yew::Callback;
-
- #[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,
- {
- let link = ctx.link().clone();
- let onclick = Callback::from(move |_| {
- link.send_message(Message::Action);
- scheduler::start_now();
- });
-
- 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);
- scheduler::start_now();
-
- (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);
- scheduler::start_now();
-
- 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 link = ctx.link().clone();
- let onblur = Callback::from(move |_| {
- link.send_message(Message::Action);
- scheduler::start_now();
- });
- 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 link = ctx.link().clone();
- let cb = Callback::from(move |_| {
- link.send_message(Message::Action);
- scheduler::start_now();
- });
- 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);
- scheduler::start_now();
- 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,
- {
- let link = ctx.link().clone();
- let onclick = Callback::from(move |_| {
- link.send_message(Message::Action);
- scheduler::start_now();
- });
-
- let link = ctx.link().clone();
- let onclick2 = Callback::from(move |e: MouseEvent| {
- e.stop_propagation();
- link.send_message(Message::Action);
- scheduler::start_now();
- });
-
- 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,
- {
- let link = ctx.link().clone();
- let onclick = Callback::from(move |_| {
- link.send_message(Message::Action);
- scheduler::start_now();
- });
-
- let link = ctx.link().clone();
- let onclick2 = Callback::from(move |e: MouseEvent| {
- e.stop_propagation();
- link.send_message(Message::Action);
- scheduler::start_now();
- });
- 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 {
- let link = ctx.link().clone();
- let onchange = Callback::from(move |e: web_sys::Event| {
- let el: web_sys::HtmlInputElement = e.target_unchecked_into();
- link.send_message(Message::SetText(el.value()));
- scheduler::start_now();
- });
-
- let link = ctx.link().clone();
- let oninput = Callback::from(move |e: web_sys::InputEvent| {
- let el: web_sys::HtmlInputElement = e.target_unchecked_into();
- link.send_message(Message::SetText(el.value()));
- scheduler::start_now();
- });
-
- 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);
- scheduler::start_now();
-
- 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 898d2517928..80e0f82028f 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,13 @@ 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::{fmt, hint::unreachable_unchecked};
/// Attribute value
#[derive(Debug)]
@@ -181,18 +179,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(&mut self, el: &Self::Element, ancestor: Self);
-}
-
/// A collection of attributes for an element
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum Attributes {
@@ -271,194 +257,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(&mut self, el: &Element, ancestor: Self) {
- #[inline]
- fn ptr_eq(a: &[T], b: &[T]) -> bool {
- a.as_ptr() == b.as_ptr()
- }
-
- match (self, 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 {
@@ -473,66 +271,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.
- ///
- /// Parent to detach is `true` if the parent element will also be detached.
- fn detach(&mut self, parent: &Element, parent_to_detach: bool);
-
- /// 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
- .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 0587a0ab9b6..76b098523a2 100644
--- a/packages/yew/src/virtual_dom/vcomp.rs
+++ b/packages/yew/src/virtual_dom/vcomp.rs
@@ -1,77 +1,41 @@
//! This module contains the implementation of a virtual component (`VComp`).
-use super::{Key, VDiff, VNode};
-use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped};
-#[cfg(feature = "ssr")]
-use futures::future::{FutureExt, LocalBoxFuture};
+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;
-#[cfg(debug_assertions)]
-use std::sync::atomic::{AtomicUsize, Ordering};
-use web_sys::Element;
#[cfg(debug_assertions)]
-thread_local! {
- static EVENT_HISTORY: std::cell::RefCell>>
- = Default::default();
- static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
-}
-
-/// Push [VComp] event to lifecycle debugging registry
-#[cfg(debug_assertions)]
-pub(crate) fn log_event(vcomp_id: usize, 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: usize) -> Vec {
- EVENT_HISTORY.with(|h| {
- h.borrow()
- .get(&vcomp_id)
- .map(|l| (*l).clone())
- .unwrap_or_default()
- })
-}
+thread_local! {}
/// A virtual component.
pub struct VComp {
- type_id: TypeId,
- scope: Option>,
- mountable: Option>,
+ pub(crate) type_id: TypeId,
+ pub(crate) mountable: Box,
pub(crate) node_ref: NodeRef,
pub(crate) key: Option,
+}
- #[cfg(debug_assertions)]
- pub(crate) id: usize,
+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("mountable", &"..")
+ .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,
- mountable: self.mountable.as_ref().map(|m| m.copy()),
+ mountable: self.mountable.copy(),
node_ref: self.node_ref.clone(),
key: self.key.clone(),
-
- #[cfg(debug_assertions)]
- id: self.id,
}
}
}
@@ -136,155 +100,10 @@ impl VComp {
VComp {
type_id: TypeId::of::(),
node_ref,
- mountable: Some(Box::new(PropsWrapper::::new(props))),
- scope: None,
+ mountable: Box::new(PropsWrapper::::new(props)),
key,
-
- #[cfg(debug_assertions)]
- id: Self::next_id(),
}
}
-
- 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)
- );
- })
- }
-
- #[cfg(debug_assertions)]
- pub(crate) fn next_id() -> usize {
- COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed))
- }
-}
-
-trait Mountable {
- fn copy(&self) -> Box;
- fn mount(
- self: Box,
- node_ref: NodeRef,
- parent_scope: &AnyScope,
- parent: Element,
- next_sibling: NodeRef,
- ) -> Box;
- fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef);
-
- #[cfg(feature = "ssr")]
- fn render_to_string<'a>(
- &'a self,
- w: &'a mut String,
- parent_scope: &'a AnyScope,
- ) -> LocalBoxFuture<'a, ()>;
-}
-
-struct PropsWrapper {
- props: Rc,
-}
-
-impl PropsWrapper {
- fn new(props: Rc) -> Self {
- Self { props }
- }
-}
-
-impl Mountable for PropsWrapper {
- fn copy(&self) -> Box {
- let wrapper: PropsWrapper = PropsWrapper {
- props: Rc::clone(&self.props),
- };
- Box::new(wrapper)
- }
-
- fn mount(
- self: Box,
- node_ref: NodeRef,
- parent_scope: &AnyScope,
- parent: Element,
- next_sibling: NodeRef,
- ) -> Box {
- let scope: Scope = Scope::new(Some(parent_scope.clone()));
- scope.mount_in_place(parent, next_sibling, node_ref, self.props);
-
- Box::new(scope)
- }
-
- fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) {
- let scope: Scope = scope.to_any().downcast();
- scope.reuse(self.props, node_ref, next_sibling);
- }
-
- #[cfg(feature = "ssr")]
- fn render_to_string<'a>(
- &'a self,
- w: &'a mut String,
- parent_scope: &'a AnyScope,
- ) -> LocalBoxFuture<'a, ()> {
- async move {
- let scope: Scope = Scope::new(Some(parent_scope.clone()));
- scope.render_to_string(w, self.props.clone()).await;
- }
- .boxed_local()
- }
-}
-
-impl VDiff for VComp {
- fn detach(&mut self, _parent: &Element, parent_to_detach: bool) {
- self.take_scope().destroy(parent_to_detach);
- }
-
- fn shift(&self, _previous_parent: &Element, 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,
- parent_scope: &AnyScope,
- parent: &Element,
- next_sibling: NodeRef,
- ancestor: Option,
- ) -> NodeRef {
- let mountable = self
- .mountable
- .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, false);
- }
-
- self.scope = Some(mountable.mount(
- self.node_ref.clone(),
- parent_scope,
- parent.to_owned(),
- next_sibling,
- ));
-
- self.node_ref.clone()
- }
}
impl PartialEq for VComp {
@@ -293,12 +112,6 @@ 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<_>")
@@ -308,608 +121,18 @@ impl fmt::Debug for VChild {
#[cfg(feature = "ssr")]
mod feat_ssr {
use super::*;
+ use crate::html::AnyScope;
impl VComp {
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
self.mountable
.as_ref()
- .map(|m| m.copy())
- .unwrap()
.render_to_string(w, parent_scope)
.await;
}
}
}
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::scheduler;
- 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 mut ancestor = html! { };
- ancestor.apply(&parent_scope, &parent_element, NodeRef::default(), None);
- scheduler::start_now();
-
- for _ in 0..10000 {
- let mut node = html! { };
- node.apply(
- &parent_scope,
- &parent_element,
- NodeRef::default(),
- Some(ancestor),
- );
- scheduler::start_now();
- ancestor = node;
- }
- }
-
- #[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().as_ref(), 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(mut node: Html, scope: &AnyScope, parent: &Element) -> String {
- // clear parent
- parent.set_inner_html("");
-
- node.apply(scope, parent, NodeRef::default(), None);
- scheduler::start_now();
- 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 mut elem: VNode = html! { };
- elem.apply(&scope, &parent, NodeRef::default(), None);
- scheduler::start_now();
- let parent_node = parent.deref();
- assert_eq!(node_ref.get(), parent_node.first_child());
- elem.detach(&parent, false);
- scheduler::start_now();
- 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;
-
- use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
-
- 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]);
- }
-}
-
#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))]
mod ssr_tests {
use tokio::test;
diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs
index e655cf873ac..9304f00d2f6 100644
--- a/packages/yew/src/virtual_dom/vlist.rs
+++ b/packages/yew/src/virtual_dom/vlist.rs
@@ -1,18 +1,15 @@
//! This module contains fragments implementation.
-use super::{Key, VDiff, VNode, VText};
-use crate::html::{AnyScope, NodeRef};
-use std::collections::HashMap;
+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,
}
@@ -41,47 +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: &mut VNode) -> Self {
- test_log!("adding: {:?}", node);
- self.write(node, None)
- }
-
- fn patch(self, node: &mut VNode, ancestor: 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);
- 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
- }
- }
-}
-
impl VList {
/// Creates a new empty [VList] instance.
pub const fn new() -> Self {
@@ -126,167 +82,12 @@ 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: &mut [VNode],
- rights: 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 mut r = rights_it.next().unwrap();
- test_log!("removing: {:?}", r);
- r.detach(parent, false);
- diff += 1;
- }
-
- for (l, r) in lefts_it.zip(rights_it) {
- writer = writer.patch(l, r);
- }
-
- 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: &mut [VNode],
- rights: 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- ,
- b: impl Iterator
- ,
- ) -> usize {
- 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());
-
- if from_start == std::cmp::min(lefts.len(), rights.len()) {
- // No key changes
- return Self::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights);
- }
-
- 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..))
- .rev()
- {
- writer = writer.patch(l, r);
- }
-
- // 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 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())
- .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);
- }
- // Add new children
- None => {
- writer = writer.add(l);
- }
- }
- next_left_key = Some(l_key);
- }
-
- // Remove any extra rights
- for (_, (mut r, _)) in rights_diff.drain() {
- test_log!("removing: {:?}", r);
- r.detach(parent, false);
- }
-
- // 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
- .rev()
- {
- writer = writer.patch(l, r);
- }
-
- writer.next_sibling
- }
}
#[cfg(feature = "ssr")]
mod feat_ssr {
use super::*;
+ use crate::html::AnyScope;
impl VList {
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
@@ -306,990 +107,6 @@ mod feat_ssr {
}
}
-impl VDiff for VList {
- fn detach(&mut self, parent: &Element, parent_to_detach: bool) {
- for mut child in self.children.drain(..) {
- child.detach(parent, parent_to_detach);
- }
- }
-
- 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 apply(
- &mut self,
- parent_scope: &AnyScope,
- parent: &Element,
- next_sibling: NodeRef,
- ancestor: Option
,
- ) -> 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 = &mut 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),
-
- // 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)
- }
-
- // No unkeyed nodes in an empty VList
- _ => (vec![], true),
- };
- test_log!("lefts: {:?}", lefts);
- test_log!("rights: {:?}", rights);
-
- #[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)
- };
- test_log!("result: {:?}", lefts);
- 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 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);
- }
-}
-
#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))]
mod ssr_tests {
use tokio::test;
diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs
index dde3d045223..3b98a0e7ae4 100644
--- a/packages/yew/src/virtual_dom/vnode.rs
+++ b/packages/yew/src/virtual_dom/vnode.rs
@@ -1,14 +1,11 @@
//! This module contains the implementation of abstract virtual node.
-use super::{Key, VChild, VComp, VDiff, VList, VPortal, VSuspense, VTag, VText};
-use crate::html::{AnyScope, BaseComponent, NodeRef};
-use gloo::console;
+use super::{Key, VChild, VComp, VList, VPortal, VSuspense, VTag, VText};
+use crate::html::BaseComponent;
use std::cmp::PartialEq;
use std::fmt;
use std::iter::FromIterator;
-use wasm_bindgen::JsCast;
-
-use web_sys::{Element, Node};
+use web_sys::Node;
/// Bind virtual element to a DOM reference.
#[derive(Clone)]
@@ -30,177 +27,21 @@ 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(),
}
}
- /// Returns true if the [VNode] has a key without needlessly cloning the key.
+ /// Returns true if the [VNode] has a key.
pub fn has_key(&self) -> bool {
- match self {
- VNode::VComp(vcomp) => vcomp.key.is_some(),
- VNode::VList(vlist) => vlist.key.is_some(),
- VNode::VRef(_) | VNode::VText(_) => false,
- VNode::VTag(vtag) => vtag.key.is_some(),
- VNode::VPortal(vportal) => vportal.node.has_key(),
- VNode::VSuspense(vsuspense) => vsuspense.key.is_some(),
- }
- }
-
- /// 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 {
- VNode::VTag(vtag) => vtag
- .reference()
- .expect("VTag is not mounted")
- .clone()
- .into(),
- VNode::VText(vtext) => {
- let text_node = vtext.reference.as_ref().expect("VText is not mounted");
- text_node.clone().into()
- }
- VNode::VComp(vcomp) => vcomp.node_ref.get().unwrap_or_else(|| {
- #[cfg(not(debug_assertions))]
- panic!("no node_ref; VComp should be mounted");
-
- #[cfg(debug_assertions)]
- panic!(
- "no node_ref; VComp should be mounted after: {:?}",
- crate::virtual_dom::vcomp::get_event_log(vcomp.id),
- );
- }),
- VNode::VList(vlist) => vlist
- .get(0)
- .expect("VList is not mounted")
- .unchecked_first_node(),
- VNode::VRef(node) => node.clone(),
- VNode::VPortal(_) => panic!("portals have no first node, they are empty inside"),
- VNode::VSuspense(vsuspense) => {
- vsuspense.first_node().expect("VSuspense is not mounted")
- }
- }
- }
-
- pub(crate) fn move_before(&self, parent: &Element, next_sibling: &Option) {
- match self {
- VNode::VList(vlist) => {
- for node in vlist.iter() {
- node.move_before(parent, next_sibling);
- }
- }
- VNode::VComp(vcomp) => {
- vcomp
- .root_vnode()
- .expect("VComp has no root vnode")
- .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 {
- /// Remove VNode from parent.
- fn detach(&mut self, parent: &Element, parent_to_detach: bool) {
- match *self {
- VNode::VTag(ref mut vtag) => vtag.detach(parent, parent_to_detach),
- VNode::VText(ref mut vtext) => vtext.detach(parent, parent_to_detach),
- VNode::VComp(ref mut vcomp) => vcomp.detach(parent, parent_to_detach),
- VNode::VList(ref mut vlist) => vlist.detach(parent, parent_to_detach),
- 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, parent_to_detach),
- VNode::VSuspense(ref mut vsuspense) => vsuspense.detach(parent, parent_to_detach),
- }
- }
-
- fn shift(&self, previous_parent: &Element, 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::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::VSuspense(ref vsuspense) => {
- vsuspense.shift(previous_parent, next_parent, next_sibling)
- }
- }
- }
-
- fn apply(
- &mut self,
- parent_scope: &AnyScope,
- parent: &Element,
- next_sibling: NodeRef,
- ancestor: Option,
- ) -> 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(mut ancestor) = ancestor {
- // We always remove VRef in case it's meant to be used somewhere else.
- if let VNode::VRef(n) = &ancestor {
- if node == n {
- return NodeRef::new(node.clone());
- }
- }
- ancestor.detach(parent, false);
- }
- super::insert_node(node, parent, next_sibling.get().as_ref());
- NodeRef::new(node.clone())
- }
- VNode::VPortal(ref mut vportal) => {
- vportal.apply(parent_scope, parent, next_sibling, ancestor)
- }
- VNode::VSuspense(ref mut vsuspense) => {
- vsuspense.apply(parent_scope, parent, next_sibling, ancestor)
- }
- }
+ self.key().is_some()
}
}
@@ -245,6 +86,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,
@@ -299,9 +147,9 @@ impl PartialEq for VNode {
#[cfg(feature = "ssr")]
mod feat_ssr {
- use futures::future::{FutureExt, LocalBoxFuture};
-
use super::*;
+ use crate::html::AnyScope;
+ use futures::future::{FutureExt, LocalBoxFuture};
impl VNode {
// Boxing is needed here, due to: https://rust-lang.github.io/async-book/07_workarounds/04_recursion.html
@@ -335,36 +183,3 @@ mod feat_ssr {
}
}
}
-
-#[cfg(test)]
-mod layout_tests {
- use super::*;
- use crate::tests::layout_tests::{diff_layouts, TestLayout};
-
- #[cfg(feature = "wasm_test")]
- use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
-
- #[cfg(feature = "wasm_test")]
- wasm_bindgen_test_configure!(run_in_browser);
-
- #[test]
- fn diff() {
- let document = gloo_utils::document();
- let vref_node_1 = VNode::VRef(document.create_element("i").unwrap().into());
- let vref_node_2 = VNode::VRef(document.create_element("b").unwrap().into());
-
- let layout1 = TestLayout {
- name: "1",
- node: vref_node_1,
- expected: " ",
- };
-
- let layout2 = TestLayout {
- name: "2",
- node: vref_node_2,
- expected: " ",
- };
-
- diff_layouts(vec![layout1, layout2]);
- }
-}
diff --git a/packages/yew/src/virtual_dom/vportal.rs b/packages/yew/src/virtual_dom/vportal.rs
index b1ef039d598..abcb4f1a19b 100644
--- a/packages/yew/src/virtual_dom/vportal.rs
+++ b/packages/yew/src/virtual_dom/vportal.rs
@@ -1,74 +1,17 @@
//! This module contains the implementation of a portal `VPortal`.
-use super::{VDiff, VNode};
-use crate::html::{AnyScope, NodeRef};
+use super::VNode;
+use crate::html::NodeRef;
use web_sys::{Element, Node};
#[derive(Debug, Clone)]
pub struct VPortal {
/// The element under which the content is inserted.
pub host: Element,
- /// The next sibling after the inserted content
- pub next_sibling: NodeRef,
+ /// The next sibling after the inserted content. Most be a child of `host`.
+ pub inner_sibling: NodeRef,
/// The inserted node
pub node: Box,
- /// The next sibling after the portal. Set when rendered
- sibling_ref: NodeRef,
-}
-
-impl VDiff for VPortal {
- fn detach(&mut self, _: &Element, _parent_to_detach: bool) {
- self.node.detach(&self.host, false);
- self.sibling_ref.set(None);
- }
-
- fn shift(&self, _previous_parent: &Element, _next_parent: &Element, _next_sibling: NodeRef) {
- // portals have nothing in it's original place of DOM, we also do nothing.
- }
-
- fn apply(
- &mut self,
- parent_scope: &AnyScope,
- parent: &Element,
- next_sibling: NodeRef,
- ancestor: Option,
- ) -> NodeRef {
- let inner_ancestor = match ancestor {
- Some(VNode::VPortal(old_portal)) => {
- let VPortal {
- host: old_host,
- next_sibling: old_sibling,
- mut node,
- ..
- } = old_portal;
- if old_host != self.host {
- // Remount the inner node somewhere else instead of diffing
- node.detach(&old_host, false);
- 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)
- }
- }
- Some(mut node) => {
- node.detach(parent, false);
- None
- }
- None => None,
- };
-
- self.node.apply(
- parent_scope,
- &self.host,
- self.next_sibling.clone(),
- inner_ancestor,
- );
- self.sibling_ref = next_sibling.clone();
- next_sibling
- }
}
impl VPortal {
@@ -76,114 +19,22 @@ impl VPortal {
pub fn new(content: VNode, host: Element) -> Self {
Self {
host,
- next_sibling: NodeRef::default(),
+ inner_sibling: NodeRef::default(),
node: Box::new(content),
- sibling_ref: NodeRef::default(),
}
}
/// Creates a [VPortal] rendering `content` in the DOM hierarchy under `host`.
/// If `next_sibling` is given, the content is inserted before that [Node].
/// The parent of `next_sibling`, if given, must be `host`.
- pub fn new_before(content: VNode, host: Element, next_sibling: Option) -> Self {
+ pub fn new_before(content: VNode, host: Element, inner_sibling: Option