diff --git a/examples/portals/src/main.rs b/examples/portals/src/main.rs index 39b46a4f72a..da751f49c28 100644 --- a/examples/portals/src/main.rs +++ b/examples/portals/src/main.rs @@ -31,7 +31,7 @@ impl Component for ShadowDOMHost { .get() .expect("rendered host") .unchecked_into::() - .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Closed)) + .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open)) .expect("installing shadow root succeeds"); let inner_host = gloo_utils::document() .create_element("div") @@ -68,34 +68,73 @@ impl Component for ShadowDOMHost { } pub struct App { - pub style_html: Html, + style_html: Html, + title_element: Element, + counter: u32, +} + +pub enum AppMessage { + IncreaseCounter, } impl Component for App { - type Message = (); + type Message = AppMessage; type Properties = (); fn create(_ctx: &Context) -> Self { let document_head = gloo_utils::document() .head() .expect("head element to be present"); + let title_element = document_head + .query_selector("title") + .expect("to find a title element") + .expect("to find a title element"); + title_element.set_text_content(None); // Clear the title element let style_html = create_portal( html! { }, document_head.into(), ); - Self { style_html } + Self { + style_html, + title_element, + counter: 0, + } + } + + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + AppMessage::IncreaseCounter => self.counter += 1, + } + true } - fn view(&self, _ctx: &Context) -> Html { + fn view(&self, ctx: &Context) -> Html { + let onclick = ctx.link().callback(|_| AppMessage::IncreaseCounter); + let title = create_portal( + html! { + if self.counter > 0 { + {format!("Clicked {} times", self.counter)} + } else { + {"Yew • Portals"} + } + }, + self.title_element.clone(), + ); html! { <> {self.style_html.clone()} + {title}

{"This paragraph is colored red, and its style is mounted into "}

{"document.head"}
{" with a portal"}

- -

{"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}

-
+
+ +

{"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}

+ {"Buttons clicked inside the shadow dom work fine."} + +
+

{format!("The button has been clicked {} times. This is also reflected in the title of the tab!", self.counter)}

+
} } diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index ce95f74204a..575845f7c49 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -76,6 +76,14 @@ wasm-bindgen-futures = "0.4" rustversion = "1" trybuild = "1" +[dev-dependencies.web-sys] +version = "0.3" +features = [ + "ShadowRoot", + "ShadowRootInit", + "ShadowRootMode", +] + [features] ssr = ["futures", "html-escape"] csr = [] diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs index a5cc9ca4cd3..06ac4501263 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/app_handle.rs @@ -1,5 +1,6 @@ //! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope. +use crate::dom_bundle::BSubtree; use crate::html::Scoped; use crate::html::{IntoComponent, NodeRef, Scope}; use std::ops::Deref; @@ -22,14 +23,19 @@ where /// similarly to the `program` function in Elm. You should provide an initial model, `update` /// function which will update the state of the model and a `view` function which /// will render the model to a virtual DOM tree. - pub(crate) fn mount_with_props(element: Element, props: Rc) -> Self { - clear_element(&element); + pub(crate) fn mount_with_props(host: Element, props: Rc) -> Self { + clear_element(&host); let app = Self { scope: Scope::new(None), }; - - app.scope - .mount_in_place(element, NodeRef::default(), NodeRef::default(), props); + let hosting_root = BSubtree::create_root(&host); + app.scope.mount_in_place( + hosting_root, + host, + NodeRef::default(), + NodeRef::default(), + props, + ); app } @@ -52,8 +58,8 @@ where } /// Removes anything from the given element. -fn clear_element(element: &Element) { - while let Some(child) = element.last_child() { - element.remove_child(&child).expect("can't remove a child"); +fn clear_element(host: &Element) { + while let Some(child) = host.last_child() { + host.remove_child(&child).expect("can't remove a child"); } } diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index 4b071d2eee0..d23b90b0adf 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -1,8 +1,7 @@ //! This module contains the bundle implementation of a virtual component [BComp]. -use super::{BNode, Reconcilable, ReconcileTarget}; -use crate::html::AnyScope; -use crate::html::Scoped; +use super::{BNode, BSubtree, Reconcilable, ReconcileTarget}; +use crate::html::{AnyScope, Scoped}; use crate::virtual_dom::{Key, VComp}; use crate::NodeRef; use std::fmt; @@ -33,7 +32,7 @@ impl fmt::Debug for BComp { } impl ReconcileTarget for BComp { - fn detach(self, _parent: &Element, parent_to_detach: bool) { + fn detach(self, _root: &BSubtree, _parent: &Element, parent_to_detach: bool) { self.scope.destroy_boxed(parent_to_detach); } @@ -47,6 +46,7 @@ impl Reconcilable for VComp { fn attach( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -59,6 +59,7 @@ impl Reconcilable for VComp { } = self; let scope = mountable.mount( + root, node_ref.clone(), parent_scope, parent.to_owned(), @@ -78,6 +79,7 @@ impl Reconcilable for VComp { fn reconcile_node( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -88,14 +90,15 @@ impl Reconcilable for VComp { 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.reconcile(root, parent_scope, parent, next_sibling, bcomp) } - _ => self.replace(parent_scope, parent, next_sibling, bundle), + _ => self.replace(root, parent_scope, parent, next_sibling, bundle), } } fn reconcile( self, + _root: &BSubtree, _parent_scope: &AnyScope, _parent: &Element, next_sibling: NodeRef, @@ -165,22 +168,15 @@ mod tests { #[test] fn update_loop() { - let document = gloo_utils::document(); - let parent_scope: AnyScope = AnyScope::test(); - let parent_element = document.create_element("div").unwrap(); + let (root, scope, parent) = setup_parent(); let comp = html! { }; - let (_, mut bundle) = comp.attach(&parent_scope, &parent_element, NodeRef::default()); + let (_, mut bundle) = comp.attach(&root, &scope, &parent, NodeRef::default()); scheduler::start_now(); for _ in 0..10000 { let node = html! { }; - node.reconcile_node( - &parent_scope, - &parent_element, - NodeRef::default(), - &mut bundle, - ); + node.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut bundle); scheduler::start_now(); } } @@ -322,27 +318,28 @@ mod tests { } } - fn setup_parent() -> (AnyScope, Element) { + fn setup_parent() -> (BSubtree, AnyScope, Element) { let scope = AnyScope::test(); let parent = document().create_element("div").unwrap(); + let root = BSubtree::create_root(&parent); document().body().unwrap().append_child(&parent).unwrap(); - (scope, parent) + (root, scope, parent) } - fn get_html(node: Html, scope: &AnyScope, parent: &Element) -> String { + fn get_html(node: Html, root: &BSubtree, scope: &AnyScope, parent: &Element) -> String { // clear parent parent.set_inner_html(""); - node.attach(scope, parent, NodeRef::default()); + node.attach(root, scope, parent, NodeRef::default()); scheduler::start_now(); parent.inner_html() } #[test] fn all_ways_of_passing_children_work() { - let (scope, parent) = setup_parent(); + let (root, scope, parent) = setup_parent(); let children: Vec<_> = vec!["a", "b", "c"] .drain(..) @@ -359,7 +356,7 @@ mod tests { let prop_method = html! { }; - assert_eq!(get_html(prop_method, &scope, &parent), expected_html); + assert_eq!(get_html(prop_method, &root, &scope, &parent), expected_html); let children_renderer_method = html! { @@ -367,7 +364,7 @@ mod tests { }; assert_eq!( - get_html(children_renderer_method, &scope, &parent), + get_html(children_renderer_method, &root, &scope, &parent), expected_html ); @@ -376,30 +373,30 @@ mod tests { { children.clone() } }; - assert_eq!(get_html(direct_method, &scope, &parent), expected_html); + assert_eq!( + get_html(direct_method, &root, &scope, &parent), + expected_html + ); let for_method = html! { { for children } }; - assert_eq!(get_html(for_method, &scope, &parent), expected_html); + assert_eq!(get_html(for_method, &root, &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 (root, scope, parent) = setup_parent(); let node_ref = NodeRef::default(); let elem = html! { }; - let (_, elem) = elem.attach(&scope, &parent, NodeRef::default()); + let (_, elem) = elem.attach(&root, &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); + elem.detach(&root, &parent, false); scheduler::start_now(); assert!(node_ref.get().is_none()); } diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index edd1cfc7605..fc0caf9201a 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -1,5 +1,5 @@ //! This module contains fragments bundles, a [BList] -use super::{test_log, BNode}; +use super::{test_log, BNode, BSubtree}; use crate::dom_bundle::{Reconcilable, ReconcileTarget}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::{Key, VList, VNode, VText}; @@ -31,6 +31,7 @@ impl Deref for BList { /// Helper struct, that keeps the position where the next element is to be placed at #[derive(Clone)] struct NodeWriter<'s> { + root: &'s BSubtree, parent_scope: &'s AnyScope, parent: &'s Element, next_sibling: NodeRef, @@ -45,7 +46,8 @@ impl<'s> NodeWriter<'s> { self.parent.outer_html(), self.next_sibling ); - let (next, bundle) = node.attach(self.parent_scope, self.parent, self.next_sibling); + let (next, bundle) = + node.attach(self.root, self.parent_scope, self.parent, self.next_sibling); test_log!(" next_position: {:?}", next); ( Self { @@ -70,7 +72,13 @@ impl<'s> NodeWriter<'s> { 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); + let next = node.reconcile_node( + self.root, + self.parent_scope, + self.parent, + self.next_sibling, + bundle, + ); test_log!(" next_position: {:?}", next); Self { next_sibling: next, @@ -135,6 +143,7 @@ impl BList { /// Diff and patch unkeyed child lists fn apply_unkeyed( + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -142,6 +151,7 @@ impl BList { rights: &mut Vec, ) -> NodeRef { let mut writer = NodeWriter { + root, parent_scope, parent, next_sibling, @@ -151,7 +161,7 @@ impl BList { if lefts.len() < rights.len() { for r in rights.drain(lefts.len()..) { test_log!("removing: {:?}", r); - r.detach(parent, false); + r.detach(root, parent, false); } } @@ -174,6 +184,7 @@ impl BList { /// Optimized for node addition or removal from either end of the list and small changes in the /// middle. fn apply_keyed( + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -204,6 +215,7 @@ impl BList { if matching_len_end == std::cmp::min(left_vdoms.len(), rev_bundles.len()) { // No key changes return Self::apply_unkeyed( + root, parent_scope, parent, next_sibling, @@ -215,6 +227,7 @@ impl BList { // We partially drain the new vnodes in several steps. let mut lefts = left_vdoms; let mut writer = NodeWriter { + root, parent_scope, parent, next_sibling, @@ -336,7 +349,7 @@ impl BList { // Step 2.3. Remove any extra rights for KeyedEntry(_, r) in spare_bundles.drain() { test_log!("removing: {:?}", r); - r.detach(parent, false); + r.detach(root, parent, false); } // Step 3. Diff matching children at the start @@ -354,9 +367,9 @@ impl BList { } impl ReconcileTarget for BList { - fn detach(self, parent: &Element, parent_to_detach: bool) { + fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) { for child in self.rev_children.into_iter() { - child.detach(parent, parent_to_detach); + child.detach(root, parent, parent_to_detach); } } @@ -372,30 +385,33 @@ impl Reconcilable for VList { fn attach( self, + root: &BSubtree, 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_); + let node_ref = self.reconcile(root, parent_scope, parent, next_sibling, &mut self_); (node_ref, self_) } fn reconcile_node( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, bundle: &mut BNode, ) -> NodeRef { - // 'Forcefully' create a pretend the existing node is a list. Creates a + // 'Forcefully' 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) + self.reconcile(root, parent_scope, parent, next_sibling, blist) } fn reconcile( mut self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -426,9 +442,9 @@ impl Reconcilable for VList { rights.reserve_exact(additional); } let first = if self.fully_keyed && blist.fully_keyed { - BList::apply_keyed(parent_scope, parent, next_sibling, lefts, rights) + BList::apply_keyed(root, parent_scope, parent, next_sibling, lefts, rights) } else { - BList::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights) + BList::apply_unkeyed(root, parent_scope, parent, next_sibling, lefts, rights) }; blist.fully_keyed = self.fully_keyed; blist.key = self.key; diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 2401ea6f27f..f04928da189 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -1,6 +1,6 @@ //! This module contains the bundle version of an abstract node [BNode] -use super::{BComp, BList, BPortal, BSuspense, BTag, BText}; +use super::{BComp, BList, BPortal, BSubtree, BSuspense, BTag, BText}; use crate::dom_bundle::{Reconcilable, ReconcileTarget}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::{Key, VNode}; @@ -43,20 +43,20 @@ impl BNode { impl ReconcileTarget for BNode { /// Remove VNode from parent. - fn detach(self, parent: &Element, parent_to_detach: bool) { + fn detach(self, root: &BSubtree, 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::Tag(vtag) => vtag.detach(root, parent, parent_to_detach), + Self::Text(btext) => btext.detach(root, parent, parent_to_detach), + Self::Comp(bsusp) => bsusp.detach(root, parent, parent_to_detach), + Self::List(blist) => blist.detach(root, 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), + Self::Portal(bportal) => bportal.detach(root, parent, parent_to_detach), + Self::Suspense(bsusp) => bsusp.detach(root, parent, parent_to_detach), } } @@ -82,25 +82,26 @@ impl Reconcilable for VNode { fn attach( self, + root: &BSubtree, 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); + let (node_ref, tag) = vtag.attach(root, parent_scope, parent, next_sibling); (node_ref, tag.into()) } VNode::VText(vtext) => { - let (node_ref, text) = vtext.attach(parent_scope, parent, next_sibling); + let (node_ref, text) = vtext.attach(root, parent_scope, parent, next_sibling); (node_ref, text.into()) } VNode::VComp(vcomp) => { - let (node_ref, comp) = vcomp.attach(parent_scope, parent, next_sibling); + let (node_ref, comp) = vcomp.attach(root, parent_scope, parent, next_sibling); (node_ref, comp.into()) } VNode::VList(vlist) => { - let (node_ref, list) = vlist.attach(parent_scope, parent, next_sibling); + let (node_ref, list) = vlist.attach(root, parent_scope, parent, next_sibling); (node_ref, list.into()) } VNode::VRef(node) => { @@ -108,11 +109,12 @@ impl Reconcilable for VNode { (NodeRef::new(node.clone()), BNode::Ref(node)) } VNode::VPortal(vportal) => { - let (node_ref, portal) = vportal.attach(parent_scope, parent, next_sibling); + let (node_ref, portal) = vportal.attach(root, parent_scope, parent, next_sibling); (node_ref, portal.into()) } VNode::VSuspense(vsuspsense) => { - let (node_ref, suspsense) = vsuspsense.attach(parent_scope, parent, next_sibling); + let (node_ref, suspsense) = + vsuspsense.attach(root, parent_scope, parent, next_sibling); (node_ref, suspsense.into()) } } @@ -120,31 +122,42 @@ impl Reconcilable for VNode { fn reconcile_node( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, bundle: &mut BNode, ) -> NodeRef { - self.reconcile(parent_scope, parent, next_sibling, bundle) + self.reconcile(root, parent_scope, parent, next_sibling, bundle) } fn reconcile( self, + root: &BSubtree, 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::VTag(vtag) => { + vtag.reconcile_node(root, parent_scope, parent, next_sibling, bundle) + } + VNode::VText(vtext) => { + vtext.reconcile_node(root, parent_scope, parent, next_sibling, bundle) + } + VNode::VComp(vcomp) => { + vcomp.reconcile_node(root, parent_scope, parent, next_sibling, bundle) + } + VNode::VList(vlist) => { + vlist.reconcile_node(root, 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( + root, parent_scope, parent, next_sibling, @@ -155,10 +168,10 @@ impl Reconcilable for VNode { NodeRef::new(node) } VNode::VPortal(vportal) => { - vportal.reconcile_node(parent_scope, parent, next_sibling, bundle) + vportal.reconcile_node(root, parent_scope, parent, next_sibling, bundle) } VNode::VSuspense(vsuspsense) => { - vsuspsense.reconcile_node(parent_scope, parent, next_sibling, bundle) + vsuspsense.reconcile_node(root, parent_scope, parent, next_sibling, bundle) } } } diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index 65fe4088a58..155f37c60ae 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -1,7 +1,6 @@ //! This module contains the bundle implementation of a portal [BPortal]. -use super::test_log; -use super::BNode; +use super::{test_log, BNode, BSubtree}; use crate::dom_bundle::{Reconcilable, ReconcileTarget}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::Key; @@ -10,7 +9,9 @@ use web_sys::Element; /// The bundle implementation to [VPortal]. #[derive(Debug)] -pub(super) struct BPortal { +pub struct BPortal { + // The inner root + inner_root: BSubtree, /// The element under which the content is inserted. host: Element, /// The next sibling after the inserted content @@ -20,10 +21,9 @@ pub(super) struct BPortal { } impl ReconcileTarget 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 detach(self, _root: &BSubtree, _parent: &Element, _parent_to_detach: bool) { + test_log!("Detaching portal from host",); + self.node.detach(&self.inner_root, &self.host, false); } fn shift(&self, _next_parent: &Element, _next_sibling: NodeRef) { @@ -36,8 +36,9 @@ impl Reconcilable for VPortal { fn attach( self, + root: &BSubtree, parent_scope: &AnyScope, - _parent: &Element, + parent: &Element, host_next_sibling: NodeRef, ) -> (NodeRef, Self::Bundle) { let Self { @@ -45,10 +46,12 @@ impl Reconcilable for VPortal { inner_sibling, node, } = self; - let (_, inner) = node.attach(parent_scope, &host, inner_sibling.clone()); + let inner_root = root.create_subroot(parent.clone(), &host); + let (_, inner) = node.attach(&inner_root, parent_scope, &host, inner_sibling.clone()); ( host_next_sibling, BPortal { + inner_root, host, node: Box::new(inner), inner_sibling, @@ -58,19 +61,23 @@ impl Reconcilable for VPortal { fn reconcile_node( self, + root: &BSubtree, 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), + BNode::Portal(portal) => { + self.reconcile(root, parent_scope, parent, next_sibling, portal) + } + _ => self.replace(root, parent_scope, parent, next_sibling, bundle), } } fn reconcile( self, + _root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -88,11 +95,16 @@ impl Reconcilable for VPortal { 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()); + let inner_sibling = portal.inner_sibling.clone(); + portal.node.shift(&portal.host, inner_sibling); } - node.reconcile_node(parent_scope, parent, next_sibling.clone(), &mut portal.node); + node.reconcile_node( + &portal.inner_root, + parent_scope, + parent, + next_sibling.clone(), + &mut portal.node, + ); next_sibling } } diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 18d2a2b152f..d653640d476 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -1,6 +1,6 @@ //! This module contains the bundle version of a supsense [BSuspense] -use super::{BNode, Reconcilable, ReconcileTarget}; +use super::{BNode, BSubtree, Reconcilable, ReconcileTarget}; use crate::html::AnyScope; use crate::virtual_dom::{Key, VSuspense}; use crate::NodeRef; @@ -31,12 +31,13 @@ impl BSuspense { } impl ReconcileTarget for BSuspense { - fn detach(self, parent: &Element, parent_to_detach: bool) { + fn detach(self, root: &BSubtree, 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); + fallback.detach(root, parent, parent_to_detach); + self.children_bundle + .detach(root, &self.detached_parent, false); } else { - self.children_bundle.detach(parent, parent_to_detach); + self.children_bundle.detach(root, parent, parent_to_detach); } } @@ -50,6 +51,7 @@ impl Reconcilable for VSuspense { fn attach( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -68,8 +70,9 @@ impl Reconcilable for VSuspense { // 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); + children.attach(root, parent_scope, &detached_parent, NodeRef::default()); + let (fallback_ref, fallback) = + fallback.attach(root, parent_scope, parent, next_sibling); ( fallback_ref, BSuspense { @@ -80,7 +83,8 @@ impl Reconcilable for VSuspense { }, ) } else { - let (child_ref, children_bundle) = children.attach(parent_scope, parent, next_sibling); + let (child_ref, children_bundle) = + children.attach(root, parent_scope, parent, next_sibling); ( child_ref, BSuspense { @@ -95,6 +99,7 @@ impl Reconcilable for VSuspense { fn reconcile_node( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -103,14 +108,15 @@ impl Reconcilable for VSuspense { match bundle { // We only preserve the child state if they are the same suspense. BNode::Suspense(m) if m.key == self.key => { - self.reconcile(parent_scope, parent, next_sibling, m) + self.reconcile(root, parent_scope, parent, next_sibling, m) } - _ => self.replace(parent_scope, parent, next_sibling, bundle), + _ => self.replace(root, parent_scope, parent, next_sibling, bundle), } } fn reconcile( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -132,30 +138,33 @@ impl Reconcilable for VSuspense { // Both suspended, reconcile children into detached_parent, fallback into the DOM (true, Some(fallback_bundle)) => { children.reconcile_node( + root, parent_scope, &suspense.detached_parent, NodeRef::default(), children_bundle, ); - fallback.reconcile_node(parent_scope, parent, next_sibling, fallback_bundle) + fallback.reconcile_node(root, 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) + children.reconcile_node(root, 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(&suspense.detached_parent, NodeRef::default()); children.reconcile_node( + root, parent_scope, &suspense.detached_parent, NodeRef::default(), children_bundle, ); // first render of fallback - let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling); + let (fallback_ref, fallback) = + fallback.attach(root, parent_scope, parent, next_sibling); suspense.fallback_bundle = Some(fallback); fallback_ref } @@ -165,10 +174,10 @@ impl Reconcilable for VSuspense { .fallback_bundle .take() .unwrap() // We just matched Some(_) - .detach(parent, false); + .detach(root, parent, false); children_bundle.shift(parent, next_sibling.clone()); - children.reconcile_node(parent_scope, parent, next_sibling, children_bundle) + children.reconcile_node(root, 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 index 761d74986ed..9d5ded00c37 100644 --- a/packages/yew/src/dom_bundle/btag/attributes.rs +++ b/packages/yew/src/dom_bundle/btag/attributes.rs @@ -1,4 +1,5 @@ use super::Apply; +use crate::dom_bundle::BSubtree; use crate::virtual_dom::vtag::{InputFields, Value}; use crate::virtual_dom::Attributes; use indexmap::IndexMap; @@ -11,14 +12,14 @@ impl Apply for Value { type Element = T; type Bundle = Self; - fn apply(self, el: &Self::Element) -> Self { + fn apply(self, _root: &BSubtree, 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) { + fn apply_diff(self, _root: &BSubtree, el: &Self::Element, bundle: &mut Self) { match (self.deref(), (*bundle).deref()) { (Some(new), Some(_)) => { // Refresh value from the DOM. It might have changed. @@ -62,21 +63,21 @@ impl Apply for InputFields { type Element = InputElement; type Bundle = Self; - fn apply(mut self, el: &Self::Element) -> Self { + fn apply(mut self, root: &BSubtree, 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.value = self.value.apply(root, el); self } - fn apply_diff(self, el: &Self::Element, bundle: &mut Self) { + fn apply_diff(self, root: &BSubtree, 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); + self.value.apply_diff(root, el, &mut bundle.value); } } @@ -186,7 +187,7 @@ impl Apply for Attributes { type Element = Element; type Bundle = Self; - fn apply(self, el: &Element) -> Self { + fn apply(self, _root: &BSubtree, el: &Element) -> Self { match &self { Self::Static(arr) => { for kv in arr.iter() { @@ -209,7 +210,7 @@ impl Apply for Attributes { self } - fn apply_diff(self, el: &Element, bundle: &mut Self) { + fn apply_diff(self, _root: &BSubtree, el: &Element, bundle: &mut Self) { #[inline] fn ptr_eq(a: &[T], b: &[T]) -> bool { std::ptr::eq(a, b) diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs index 687e92106ef..d5676316d86 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -1,42 +1,38 @@ use super::Apply; -use crate::dom_bundle::test_log; -use crate::virtual_dom::{Listener, ListenerKind, Listeners}; -use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase}; +use crate::dom_bundle::{test_log, BSubtree, EventDescriptor}; +use crate::virtual_dom::{Listener, Listeners}; +use ::wasm_bindgen::{prelude::wasm_bindgen, JsCast}; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; 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(); +use web_sys::{Element, Event, EventTarget as HtmlEventTarget}; + +#[wasm_bindgen] +extern "C" { + // Duck-typing, not a real class on js-side. On rust-side, use impls of EventTarget below + type EventTargetable; + #[wasm_bindgen(method, getter = __yew_listener_id, structural)] + fn listener_id(this: &EventTargetable) -> Option; + #[wasm_bindgen(method, setter = __yew_listener_id, structural)] + fn set_listener_id(this: &EventTargetable, id: u32); +} - /// Cached reference to the document body - static BODY: web_sys::HtmlElement = gloo_utils::document().body().unwrap(); +/// DOM-Types that can have listeners registered on them. +/// Uses the duck-typed interface from above in impls. +pub trait EventListening { + fn listener_id(&self) -> Option; + fn set_listener_id(&self, id: u32); } -/// 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. -#[cfg_attr(documenting, doc(cfg(feature = "render")))] -pub fn set_event_bubbling(bubble: bool) { - BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); +impl EventListening for Element { + fn listener_id(&self) -> Option { + self.unchecked_ref::().listener_id() + } + + fn set_listener_id(&self, id: u32) { + self.unchecked_ref::().set_listener_id(id); + } } /// An active set of listeners on an element @@ -52,14 +48,14 @@ impl Apply for Listeners { type Element = Element; type Bundle = ListenerRegistration; - fn apply(self, el: &Self::Element) -> ListenerRegistration { + fn apply(self, root: &BSubtree, el: &Self::Element) -> ListenerRegistration { match self { - Self::Pending(pending) => ListenerRegistration::register(el, &pending), + Self::Pending(pending) => ListenerRegistration::register(root, el, &pending), Self::None => ListenerRegistration::NoReg, } } - fn apply_diff(self, el: &Self::Element, bundle: &mut ListenerRegistration) { + fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut ListenerRegistration) { use ListenerRegistration::*; use Listeners::*; @@ -67,10 +63,10 @@ impl Apply for Listeners { (Pending(pending), Registered(ref id)) => { // Reuse the ID test_log!("reusing listeners for {}", id); - Registry::with(|reg| reg.patch(id, &*pending)); + root.with_listener_registry(|reg| reg.patch(root, id, &*pending)); } (Pending(pending), bundle @ NoReg) => { - *bundle = ListenerRegistration::register(el, &pending); + *bundle = ListenerRegistration::register(root, el, &pending); test_log!( "registering listeners for {}", match bundle { @@ -85,7 +81,7 @@ impl Apply for Listeners { _ => unreachable!(), }; test_log!("unregistering listeners for {}", id); - Registry::with(|reg| reg.unregister(id)); + root.with_listener_registry(|reg| reg.unregister(id)); *bundle = NoReg; } (None, NoReg) => { @@ -97,116 +93,75 @@ impl Apply for 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); + fn register(root: &BSubtree, el: &Element, pending: &[Option>]) -> Self { + Self::Registered(root.with_listener_registry(|reg| { + let id = reg.set_listener_id(root, el); + reg.register(root, id, pending); id })) } /// Remove any registered event listeners from the global registry - pub fn unregister(&self) { + pub fn unregister(&self, root: &BSubtree) { 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); + root.with_listener_registry(|r| r.unregister(id)); } } } /// Global multiplexing event handler registry -#[derive(Default, Debug)] -struct Registry { +#[derive(Debug)] +pub 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())) + pub fn new() -> Self { + Self { + id_counter: u32::default(), + by_id: HashMap::default(), + } + } + + /// Handle a single event, given the listening element and event descriptor. + pub fn get_handler( + registry: &RefCell, + listening: &dyn EventListening, + desc: &EventDescriptor, + ) -> Option { + // The tricky part is that we want to drop the reference to the registry before + // calling any actual listeners (since that might end up running lifecycle methods + // and modify the registry). So we clone the current listeners and return a closure + let listener_id = listening.listener_id()?; + let registry_ref = registry.borrow(); + let handlers = registry_ref.by_id.get(&listener_id)?; + let listeners = handlers.get(desc)?.clone(); + drop(registry_ref); // unborrow the registry, before running any listeners + Some(move |event: &Event| { + for l in listeners { + l.handle(event.clone()); + } + }) } /// Register all passed listeners under ID - fn register(&mut self, id: u32, listeners: &[Option>]) { + fn register(&mut self, root: &BSubtree, 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()); + root.ensure_handled(&desc); by_desc.entry(desc).or_default().push(l); } self.by_id.insert(id, by_desc); } /// Patch an already registered set of handlers - fn patch(&mut self, id: &u32, listeners: &[Option>]) { + fn patch(&mut self, root: &BSubtree, 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() { @@ -215,7 +170,7 @@ impl Registry { for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() { let desc = EventDescriptor::from(l.deref()); - self.global.ensure_handled(desc.clone()); + root.ensure_handled(&desc); by_desc.entry(desc).or_default().push(l); } } @@ -227,76 +182,30 @@ impl Registry { } /// Set unique listener ID onto element and return it - fn set_listener_id(&mut self, el: &Element) -> u32 { + fn set_listener_id(&mut self, root: &BSubtree, 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"); - } - }); + root.brand_element(el as &HtmlEventTarget); + el.set_listener_id(id); 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"))] +#[cfg(feature = "wasm_test")] +#[cfg(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}; + use web_sys::{Event, EventInit, HtmlElement, MouseEvent}; wasm_bindgen_test_configure!(run_in_browser); - use crate::{html, html::TargetCast, scheduler, AppHandle, Component, Context, Html}; + use crate::{ + create_portal, html, html::TargetCast, scheduler, virtual_dom::VNode, AppHandle, Component, + Context, Html, NodeRef, Properties, + }; use gloo_utils::document; use wasm_bindgen::JsCast; use yew::Callback; @@ -315,29 +224,16 @@ mod tests { text: String, } - trait Mixin { + #[derive(Default, PartialEq, Properties)] + struct MixinProps { + state_ref: NodeRef, + wrapped: M, + } + + trait Mixin: Properties + Sized { 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} - - } - } - } + C: Component>; } struct Comp @@ -350,10 +246,10 @@ mod tests { impl Component for Comp where - M: Mixin + 'static, + M: Mixin + Properties + 'static, { type Message = Message; - type Properties = (); + type Properties = MixinProps; fn create(_: &Context) -> Self { Comp { @@ -382,68 +278,103 @@ mod tests { } } - fn assert_count(el: &web_sys::HtmlElement, count: isize) { - assert_eq!(el.text_content(), Some(count.to_string())) + #[track_caller] + fn assert_count(el: &NodeRef, count: isize) { + let text = el + .get() + .expect("State ref not bound in the test case?") + .text_content(); + assert_eq!(text, Some(count.to_string())) + } + + #[track_caller] + fn click(el: &NodeRef) { + el.get().unwrap().dyn_into::().unwrap().click(); + scheduler::start_now(); } - fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement { + fn get_el_by_selector(selector: &str) -> web_sys::HtmlElement { document() - .query_selector(tag) + .query_selector(selector) .unwrap() .unwrap() .dyn_into::() .unwrap() } - fn init(tag: &str) -> (AppHandle>, web_sys::HtmlElement) + fn init() -> (AppHandle>, NodeRef) where - M: Mixin, + M: Mixin + Properties + Default, { - // 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(); + // Remove any existing elements + let body = document().body().unwrap(); + while let Some(child) = body.query_selector("div#testroot").unwrap() { + body.remove_child(&child).unwrap(); } let root = document().create_element("div").unwrap(); - document().body().unwrap().append_child(&root).unwrap(); - let app = crate::Renderer::>::with_root(root).render(); + root.set_id("testroot"); + body.append_child(&root).unwrap(); + let props = as Component>::Properties::default(); + let el_ref = props.state_ref.clone(); + let app = crate::Renderer::>::with_root_and_props(root, props).render(); scheduler::start_now(); - (app, get_el_by_tag(tag)) + (app, el_ref) } #[test] fn synchronous() { + #[derive(Default, PartialEq, Properties)] struct Synchronous; - impl Mixin for Synchronous {} + impl Mixin for Synchronous { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component>, + { + let onclick = ctx.link().callback(|_| Message::Action); + + if state.stop_listening { + html! { + {state.action} + } + } else { + html! { + + {state.action} + + } + } + } + } - let (link, el) = init::("a"); + let (link, el) = init::(); assert_count(&el, 0); - el.click(); + click(&el); assert_count(&el, 1); - el.click(); + click(&el); assert_count(&el, 2); link.send_message(Message::StopListening); scheduler::start_now(); - el.click(); + click(&el); assert_count(&el, 2); } #[test] async fn non_bubbling_event() { + #[derive(Default, PartialEq, Properties)] struct NonBubbling; impl Mixin for NonBubbling { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + C: Component>, { let link = ctx.link().clone(); let onblur = Callback::from(move |_| { @@ -452,7 +383,7 @@ mod tests { }); html! {
- + {state.action} @@ -461,7 +392,7 @@ mod tests { } } - let (_, el) = init::("a"); + let (_, el) = init::(); assert_count(&el, 0); @@ -483,30 +414,27 @@ mod tests { #[test] fn bubbling() { + #[derive(Default, PartialEq, Properties)] struct Bubbling; impl Mixin for Bubbling { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + 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(); - }); + let cb = ctx.link().callback(|_| Message::Action); html! { @@ -515,47 +443,39 @@ mod tests { } } - let (link, el) = init::("a"); + let (link, el) = init::(); assert_count(&el, 0); - - el.click(); + click(&el); assert_count(&el, 2); - - el.click(); + click(&el); assert_count(&el, 4); link.send_message(Message::StopListening); scheduler::start_now(); - el.click(); + click(&el); assert_count(&el, 4); } #[test] fn cancel_bubbling() { + #[derive(Default, PartialEq, Properties)] struct CancelBubbling; impl Mixin for CancelBubbling { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + 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| { + let onclick = ctx.link().callback(|_| Message::Action); + let onclick2 = ctx.link().callback(|e: MouseEvent| { e.stop_propagation(); - link.send_message(Message::Action); - scheduler::start_now(); + Message::Action }); html! { @@ -563,14 +483,12 @@ mod tests { } } - let (_, el) = init::("a"); + let (_, el) = init::(); assert_count(&el, 0); - - el.click(); + click(&el); assert_count(&el, 1); - - el.click(); + click(&el); assert_count(&el, 2); } @@ -579,29 +497,23 @@ mod tests { // 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. + #[derive(Default, PartialEq, Properties)] struct CancelBubbling; impl Mixin for CancelBubbling { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + 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| { + let onclick = ctx.link().callback(|_| Message::Action); + let onclick2 = ctx.link().callback(|e: MouseEvent| { e.stop_propagation(); - link.send_message(Message::Action); - scheduler::start_now(); + Message::Action }); html! {
@@ -610,65 +522,153 @@ mod tests { } } - let (_, el) = init::("a"); + let (_, el) = init::(); assert_count(&el, 0); + click(&el); + assert_count(&el, 1); + click(&el); + assert_count(&el, 2); + } - el.click(); + /// Here an event is being delivered to a DOM node which is contained + /// in a portal. It should bubble through the portal and reach the containing + /// element. + #[test] + fn portal_bubbling() { + #[derive(PartialEq, Properties)] + struct PortalBubbling { + host: web_sys::Element, + } + impl Default for PortalBubbling { + fn default() -> Self { + let host = document().create_element("div").unwrap(); + PortalBubbling { host } + } + } + + impl Mixin for PortalBubbling { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component>, + { + let portal_target = ctx.props().wrapped.host.clone(); + let onclick = ctx.link().callback(|_| Message::Action); + html! { + <> +
+ {create_portal(html! { + + {state.action} + + }, portal_target.clone())} +
+ {VNode::VRef(portal_target.into())} + + } + } + } + + let (_, el) = init::(); + + assert_count(&el, 0); + click(&el); assert_count(&el, 1); + } - el.click(); - assert_count(&el, 2); + /// Here an event is being from inside a shadow root. It should only be caught exactly once on each handler + #[test] + fn open_shadow_dom_bubbling() { + use web_sys::{ShadowRootInit, ShadowRootMode}; + #[derive(PartialEq, Properties)] + struct OpenShadowDom { + host: web_sys::Element, + inner_root: web_sys::Element, + } + impl Default for OpenShadowDom { + fn default() -> Self { + let host = document().create_element("div").unwrap(); + let inner_root = document().create_element("div").unwrap(); + let shadow = host + .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open)) + .unwrap(); + shadow.append_child(&inner_root).unwrap(); + OpenShadowDom { host, inner_root } + } + } + impl Mixin for OpenShadowDom { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component>, + { + let onclick = ctx.link().callback(|_| Message::Action); + let mixin = &ctx.props().wrapped; + html! { +
+
+ {create_portal(html! { + + {state.action} + + }, mixin.inner_root.clone())} +
+ {VNode::VRef(mixin.host.clone().into())} +
+ } + } + } + let (_, el) = init::(); + + assert_count(&el, 0); + click(&el); + assert_count(&el, 2); // Once caught per handler } fn test_input_listener(make_event: impl Fn() -> E) where - E: JsCast + std::fmt::Debug, + E: Into + std::fmt::Debug, { + #[derive(Default, PartialEq, Properties)] struct Input; impl Mixin for Input { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + C: Component>, { if state.stop_listening { html! {
-

{state.text.clone()}

+

{state.text.clone()}

} } else { - let link = ctx.link().clone(); - let onchange = Callback::from(move |e: web_sys::Event| { + let onchange = ctx.link().callback(|e: web_sys::Event| { let el: web_sys::HtmlInputElement = e.target_unchecked_into(); - link.send_message(Message::SetText(el.value())); - scheduler::start_now(); + Message::SetText(el.value()) }); - - let link = ctx.link().clone(); - let oninput = Callback::from(move |e: web_sys::InputEvent| { + let oninput = ctx.link().callback(|e: web_sys::InputEvent| { let el: web_sys::HtmlInputElement = e.target_unchecked_into(); - link.send_message(Message::SetText(el.value())); - scheduler::start_now(); + Message::SetText(el.value()) }); html! {
-

{state.text.clone()}

+

{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"); + let (link, state_ref) = init::(); + let input_el = get_el_by_selector("input") + .dyn_into::() + .unwrap(); - assert_eq!(&p_el.text_content().unwrap(), ""); + assert_eq!(&state_ref.get().unwrap().text_content().unwrap(), ""); for mut s in ["foo", "bar", "baz"].iter() { input_el.set_value(s); if s == &"baz" { @@ -677,12 +677,9 @@ mod tests { s = &"bar"; } - input_el - .dyn_ref::() - .unwrap() - .dispatch_event(&make_event().dyn_into().unwrap()) - .unwrap(); - assert_eq!(&p_el.text_content().unwrap(), s); + input_el.dispatch_event(&make_event().into()).unwrap(); + scheduler::start_now(); + assert_eq!(&state_ref.get().unwrap().text_content().unwrap(), s); } } diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index ed902baa1f6..c7a63f38849 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -3,9 +3,9 @@ mod attributes; mod listeners; -pub use listeners::set_event_bubbling; +pub use listeners::Registry; -use super::{insert_node, BList, BNode, Reconcilable, ReconcileTarget}; +use super::{insert_node, BList, BNode, BSubtree, Reconcilable, ReconcileTarget}; use crate::html::AnyScope; use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE}; use crate::virtual_dom::{Attributes, Key, VTag}; @@ -25,10 +25,10 @@ trait Apply { type Bundle; /// Apply contained values to [Element](Self::Element) with no ancestor - fn apply(self, el: &Self::Element) -> Self::Bundle; + fn apply(self, root: &BSubtree, 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); + fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut Self::Bundle); } /// [BTag] fields that are specific to different [BTag] kinds. @@ -69,14 +69,14 @@ pub(super) struct BTag { } impl ReconcileTarget for BTag { - fn detach(self, parent: &Element, parent_to_detach: bool) { - self.listeners.unregister(); + fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) { + self.listeners.unregister(root); 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); + child_bundle.detach(root, &node, true); } if !parent_to_detach { let result = parent.remove_child(&node); @@ -104,6 +104,7 @@ impl Reconcilable for VTag { fn attach( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -118,20 +119,21 @@ impl Reconcilable for VTag { } = self; insert_node(&el, parent, next_sibling.get().as_ref()); - let attributes = attributes.apply(&el); - let listeners = listeners.apply(&el); + let attributes = attributes.apply(root, &el); + let listeners = listeners.apply(root, &el); let inner = match self.inner { VTagInner::Input(f) => { - let f = f.apply(el.unchecked_ref()); + let f = f.apply(root, el.unchecked_ref()); BTagInner::Input(f) } VTagInner::Textarea { value } => { - let value = value.apply(el.unchecked_ref()); + let value = value.apply(root, el.unchecked_ref()); BTagInner::Textarea { value } } VTagInner::Other { children, tag } => { - let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default()); + let (_, child_bundle) = + children.attach(root, parent_scope, &el, NodeRef::default()); BTagInner::Other { child_bundle, tag } } }; @@ -151,6 +153,7 @@ impl Reconcilable for VTag { fn reconcile_node( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -173,31 +176,38 @@ impl Reconcilable for VTag { } _ => false, } { - return self.reconcile(parent_scope, parent, next_sibling, ex.deref_mut()); + return self.reconcile( + root, + parent_scope, + parent, + next_sibling, + ex.deref_mut(), + ); } } _ => {} }; - self.replace(parent_scope, parent, next_sibling, bundle) + self.replace(root, parent_scope, parent, next_sibling, bundle) } fn reconcile( self, + root: &BSubtree, 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); + self.attributes.apply_diff(root, el, &mut tag.attributes); + self.listeners.apply_diff(root, el, &mut tag.listeners); match (self.inner, &mut tag.inner) { (VTagInner::Input(new), BTagInner::Input(old)) => { - new.apply_diff(el.unchecked_ref(), old); + new.apply_diff(root, el.unchecked_ref(), old); } (VTagInner::Textarea { value: new }, BTagInner::Textarea { value: old }) => { - new.apply_diff(el.unchecked_ref(), old); + new.apply_diff(root, el.unchecked_ref(), old); } ( VTagInner::Other { children: new, .. }, @@ -205,7 +215,7 @@ impl Reconcilable for VTag { child_bundle: old, .. }, ) => { - new.reconcile(parent_scope, el, NodeRef::default(), old); + new.reconcile(root, parent_scope, el, NodeRef::default(), old); } // Can not happen, because we checked for tag equability above _ => unsafe { unreachable_unchecked() }, @@ -295,8 +305,14 @@ mod tests { wasm_bindgen_test_configure!(run_in_browser); - fn test_scope() -> AnyScope { - AnyScope::test() + fn setup_parent() -> (BSubtree, AnyScope, Element) { + let scope = AnyScope::test(); + let parent = document().create_element("div").unwrap(); + let root = BSubtree::create_root(&parent); + + document().body().unwrap().append_child(&parent).unwrap(); + + (root, scope, parent) } #[test] @@ -475,10 +491,9 @@ mod tests { #[test] fn supports_svg() { + let (root, scope, parent) = setup_parent(); 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(); @@ -488,17 +503,17 @@ mod tests { let svg_node = html! { {path_node} }; let svg_tag = assert_vtag(svg_node); - let (_, svg_tag) = svg_tag.attach(&scope, &div_el, NodeRef::default()); + let (_, svg_tag) = svg_tag.attach(&root, &scope, &parent, 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()); + let (_, g_tag) = g_tag.attach(&root, &scope, &parent, 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()); + let (_, g_tag) = g_tag.attach(&root, &scope, &svg_el, NodeRef::default()); assert_namespace(&g_tag, SVG_NAMESPACE); } @@ -594,26 +609,20 @@ mod tests { #[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 (root, scope, parent) = setup_parent(); let elem = html! {
}; - let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem.attach(&root, &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 (root, scope, parent) = setup_parent(); let elem = gen_html(); - let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); let vtag = assert_btag_mut(&mut elem); // test if the className has been set assert!(vtag.reference().has_attribute("class")); @@ -631,16 +640,13 @@ mod tests { #[test] fn controlled_input_synced() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); + let (root, scope, parent) = setup_parent(); let expected = "not_changed_value"; // Initial state let elem = html! { }; - let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); let vtag = assert_btag_ref(&elem); // User input @@ -652,7 +658,7 @@ mod tests { let elem_vtag = assert_vtag(next_elem); // Sync happens here - elem_vtag.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); + elem_vtag.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); let vtag = assert_btag_ref(&elem); // Get new current value of the input element @@ -667,14 +673,11 @@ mod tests { #[test] fn uncontrolled_input_unsynced() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); + let (root, scope, parent) = setup_parent(); // Initial state let elem = html! { }; - let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); let vtag = assert_btag_ref(&elem); // User input @@ -686,7 +689,7 @@ mod tests { let elem_vtag = assert_vtag(next_elem); // Value should not be refreshed - elem_vtag.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); + elem_vtag.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); let vtag = assert_btag_ref(&elem); // Get user value of the input element @@ -705,10 +708,7 @@ mod tests { #[test] fn dynamic_tags_work() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); + let (root, scope, parent) = setup_parent(); let elem = html! { <@{ let mut builder = String::new(); @@ -716,7 +716,7 @@ mod tests { builder }/> }; - let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); + let (_, mut elem) = elem.attach(&root, &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"); @@ -758,36 +758,31 @@ mod tests { #[test] fn reset_node_ref() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); + let (root, scope, parent) = setup_parent(); let node_ref = NodeRef::default(); let elem: VNode = html! {
}; assert_vtag_ref(&elem); - let (_, elem) = elem.attach(&scope, &parent, NodeRef::default()); + let (_, elem) = elem.attach(&root, &scope, &parent, NodeRef::default()); assert_eq!(node_ref.get(), parent.first_child()); - elem.detach(&parent, false); + elem.detach(&root, &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 (root, scope, parent) = setup_parent(); let node_ref_a = NodeRef::default(); let elem_a = html! {
}; - let (_, mut elem) = elem_a.attach(&scope, &parent, NodeRef::default()); + let (_, mut elem) = elem_a.attach(&root, &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); + elem_b.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); let node_b = node_ref_b.get().unwrap(); @@ -800,9 +795,7 @@ mod tests { #[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 (root, scope, parent) = setup_parent(); let test_ref = NodeRef::default(); let before = html! { @@ -819,8 +812,8 @@ mod tests { // The point of this diff is to first render the "after" div and then detach the "before" div, // while both should be bound to the same node ref - let (_, mut elem) = before.attach(&scope, &parent, NodeRef::default()); - after.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); + let (_, mut elem) = before.attach(&root, &scope, &parent, NodeRef::default()); + after.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem); assert_eq!( test_ref diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index 51c54f0a4c5..969ce3eef02 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -1,6 +1,6 @@ //! This module contains the bundle implementation of text [BText]. -use super::{insert_node, BNode, Reconcilable, ReconcileTarget}; +use super::{insert_node, BNode, BSubtree, Reconcilable, ReconcileTarget}; use crate::html::AnyScope; use crate::virtual_dom::{AttrValue, VText}; use crate::NodeRef; @@ -15,7 +15,7 @@ pub(super) struct BText { } impl ReconcileTarget for BText { - fn detach(self, parent: &Element, parent_to_detach: bool) { + fn detach(self, _root: &BSubtree, parent: &Element, parent_to_detach: bool) { if !parent_to_detach { let result = parent.remove_child(&self.text_node); @@ -39,6 +39,7 @@ impl Reconcilable for VText { fn attach( self, + _root: &BSubtree, _parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -53,18 +54,21 @@ impl Reconcilable for VText { /// Renders virtual node over existing `TextNode`, but only if value of text has changed. fn reconcile_node( self, + root: &BSubtree, 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), + BNode::Text(btext) => self.reconcile(root, parent_scope, parent, next_sibling, btext), + _ => self.replace(root, parent_scope, parent, next_sibling, bundle), } } + fn reconcile( self, + _root: &BSubtree, _parent_scope: &AnyScope, _parent: &Element, _next_sibling: NodeRef, diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index b1aa8da3045..16df0b97db3 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -12,11 +12,12 @@ mod bportal; mod bsuspense; mod btag; mod btext; +mod subtree_root; + mod traits; mod utils; -use gloo::utils::document; -use web_sys::{Element, Node}; +use web_sys::Element; use crate::html::AnyScope; use crate::html::NodeRef; @@ -27,13 +28,16 @@ use blist::BList; use bnode::BNode; use bportal::BPortal; use bsuspense::BSuspense; -use btag::BTag; +use btag::{BTag, Registry}; use btext::BText; +use subtree_root::EventDescriptor; use traits::{Reconcilable, ReconcileTarget}; use utils::{insert_node, test_log}; #[doc(hidden)] // Publically exported from crate::events -pub use self::btag::set_event_bubbling; +pub use subtree_root::set_event_bubbling; + +pub(crate) use subtree_root::BSubtree; /// A Bundle. /// @@ -46,11 +50,8 @@ pub(crate) struct Bundle(BNode); impl Bundle { /// Creates a new bundle. - pub fn new(parent: &Element, next_sibling: &NodeRef, node_ref: &NodeRef) -> Self { - let placeholder: Node = document().create_text_node("").into(); - insert_node(&placeholder, parent, next_sibling.get().as_ref()); - node_ref.set(Some(placeholder.clone())); - Self(BNode::Ref(placeholder)) + pub const fn new() -> Self { + Self(BNode::List(BList::new())) } /// Shifts the bundle into a different position. @@ -61,16 +62,17 @@ impl Bundle { /// Applies a virtual dom layout to current bundle. pub fn reconcile( &mut self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, next_node: VNode, ) -> NodeRef { - next_node.reconcile_node(parent_scope, parent, next_sibling, &mut self.0) + next_node.reconcile_node(root, parent_scope, parent, next_sibling, &mut self.0) } /// Detaches current bundle. - pub fn detach(self, parent: &Element, parent_to_detach: bool) { - self.0.detach(parent, parent_to_detach); + pub fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) { + self.0.detach(root, parent, parent_to_detach); } } diff --git a/packages/yew/src/dom_bundle/subtree_root.rs b/packages/yew/src/dom_bundle/subtree_root.rs new file mode 100644 index 00000000000..34b8e007bbd --- /dev/null +++ b/packages/yew/src/dom_bundle/subtree_root.rs @@ -0,0 +1,476 @@ +//! Per-subtree state of apps + +use super::{test_log, Registry}; +use crate::virtual_dom::{Listener, ListenerKind}; +use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase}; +use std::cell::RefCell; +use std::collections::HashSet; +use std::hash::{Hash, Hasher}; +use std::rc::{Rc, Weak}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use web_sys::{Element, Event, EventTarget as HtmlEventTarget}; + +/// DOM-Types that capture (bubbling) events. This generally includes event targets, +/// but also subtree roots. +pub trait EventGrating { + fn subtree_id(&self) -> Option; + fn set_subtree_id(&self, tree_id: TreeId); + // When caching, we key on the length of the `composed_path`. Important to check + // considering event retargeting! + fn cache_key(&self) -> Option; + fn set_cache_key(&self, key: u32); +} + +#[wasm_bindgen] +extern "C" { + // Duck-typing, not a real class on js-side. On rust-side, use impls of EventGrating below + type EventTargetable; + #[wasm_bindgen(method, getter = __yew_subtree_id, structural)] + fn subtree_id(this: &EventTargetable) -> Option; + #[wasm_bindgen(method, setter = __yew_subtree_id, structural)] + fn set_subtree_id(this: &EventTargetable, id: TreeId); + #[wasm_bindgen(method, getter = __yew_subtree_cache_key, structural)] + fn cache_key(this: &EventTargetable) -> Option; + #[wasm_bindgen(method, setter = __yew_subtree_cache_key, structural)] + fn set_cache_key(this: &EventTargetable, key: u32); +} + +macro_rules! impl_event_grating { + ($($t:ty);* $(;)?) => { + $( + impl EventGrating for $t { + fn subtree_id(&self) -> Option { + self.unchecked_ref::().subtree_id() + } + fn set_subtree_id(&self, tree_id: TreeId) { + self.unchecked_ref::() + .set_subtree_id(tree_id); + } + fn cache_key(&self) -> Option { + self.unchecked_ref::().cache_key() + } + fn set_cache_key(&self, key: u32) { + self.unchecked_ref::().set_cache_key(key) + } + } + )* + } +} + +impl_event_grating!( + HtmlEventTarget; + Event; // We cache the found subtree id on the event. This should speed up repeated searches +); + +/// The TreeId is the additional payload attached to each listening element +/// It identifies the host responsible for the target. Events not matching +/// are ignored during handling +type TreeId = u32; + +/// Special id for caching the fact that some event should not be handled +static NONE_TREE_ID: TreeId = 0; +static NEXT_ROOT_ID: AtomicU32 = AtomicU32::new(1); + +fn next_root_id() -> TreeId { + NEXT_ROOT_ID.fetch_add(1, Ordering::SeqCst) +} + +/// Data kept per controlled subtree. [Portal] and [AppHandle] serve as +/// hosts. Two controlled subtrees should never overlap. +/// +/// [Portal]: super::bportal::BPortal +/// [AppHandle]: super::app_handle::AppHandle +#[derive(Debug, Clone)] +pub struct BSubtree(Rc); + +/// The parent is the logical location where a subtree is mounted +/// Used to bubble events through portals, which are physically somewhere else in the DOM tree +/// but should bubble to logical ancestors in the virtual DOM tree +#[derive(Debug)] +struct ParentingInformation { + parent_root: Rc, + // Logical parent of the subtree. Might be the host element of another subtree, + // if mounted as a direct child, or a controlled element. + mount_element: Element, +} + +#[derive(Clone, Hash, Eq, PartialEq, Debug)] +pub 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 event handler registration. +// +// Separate struct to DRY, while avoiding partial struct mutability. +#[derive(Debug)] +struct HostHandlers { + /// The host element where events are registered + host: HtmlEventTarget, + + /// 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 HostHandlers { + fn new(host: HtmlEventTarget) -> Self { + Self { + host, + #[cfg(test)] + registered: Vec::default(), + } + } + + fn add_listener(&mut self, desc: &EventDescriptor, callback: impl 'static + FnMut(&Event)) { + let cl = { + let desc = desc.clone(); + let options = EventListenerOptions { + phase: EventListenerPhase::Capture, + passive: desc.passive, + }; + EventListener::new_with_options(&self.host, desc.kind.type_name(), options, callback) + }; + + // Never drop the closure as this event handler is static + #[cfg(not(test))] + cl.forget(); + #[cfg(test)] + self.registered.push((desc.kind.clone(), cl)); + } +} + +/// Per subtree data +#[derive(Debug)] +struct SubtreeData { + /// Data shared between all trees in an app + app_data: Rc>, + /// Parent subtree + parent: Option, + + subtree_id: TreeId, + host: HtmlEventTarget, + event_registry: RefCell, + global: RefCell, +} + +#[derive(Debug)] +struct WeakSubtree { + subtree_id: TreeId, + weak_ref: Weak, +} + +impl Hash for WeakSubtree { + fn hash(&self, state: &mut H) { + self.subtree_id.hash(state) + } +} + +impl PartialEq for WeakSubtree { + fn eq(&self, other: &Self) -> bool { + self.subtree_id == other.subtree_id + } +} +impl Eq for WeakSubtree {} + +/// Per tree data, shared between all subtrees in the hierarchy +#[derive(Debug, Default)] +struct AppData { + subtrees: HashSet, + listening: HashSet, +} + +impl AppData { + fn add_subtree(&mut self, subtree: &Rc) { + for event in self.listening.iter() { + subtree.add_listener(event); + } + self.subtrees.insert(WeakSubtree { + subtree_id: subtree.subtree_id, + weak_ref: Rc::downgrade(subtree), + }); + } + fn ensure_handled(&mut self, desc: &EventDescriptor) { + if !self.listening.insert(desc.clone()) { + return; + } + self.subtrees.retain(|subtree| { + if let Some(subtree) = subtree.weak_ref.upgrade() { + subtree.add_listener(desc); + true + } else { + false + } + }) + } +} + +/// 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. +#[cfg_attr(documenting, doc(cfg(feature = "csr")))] +pub fn set_event_bubbling(bubble: bool) { + BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); +} + +struct BrandingSearchResult { + branding: TreeId, + closest_branded_ancestor: Element, +} + +/// Deduce the subtree an element is part of. This already partially starts the bubbling +/// process, as long as no listeners are encountered. +/// Subtree roots are always branded with their own subtree id. +fn find_closest_branded_element(mut el: Element, do_bubble: bool) -> Option { + if !do_bubble { + let branding = el.subtree_id()?; + Some(BrandingSearchResult { + branding, + closest_branded_ancestor: el, + }) + } else { + let responsible_tree_id = loop { + if let Some(tree_id) = el.subtree_id() { + break tree_id; + } + el = el.parent_element()?; + }; + Some(BrandingSearchResult { + branding: responsible_tree_id, + closest_branded_ancestor: el, + }) + } +} + +/// Iterate over all potentially listening elements in bubbling order. +/// If bubbling is turned off, yields at most a single element. +fn start_bubbling_from( + subtree: &SubtreeData, + root_or_listener: Element, + should_bubble: bool, +) -> impl '_ + Iterator { + let start = subtree.bubble_to_inner_element(root_or_listener, should_bubble); + + std::iter::successors(start, move |(subtree, element)| { + if !should_bubble { + return None; + } + let parent = element.parent_element()?; + subtree.bubble_to_inner_element(parent, true) + }) +} + +impl SubtreeData { + fn new_ref(host_element: &HtmlEventTarget, parent: Option) -> Rc { + let tree_root_id = next_root_id(); + let event_registry = Registry::new(); + let host_handlers = HostHandlers::new(host_element.clone()); + let app_data = match parent { + Some(ref parent) => parent.parent_root.app_data.clone(), + None => Rc::default(), + }; + let subtree = Rc::new(SubtreeData { + parent, + app_data, + + subtree_id: tree_root_id, + host: host_element.clone(), + event_registry: RefCell::new(event_registry), + global: RefCell::new(host_handlers), + }); + subtree.app_data.borrow_mut().add_subtree(&subtree); + subtree + } + + fn event_registry(&self) -> &RefCell { + &self.event_registry + } + + fn host_handlers(&self) -> &RefCell { + &self.global + } + + // Bubble a potential parent until it reaches an internal element + fn bubble_to_inner_element( + &self, + parent_el: Element, + should_bubble: bool, + ) -> Option<(&Self, Element)> { + let mut next_subtree = self; + let mut next_el = parent_el; + if !should_bubble && next_subtree.host.eq(&next_el) { + return None; + } + while next_subtree.host.eq(&next_el) { + // we've reached the host, delegate to a parent if one exists + let parent = next_subtree.parent.as_ref()?; + next_subtree = &parent.parent_root; + next_el = parent.mount_element.clone(); + } + Some((next_subtree, next_el)) + } + + fn start_bubbling_if_responsible<'s>( + &'s self, + event: &'s Event, + ) -> Option> { + // Note: the event is not necessarily indentically the same object for all installed handlers + // hence this cache can be unreliable. Hence the cached repsonsible_tree_id might be missing. + // On the other hand, due to event retargeting at shadow roots, the cache might be wrong! + // Keep in mind that we handle events in the capture phase, so top-down. When descending and + // retargeting into closed shadow-dom, the event might have been handled 'prematurely'. + // TODO: figure out how to prevent this and establish correct event handling for closed shadow root. + // Note: Other frameworks also get this wrong and dispatch such events multiple times. + let event_path = event.composed_path(); + let derived_cached_key = event_path.length(); + let cached_branding = if matches!(event.cache_key(), Some(cache_key) if cache_key == derived_cached_key) + { + event.subtree_id() + } else { + None + }; + if matches!(cached_branding, Some(responsible_tree_id) if responsible_tree_id != self.subtree_id) + { + // some other handler has determined (via this function, but other `self`) a subtree that is + // responsible for handling this event, and it's not this subtree. + return None; + } + // We're tasked with finding the subtree that is reponsible with handling the event, and/or + // run the handling if that's `self`. + let target = event_path.get(0).dyn_into::().ok()?; + let should_bubble = BUBBLE_EVENTS.load(Ordering::Relaxed); + // We say that the most deeply nested subtree is "responsible" for handling the event. + let (responsible_tree_id, bubbling_start) = if let Some(branding) = cached_branding { + (branding, target.clone()) + } else if let Some(branding) = find_closest_branded_element(target.clone(), should_bubble) { + let BrandingSearchResult { + branding, + closest_branded_ancestor, + } = branding; + event.set_subtree_id(branding); + event.set_cache_key(derived_cached_key); + (branding, closest_branded_ancestor) + } else { + // Possible only? if bubbling is disabled + // No tree should handle this event + event.set_subtree_id(NONE_TREE_ID); + event.set_cache_key(derived_cached_key); + return None; + }; + if self.subtree_id != responsible_tree_id { + return None; + } + if self.host.eq(&target) { + // One more special case: don't handle events that get fired directly on a subtree host + return None; + } + Some(start_bubbling_from(self, bubbling_start, should_bubble)) + // # More details: When nesting occurs + // + // Event listeners are installed only on the subtree roots. Still, those roots can + // nest. This could lead to events getting handled multiple times. We want event handling to start + // at the most deeply nested subtree. + // + // A nested subtree portals into an element that is controlled by the user and rendered + // with VNode::VRef. We get the following dom nesting: + // + // AppRoot > .. > UserControlledVRef > .. > NestedTree(PortalExit) > .. + // -------------- ---------------------------- + // The underlined parts of the hierarchy are controlled by Yew. + // + // from the following virtual_dom + // + // {VNode::VRef(
)} + // {create_portal(, #portal_target)} + // + } + /// Handle a global event firing + fn handle(&self, desc: EventDescriptor, event: Event) { + let run_handler = |root: &Self, el: &Element| { + let handler = Registry::get_handler(root.event_registry(), el, &desc); + if let Some(handler) = handler { + handler(&event) + } + }; + if let Some(bubbling_it) = self.start_bubbling_if_responsible(&event) { + test_log!("Running handler on subtree {}", self.subtree_id); + for (subtree, el) in bubbling_it { + if event.cancel_bubble() { + break; + } + run_handler(subtree, &el); + } + } + } + fn add_listener(self: &Rc, desc: &EventDescriptor) { + let this = self.clone(); + let listener = { + let desc = desc.clone(); + move |e: &Event| { + this.handle(desc.clone(), e.clone()); + } + }; + self.host_handlers() + .borrow_mut() + .add_listener(desc, listener); + } +} + +impl BSubtree { + fn do_create_root( + host_element: &HtmlEventTarget, + parent: Option, + ) -> Self { + let shared_inner = SubtreeData::new_ref(host_element, parent); + let root = BSubtree(shared_inner); + root.brand_element(host_element); + root + } + /// Create a bundle root at the specified host element + pub fn create_root(host_element: &HtmlEventTarget) -> Self { + Self::do_create_root(host_element, None) + } + /// Create a bundle root at the specified host element, that is logically + /// mounted under the specified element in this tree. + pub fn create_subroot(&self, mount_point: Element, host_element: &HtmlEventTarget) -> Self { + let parent_information = ParentingInformation { + parent_root: self.0.clone(), + mount_element: mount_point, + }; + Self::do_create_root(host_element, Some(parent_information)) + } + /// Ensure the event described is handled on all subtrees + pub fn ensure_handled(&self, desc: &EventDescriptor) { + self.0.app_data.borrow_mut().ensure_handled(desc); + } + /// Run f with access to global Registry + #[inline] + pub fn with_listener_registry(&self, f: impl FnOnce(&mut Registry) -> R) -> R { + f(&mut *self.0.event_registry().borrow_mut()) + } + pub fn brand_element(&self, el: &dyn EventGrating) { + el.set_subtree_id(self.0.subtree_id); + } +} diff --git a/packages/yew/src/dom_bundle/traits.rs b/packages/yew/src/dom_bundle/traits.rs index a3873da9bfe..a9c1639e91c 100644 --- a/packages/yew/src/dom_bundle/traits.rs +++ b/packages/yew/src/dom_bundle/traits.rs @@ -1,6 +1,5 @@ -use super::BNode; -use crate::html::AnyScope; -use crate::html::NodeRef; +use super::{BNode, BSubtree}; +use crate::html::{AnyScope, NodeRef}; use web_sys::Element; /// A Reconcile Target. @@ -11,7 +10,7 @@ pub(super) trait ReconcileTarget { /// 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); + fn detach(self, root: &BSubtree, 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 @@ -26,6 +25,7 @@ pub(super) trait Reconcilable { /// Attach a virtual node to the DOM tree. /// /// Parameters: + /// - `root`: bundle of the subtree root /// - `parent_scope`: the parent `Scope` used for passing messages to the /// parent `Component`. /// - `parent`: the parent node in the DOM. @@ -34,6 +34,7 @@ pub(super) trait Reconcilable { /// Returns a reference to the newly inserted element. fn attach( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -58,6 +59,7 @@ pub(super) trait Reconcilable { /// Returns a reference to the newly inserted element. fn reconcile_node( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -66,6 +68,7 @@ pub(super) trait Reconcilable { fn reconcile( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -75,6 +78,7 @@ pub(super) trait Reconcilable { /// Replace an existing bundle by attaching self and detaching the existing one fn replace( self, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -84,9 +88,9 @@ pub(super) trait Reconcilable { Self: Sized, Self::Bundle: Into, { - let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling); + let (self_ref, self_) = self.attach(root, parent_scope, parent, next_sibling); let ancestor = std::mem::replace(bundle, self_.into()); - ancestor.detach(parent, false); + ancestor.detach(root, parent, false); self_ref } } diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 92056f2a6f6..5d5e43a6dc6 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -10,7 +10,7 @@ use std::any::Any; use std::rc::Rc; #[cfg(feature = "csr")] -use crate::dom_bundle::Bundle; +use crate::dom_bundle::{BSubtree, Bundle}; #[cfg(feature = "csr")] use crate::html::NodeRef; #[cfg(feature = "csr")] @@ -20,7 +20,8 @@ pub(crate) enum ComponentRenderState { #[cfg(feature = "csr")] Render { bundle: Bundle, - parent: web_sys::Element, + root: BSubtree, + parent: Element, next_sibling: NodeRef, node_ref: NodeRef, }, @@ -37,12 +38,14 @@ impl std::fmt::Debug for ComponentRenderState { #[cfg(feature = "csr")] Self::Render { ref bundle, + ref root, ref parent, ref next_sibling, ref node_ref, } => f .debug_struct("ComponentRenderState::Render") .field("bundle", bundle) + .field("root", root) .field("parent", parent) .field("next_sibling", next_sibling) .field("node_ref", node_ref) @@ -63,6 +66,32 @@ impl std::fmt::Debug for ComponentRenderState { } } +#[cfg(feature = "csr")] +impl ComponentRenderState { + pub(crate) fn shift(&mut self, next_parent: Element, next_next_sibling: NodeRef) { + match self { + #[cfg(feature = "csr")] + Self::Render { + bundle, + parent, + next_sibling, + .. + } => { + bundle.shift(&next_parent, next_next_sibling.clone()); + + *parent = next_parent; + *next_sibling = next_next_sibling; + } + + #[cfg(feature = "ssr")] + Self::Ssr { .. } => { + #[cfg(debug_assertions)] + panic!("shifting is not possible during SSR"); + } + } + } +} + struct CompStateInner where COMP: BaseComponent, @@ -221,9 +250,6 @@ pub(crate) enum UpdateEvent { /// Wraps properties, node ref, and next sibling for a component #[cfg(feature = "csr")] Properties(Rc, NodeRef, NodeRef), - /// Shift Scope. - #[cfg(feature = "csr")] - Shift(Element, NodeRef), } pub(crate) struct UpdateRunner { @@ -264,32 +290,6 @@ impl Runnable for UpdateRunner { } } } - - #[cfg(feature = "csr")] - UpdateEvent::Shift(next_parent, next_sibling) => { - match state.render_state { - ComponentRenderState::Render { - ref bundle, - ref mut parent, - next_sibling: ref mut current_next_sibling, - .. - } => { - bundle.shift(&next_parent, next_sibling.clone()); - - *parent = next_parent; - *current_next_sibling = next_sibling; - } - - // Shifting is not possible during SSR. - #[cfg(feature = "ssr")] - ComponentRenderState::Ssr { .. } => { - #[cfg(debug_assertions)] - panic!("shifting is not possible during SSR"); - } - } - - false - } }; #[cfg(debug_assertions)] @@ -331,10 +331,11 @@ impl Runnable for DestroyRunner { ComponentRenderState::Render { bundle, ref parent, + ref root, ref node_ref, .. } => { - bundle.detach(parent, self.parent_to_detach); + bundle.detach(root, parent, self.parent_to_detach); node_ref.set(None); } @@ -429,12 +430,14 @@ impl RenderRunner { ComponentRenderState::Render { ref mut bundle, ref parent, + ref root, ref next_sibling, ref node_ref, .. } => { let scope = state.inner.any_scope(); - let new_node_ref = bundle.reconcile(&scope, parent, next_sibling.clone(), new_root); + let new_node_ref = + bundle.reconcile(root, &scope, parent, next_sibling.clone(), new_root); node_ref.link(new_node_ref); let first_render = !state.has_rendered; @@ -492,6 +495,7 @@ mod tests { extern crate self as yew; use super::*; + use crate::dom_bundle::BSubtree; use crate::html; use crate::html::*; use crate::Properties; @@ -612,12 +616,19 @@ mod tests { fn test_lifecycle(props: Props, expected: &[&str]) { let document = gloo_utils::document(); let scope = Scope::::new(None); - let el = document.create_element("div").unwrap(); - let node_ref = NodeRef::default(); + let parent = document.create_element("div").unwrap(); + let root = BSubtree::create_root(&parent); + let lifecycle = props.lifecycle.clone(); lifecycle.borrow_mut().clear(); - scope.mount_in_place(el, NodeRef::default(), node_ref, Rc::new(props)); + scope.mount_in_place( + root, + parent, + NodeRef::default(), + NodeRef::default(), + Rc::new(props), + ); crate::scheduler::start_now(); assert_eq!(&lifecycle.borrow_mut().deref()[..], expected); diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 04582114fb9..f48168f9e44 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -387,7 +387,7 @@ pub(crate) use feat_csr_ssr::*; #[cfg(feature = "csr")] mod feat_csr { use super::*; - use crate::dom_bundle::Bundle; + use crate::dom_bundle::{BSubtree, Bundle}; use crate::html::component::lifecycle::{ ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner, }; @@ -403,14 +403,17 @@ mod feat_csr { /// Mounts a component with `props` to the specified `element` in the DOM. pub(crate) fn mount_in_place( &self, + root: BSubtree, parent: Element, next_sibling: NodeRef, node_ref: NodeRef, props: Rc, ) { - let bundle = Bundle::new(&parent, &next_sibling, &node_ref); + let bundle = Bundle::new(); + node_ref.link(next_sibling.clone()); let state = ComponentRenderState::Render { bundle, + root, node_ref, parent, next_sibling, @@ -486,10 +489,10 @@ mod feat_csr { } fn shift_node(&self, parent: Element, next_sibling: NodeRef) { - scheduler::push_component_update(Box::new(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) + } } } } diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/tests/layout_tests.rs index 682f7f596c8..a6509679562 100644 --- a/packages/yew/src/tests/layout_tests.rs +++ b/packages/yew/src/tests/layout_tests.rs @@ -1,10 +1,9 @@ -use crate::dom_bundle::Bundle; +use crate::dom_bundle::{BSubtree, Bundle}; use crate::html::AnyScope; use crate::scheduler; use crate::virtual_dom::VNode; use crate::{Component, Context, Html}; use gloo::console::log; -use web_sys::Node; use yew::NodeRef; struct Comp; @@ -38,11 +37,12 @@ pub struct TestLayout<'a> { pub fn diff_layouts(layouts: Vec>) { let document = gloo_utils::document(); - let parent_scope: AnyScope = AnyScope::test(); + let scope: AnyScope = AnyScope::test(); let parent_element = document.create_element("div").unwrap(); - let parent_node: Node = parent_element.clone().into(); + let root = BSubtree::create_root(&parent_element); + let end_node = document.create_text_node("END"); - parent_node.append_child(&end_node).unwrap(); + parent_element.append_child(&end_node).unwrap(); // Tests each layout independently let next_sibling = NodeRef::new(end_node.into()); @@ -51,10 +51,8 @@ pub fn diff_layouts(layouts: Vec>) { let vnode = layout.node.clone(); log!("Independently apply layout '{}'", layout.name); - let node_ref = NodeRef::default(); - - let mut bundle = Bundle::new(&parent_element, &next_sibling, &node_ref); - bundle.reconcile(&parent_scope, &parent_element, next_sibling.clone(), vnode); + let mut bundle = Bundle::new(); + bundle.reconcile(&root, &scope, &parent_element, next_sibling.clone(), vnode); scheduler::start_now(); assert_eq!( parent_element.inner_html(), @@ -68,7 +66,7 @@ pub fn diff_layouts(layouts: Vec>) { log!("Independently reapply layout '{}'", layout.name); - bundle.reconcile(&parent_scope, &parent_element, next_sibling.clone(), vnode); + bundle.reconcile(&root, &scope, &parent_element, next_sibling.clone(), vnode); scheduler::start_now(); assert_eq!( parent_element.inner_html(), @@ -78,7 +76,7 @@ pub fn diff_layouts(layouts: Vec>) { ); // Detach - bundle.detach(&parent_element, false); + bundle.detach(&root, &parent_element, false); scheduler::start_now(); assert_eq!( parent_element.inner_html(), @@ -89,14 +87,14 @@ pub fn diff_layouts(layouts: Vec>) { } // Sequentially apply each layout - let node_ref = NodeRef::default(); - let mut bundle = Bundle::new(&parent_element, &next_sibling, &node_ref); + let mut bundle = Bundle::new(); for layout in layouts.iter() { let next_vnode = layout.node.clone(); log!("Sequentially apply layout '{}'", layout.name); bundle.reconcile( - &parent_scope, + &root, + &scope, &parent_element, next_sibling.clone(), next_vnode, @@ -117,7 +115,8 @@ pub fn diff_layouts(layouts: Vec>) { log!("Sequentially detach layout '{}'", layout.name); bundle.reconcile( - &parent_scope, + &root, + &scope, &parent_element, next_sibling.clone(), next_vnode, @@ -133,7 +132,7 @@ pub fn diff_layouts(layouts: Vec>) { } // Detach last layout - bundle.detach(&parent_element, false); + bundle.detach(&root, &parent_element, false); scheduler::start_now(); assert_eq!( parent_element.inner_html(), diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index aebd72a366f..7912238a007 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -9,6 +9,8 @@ use std::rc::Rc; #[cfg(any(feature = "ssr", feature = "csr"))] use crate::html::{AnyScope, Scope}; +#[cfg(feature = "csr")] +use crate::dom_bundle::BSubtree; #[cfg(feature = "csr")] use crate::html::Scoped; #[cfg(feature = "csr")] @@ -53,6 +55,7 @@ pub(crate) trait Mountable { #[cfg(feature = "csr")] fn mount( self: Box, + root: &BSubtree, node_ref: NodeRef, parent_scope: &AnyScope, parent: Element, @@ -91,13 +94,14 @@ impl Mountable for PropsWrapper { #[cfg(feature = "csr")] fn mount( self: Box, + root: &BSubtree, 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); + scope.mount_in_place(root.clone(), parent, next_sibling, node_ref, self.props); Box::new(scope) } diff --git a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr index a7e774e1dc7..563ac8e341e 100644 --- a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr +++ b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr @@ -1,12 +1,12 @@ error[E0277]: the trait bound `Comp: yew::Component` is not satisfied - --> tests/failed_tests/base_component_impl-fail.rs:6:6 - | -6 | impl BaseComponent for Comp { - | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp` - | - = note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp` + --> tests/failed_tests/base_component_impl-fail.rs:6:6 + | +6 | impl BaseComponent for Comp { + | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp` + | + = note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp` note: required by a bound in `BaseComponent` - --> src/html/component/mod.rs - | - | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent` + --> src/html/component/mod.rs + | + | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent` diff --git a/website/docs/advanced-topics/portals.mdx b/website/docs/advanced-topics/portals.mdx index 8551dfcc17e..2a355f98ad7 100644 --- a/website/docs/advanced-topics/portals.mdx +++ b/website/docs/advanced-topics/portals.mdx @@ -23,7 +23,7 @@ simple modal dialogue that renders its `children` into an element outside `yew`' identified by the `id="modal_host"`. ```rust -use yew::{html, create_portal, function_component, Children, Properties, Html}; +use yew::prelude::*; #[derive(Properties, PartialEq)] pub struct ModalProps { @@ -31,11 +31,11 @@ pub struct ModalProps { pub children: Children, } -#[function_component(Modal)] -fn modal(props: &ModalProps) -> Html { +#[function_component] +fn Modal(props: &ModalProps) -> Html { let modal_host = gloo::utils::document() .get_element_by_id("modal_host") - .expect("a #modal_host element"); + .expect("Expected to find a #modal_host element"); create_portal( html!{ {for props.children.iter()} }, @@ -44,5 +44,20 @@ fn modal(props: &ModalProps) -> Html { } ``` +## Event handling + +Events emitted on elements inside portals follow the virtual DOM when bubbling up. That is, +if a portal is rendered as the child of an element, then an event listener on that element +will catch events dispatched from inside the portal, even if the portal renders its contents +in an unrelated location in the actual DOM. + +This allows developers to be oblivious of whether a component they consume, is implemented with +or without portals. Events fired on its children will bubble up regardless. + +A known issue is that events from portals into **closed** shadow roots will be dispatched twice, +once targeting the element inside the shadow root and once targeting the host element itself. Keep +in mind that **open** shadow roots work fine. If this impacts you, feel free to open a bug report +about it. + ## Further reading - [Portals example](https://github.com/yewstack/yew/tree/master/examples/portals) diff --git a/website/docs/concepts/html/events.mdx b/website/docs/concepts/html/events.mdx index 86f51f6bd04..4076102b72d 100644 --- a/website/docs/concepts/html/events.mdx +++ b/website/docs/concepts/html/events.mdx @@ -135,6 +135,23 @@ listens for `click` events. | `ontransitionrun` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) | | `ontransitionstart` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) | +## Event bubbling + +Events dispatched by Yew follow the virtual DOM hierarchy when bubbling up to listeners. Currently, only the bubbling phase +is supported for listeners. Note that the virtual DOM hierarchy is most often, but not always, identical to the actual +DOM hierarchy. The distinction is important when working with [portals](../../advanced-topics/portals.mdx) and other +more advanced techniques. The intuition for well implemented components should be that events bubble from children +to parents, so that the hierarchy in your coded `html!` is the one observed by event handlers. + +If you are not interested in event bubbling, you can turn it off by calling + +```rust +yew::set_event_bubbling(false); +``` + +*before* starting your app. This speeds up event handling, but some components may break from not receiving events they expect. +Use this with care! + ## Typed event target :::caution