From e2d1dc4b5d92869c184d2d90463cd96fa288e033 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Sun, 6 Mar 2022 17:30:19 +0100 Subject: [PATCH 01/14] move host element to Registry --- packages/yew/src/dom_bundle/btag/listeners.rs | 124 +++++++++++------- 1 file changed, 79 insertions(+), 45 deletions(-) diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs index 66f14363b3f..a1e642710ac 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -1,24 +1,46 @@ use super::Apply; use crate::dom_bundle::test_log; use crate::virtual_dom::{Listener, ListenerKind, Listeners}; +use ::wasm_bindgen::{prelude::wasm_bindgen, JsCast}; use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::ops::Deref; use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; -use wasm_bindgen::JsCast; -use web_sys::{Element, Event}; +use web_sys::{Element, Event, EventTarget as HtmlEventTarget}; -thread_local! { - /// Global event listener registry - static REGISTRY: RefCell = Default::default(); +#[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); +} + +/// DOM-Types that can have listeners registered on them. Uses the duck-typed interface from above +/// in impls. +trait EventTarget { + fn listener_id(&self) -> Option; + fn set_listener_id(&self, id: u32); +} + +impl EventTarget for Element { + fn listener_id(&self) -> Option { + self.unchecked_ref::().listener_id() + } - /// Key used to store listener id on element - static LISTENER_ID_PROP: wasm_bindgen::JsValue = "__yew_listener_id".into(); + fn set_listener_id(&self, id: u32) { + self.unchecked_ref::().set_listener_id(id) + } +} - /// Cached reference to the document body - static BODY: web_sys::HtmlElement = gloo_utils::document().body().unwrap(); +thread_local! { + /// Global event listener registry + static REGISTRY: RefCell = RefCell::new(Registry::new_global()); } /// Bubble events during delegation @@ -127,11 +149,14 @@ impl From<&dyn Listener> for EventDescriptor { } } -/// Ensures global event handler registration. +/// Ensures event handler registration. // // Separate struct to DRY, while avoiding partial struct mutability. -#[derive(Default, Debug)] -struct GlobalHandlers { +#[derive(Debug)] +struct HostHandlers { + /// The host element where events are registered + host: HtmlEventTarget, + /// Events with registered handlers that are possibly passive handling: HashSet, @@ -141,24 +166,31 @@ struct GlobalHandlers { registered: Vec<(ListenerKind, EventListener)>, } -impl GlobalHandlers { +impl HostHandlers { + fn new(host: HtmlEventTarget) -> Self { + Self { + host, + handling: HashSet::default(), + #[cfg(test)] + registered: Vec::default(), + } + } + /// 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()), - ) - }) + let options = EventListenerOptions { + phase: EventListenerPhase::Capture, + passive: desc.passive, + }; + EventListener::new_with_options( + &self.host, + desc.kind.type_name(), + options, + move |e: &Event| Registry::handle(desc.clone(), e.clone()), + ) }; // Never drop the closure as this event handler is static @@ -173,19 +205,32 @@ impl GlobalHandlers { } /// Global multiplexing event handler registry -#[derive(Default, Debug)] +#[derive(Debug)] struct Registry { /// Counter for assigning new IDs id_counter: u32, /// Registered global event handlers - global: GlobalHandlers, + global: HostHandlers, /// Contains all registered event listeners by listener ID by_id: HashMap>>>, } impl Registry { + fn new(host: HtmlEventTarget) -> Self { + Self { + id_counter: u32::default(), + global: HostHandlers::new(host), + by_id: HashMap::default(), + } + } + + fn new_global() -> Self { + let body = gloo_utils::document().body().unwrap(); + Self::new(body.into()) + } + /// Run f with access to global Registry #[inline] fn with(f: impl FnOnce(&mut Registry) -> R) -> R { @@ -230,11 +275,7 @@ impl Registry { 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"); - } - }); + el.set_listener_id(id); id } @@ -253,19 +294,12 @@ impl Registry { } fn run_handlers(desc: EventDescriptor, event: Event, target: web_sys::Element) { + let get_handlers = |el: &dyn EventTarget| -> Option>> { + let id = el.listener_id()?; + Registry::with(|r| r.by_id.get(&id)?.get(&desc).cloned()) + }; 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() - }) - }) - { + if let Some(l) = get_handlers(el) { for l in l { l.handle(event.clone()); } @@ -399,7 +433,7 @@ mod tests { M: Mixin, { // Remove any existing listeners and elements - super::Registry::with(|r| *r = Default::default()); + super::Registry::with(|r| *r = super::Registry::new_global()); if let Some(el) = document().query_selector(tag).unwrap() { el.parent_element().unwrap().remove(); } From 1e90d91758172b3698f24a0d19dcc6f6260c9fb1 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 10 Mar 2022 08:13:21 +0100 Subject: [PATCH 02/14] add BundleRoot argument BundleRoot controls the element where listeners are registered. Current impl always uses the global registry on document.body --- packages/yew/src/dom_bundle/app_handle.rs | 9 +- packages/yew/src/dom_bundle/bcomp.rs | 112 +++++++++------ packages/yew/src/dom_bundle/blist.rs | 46 ++++-- packages/yew/src/dom_bundle/bnode.rs | 69 +++++---- packages/yew/src/dom_bundle/bportal.rs | 43 ++++-- packages/yew/src/dom_bundle/bsuspense.rs | 48 ++++--- .../yew/src/dom_bundle/btag/attributes.rs | 17 +-- packages/yew/src/dom_bundle/btag/listeners.rs | 62 ++++++--- packages/yew/src/dom_bundle/btag/mod.rs | 131 +++++++++--------- packages/yew/src/dom_bundle/btext.rs | 14 +- packages/yew/src/dom_bundle/mod.rs | 15 +- .../yew/src/dom_bundle/tests/layout_tests.rs | 20 +-- packages/yew/src/dom_bundle/tests/mod.rs | 8 +- packages/yew/src/dom_bundle/tree_root.rs | 9 ++ packages/yew/src/html/component/lifecycle.rs | 8 +- packages/yew/src/html/component/scope.rs | 8 +- 16 files changed, 368 insertions(+), 251 deletions(-) create mode 100644 packages/yew/src/dom_bundle/tree_root.rs diff --git a/packages/yew/src/dom_bundle/app_handle.rs b/packages/yew/src/dom_bundle/app_handle.rs index 7cd49e94ee1..fb6ebec4ab5 100644 --- a/packages/yew/src/dom_bundle/app_handle.rs +++ b/packages/yew/src/dom_bundle/app_handle.rs @@ -1,6 +1,6 @@ //! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope. -use super::{ComponentRenderState, Scoped}; +use super::{BundleRoot, ComponentRenderState, Scoped}; use crate::html::{IntoComponent, NodeRef, Scope}; use std::ops::Deref; use std::rc::Rc; @@ -21,14 +21,15 @@ 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), }; let node_ref = NodeRef::default(); + let hosting_root = BundleRoot; let initial_render_state = - ComponentRenderState::new(element, NodeRef::default(), &node_ref); + ComponentRenderState::new(hosting_root, host, NodeRef::default(), &node_ref); app.scope .mount_in_place(initial_render_state, node_ref, props); diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index c68db83af9a..cbfc0c596d7 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -1,6 +1,6 @@ //! This module contains the bundle implementation of a virtual component [BComp]. -use super::{insert_node, BNode, DomBundle, Reconcilable}; +use super::{insert_node, BNode, BundleRoot, DomBundle, Reconcilable}; use crate::html::{AnyScope, BaseComponent, Scope}; use crate::virtual_dom::{Key, VComp, VNode}; use crate::NodeRef; @@ -40,12 +40,13 @@ impl fmt::Debug for BComp { } impl DomBundle for BComp { - fn detach(self, _parent: &Element, parent_to_detach: bool) { + fn detach(self, _root: &BundleRoot, _parent: &Element, parent_to_detach: bool) { self.scope.destroy_boxed(parent_to_detach); } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - self.scope.shift_node(next_parent.clone(), next_sibling); + fn shift(&self, next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { + self.scope + .shift_node(next_root, next_parent.clone(), next_sibling); } } @@ -54,6 +55,7 @@ impl Reconcilable for VComp { fn attach( self, + root: &BundleRoot, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -67,6 +69,7 @@ impl Reconcilable for VComp { let scope = mountable.mount( node_ref.clone(), + root, parent_scope, parent.to_owned(), next_sibling, @@ -85,6 +88,7 @@ impl Reconcilable for VComp { fn reconcile_node( self, + root: &BundleRoot, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -95,14 +99,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: &BundleRoot, _parent_scope: &AnyScope, _parent: &Element, next_sibling: NodeRef, @@ -128,6 +133,7 @@ pub trait Mountable { fn mount( self: Box, node_ref: NodeRef, + root: &BundleRoot, parent_scope: &AnyScope, parent: Element, next_sibling: NodeRef, @@ -163,12 +169,14 @@ impl Mountable for PropsWrapper { fn mount( self: Box, node_ref: NodeRef, + root: &BundleRoot, parent_scope: &AnyScope, parent: Element, next_sibling: NodeRef, ) -> Box { let scope: Scope = Scope::new(Some(parent_scope.clone())); - let initial_render_state = ComponentRenderState::new(parent, next_sibling, &node_ref); + let initial_render_state = + ComponentRenderState::new(root.clone(), parent, next_sibling, &node_ref); scope.mount_in_place(initial_render_state, node_ref, self.props); Box::new(scope) @@ -194,7 +202,8 @@ impl Mountable for PropsWrapper { } pub struct ComponentRenderState { - root_node: BNode, + hosting_root: BundleRoot, + view_node: BNode, /// When a component has no parent, it means that it should not be rendered. parent: Option, next_sibling: NodeRef, @@ -205,13 +214,18 @@ pub struct ComponentRenderState { impl std::fmt::Debug for ComponentRenderState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.root_node.fmt(f) + self.view_node.fmt(f) } } impl ComponentRenderState { /// Prepare a place in the DOM to hold the eventual [VNode] from rendering a component - pub(crate) fn new(parent: Element, next_sibling: NodeRef, node_ref: &NodeRef) -> Self { + pub(crate) fn new( + hosting_root: BundleRoot, + parent: Element, + next_sibling: NodeRef, + node_ref: &NodeRef, + ) -> Self { let placeholder = { let placeholder: Node = document().create_text_node("").into(); insert_node(&placeholder, &parent, next_sibling.get().as_ref()); @@ -219,7 +233,8 @@ impl ComponentRenderState { BNode::Ref(placeholder) }; Self { - root_node: placeholder, + hosting_root, + view_node: placeholder, parent: Some(parent), next_sibling, #[cfg(feature = "ssr")] @@ -232,7 +247,8 @@ impl ComponentRenderState { use super::blist::BList; Self { - root_node: BNode::List(BList::new()), + hosting_root: BundleRoot, + view_node: BNode::List(BList::new()), parent: None, next_sibling: NodeRef::default(), html_sender: Some(tx), @@ -243,22 +259,35 @@ impl ComponentRenderState { self.next_sibling = next_sibling; } /// Shift the rendered content to a new DOM position - pub(crate) fn shift(&mut self, new_parent: Element, next_sibling: NodeRef) { - self.root_node.shift(&new_parent, next_sibling.clone()); + pub(crate) fn shift( + &mut self, + next_root: &BundleRoot, + new_parent: Element, + next_sibling: NodeRef, + ) { + self.view_node + .shift(next_root, &new_parent, next_sibling.clone()); + self.hosting_root = next_root.clone(); self.parent = Some(new_parent); self.next_sibling = next_sibling; } /// Reconcile the rendered content with a new [VNode] - pub(crate) fn reconcile(&mut self, root: VNode, scope: &AnyScope) -> NodeRef { + pub(crate) fn reconcile(&mut self, view: VNode, scope: &AnyScope) -> NodeRef { if let Some(ref parent) = self.parent { let next_sibling = self.next_sibling.clone(); - root.reconcile_node(scope, parent, next_sibling, &mut self.root_node) + view.reconcile_node( + &self.hosting_root, + scope, + parent, + next_sibling, + &mut self.view_node, + ) } else { #[cfg(feature = "ssr")] if let Some(tx) = self.html_sender.take() { - tx.send(root).unwrap(); + tx.send(view).unwrap(); } NodeRef::default() } @@ -266,7 +295,8 @@ impl ComponentRenderState { /// Detach the rendered content from the DOM pub(crate) fn detach(self, parent_to_detach: bool) { if let Some(ref m) = self.parent { - self.root_node.detach(m, parent_to_detach); + self.view_node + .detach(&self.hosting_root, m, parent_to_detach); } } @@ -280,7 +310,7 @@ pub trait Scoped { /// Get the render state if it hasn't already been destroyed fn render_state(&self) -> Option>; /// Shift the node associated with this scope to a new place - fn shift_node(&self, parent: Element, next_sibling: NodeRef); + fn shift_node(&self, next_root: &BundleRoot, parent: Element, next_sibling: NodeRef); /// Process an event to destroy a component fn destroy(self, parent_to_detach: bool); fn destroy_boxed(self: Box, parent_to_detach: bool); @@ -336,22 +366,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(); } } @@ -493,27 +516,28 @@ mod tests { } } - fn setup_parent() -> (AnyScope, Element) { + fn setup_parent() -> (BundleRoot, AnyScope, Element) { let scope = AnyScope::test(); let parent = document().create_element("div").unwrap(); + let root = BundleRoot; 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: &BundleRoot, 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(..) @@ -530,7 +554,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! { @@ -538,7 +562,7 @@ mod tests { }; assert_eq!( - get_html(children_renderer_method, &scope, &parent), + get_html(children_renderer_method, &root, &scope, &parent), expected_html ); @@ -547,30 +571,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 21e66fc60ec..239037e3034 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, BundleRoot}; use crate::dom_bundle::{DomBundle, Reconcilable}; 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 BundleRoot, 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 { @@ -58,7 +60,7 @@ impl<'s> NodeWriter<'s> { /// Shift a bundle into place without patching it fn shift(&self, bundle: &mut BNode) { - bundle.shift(self.parent, self.next_sibling.clone()); + bundle.shift(self.root, self.parent, self.next_sibling.clone()); } /// Patch a bundle with a new node @@ -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: &BundleRoot, 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: &BundleRoot, 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,15 +367,15 @@ impl BList { } impl DomBundle for BList { - fn detach(self, parent: &Element, parent_to_detach: bool) { + fn detach(self, root: &BundleRoot, 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); } } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { for node in self.rev_children.iter().rev() { - node.shift(next_parent, next_sibling.clone()); + node.shift(next_root, next_parent, next_sibling.clone()); } } } @@ -372,30 +385,33 @@ impl Reconcilable for VList { fn attach( self, + root: &BundleRoot, 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: &BundleRoot, 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: &BundleRoot, 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 0e80563fd30..7dc456ea6c1 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, BSuspense, BTag, BText, BundleRoot}; use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::{Key, VNode}; @@ -43,36 +43,36 @@ impl BNode { impl DomBundle for BNode { /// Remove VNode from parent. - fn detach(self, parent: &Element, parent_to_detach: bool) { + fn detach(self, root: &BundleRoot, 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), } } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { match self { - Self::Tag(ref vtag) => vtag.shift(next_parent, next_sibling), - Self::Text(ref btext) => btext.shift(next_parent, next_sibling), - Self::Comp(ref bsusp) => bsusp.shift(next_parent, next_sibling), - Self::List(ref vlist) => vlist.shift(next_parent, next_sibling), + Self::Tag(ref vtag) => vtag.shift(next_root, next_parent, next_sibling), + Self::Text(ref btext) => btext.shift(next_root, next_parent, next_sibling), + Self::Comp(ref bsusp) => bsusp.shift(next_root, next_parent, next_sibling), + Self::List(ref vlist) => vlist.shift(next_root, next_parent, next_sibling), Self::Ref(ref node) => { next_parent .insert_before(node, next_sibling.get().as_ref()) .unwrap(); } - Self::Portal(ref vportal) => vportal.shift(next_parent, next_sibling), - Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling), + Self::Portal(ref vportal) => vportal.shift(next_root, next_parent, next_sibling), + Self::Suspense(ref vsuspense) => vsuspense.shift(next_root, next_parent, next_sibling), } } } @@ -82,25 +82,26 @@ impl Reconcilable for VNode { fn attach( self, + root: &BundleRoot, 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: &BundleRoot, 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: &BundleRoot, 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 a5c6d769181..fa10e4871a3 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, BundleRoot}; use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::Key; @@ -11,6 +10,8 @@ use web_sys::Element; /// The bundle implementation to [VPortal]. #[derive(Debug)] pub struct BPortal { + // The inner root + inner_root: BundleRoot, /// The element under which the content is inserted. host: Element, /// The next sibling after the inserted content @@ -20,13 +21,12 @@ pub struct BPortal { } impl DomBundle for BPortal { - fn detach(self, _: &Element, _parent_to_detach: bool) { - test_log!("Detaching portal from host{:?}", self.host.outer_html()); - self.node.detach(&self.host, false); - test_log!("Detached portal from host{:?}", self.host.outer_html()); + fn detach(self, _root: &BundleRoot, _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) { + fn shift(&self, _next_root: &BundleRoot, _next_parent: &Element, _next_sibling: NodeRef) { // portals have nothing in it's original place of DOM, we also do nothing. } } @@ -36,19 +36,22 @@ impl Reconcilable for VPortal { fn attach( self, + _root: &BundleRoot, parent_scope: &AnyScope, _parent: &Element, host_next_sibling: NodeRef, ) -> (NodeRef, Self::Bundle) { + let inner_root = BundleRoot; let Self { host, inner_sibling, node, } = self; - let (_, inner) = node.attach(parent_scope, &host, inner_sibling.clone()); + 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: &BundleRoot, 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: &BundleRoot, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -88,11 +95,19 @@ 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()); + portal.node.shift( + &portal.inner_root, + &portal.host, + portal.inner_sibling.clone(), + ); } - 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 0781b512e7d..0d7770e4034 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, DomBundle, Reconcilable}; +use super::{BNode, BundleRoot, DomBundle, Reconcilable}; use crate::html::AnyScope; use crate::virtual_dom::{Key, VSuspense}; use crate::NodeRef; @@ -30,17 +30,19 @@ impl BSuspense { } impl DomBundle for BSuspense { - fn detach(self, parent: &Element, parent_to_detach: bool) { + fn detach(self, root: &BundleRoot, 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); } } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { - self.active_node().shift(next_parent, next_sibling) + fn shift(&self, next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { + self.active_node() + .shift(next_root, next_parent, next_sibling) } } @@ -49,6 +51,7 @@ impl Reconcilable for VSuspense { fn attach( self, + root: &BundleRoot, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -66,8 +69,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 { @@ -78,7 +82,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 { @@ -93,6 +98,7 @@ impl Reconcilable for VSuspense { fn reconcile_node( self, + root: &BundleRoot, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -104,14 +110,15 @@ impl Reconcilable for VSuspense { if m.key == self.key && self.detached_parent.as_ref() == Some(&m.detached_parent) => { - 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: &BundleRoot, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -135,30 +142,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, &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(&detached_parent, NodeRef::default()); + children_bundle.shift(root, &detached_parent, NodeRef::default()); children.reconcile_node( + root, parent_scope, &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 } @@ -168,10 +178,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_bundle.shift(root, parent, next_sibling.clone()); + 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 cdec0630e6b..9892dbeaaa2 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::BundleRoot; 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: &BundleRoot, 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: &BundleRoot, 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: &BundleRoot, 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: &BundleRoot, 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: &BundleRoot, 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: &BundleRoot, 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 a1e642710ac..9046cdfd591 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -1,5 +1,5 @@ use super::Apply; -use crate::dom_bundle::test_log; +use crate::dom_bundle::{test_log, BundleRoot}; use crate::virtual_dom::{Listener, ListenerKind, Listeners}; use ::wasm_bindgen::{prelude::wasm_bindgen, JsCast}; use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase}; @@ -73,14 +73,14 @@ impl Apply for Listeners { type Element = Element; type Bundle = ListenerRegistration; - fn apply(self, el: &Self::Element) -> ListenerRegistration { + fn apply(self, root: &BundleRoot, 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: &BundleRoot, el: &Self::Element, bundle: &mut ListenerRegistration) { use ListenerRegistration::*; use Listeners::*; @@ -88,10 +88,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(id, &*pending)); } (Pending(pending), bundle @ NoReg) => { - *bundle = ListenerRegistration::register(el, &pending); + *bundle = ListenerRegistration::register(root, el, &pending); test_log!( "registering listeners for {}", match bundle { @@ -106,7 +106,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) => { @@ -118,8 +118,8 @@ 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| { + fn register(root: &BundleRoot, el: &Element, pending: &[Option>]) -> Self { + Self::Registered(root.with_listener_registry(|reg| { let id = reg.set_listener_id(el); reg.register(id, pending); id @@ -127,9 +127,9 @@ impl ListenerRegistration { } /// Remove any registered event listeners from the global registry - pub(super) fn unregister(&self) { + pub(super) fn unregister(&self, root: &BundleRoot) { if let Self::Registered(id) = self { - Registry::with(|r| r.unregister(id)); + root.with_listener_registry(|r| r.unregister(id)); } } } @@ -166,6 +166,22 @@ struct HostHandlers { registered: Vec<(ListenerKind, EventListener)>, } +impl HostHandlers { + fn event_listener(&self, desc: EventDescriptor) -> impl 'static + FnMut(&Event) { + move |e: &Event| { + REGISTRY.with(|reg| Registry::handle(reg, desc.clone(), e.clone())); + } + } +} + +impl BundleRoot { + /// Run f with access to global Registry + #[inline] + fn with_listener_registry(&self, f: impl FnOnce(&mut Registry) -> R) -> R { + REGISTRY.with(|r| f(&mut *r.borrow_mut())) + } +} + impl HostHandlers { fn new(host: HtmlEventTarget) -> Self { Self { @@ -189,7 +205,7 @@ impl HostHandlers { &self.host, desc.kind.type_name(), options, - move |e: &Event| Registry::handle(desc.clone(), e.clone()), + self.event_listener(desc), ) }; @@ -231,12 +247,6 @@ impl Registry { Self::new(body.into()) } - /// Run f with access to global Registry - #[inline] - fn with(f: impl FnOnce(&mut Registry) -> R) -> R { - REGISTRY.with(|r| f(&mut *r.borrow_mut())) - } - /// Register all passed listeners under ID fn register(&mut self, id: u32, listeners: &[Option>]) { let mut by_desc = @@ -281,7 +291,7 @@ impl Registry { } /// Handle a global event firing - fn handle(desc: EventDescriptor, event: Event) { + fn handle(weak_registry: &RefCell, desc: EventDescriptor, event: Event) { let target = match event .target() .and_then(|el| el.dyn_into::().ok()) @@ -290,13 +300,19 @@ impl Registry { None => return, }; - Self::run_handlers(desc, event, target); + Self::run_handlers(weak_registry, desc, event, target); } - fn run_handlers(desc: EventDescriptor, event: Event, target: web_sys::Element) { + fn run_handlers( + weak_registry: &RefCell, + desc: EventDescriptor, + event: Event, + target: web_sys::Element, + ) { let get_handlers = |el: &dyn EventTarget| -> Option>> { let id = el.listener_id()?; - Registry::with(|r| r.by_id.get(&id)?.get(&desc).cloned()) + let reg = weak_registry.borrow_mut(); + reg.by_id.get(&id)?.get(&desc).cloned() }; let run_handler = |el: &web_sys::Element| { if let Some(l) = get_handlers(el) { @@ -433,7 +449,7 @@ mod tests { M: Mixin, { // Remove any existing listeners and elements - super::Registry::with(|r| *r = super::Registry::new_global()); + super::REGISTRY.with(|r| *r.borrow_mut() = super::Registry::new_global()); if let Some(el) = document().query_selector(tag).unwrap() { el.parent_element().unwrap().remove(); } diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index 1d172c1f87d..86dd468121f 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -5,7 +5,7 @@ mod listeners; pub use listeners::set_event_bubbling; -use super::{insert_node, BList, BNode, DomBundle, Reconcilable}; +use super::{insert_node, BList, BNode, BundleRoot, DomBundle, Reconcilable}; 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: &BundleRoot, 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: &BundleRoot, el: &Self::Element, bundle: &mut Self::Bundle); } /// [BTag] fields that are specific to different [BTag] kinds. @@ -69,14 +69,14 @@ pub struct BTag { } impl DomBundle for BTag { - fn detach(self, parent: &Element, parent_to_detach: bool) { - self.listeners.unregister(); + fn detach(self, root: &BundleRoot, 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); @@ -92,7 +92,7 @@ impl DomBundle for BTag { } } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, _next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { next_parent .insert_before(&self.reference, next_sibling.get().as_ref()) .unwrap(); @@ -104,6 +104,7 @@ impl Reconcilable for VTag { fn attach( self, + root: &BundleRoot, 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: &BundleRoot, 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: &BundleRoot, 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() }, @@ -293,8 +303,14 @@ mod tests { #[cfg(feature = "wasm_test")] wasm_bindgen_test_configure!(run_in_browser); - fn test_scope() -> AnyScope { - AnyScope::test() + fn setup_parent() -> (BundleRoot, AnyScope, Element) { + let scope = AnyScope::test(); + let parent = document().create_element("div").unwrap(); + let root = BundleRoot; + + document().body().unwrap().append_child(&parent).unwrap(); + + (root, scope, parent) } #[test] @@ -473,10 +489,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(); @@ -486,17 +501,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); } @@ -592,26 +607,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")); @@ -629,16 +638,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 @@ -650,7 +656,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 @@ -665,14 +671,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 @@ -684,7 +687,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 @@ -703,10 +706,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(); @@ -714,7 +714,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"); @@ -756,36 +756,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(); @@ -798,9 +793,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! { @@ -817,8 +810,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 af152955daf..f85c8e1d047 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, DomBundle, Reconcilable}; +use super::{insert_node, BNode, BundleRoot, DomBundle, Reconcilable}; use crate::html::AnyScope; use crate::virtual_dom::{AttrValue, VText}; use crate::NodeRef; @@ -15,7 +15,7 @@ pub struct BText { } impl DomBundle for BText { - fn detach(self, parent: &Element, parent_to_detach: bool) { + fn detach(self, _root: &BundleRoot, parent: &Element, parent_to_detach: bool) { if !parent_to_detach { let result = parent.remove_child(&self.text_node); @@ -25,7 +25,7 @@ impl DomBundle for BText { } } - fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, _next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { let node = &self.text_node; next_parent @@ -39,6 +39,7 @@ impl Reconcilable for VText { fn attach( self, + _root: &BundleRoot, _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: &BundleRoot, 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: &BundleRoot, _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 a1f0b596a14..759507511da 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -13,6 +13,7 @@ mod bportal; mod bsuspense; mod btag; mod btext; +mod tree_root; #[cfg(test)] mod tests; @@ -26,6 +27,7 @@ use self::btag::BTag; use self::btext::BText; pub(crate) use self::bcomp::{ComponentRenderState, Mountable, PropsWrapper, Scoped}; +pub(crate) use self::tree_root::BundleRoot; #[doc(hidden)] // Publically exported from crate::app_handle pub use self::app_handle::AppHandle; @@ -43,12 +45,12 @@ trait DomBundle { /// Remove self from parent. /// /// Parent to detach is `true` if the parent element will also be detached. - fn detach(self, parent: &Element, parent_to_detach: bool); + fn detach(self, root: &BundleRoot, parent: &Element, parent_to_detach: bool); /// Move elements from one parent to another parent. /// This is for example used by `VSuspense` to preserve component state without detaching /// (which destroys component state). - fn shift(&self, next_parent: &Element, next_sibling: NodeRef); + fn shift(&self, next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef); } /// This trait provides features to update a tree by calculating a difference against another tree. @@ -58,6 +60,7 @@ 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. @@ -66,6 +69,7 @@ trait Reconcilable { /// Returns a reference to the newly inserted element. fn attach( self, + root: &BundleRoot, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -90,6 +94,7 @@ trait Reconcilable { /// Returns a reference to the newly inserted element. fn reconcile_node( self, + root: &BundleRoot, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -98,6 +103,7 @@ trait Reconcilable { fn reconcile( self, + root: &BundleRoot, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -107,6 +113,7 @@ trait Reconcilable { /// Replace an existing bundle by attaching self and detaching the existing one fn replace( self, + root: &BundleRoot, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -116,9 +123,9 @@ 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/dom_bundle/tests/layout_tests.rs b/packages/yew/src/dom_bundle/tests/layout_tests.rs index d0ef714a5fc..f8845b878ad 100644 --- a/packages/yew/src/dom_bundle/tests/layout_tests.rs +++ b/packages/yew/src/dom_bundle/tests/layout_tests.rs @@ -1,4 +1,4 @@ -use crate::dom_bundle::{BNode, DomBundle, Reconcilable}; +use crate::dom_bundle::{BNode, BundleRoot, DomBundle, Reconcilable}; use crate::html::AnyScope; use crate::scheduler; use crate::virtual_dom::VNode; @@ -38,8 +38,9 @@ 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 root = BundleRoot; let parent_node: Node = parent_element.clone().into(); let end_node = document.create_text_node("END"); parent_node.append_child(&end_node).unwrap(); @@ -51,7 +52,7 @@ pub fn diff_layouts(layouts: Vec>) { let vnode = layout.node.clone(); log!("Independently apply layout '{}'", layout.name); - let (_, mut bundle) = vnode.attach(&parent_scope, &parent_element, next_sibling.clone()); + let (_, mut bundle) = vnode.attach(&root, &scope, &parent_element, next_sibling.clone()); scheduler::start_now(); assert_eq!( parent_element.inner_html(), @@ -66,7 +67,8 @@ pub fn diff_layouts(layouts: Vec>) { log!("Independently reapply layout '{}'", layout.name); vnode.reconcile_node( - &parent_scope, + &root, + &scope, &parent_element, next_sibling.clone(), &mut bundle, @@ -80,7 +82,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(), @@ -97,7 +99,8 @@ pub fn diff_layouts(layouts: Vec>) { log!("Sequentially apply layout '{}'", layout.name); next_vnode.reconcile_sequentially( - &parent_scope, + &root, + &scope, &parent_element, next_sibling.clone(), &mut bundle, @@ -117,7 +120,8 @@ pub fn diff_layouts(layouts: Vec>) { log!("Sequentially detach layout '{}'", layout.name); next_vnode.reconcile_sequentially( - &parent_scope, + &root, + &scope, &parent_element, next_sibling.clone(), &mut bundle, @@ -133,7 +137,7 @@ pub fn diff_layouts(layouts: Vec>) { // Detach last layout if let Some(bundle) = bundle { - bundle.detach(&parent_element, false); + bundle.detach(&root, &parent_element, false); } scheduler::start_now(); assert_eq!( diff --git a/packages/yew/src/dom_bundle/tests/mod.rs b/packages/yew/src/dom_bundle/tests/mod.rs index 1208f4409c5..47f8496ead6 100644 --- a/packages/yew/src/dom_bundle/tests/mod.rs +++ b/packages/yew/src/dom_bundle/tests/mod.rs @@ -1,7 +1,6 @@ pub mod layout_tests; -use super::Reconcilable; - +use super::{BundleRoot, Reconcilable}; use crate::virtual_dom::VNode; use crate::{dom_bundle::BNode, html::AnyScope, NodeRef}; use web_sys::Element; @@ -9,6 +8,7 @@ use web_sys::Element; impl VNode { fn reconcile_sequentially( self, + root: &BundleRoot, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -16,11 +16,11 @@ impl VNode { ) -> NodeRef { match bundle { None => { - let (self_ref, node) = self.attach(parent_scope, parent, next_sibling); + let (self_ref, node) = self.attach(root, parent_scope, parent, next_sibling); *bundle = Some(node); self_ref } - Some(bundle) => self.reconcile_node(parent_scope, parent, next_sibling, bundle), + Some(bundle) => self.reconcile_node(root, parent_scope, parent, next_sibling, bundle), } } } diff --git a/packages/yew/src/dom_bundle/tree_root.rs b/packages/yew/src/dom_bundle/tree_root.rs new file mode 100644 index 00000000000..c63bd11fb5b --- /dev/null +++ b/packages/yew/src/dom_bundle/tree_root.rs @@ -0,0 +1,9 @@ +//! Per-subtree state of apps + +/// Data kept per controlled subtree. [Portal] and [AppHandle] serve as +/// host of (pairwise) unrelated subtrees. +/// +/// [Portal]: super::bportal::BPortal +/// [AppHandle]: super::app_handle::AppHandle +#[derive(Debug, Clone)] +pub struct BundleRoot; diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index ed833c4d46e..1410ea54ee2 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -336,7 +336,7 @@ impl Runnable for RenderedRunner { mod tests { extern crate self as yew; - use crate::dom_bundle::ComponentRenderState; + use crate::dom_bundle::{BundleRoot, ComponentRenderState}; use crate::html; use crate::html::*; use crate::Properties; @@ -459,9 +459,11 @@ 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 parent = document.create_element("div").unwrap(); + let root = BundleRoot; + let node_ref = NodeRef::default(); - let render_state = ComponentRenderState::new(el, NodeRef::default(), &node_ref); + let render_state = ComponentRenderState::new(root, parent, NodeRef::default(), &node_ref); let lifecycle = props.lifecycle.clone(); lifecycle.borrow_mut().clear(); diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 1456438ae6b..df11848a4ad 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -9,7 +9,7 @@ use super::{ }; use crate::callback::Callback; use crate::context::{ContextHandle, ContextProvider}; -use crate::dom_bundle::{ComponentRenderState, Scoped}; +use crate::dom_bundle::{BundleRoot, ComponentRenderState, Scoped}; use crate::html::IntoComponent; use crate::html::NodeRef; use crate::scheduler::{self, Shared}; @@ -172,10 +172,12 @@ impl Scoped for Scope { self.destroy(parent_to_detach) } - fn shift_node(&self, parent: Element, next_sibling: NodeRef) { + fn shift_node(&self, next_root: &BundleRoot, parent: Element, next_sibling: NodeRef) { 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) + render_state + .render_state + .shift(next_root, parent, next_sibling) } } } From 72525c3ceaca5d43402fb496b1fa73414fd27eb6 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 10 Mar 2022 11:10:42 +0100 Subject: [PATCH 03/14] surface level internal API for BundleRoot we have create_root and create_ssr --- packages/yew/src/dom_bundle/app_handle.rs | 2 +- packages/yew/src/dom_bundle/bcomp.rs | 4 +- packages/yew/src/dom_bundle/bportal.rs | 2 +- packages/yew/src/dom_bundle/btag/listeners.rs | 47 ++++---------- packages/yew/src/dom_bundle/btag/mod.rs | 3 +- packages/yew/src/dom_bundle/mod.rs | 8 +-- .../yew/src/dom_bundle/tests/layout_tests.rs | 7 +-- packages/yew/src/dom_bundle/tree_root.rs | 63 ++++++++++++++++++- packages/yew/src/html/component/lifecycle.rs | 2 +- 9 files changed, 88 insertions(+), 50 deletions(-) diff --git a/packages/yew/src/dom_bundle/app_handle.rs b/packages/yew/src/dom_bundle/app_handle.rs index fb6ebec4ab5..140665d4611 100644 --- a/packages/yew/src/dom_bundle/app_handle.rs +++ b/packages/yew/src/dom_bundle/app_handle.rs @@ -27,7 +27,7 @@ where scope: Scope::new(None), }; let node_ref = NodeRef::default(); - let hosting_root = BundleRoot; + let hosting_root = BundleRoot::create_root(&host); let initial_render_state = ComponentRenderState::new(hosting_root, host, NodeRef::default(), &node_ref); app.scope diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index cbfc0c596d7..a08acd52c96 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -247,7 +247,7 @@ impl ComponentRenderState { use super::blist::BList; Self { - hosting_root: BundleRoot, + hosting_root: BundleRoot::create_ssr(), view_node: BNode::List(BList::new()), parent: None, next_sibling: NodeRef::default(), @@ -519,7 +519,7 @@ mod tests { fn setup_parent() -> (BundleRoot, AnyScope, Element) { let scope = AnyScope::test(); let parent = document().create_element("div").unwrap(); - let root = BundleRoot; + let root = BundleRoot::create_root(&parent); document().body().unwrap().append_child(&parent).unwrap(); diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index fa10e4871a3..48c4d946796 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -41,12 +41,12 @@ impl Reconcilable for VPortal { _parent: &Element, host_next_sibling: NodeRef, ) -> (NodeRef, Self::Bundle) { - let inner_root = BundleRoot; let Self { host, inner_sibling, node, } = self; + let inner_root = BundleRoot::create_root(&host); let (_, inner) = node.attach(&inner_root, parent_scope, &host, inner_sibling.clone()); ( host_next_sibling, diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs index 9046cdfd591..bcc81eaed51 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -38,11 +38,6 @@ impl EventTarget for Element { } } -thread_local! { - /// Global event listener registry - static REGISTRY: RefCell = RefCell::new(Registry::new_global()); -} - /// Bubble events during delegation static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); @@ -88,7 +83,7 @@ impl Apply for Listeners { (Pending(pending), Registered(ref id)) => { // Reuse the ID test_log!("reusing listeners for {}", id); - root.with_listener_registry(|reg| reg.patch(id, &*pending)); + root.with_listener_registry(|reg| reg.patch(root, id, &*pending)); } (Pending(pending), bundle @ NoReg) => { *bundle = ListenerRegistration::register(root, el, &pending); @@ -121,7 +116,7 @@ impl ListenerRegistration { fn register(root: &BundleRoot, el: &Element, pending: &[Option>]) -> Self { Self::Registered(root.with_listener_registry(|reg| { let id = reg.set_listener_id(el); - reg.register(id, pending); + reg.register(root, id, pending); id })) } @@ -135,7 +130,7 @@ impl ListenerRegistration { } #[derive(Clone, Hash, Eq, PartialEq, Debug)] -struct EventDescriptor { +pub struct EventDescriptor { kind: ListenerKind, passive: bool, } @@ -166,22 +161,6 @@ struct HostHandlers { registered: Vec<(ListenerKind, EventListener)>, } -impl HostHandlers { - fn event_listener(&self, desc: EventDescriptor) -> impl 'static + FnMut(&Event) { - move |e: &Event| { - REGISTRY.with(|reg| Registry::handle(reg, desc.clone(), e.clone())); - } - } -} - -impl BundleRoot { - /// Run f with access to global Registry - #[inline] - fn with_listener_registry(&self, f: impl FnOnce(&mut Registry) -> R) -> R { - REGISTRY.with(|r| f(&mut *r.borrow_mut())) - } -} - impl HostHandlers { fn new(host: HtmlEventTarget) -> Self { Self { @@ -193,7 +172,7 @@ impl HostHandlers { } /// Ensure a descriptor has a global event handler assigned - fn ensure_handled(&mut self, desc: EventDescriptor) { + fn ensure_handled(&mut self, root: &BundleRoot, desc: EventDescriptor) { if !self.handling.contains(&desc) { let cl = { let desc = desc.clone(); @@ -205,7 +184,7 @@ impl HostHandlers { &self.host, desc.kind.type_name(), options, - self.event_listener(desc), + root.event_listener(desc), ) }; @@ -222,7 +201,7 @@ impl HostHandlers { /// Global multiplexing event handler registry #[derive(Debug)] -struct Registry { +pub struct Registry { /// Counter for assigning new IDs id_counter: u32, @@ -242,25 +221,25 @@ impl Registry { } } - fn new_global() -> Self { + pub fn new_global() -> Self { let body = gloo_utils::document().body().unwrap(); Self::new(body.into()) } /// Register all passed listeners under ID - fn register(&mut self, id: u32, listeners: &[Option>]) { + fn register(&mut self, root: &BundleRoot, 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()); + self.global.ensure_handled(root, desc.clone()); by_desc.entry(desc).or_default().push(l); } self.by_id.insert(id, by_desc); } /// Patch an already registered set of handlers - fn patch(&mut self, id: &u32, listeners: &[Option>]) { + fn patch(&mut self, root: &BundleRoot, 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() { @@ -269,7 +248,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()); + self.global.ensure_handled(root, desc.clone()); by_desc.entry(desc).or_default().push(l); } } @@ -291,7 +270,7 @@ impl Registry { } /// Handle a global event firing - fn handle(weak_registry: &RefCell, desc: EventDescriptor, event: Event) { + pub fn handle(weak_registry: &RefCell, desc: EventDescriptor, event: Event) { let target = match event .target() .and_then(|el| el.dyn_into::().ok()) @@ -449,7 +428,7 @@ mod tests { M: Mixin, { // Remove any existing listeners and elements - super::REGISTRY.with(|r| *r.borrow_mut() = super::Registry::new_global()); + super::BundleRoot::clear_global_listeners(); if let Some(el) = document().query_selector(tag).unwrap() { el.parent_element().unwrap().remove(); } diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index 86dd468121f..9c3f64f276c 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -4,6 +4,7 @@ mod attributes; mod listeners; pub use listeners::set_event_bubbling; +pub use listeners::{EventDescriptor, Registry}; use super::{insert_node, BList, BNode, BundleRoot, DomBundle, Reconcilable}; use crate::html::AnyScope; @@ -306,7 +307,7 @@ mod tests { fn setup_parent() -> (BundleRoot, AnyScope, Element) { let scope = AnyScope::test(); let parent = document().create_element("div").unwrap(); - let root = BundleRoot; + let root = BundleRoot::create_root(&parent); document().body().unwrap().append_child(&parent).unwrap(); diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 759507511da..224c10da9fe 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -23,18 +23,18 @@ use self::blist::BList; use self::bnode::BNode; use self::bportal::BPortal; use self::bsuspense::BSuspense; -use self::btag::BTag; +use self::btag::{BTag, EventDescriptor, Registry}; use self::btext::BText; pub(crate) use self::bcomp::{ComponentRenderState, Mountable, PropsWrapper, Scoped}; pub(crate) use self::tree_root::BundleRoot; -#[doc(hidden)] // Publically exported from crate::app_handle +#[doc(hidden)] // Publicly exported from crate::app_handle pub use self::app_handle::AppHandle; -#[doc(hidden)] // Publically exported from crate::events +#[doc(hidden)] // Publicly exported from crate::events pub use self::btag::set_event_bubbling; #[cfg(test)] -#[doc(hidden)] // Publically exported from crate::tests +#[doc(hidden)] // Publicly exported from crate::tests pub use self::tests::layout_tests; use crate::html::AnyScope; diff --git a/packages/yew/src/dom_bundle/tests/layout_tests.rs b/packages/yew/src/dom_bundle/tests/layout_tests.rs index f8845b878ad..5b6db86ea1c 100644 --- a/packages/yew/src/dom_bundle/tests/layout_tests.rs +++ b/packages/yew/src/dom_bundle/tests/layout_tests.rs @@ -4,7 +4,6 @@ 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; @@ -40,10 +39,10 @@ pub fn diff_layouts(layouts: Vec>) { let document = gloo_utils::document(); let scope: AnyScope = AnyScope::test(); let parent_element = document.create_element("div").unwrap(); - let root = BundleRoot; - let parent_node: Node = parent_element.clone().into(); + let root = BundleRoot::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()); diff --git a/packages/yew/src/dom_bundle/tree_root.rs b/packages/yew/src/dom_bundle/tree_root.rs index c63bd11fb5b..950a6fc9bba 100644 --- a/packages/yew/src/dom_bundle/tree_root.rs +++ b/packages/yew/src/dom_bundle/tree_root.rs @@ -1,9 +1,68 @@ //! Per-subtree state of apps +use super::{EventDescriptor, Registry}; +use std::cell::RefCell; +use std::rc::Rc; +use web_sys::{Event, EventTarget}; + +thread_local! { + /// Global event listener registry + static GLOBAL: BundleRoot = { + let event_registry = RefCell::new(Registry::new_global()); + BundleRoot(Rc::new(InnerBundleRoot { + event_registry: Some(event_registry), + })) + } +} + /// Data kept per controlled subtree. [Portal] and [AppHandle] serve as -/// host of (pairwise) unrelated subtrees. +/// hosts. Two controlled subtrees should never overlap. /// /// [Portal]: super::bportal::BPortal /// [AppHandle]: super::app_handle::AppHandle #[derive(Debug, Clone)] -pub struct BundleRoot; +pub struct BundleRoot(Rc); + +#[derive(Debug)] + +struct InnerBundleRoot { + /// None only during ssr. + event_registry: Option>, +} + +impl BundleRoot { + /// Create a bundle root at the specified host element + pub fn create_root(_root_element: &EventTarget) -> Self { + GLOBAL.with(|root| root.clone()) + } + /// Create a bundle root for ssr + #[cfg(feature = "ssr")] + pub fn create_ssr() -> Self { + BundleRoot(Rc::new(InnerBundleRoot { + event_registry: None, + })) + } + + fn event_registry(&self) -> &RefCell { + self.0 + .event_registry + .as_ref() + .expect("can't access event registry during SSR") + } + /// Run f with access to global Registry + #[inline] + pub fn with_listener_registry(&self, f: impl FnOnce(&mut Registry) -> R) -> R { + f(&mut *self.event_registry().borrow_mut()) + } + /// Return a closure that should be installed as an event listener on the root element for a specific + /// kind of event. + pub fn event_listener(&self, desc: EventDescriptor) -> impl 'static + FnMut(&Event) { + move |e: &Event| { + GLOBAL.with(|root| Registry::handle(root.event_registry(), desc.clone(), e.clone())); + } + } + #[cfg(all(test, feature = "wasm_test"))] + pub fn clear_global_listeners() { + GLOBAL.with(|root| *root.event_registry().borrow_mut() = Registry::new_global()); + } +} diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 1410ea54ee2..b7f6dfc1b67 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -460,7 +460,7 @@ mod tests { let document = gloo_utils::document(); let scope = Scope::::new(None); let parent = document.create_element("div").unwrap(); - let root = BundleRoot; + let root = BundleRoot::create_root(&parent); let node_ref = NodeRef::default(); let render_state = ComponentRenderState::new(root, parent, NodeRef::default(), &node_ref); From 87b77f9d24e03264f0eacfb7f65f0b252d4ef530 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Fri, 11 Mar 2022 08:16:50 +0100 Subject: [PATCH 04/14] implement event handling with multiple subtree roots --- packages/yew/src/dom_bundle/app_handle.rs | 4 +- packages/yew/src/dom_bundle/bcomp.rs | 32 +- packages/yew/src/dom_bundle/blist.rs | 18 +- packages/yew/src/dom_bundle/bnode.rs | 12 +- packages/yew/src/dom_bundle/bportal.rs | 18 +- packages/yew/src/dom_bundle/bsuspense.rs | 12 +- .../yew/src/dom_bundle/btag/attributes.rs | 14 +- packages/yew/src/dom_bundle/btag/listeners.rs | 121 +++---- packages/yew/src/dom_bundle/btag/mod.rs | 21 +- packages/yew/src/dom_bundle/btext.rs | 12 +- packages/yew/src/dom_bundle/mod.rs | 22 +- packages/yew/src/dom_bundle/subtree_root.rs | 296 ++++++++++++++++++ .../yew/src/dom_bundle/tests/layout_tests.rs | 4 +- packages/yew/src/dom_bundle/tests/mod.rs | 4 +- packages/yew/src/dom_bundle/tree_root.rs | 68 ---- packages/yew/src/html/component/lifecycle.rs | 4 +- packages/yew/src/html/component/scope.rs | 4 +- 17 files changed, 422 insertions(+), 244 deletions(-) create mode 100644 packages/yew/src/dom_bundle/subtree_root.rs delete mode 100644 packages/yew/src/dom_bundle/tree_root.rs diff --git a/packages/yew/src/dom_bundle/app_handle.rs b/packages/yew/src/dom_bundle/app_handle.rs index 140665d4611..13e7bf35b01 100644 --- a/packages/yew/src/dom_bundle/app_handle.rs +++ b/packages/yew/src/dom_bundle/app_handle.rs @@ -1,6 +1,6 @@ //! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope. -use super::{BundleRoot, ComponentRenderState, Scoped}; +use super::{BSubtree, ComponentRenderState, Scoped}; use crate::html::{IntoComponent, NodeRef, Scope}; use std::ops::Deref; use std::rc::Rc; @@ -27,7 +27,7 @@ where scope: Scope::new(None), }; let node_ref = NodeRef::default(); - let hosting_root = BundleRoot::create_root(&host); + let hosting_root = BSubtree::create_root(&host); let initial_render_state = ComponentRenderState::new(hosting_root, host, NodeRef::default(), &node_ref); app.scope diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index a08acd52c96..37404d9eec4 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -1,6 +1,6 @@ //! This module contains the bundle implementation of a virtual component [BComp]. -use super::{insert_node, BNode, BundleRoot, DomBundle, Reconcilable}; +use super::{insert_node, BNode, BSubtree, DomBundle, Reconcilable}; use crate::html::{AnyScope, BaseComponent, Scope}; use crate::virtual_dom::{Key, VComp, VNode}; use crate::NodeRef; @@ -40,11 +40,11 @@ impl fmt::Debug for BComp { } impl DomBundle for BComp { - fn detach(self, _root: &BundleRoot, _parent: &Element, parent_to_detach: bool) { + fn detach(self, _root: &BSubtree, _parent: &Element, parent_to_detach: bool) { self.scope.destroy_boxed(parent_to_detach); } - fn shift(&self, next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { self.scope .shift_node(next_root, next_parent.clone(), next_sibling); } @@ -55,7 +55,7 @@ impl Reconcilable for VComp { fn attach( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -88,7 +88,7 @@ impl Reconcilable for VComp { fn reconcile_node( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -107,7 +107,7 @@ impl Reconcilable for VComp { fn reconcile( self, - _root: &BundleRoot, + _root: &BSubtree, _parent_scope: &AnyScope, _parent: &Element, next_sibling: NodeRef, @@ -133,7 +133,7 @@ pub trait Mountable { fn mount( self: Box, node_ref: NodeRef, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: Element, next_sibling: NodeRef, @@ -169,7 +169,7 @@ impl Mountable for PropsWrapper { fn mount( self: Box, node_ref: NodeRef, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: Element, next_sibling: NodeRef, @@ -202,7 +202,7 @@ impl Mountable for PropsWrapper { } pub struct ComponentRenderState { - hosting_root: BundleRoot, + hosting_root: BSubtree, view_node: BNode, /// When a component has no parent, it means that it should not be rendered. parent: Option, @@ -221,7 +221,7 @@ impl std::fmt::Debug for ComponentRenderState { impl ComponentRenderState { /// Prepare a place in the DOM to hold the eventual [VNode] from rendering a component pub(crate) fn new( - hosting_root: BundleRoot, + hosting_root: BSubtree, parent: Element, next_sibling: NodeRef, node_ref: &NodeRef, @@ -247,7 +247,7 @@ impl ComponentRenderState { use super::blist::BList; Self { - hosting_root: BundleRoot::create_ssr(), + hosting_root: BSubtree::create_ssr(), view_node: BNode::List(BList::new()), parent: None, next_sibling: NodeRef::default(), @@ -261,7 +261,7 @@ impl ComponentRenderState { /// Shift the rendered content to a new DOM position pub(crate) fn shift( &mut self, - next_root: &BundleRoot, + next_root: &BSubtree, new_parent: Element, next_sibling: NodeRef, ) { @@ -310,7 +310,7 @@ pub trait Scoped { /// Get the render state if it hasn't already been destroyed fn render_state(&self) -> Option>; /// Shift the node associated with this scope to a new place - fn shift_node(&self, next_root: &BundleRoot, parent: Element, next_sibling: NodeRef); + fn shift_node(&self, next_root: &BSubtree, parent: Element, next_sibling: NodeRef); /// Process an event to destroy a component fn destroy(self, parent_to_detach: bool); fn destroy_boxed(self: Box, parent_to_detach: bool); @@ -516,17 +516,17 @@ mod tests { } } - fn setup_parent() -> (BundleRoot, AnyScope, Element) { + fn setup_parent() -> (BSubtree, AnyScope, Element) { let scope = AnyScope::test(); let parent = document().create_element("div").unwrap(); - let root = BundleRoot::create_root(&parent); + let root = BSubtree::create_root(&parent); document().body().unwrap().append_child(&parent).unwrap(); (root, scope, parent) } - fn get_html(node: Html, root: &BundleRoot, scope: &AnyScope, parent: &Element) -> String { + fn get_html(node: Html, root: &BSubtree, scope: &AnyScope, parent: &Element) -> String { // clear parent parent.set_inner_html(""); diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 239037e3034..4c5782092bf 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, BundleRoot}; +use super::{test_log, BNode, BSubtree}; use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::{Key, VList, VNode, VText}; @@ -31,7 +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 BundleRoot, + root: &'s BSubtree, parent_scope: &'s AnyScope, parent: &'s Element, next_sibling: NodeRef, @@ -143,7 +143,7 @@ impl BList { /// Diff and patch unkeyed child lists fn apply_unkeyed( - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -184,7 +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: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -367,13 +367,13 @@ impl BList { } impl DomBundle for BList { - fn detach(self, root: &BundleRoot, 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(root, parent, parent_to_detach); } } - fn shift(&self, next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { for node in self.rev_children.iter().rev() { node.shift(next_root, next_parent, next_sibling.clone()); } @@ -385,7 +385,7 @@ impl Reconcilable for VList { fn attach( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -397,7 +397,7 @@ impl Reconcilable for VList { fn reconcile_node( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -411,7 +411,7 @@ impl Reconcilable for VList { fn reconcile( mut self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 7dc456ea6c1..ff9215eba94 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, BundleRoot}; +use super::{BComp, BList, BPortal, BSubtree, BSuspense, BTag, BText}; use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::{Key, VNode}; @@ -43,7 +43,7 @@ impl BNode { impl DomBundle for BNode { /// Remove VNode from parent. - fn detach(self, root: &BundleRoot, parent: &Element, parent_to_detach: bool) { + fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) { match self { Self::Tag(vtag) => vtag.detach(root, parent, parent_to_detach), Self::Text(btext) => btext.detach(root, parent, parent_to_detach), @@ -60,7 +60,7 @@ impl DomBundle for BNode { } } - fn shift(&self, next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { match self { Self::Tag(ref vtag) => vtag.shift(next_root, next_parent, next_sibling), Self::Text(ref btext) => btext.shift(next_root, next_parent, next_sibling), @@ -82,7 +82,7 @@ impl Reconcilable for VNode { fn attach( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -122,7 +122,7 @@ impl Reconcilable for VNode { fn reconcile_node( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -133,7 +133,7 @@ impl Reconcilable for VNode { fn reconcile( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index 48c4d946796..29b19de7fe5 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -1,6 +1,6 @@ //! This module contains the bundle implementation of a portal [BPortal]. -use super::{test_log, BNode, BundleRoot}; +use super::{test_log, BNode, BSubtree}; use crate::dom_bundle::{DomBundle, Reconcilable}; use crate::html::{AnyScope, NodeRef}; use crate::virtual_dom::Key; @@ -11,7 +11,7 @@ use web_sys::Element; #[derive(Debug)] pub struct BPortal { // The inner root - inner_root: BundleRoot, + inner_root: BSubtree, /// The element under which the content is inserted. host: Element, /// The next sibling after the inserted content @@ -21,12 +21,12 @@ pub struct BPortal { } impl DomBundle for BPortal { - fn detach(self, _root: &BundleRoot, _parent: &Element, _parent_to_detach: bool) { + 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_root: &BundleRoot, _next_parent: &Element, _next_sibling: NodeRef) { + fn shift(&self, _next_root: &BSubtree, _next_parent: &Element, _next_sibling: NodeRef) { // portals have nothing in it's original place of DOM, we also do nothing. } } @@ -36,9 +36,9 @@ impl Reconcilable for VPortal { fn attach( self, - _root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, - _parent: &Element, + parent: &Element, host_next_sibling: NodeRef, ) -> (NodeRef, Self::Bundle) { let Self { @@ -46,7 +46,7 @@ impl Reconcilable for VPortal { inner_sibling, node, } = self; - let inner_root = BundleRoot::create_root(&host); + let inner_root = root.create_subroot(parent.clone(), &host); let (_, inner) = node.attach(&inner_root, parent_scope, &host, inner_sibling.clone()); ( host_next_sibling, @@ -61,7 +61,7 @@ impl Reconcilable for VPortal { fn reconcile_node( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -77,7 +77,7 @@ impl Reconcilable for VPortal { fn reconcile( self, - _root: &BundleRoot, + _root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 0d7770e4034..e45fdbc98d8 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, BundleRoot, DomBundle, Reconcilable}; +use super::{BNode, BSubtree, DomBundle, Reconcilable}; use crate::html::AnyScope; use crate::virtual_dom::{Key, VSuspense}; use crate::NodeRef; @@ -30,7 +30,7 @@ impl BSuspense { } impl DomBundle for BSuspense { - fn detach(self, root: &BundleRoot, 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(root, parent, parent_to_detach); self.children_bundle @@ -40,7 +40,7 @@ impl DomBundle for BSuspense { } } - fn shift(&self, next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { self.active_node() .shift(next_root, next_parent, next_sibling) } @@ -51,7 +51,7 @@ impl Reconcilable for VSuspense { fn attach( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -98,7 +98,7 @@ impl Reconcilable for VSuspense { fn reconcile_node( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -118,7 +118,7 @@ impl Reconcilable for VSuspense { fn reconcile( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, diff --git a/packages/yew/src/dom_bundle/btag/attributes.rs b/packages/yew/src/dom_bundle/btag/attributes.rs index 9892dbeaaa2..408111624ea 100644 --- a/packages/yew/src/dom_bundle/btag/attributes.rs +++ b/packages/yew/src/dom_bundle/btag/attributes.rs @@ -1,5 +1,5 @@ use super::Apply; -use crate::dom_bundle::BundleRoot; +use crate::dom_bundle::BSubtree; use crate::virtual_dom::vtag::{InputFields, Value}; use crate::virtual_dom::Attributes; use indexmap::IndexMap; @@ -12,14 +12,14 @@ impl Apply for Value { type Element = T; type Bundle = Self; - fn apply(self, _root: &BundleRoot, 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, _root: &BundleRoot, 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. @@ -63,7 +63,7 @@ impl Apply for InputFields { type Element = InputElement; type Bundle = Self; - fn apply(mut self, root: &BundleRoot, 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); @@ -72,7 +72,7 @@ impl Apply for InputFields { self } - fn apply_diff(self, root: &BundleRoot, 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); @@ -187,7 +187,7 @@ impl Apply for Attributes { type Element = Element; type Bundle = Self; - fn apply(self, _root: &BundleRoot, el: &Element) -> Self { + fn apply(self, _root: &BSubtree, el: &Element) -> Self { match &self { Self::Static(arr) => { for kv in arr.iter() { @@ -210,7 +210,7 @@ impl Apply for Attributes { self } - fn apply_diff(self, _root: &BundleRoot, 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 bcc81eaed51..b558a69f4c9 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -1,5 +1,5 @@ use super::Apply; -use crate::dom_bundle::{test_log, BundleRoot}; +use crate::dom_bundle::{test_log, BSubtree}; use crate::virtual_dom::{Listener, ListenerKind, Listeners}; use ::wasm_bindgen::{prelude::wasm_bindgen, JsCast}; use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase}; @@ -7,7 +7,6 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::ops::Deref; use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering}; use web_sys::{Element, Event, EventTarget as HtmlEventTarget}; #[wasm_bindgen] @@ -16,14 +15,13 @@ extern "C" { 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); } -/// DOM-Types that can have listeners registered on them. Uses the duck-typed interface from above -/// in impls. -trait EventTarget { +/// DOM-Types that can have listeners registered on them. +/// Uses the duck-typed interface from above in impls. +pub trait EventTarget { fn listener_id(&self) -> Option; fn set_listener_id(&self, id: u32); } @@ -34,27 +32,10 @@ impl EventTarget for Element { } fn set_listener_id(&self, id: u32) { - self.unchecked_ref::().set_listener_id(id) + self.unchecked_ref::().set_listener_id(id); } } -/// Bubble events during delegation -static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); - -/// Set, if events should bubble up the DOM tree, calling any matching callbacks. -/// -/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event -/// handling performance. -/// -/// Note that yew uses event delegation and implements internal even bubbling for performance -/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event -/// handler has no effect. -/// -/// This function should be called before any component is mounted. -pub fn set_event_bubbling(bubble: bool) { - BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); -} - /// An active set of listeners on an element #[derive(Debug)] pub(super) enum ListenerRegistration { @@ -68,14 +49,14 @@ impl Apply for Listeners { type Element = Element; type Bundle = ListenerRegistration; - fn apply(self, root: &BundleRoot, el: &Self::Element) -> ListenerRegistration { + fn apply(self, root: &BSubtree, el: &Self::Element) -> ListenerRegistration { match self { Self::Pending(pending) => ListenerRegistration::register(root, el, &pending), Self::None => ListenerRegistration::NoReg, } } - fn apply_diff(self, root: &BundleRoot, el: &Self::Element, bundle: &mut ListenerRegistration) { + fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut ListenerRegistration) { use ListenerRegistration::*; use Listeners::*; @@ -113,16 +94,16 @@ impl Apply for Listeners { impl ListenerRegistration { /// Register listeners and return their handle ID - fn register(root: &BundleRoot, el: &Element, pending: &[Option>]) -> Self { + fn register(root: &BSubtree, el: &Element, pending: &[Option>]) -> Self { Self::Registered(root.with_listener_registry(|reg| { - let id = reg.set_listener_id(el); + let id = reg.set_listener_id(root, el); reg.register(root, id, pending); id })) } /// Remove any registered event listeners from the global registry - pub(super) fn unregister(&self, root: &BundleRoot) { + pub(super) fn unregister(&self, root: &BSubtree) { if let Self::Registered(id) = self { root.with_listener_registry(|r| r.unregister(id)); } @@ -172,7 +153,7 @@ impl HostHandlers { } /// Ensure a descriptor has a global event handler assigned - fn ensure_handled(&mut self, root: &BundleRoot, desc: EventDescriptor) { + fn ensure_handled(&mut self, root: &BSubtree, desc: EventDescriptor) { if !self.handling.contains(&desc) { let cl = { let desc = desc.clone(); @@ -213,7 +194,7 @@ pub struct Registry { } impl Registry { - fn new(host: HtmlEventTarget) -> Self { + pub fn new(host: HtmlEventTarget) -> Self { Self { id_counter: u32::default(), global: HostHandlers::new(host), @@ -221,13 +202,29 @@ impl Registry { } } - pub fn new_global() -> Self { - let body = gloo_utils::document().body().unwrap(); - Self::new(body.into()) + // Handle a single event, given the listener and event descriptor. + pub fn get_handler( + registry: &RefCell, + listener: &dyn EventTarget, + 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 = listener.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, root: &BundleRoot, 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() { @@ -239,7 +236,7 @@ impl Registry { } /// Patch an already registered set of handlers - fn patch(&mut self, root: &BundleRoot, 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() { @@ -260,60 +257,15 @@ 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; + root.brand_element(el); el.set_listener_id(id); id } - - /// Handle a global event firing - pub fn handle(weak_registry: &RefCell, desc: EventDescriptor, event: Event) { - let target = match event - .target() - .and_then(|el| el.dyn_into::().ok()) - { - Some(el) => el, - None => return, - }; - - Self::run_handlers(weak_registry, desc, event, target); - } - - fn run_handlers( - weak_registry: &RefCell, - desc: EventDescriptor, - event: Event, - target: web_sys::Element, - ) { - let get_handlers = |el: &dyn EventTarget| -> Option>> { - let id = el.listener_id()?; - let reg = weak_registry.borrow_mut(); - reg.by_id.get(&id)?.get(&desc).cloned() - }; - let run_handler = |el: &web_sys::Element| { - if let Some(l) = get_handlers(el) { - 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"))] @@ -427,8 +379,7 @@ mod tests { where M: Mixin, { - // Remove any existing listeners and elements - super::BundleRoot::clear_global_listeners(); + // Remove any existing elements if let Some(el) = document().query_selector(tag).unwrap() { el.parent_element().unwrap().remove(); } diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index 9c3f64f276c..18bb25a264e 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -3,10 +3,9 @@ mod attributes; mod listeners; -pub use listeners::set_event_bubbling; pub use listeners::{EventDescriptor, Registry}; -use super::{insert_node, BList, BNode, BundleRoot, DomBundle, Reconcilable}; +use super::{insert_node, BList, BNode, BSubtree, DomBundle, Reconcilable}; use crate::html::AnyScope; use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE}; use crate::virtual_dom::{Attributes, Key, VTag}; @@ -26,10 +25,10 @@ trait Apply { type Bundle; /// Apply contained values to [Element](Self::Element) with no ancestor - fn apply(self, root: &BundleRoot, 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, root: &BundleRoot, 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. @@ -70,7 +69,7 @@ pub struct BTag { } impl DomBundle for BTag { - fn detach(self, root: &BundleRoot, parent: &Element, parent_to_detach: bool) { + fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) { self.listeners.unregister(root); let node = self.reference; @@ -93,7 +92,7 @@ impl DomBundle for BTag { } } - fn shift(&self, _next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, _next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { next_parent .insert_before(&self.reference, next_sibling.get().as_ref()) .unwrap(); @@ -105,7 +104,7 @@ impl Reconcilable for VTag { fn attach( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -154,7 +153,7 @@ impl Reconcilable for VTag { fn reconcile_node( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -193,7 +192,7 @@ impl Reconcilable for VTag { fn reconcile( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, _parent: &Element, _next_sibling: NodeRef, @@ -304,10 +303,10 @@ mod tests { #[cfg(feature = "wasm_test")] wasm_bindgen_test_configure!(run_in_browser); - fn setup_parent() -> (BundleRoot, AnyScope, Element) { + fn setup_parent() -> (BSubtree, AnyScope, Element) { let scope = AnyScope::test(); let parent = document().create_element("div").unwrap(); - let root = BundleRoot::create_root(&parent); + let root = BSubtree::create_root(&parent); document().body().unwrap().append_child(&parent).unwrap(); diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index f85c8e1d047..8940b2de6b5 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, BundleRoot, DomBundle, Reconcilable}; +use super::{insert_node, BNode, BSubtree, DomBundle, Reconcilable}; use crate::html::AnyScope; use crate::virtual_dom::{AttrValue, VText}; use crate::NodeRef; @@ -15,7 +15,7 @@ pub struct BText { } impl DomBundle for BText { - fn detach(self, _root: &BundleRoot, 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); @@ -25,7 +25,7 @@ impl DomBundle for BText { } } - fn shift(&self, _next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, _next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { let node = &self.text_node; next_parent @@ -39,7 +39,7 @@ impl Reconcilable for VText { fn attach( self, - _root: &BundleRoot, + _root: &BSubtree, _parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -54,7 +54,7 @@ impl Reconcilable for VText { /// Renders virtual node over existing `TextNode`, but only if value of text has changed. fn reconcile_node( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -68,7 +68,7 @@ impl Reconcilable for VText { fn reconcile( self, - _root: &BundleRoot, + _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 224c10da9fe..523eb223e5c 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -13,7 +13,7 @@ mod bportal; mod bsuspense; mod btag; mod btext; -mod tree_root; +mod subtree_root; #[cfg(test)] mod tests; @@ -27,12 +27,12 @@ use self::btag::{BTag, EventDescriptor, Registry}; use self::btext::BText; pub(crate) use self::bcomp::{ComponentRenderState, Mountable, PropsWrapper, Scoped}; -pub(crate) use self::tree_root::BundleRoot; +pub(crate) use self::subtree_root::BSubtree; #[doc(hidden)] // Publicly exported from crate::app_handle pub use self::app_handle::AppHandle; #[doc(hidden)] // Publicly exported from crate::events -pub use self::btag::set_event_bubbling; +pub use self::subtree_root::set_event_bubbling; #[cfg(test)] #[doc(hidden)] // Publicly exported from crate::tests pub use self::tests::layout_tests; @@ -45,12 +45,12 @@ trait DomBundle { /// Remove self from parent. /// /// Parent to detach is `true` if the parent element will also be detached. - fn detach(self, root: &BundleRoot, 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 /// (which destroys component state). - fn shift(&self, next_root: &BundleRoot, next_parent: &Element, next_sibling: NodeRef); + fn shift(&self, next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef); } /// This trait provides features to update a tree by calculating a difference against another tree. @@ -69,7 +69,7 @@ trait Reconcilable { /// Returns a reference to the newly inserted element. fn attach( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -94,7 +94,7 @@ trait Reconcilable { /// Returns a reference to the newly inserted element. fn reconcile_node( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -103,7 +103,7 @@ trait Reconcilable { fn reconcile( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -113,7 +113,7 @@ trait Reconcilable { /// Replace an existing bundle by attaching self and detaching the existing one fn replace( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, @@ -142,13 +142,13 @@ fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) { #[cfg(all(test, feature = "wasm_test", verbose_tests))] macro_rules! test_log { - ($fmt:literal, $($arg:expr),* $(,)?) => { + ($fmt:literal $(,$arg:expr)* $(,)?) => { ::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*); }; } #[cfg(not(all(test, feature = "wasm_test", verbose_tests)))] macro_rules! test_log { - ($fmt:literal, $($arg:expr),* $(,)?) => { + ($fmt:literal $(,$arg:expr)* $(,)?) => { // Only type-check the format expression, do not run any side effects let _ = || { std::format_args!(concat!("\t ", $fmt), $($arg),*); }; }; diff --git a/packages/yew/src/dom_bundle/subtree_root.rs b/packages/yew/src/dom_bundle/subtree_root.rs new file mode 100644 index 00000000000..568015758f1 --- /dev/null +++ b/packages/yew/src/dom_bundle/subtree_root.rs @@ -0,0 +1,296 @@ +//! Per-subtree state of apps + +use super::{test_log, EventDescriptor, Registry}; +use std::cell::RefCell; +use std::rc::Rc; +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}; + +/// Bubble events during delegation +static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); + +/// Set, if events should bubble up the DOM tree, calling any matching callbacks. +/// +/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event +/// handling performance. +/// +/// Note that yew uses event delegation and implements internal even bubbling for performance +/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event +/// handler has no effect. +/// +/// This function should be called before any component is mounted. +pub fn set_event_bubbling(bubble: bool) { + BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); +} + +// The TreeId is an 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; +/// DOM-Types that capture bubbling events. This generally includes event targets, +/// but also subtree roots. +pub trait EventGrating { + fn responsible_tree_id(&self) -> Option; + fn set_responsible_tree_id(&self, tree_id: TreeId); +} + +#[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_root_id, structural)] + fn subtree_id(this: &EventTargetable) -> Option; + #[wasm_bindgen(method, setter = __yew_subtree_root_id, structural)] + fn set_subtree_id(this: &EventTargetable, id: u32); +} + +impl EventGrating for Element { + fn responsible_tree_id(&self) -> Option { + self.unchecked_ref::().subtree_id() + } + fn set_responsible_tree_id(&self, tree_id: TreeId) { + self.unchecked_ref::() + .set_subtree_id(tree_id); + } +} + +impl EventGrating for HtmlEventTarget { + fn responsible_tree_id(&self) -> Option { + self.unchecked_ref::().subtree_id() + } + fn set_responsible_tree_id(&self, tree_id: TreeId) { + self.unchecked_ref::() + .set_subtree_id(tree_id); + } +} + +/// We cache the found subtree id on the event. This should speed up repeated searches +impl EventGrating for Event { + fn responsible_tree_id(&self) -> Option { + self.unchecked_ref::().subtree_id() + } + fn set_responsible_tree_id(&self, tree_id: TreeId) { + self.unchecked_ref::() + .set_subtree_id(tree_id); + } +} + +static NEXT_ROOT_ID: AtomicU32 = AtomicU32::new(1); // Skip 0, used for ssr + +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(/* None during SSR */ Option>); + +// 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: Option>, + mount_element: Element, +} + +#[derive(Debug)] + +struct InnerBundleRoot { + host: HtmlEventTarget, + parent: Option, + tree_root_id: TreeId, + event_registry: RefCell, +} + +struct ClosestInstanceSearchResult { + root_or_listener: Element, + responsible_tree_id: TreeId, + did_bubble: bool, +} + +/// Deduce the subtree responsible for handling this event. This already +/// partially starts the bubbling process, as long as no listeners are encountered, +/// but stops at subtree roots. +/// Event listeners are installed only on the subtree roots. Still, those roots can +/// nest [1]. This would lead to events getting handled multiple times. We want event +/// handling to start at the most deeply nested subtree. +/// +/// # When nesting occurs +/// The nested subtree portals into a element that is controlled by the user and rendered +/// with VNode::VRef. We get the following nesting: +/// AppRoot > .. > UserControlledVRef > .. > NestedTree(PortalExit) > .. +/// -------------- ---------------------------- +/// The underlined parts of the hierarchy are controlled by Yew. +fn find_closest_responsible_instance(event: &Event) -> Option { + let target = event.target()?.dyn_into::().ok()?; + if let Some(cached_id) = event.responsible_tree_id() { + return Some(ClosestInstanceSearchResult { + root_or_listener: target, + responsible_tree_id: cached_id, + did_bubble: false, + }); + } + + let mut el = target; + let mut did_bubble = false; + let responsible_tree_id = loop { + if let Some(tree_id) = el.responsible_tree_id() { + break tree_id; + } + el = el.parent_element()?; + did_bubble = true; + }; + event.set_responsible_tree_id(responsible_tree_id); + Some(ClosestInstanceSearchResult { + root_or_listener: el, + responsible_tree_id, + did_bubble, + }) +} + +impl InnerBundleRoot { + fn event_registry(&self) -> &RefCell { + &self.event_registry + } + /// Handle a global event firing + fn handle(self: &Rc, desc: EventDescriptor, event: Event) { + let closest_instance = match find_closest_responsible_instance(&event) { + Some(closest_instance) if closest_instance.responsible_tree_id == self.tree_root_id => { + closest_instance + } + _ => return, // Don't handle this event + }; + test_log!("Running handler on subtree {}", self.tree_root_id); + if self.host.eq(&closest_instance.root_or_listener) { + let (self_, target) = match self.bubble_at_root() { + Some(bubbled_target) => bubbled_target, + None => return, // No relevant listener + }; + self_.run_handlers(desc, event, target, true); + } else { + let target = closest_instance.root_or_listener; + let did_bubble = closest_instance.did_bubble; + self.run_handlers(desc, event, target, did_bubble); + } + } + + #[allow(clippy::needless_lifetimes)] // I don't see a way to omit the lifetimes here + fn bubble_at_root<'s>(self: &'s Rc) -> Option<(&'s Rc, Element)> { + // we've reached the physical host, delegate to a parent if one exists + let parent = self.parent.as_ref()?; + let parent_root = parent + .parent_root + .as_ref() + .expect("Can't access listeners in SSR"); + Some((parent_root, parent.mount_element.clone())) + } + + #[allow(clippy::needless_lifetimes)] // I don't see a way to omit the lifetimes here + fn bubble<'s>(self: &'s Rc, el: Element) -> Option<(&'s Rc, Element)> { + let parent = el.parent_element()?; + if self.host.eq(&parent) { + self.bubble_at_root() + } else { + Some((self, parent)) + } + } + + fn run_handlers( + self: &Rc, + desc: EventDescriptor, + event: Event, + closest_target: Element, + did_bubble: bool, // did bubble to find the closest target? + ) { + let run_handler = |root: &Rc, el: &Element| { + let handler = Registry::get_handler(root.event_registry(), el, &desc); + if let Some(handler) = handler { + handler(&event) + } + }; + + let should_bubble = BUBBLE_EVENTS.load(Ordering::Relaxed); + + // If we bubbled to find closest_target, respect BUBBLE_EVENTS setting + if should_bubble || !did_bubble { + run_handler(self, &closest_target); + } + + let mut current_root = self; + if should_bubble { + let mut el = closest_target; + while !event.cancel_bubble() { + let next = match current_root.bubble(el) { + Some(next) => next, + None => break, + }; + // Destructuring assignments are unstable + current_root = next.0; + el = next.1; + + run_handler(self, &el); + } + } + } +} + +impl BSubtree { + fn do_create_root( + host_element: &HtmlEventTarget, + parent: Option, + ) -> Self { + let event_registry = Registry::new(host_element.clone()); + let root = BSubtree(Some(Rc::new(InnerBundleRoot { + host: host_element.clone(), + parent, + tree_root_id: next_root_id(), + event_registry: RefCell::new(event_registry), + }))); + 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)) + } + /// Create a bundle root for ssr + #[cfg(feature = "ssr")] + pub fn create_ssr() -> Self { + BSubtree(None) + } + /// Run f with access to global Registry + #[inline] + pub fn with_listener_registry(&self, f: impl FnOnce(&mut Registry) -> R) -> R { + let inner = self.0.as_deref().expect("Can't access listeners in SSR"); + f(&mut *inner.event_registry().borrow_mut()) + } + /// Return a closure that should be installed as an event listener on the root element for a specific + /// kind of event. + pub fn event_listener(&self, desc: EventDescriptor) -> impl 'static + FnMut(&Event) { + let inner = self.0.clone().expect("Can't access listeners in SSR"); // capture the registry + move |e: &Event| { + inner.handle(desc.clone(), e.clone()); + } + } + + pub fn brand_element(&self, el: &dyn EventGrating) { + let inner = self.0.as_deref().expect("Can't access listeners in SSR"); + el.set_responsible_tree_id(inner.tree_root_id); + } +} diff --git a/packages/yew/src/dom_bundle/tests/layout_tests.rs b/packages/yew/src/dom_bundle/tests/layout_tests.rs index 5b6db86ea1c..45a4ec00bfa 100644 --- a/packages/yew/src/dom_bundle/tests/layout_tests.rs +++ b/packages/yew/src/dom_bundle/tests/layout_tests.rs @@ -1,4 +1,4 @@ -use crate::dom_bundle::{BNode, BundleRoot, DomBundle, Reconcilable}; +use crate::dom_bundle::{BNode, BSubtree, DomBundle, Reconcilable}; use crate::html::AnyScope; use crate::scheduler; use crate::virtual_dom::VNode; @@ -39,7 +39,7 @@ pub fn diff_layouts(layouts: Vec>) { let document = gloo_utils::document(); let scope: AnyScope = AnyScope::test(); let parent_element = document.create_element("div").unwrap(); - let root = BundleRoot::create_root(&parent_element); + let root = BSubtree::create_root(&parent_element); let end_node = document.create_text_node("END"); parent_element.append_child(&end_node).unwrap(); diff --git a/packages/yew/src/dom_bundle/tests/mod.rs b/packages/yew/src/dom_bundle/tests/mod.rs index 47f8496ead6..f4575371592 100644 --- a/packages/yew/src/dom_bundle/tests/mod.rs +++ b/packages/yew/src/dom_bundle/tests/mod.rs @@ -1,6 +1,6 @@ pub mod layout_tests; -use super::{BundleRoot, Reconcilable}; +use super::{BSubtree, Reconcilable}; use crate::virtual_dom::VNode; use crate::{dom_bundle::BNode, html::AnyScope, NodeRef}; use web_sys::Element; @@ -8,7 +8,7 @@ use web_sys::Element; impl VNode { fn reconcile_sequentially( self, - root: &BundleRoot, + root: &BSubtree, parent_scope: &AnyScope, parent: &Element, next_sibling: NodeRef, diff --git a/packages/yew/src/dom_bundle/tree_root.rs b/packages/yew/src/dom_bundle/tree_root.rs deleted file mode 100644 index 950a6fc9bba..00000000000 --- a/packages/yew/src/dom_bundle/tree_root.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Per-subtree state of apps - -use super::{EventDescriptor, Registry}; -use std::cell::RefCell; -use std::rc::Rc; -use web_sys::{Event, EventTarget}; - -thread_local! { - /// Global event listener registry - static GLOBAL: BundleRoot = { - let event_registry = RefCell::new(Registry::new_global()); - BundleRoot(Rc::new(InnerBundleRoot { - event_registry: Some(event_registry), - })) - } -} - -/// 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 BundleRoot(Rc); - -#[derive(Debug)] - -struct InnerBundleRoot { - /// None only during ssr. - event_registry: Option>, -} - -impl BundleRoot { - /// Create a bundle root at the specified host element - pub fn create_root(_root_element: &EventTarget) -> Self { - GLOBAL.with(|root| root.clone()) - } - /// Create a bundle root for ssr - #[cfg(feature = "ssr")] - pub fn create_ssr() -> Self { - BundleRoot(Rc::new(InnerBundleRoot { - event_registry: None, - })) - } - - fn event_registry(&self) -> &RefCell { - self.0 - .event_registry - .as_ref() - .expect("can't access event registry during SSR") - } - /// Run f with access to global Registry - #[inline] - pub fn with_listener_registry(&self, f: impl FnOnce(&mut Registry) -> R) -> R { - f(&mut *self.event_registry().borrow_mut()) - } - /// Return a closure that should be installed as an event listener on the root element for a specific - /// kind of event. - pub fn event_listener(&self, desc: EventDescriptor) -> impl 'static + FnMut(&Event) { - move |e: &Event| { - GLOBAL.with(|root| Registry::handle(root.event_registry(), desc.clone(), e.clone())); - } - } - #[cfg(all(test, feature = "wasm_test"))] - pub fn clear_global_listeners() { - GLOBAL.with(|root| *root.event_registry().borrow_mut() = Registry::new_global()); - } -} diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index b7f6dfc1b67..82d781f390d 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -336,7 +336,7 @@ impl Runnable for RenderedRunner { mod tests { extern crate self as yew; - use crate::dom_bundle::{BundleRoot, ComponentRenderState}; + use crate::dom_bundle::{BSubtree, ComponentRenderState}; use crate::html; use crate::html::*; use crate::Properties; @@ -460,7 +460,7 @@ mod tests { let document = gloo_utils::document(); let scope = Scope::::new(None); let parent = document.create_element("div").unwrap(); - let root = BundleRoot::create_root(&parent); + let root = BSubtree::create_root(&parent); let node_ref = NodeRef::default(); let render_state = ComponentRenderState::new(root, parent, NodeRef::default(), &node_ref); diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index df11848a4ad..559bf7e8a4e 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -9,7 +9,7 @@ use super::{ }; use crate::callback::Callback; use crate::context::{ContextHandle, ContextProvider}; -use crate::dom_bundle::{BundleRoot, ComponentRenderState, Scoped}; +use crate::dom_bundle::{BSubtree, ComponentRenderState, Scoped}; use crate::html::IntoComponent; use crate::html::NodeRef; use crate::scheduler::{self, Shared}; @@ -172,7 +172,7 @@ impl Scoped for Scope { self.destroy(parent_to_detach) } - fn shift_node(&self, next_root: &BundleRoot, parent: Element, next_sibling: NodeRef) { + fn shift_node(&self, next_root: &BSubtree, parent: Element, next_sibling: NodeRef) { let mut state_ref = self.state.borrow_mut(); if let Some(render_state) = state_ref.as_mut() { render_state From 9d07ce4359470c9a55b0bb3860df965ffe235189 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 17 Mar 2022 18:57:01 +0100 Subject: [PATCH 05/14] Add test case for hierarchical event bubbling Async event dispatching is surprisingly complicated. Make sure to see #2510 for details, comments and discussion --- packages/yew/src/dom_bundle/btag/listeners.rs | 144 ++++-- packages/yew/src/dom_bundle/subtree_root.rs | 476 +++++++++++------- 2 files changed, 419 insertions(+), 201 deletions(-) diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs index b558a69f4c9..f8cdc03d5c3 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -21,12 +21,12 @@ extern "C" { /// DOM-Types that can have listeners registered on them. /// Uses the duck-typed interface from above in impls. -pub trait EventTarget { +pub trait EventListening { fn listener_id(&self) -> Option; fn set_listener_id(&self, id: u32); } -impl EventTarget for Element { +impl EventListening for Element { fn listener_id(&self) -> Option { self.unchecked_ref::().listener_id() } @@ -202,16 +202,21 @@ impl Registry { } } - // Handle a single event, given the listener and event descriptor. + /// Check if this registry has any listeners for the given event descriptor + pub fn has_any_listeners(&self, desc: &EventDescriptor) -> bool { + self.global.handling.contains(desc) + } + + /// Handle a single event, given the listening element and event descriptor. pub fn get_handler( registry: &RefCell, - listener: &dyn EventTarget, + 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 = listener.listener_id()?; + 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(); @@ -261,14 +266,15 @@ impl Registry { let id = self.id_counter; self.id_counter += 1; - root.brand_element(el); + root.brand_element(el as &HtmlEventTarget); el.set_listener_id(id); id } } -#[cfg(all(test, feature = "wasm_test"))] +#[cfg(feature = "wasm_test")] +#[cfg(test)] mod tests { use std::marker::PhantomData; @@ -276,7 +282,10 @@ mod tests { use web_sys::{Event, EventInit, 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, Properties, + }; use gloo_utils::document; use wasm_bindgen::JsCast; use yew::Callback; @@ -298,26 +307,7 @@ mod tests { trait Mixin { fn view(ctx: &Context, state: &State) -> Html where - C: Component, - { - let link = ctx.link().clone(); - let onclick = Callback::from(move |_| { - link.send_message(Message::Action); - scheduler::start_now(); - }); - - if state.stop_listening { - html! { - {state.action} - } - } else { - html! { - - {state.action} - - } - } - } + C: Component; } struct Comp @@ -330,10 +320,10 @@ mod tests { impl Component for Comp where - M: Mixin + 'static, + M: Mixin + Properties + 'static, { type Message = Message; - type Properties = (); + type Properties = M; fn create(_: &Context) -> Self { Comp { @@ -362,6 +352,7 @@ mod tests { } } + #[track_caller] fn assert_count(el: &web_sys::HtmlElement, count: isize) { assert_eq!(el.text_content(), Some(count.to_string())) } @@ -377,7 +368,7 @@ mod tests { fn init(tag: &str) -> (AppHandle>, web_sys::HtmlElement) where - M: Mixin, + M: Mixin + Properties + Default, { // Remove any existing elements if let Some(el) = document().query_selector(tag).unwrap() { @@ -394,9 +385,33 @@ mod tests { #[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 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} + + } + } + } + } let (link, el) = init::("a"); @@ -417,6 +432,7 @@ mod tests { #[test] async fn non_bubbling_event() { + #[derive(Default, PartialEq, Properties)] struct NonBubbling; impl Mixin for NonBubbling { @@ -462,6 +478,7 @@ mod tests { #[test] fn bubbling() { + #[derive(Default, PartialEq, Properties)] struct Bubbling; impl Mixin for Bubbling { @@ -512,6 +529,7 @@ mod tests { #[test] fn cancel_bubbling() { + #[derive(Default, PartialEq, Properties)] struct CancelBubbling; impl Mixin for CancelBubbling { @@ -558,6 +576,7 @@ 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 { @@ -600,10 +619,71 @@ mod tests { assert_count(&el, 2); } + #[test] + fn portal_bubbling() { + // 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 + #[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().host.clone(); + let onclick = { + let link = ctx.link().clone(); + Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }) + }; + let portal = create_portal( + html! { + + {state.action} + + }, + portal_target.clone(), + ); + + html! { + <> +
+ {portal} +
+ {VNode::VRef(portal_target.into())} + + } + } + } + + let (_, el) = init::("a"); + + assert_count(&el, 0); + + el.click(); + assert_count(&el, 1); + + el.click(); + assert_count(&el, 2); + } + fn test_input_listener(make_event: impl Fn() -> E) where E: JsCast + std::fmt::Debug, { + #[derive(Default, PartialEq, Properties)] struct Input; impl Mixin for Input { diff --git a/packages/yew/src/dom_bundle/subtree_root.rs b/packages/yew/src/dom_bundle/subtree_root.rs index 568015758f1..31ce2eb59dd 100644 --- a/packages/yew/src/dom_bundle/subtree_root.rs +++ b/packages/yew/src/dom_bundle/subtree_root.rs @@ -2,38 +2,19 @@ use super::{test_log, EventDescriptor, Registry}; use std::cell::RefCell; -use std::rc::Rc; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::rc::{Rc, Weak}; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsCast; use web_sys::{Element, Event, EventTarget as HtmlEventTarget}; -/// Bubble events during delegation -static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); - -/// Set, if events should bubble up the DOM tree, calling any matching callbacks. -/// -/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event -/// handling performance. -/// -/// Note that yew uses event delegation and implements internal even bubbling for performance -/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event -/// handler has no effect. -/// -/// This function should be called before any component is mounted. -pub fn set_event_bubbling(bubble: bool) { - BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); -} - -// The TreeId is an 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; -/// DOM-Types that capture bubbling events. This generally includes event targets, +/// DOM-Types that capture (bubbling) events. This generally includes event targets, /// but also subtree roots. pub trait EventGrating { - fn responsible_tree_id(&self) -> Option; - fn set_responsible_tree_id(&self, tree_id: TreeId); + fn subtree_id(&self) -> Option; + fn set_subtree_id(&self, tree_id: TreeId); } #[wasm_bindgen] @@ -41,201 +22,363 @@ 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_root_id, structural)] - fn subtree_id(this: &EventTargetable) -> Option; + fn subtree_id(this: &EventTargetable) -> Option; #[wasm_bindgen(method, setter = __yew_subtree_root_id, structural)] - fn set_subtree_id(this: &EventTargetable, id: u32); + fn set_subtree_id(this: &EventTargetable, id: TreeId); } -impl EventGrating for Element { - fn responsible_tree_id(&self) -> Option { - self.unchecked_ref::().subtree_id() - } - fn set_responsible_tree_id(&self, tree_id: TreeId) { - self.unchecked_ref::() - .set_subtree_id(tree_id); +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); + } + } + )* } } -impl EventGrating for HtmlEventTarget { - fn responsible_tree_id(&self) -> Option { - self.unchecked_ref::().subtree_id() - } - fn set_responsible_tree_id(&self, tree_id: TreeId) { - self.unchecked_ref::() - .set_subtree_id(tree_id); - } -} +impl_event_grating!( + HtmlEventTarget; + Event; // We cache the found subtree id on the event. This should speed up repeated searches +); -/// We cache the found subtree id on the event. This should speed up repeated searches -impl EventGrating for Event { - fn responsible_tree_id(&self) -> Option { - self.unchecked_ref::().subtree_id() - } - fn set_responsible_tree_id(&self, tree_id: TreeId) { - self.unchecked_ref::() - .set_subtree_id(tree_id); - } -} +/// 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 = i32; -static NEXT_ROOT_ID: AtomicU32 = AtomicU32::new(1); // Skip 0, used for ssr +/// Special id for caching the fact that some event should not be handled +static NONE_TREE_ID: TreeId = 0; +static NEXT_ROOT_ID: AtomicI32 = AtomicI32::new(1); fn next_root_id() -> TreeId { NEXT_ROOT_ID.fetch_add(1, Ordering::SeqCst) } +type KnownSubtrees = HashMap>; +thread_local! { + static KNOWN_ROOTS: RefCell = RefCell::default(); +} + /// 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(/* None during SSR */ Option>); +pub struct BSubtree( + Option>, // None during SSR +); // 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: Option>, + parent_root: Option>, + // 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(Debug)] -struct InnerBundleRoot { +struct SubtreeData { + subtree_id: TreeId, host: HtmlEventTarget, parent: Option, - tree_root_id: TreeId, event_registry: RefCell, } -struct ClosestInstanceSearchResult { - root_or_listener: Element, - responsible_tree_id: TreeId, - did_bubble: bool, -} +/// Bubble events during delegation +static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); -/// Deduce the subtree responsible for handling this event. This already -/// partially starts the bubbling process, as long as no listeners are encountered, -/// but stops at subtree roots. -/// Event listeners are installed only on the subtree roots. Still, those roots can -/// nest [1]. This would lead to events getting handled multiple times. We want event -/// handling to start at the most deeply nested subtree. +/// Set, if events should bubble up the DOM tree, calling any matching callbacks. /// -/// # When nesting occurs -/// The nested subtree portals into a element that is controlled by the user and rendered -/// with VNode::VRef. We get the following nesting: -/// AppRoot > .. > UserControlledVRef > .. > NestedTree(PortalExit) > .. -/// -------------- ---------------------------- -/// The underlined parts of the hierarchy are controlled by Yew. -fn find_closest_responsible_instance(event: &Event) -> Option { - let target = event.target()?.dyn_into::().ok()?; - if let Some(cached_id) = event.responsible_tree_id() { - return Some(ClosestInstanceSearchResult { - root_or_listener: target, - responsible_tree_id: cached_id, - did_bubble: false, - }); +/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event +/// handling performance. +/// +/// Note that yew uses event delegation and implements internal even bubbling for performance +/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event +/// handler has no effect. +/// +/// This function should be called before any component is mounted. +pub fn set_event_bubbling(bubble: bool) { + BUBBLE_EVENTS.store(bubble, Ordering::Relaxed); +} + +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 { + Some(BrandingSearchResult { + branding: el.subtree_id()?, + 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. +struct BubblingIterator<'tree> { + event: &'tree Event, + subtree: &'tree Rc, + next_el: Option, + should_bubble: bool, +} + +impl<'tree> Iterator for BubblingIterator<'tree> { + type Item = (&'tree Rc, Element); + + fn next(&mut self) -> Option { + let candidate = self.next_el.take()?; + if self.event.cancel_bubble() { + return None; + } + if self.should_bubble { + if let Some((next_subtree, parent)) = candidate + .parent_element() + .and_then(|parent| self.subtree.bubble_to_inner_element(parent, true)) + { + self.subtree = next_subtree; + self.next_el = Some(parent); + } + } + Some((self.subtree, candidate)) + } +} + +impl<'tree> BubblingIterator<'tree> { + fn start_from( + subtree: &'tree Rc, + root_or_listener: Element, + event: &'tree Event, + should_bubble: bool, + ) -> Self { + let start = match subtree.bubble_to_inner_element(root_or_listener, should_bubble) { + Some((subtree, next_el)) => (subtree, Some(next_el)), + None => (subtree, None), + }; + Self { + event, + subtree: start.0, + next_el: start.1, + should_bubble, + } } +} - let mut el = target; - let mut did_bubble = false; - let responsible_tree_id = loop { - if let Some(tree_id) = el.responsible_tree_id() { - break tree_id; +struct SubtreeHierarchyIterator<'tree> { + current: Option<(&'tree Rc, &'tree Element)>, +} + +impl<'tree> Iterator for SubtreeHierarchyIterator<'tree> { + type Item = (&'tree Rc, &'tree Element); + + fn next(&mut self) -> Option { + let next = self.current.take()?; + if let Some(parenting_info) = next.0.parent.as_ref() { + let parent_root = parenting_info + .parent_root + .as_ref() + .expect("Not in SSR, this shouldn't be None"); + self.current = Some((parent_root, &parenting_info.mount_element)); } - el = el.parent_element()?; - did_bubble = true; - }; - event.set_responsible_tree_id(responsible_tree_id); - Some(ClosestInstanceSearchResult { - root_or_listener: el, - responsible_tree_id, - did_bubble, - }) + Some(next) + } } -impl InnerBundleRoot { +impl<'tree> SubtreeHierarchyIterator<'tree> { + fn start_from(subtree: &'tree Rc, el: &'tree Element) -> Self { + Self { + current: Some((subtree, el)), + } + } +} + +impl SubtreeData { + fn new_ref(host_element: &HtmlEventTarget, parent: Option) -> Rc { + let tree_root_id = next_root_id(); + let event_registry = Registry::new(host_element.clone()); + let subtree = Rc::new(SubtreeData { + subtree_id: tree_root_id, + host: host_element.clone(), + parent, + event_registry: RefCell::new(event_registry), + }); + KNOWN_ROOTS.with(|roots| { + roots + .borrow_mut() + .insert(tree_root_id, Rc::downgrade(&subtree)) + }); + subtree + } + fn event_registry(&self) -> &RefCell { &self.event_registry } - /// Handle a global event firing - fn handle(self: &Rc, desc: EventDescriptor, event: Event) { - let closest_instance = match find_closest_responsible_instance(&event) { - Some(closest_instance) if closest_instance.responsible_tree_id == self.tree_root_id => { - closest_instance - } - _ => return, // Don't handle this event - }; - test_log!("Running handler on subtree {}", self.tree_root_id); - if self.host.eq(&closest_instance.root_or_listener) { - let (self_, target) = match self.bubble_at_root() { - Some(bubbled_target) => bubbled_target, - None => return, // No relevant listener + + fn find_by_id(tree_id: TreeId) -> Option> { + KNOWN_ROOTS.with(|roots| { + let mut roots = roots.borrow_mut(); + let subtree = match roots.entry(tree_id) { + Entry::Occupied(subtree) => subtree, + _ => return None, }; - self_.run_handlers(desc, event, target, true); - } else { - let target = closest_instance.root_or_listener; - let did_bubble = closest_instance.did_bubble; - self.run_handlers(desc, event, target, did_bubble); - } + match subtree.get().upgrade() { + Some(subtree) => Some(subtree), + None => { + // Remove stale entry + subtree.remove(); + None + } + } + }) } + // Bubble a potential parent until it reaches an internal element #[allow(clippy::needless_lifetimes)] // I don't see a way to omit the lifetimes here - fn bubble_at_root<'s>(self: &'s Rc) -> Option<(&'s Rc, Element)> { - // we've reached the physical host, delegate to a parent if one exists - let parent = self.parent.as_ref()?; - let parent_root = parent - .parent_root - .as_ref() - .expect("Can't access listeners in SSR"); - Some((parent_root, parent.mount_element.clone())) + fn bubble_to_inner_element<'s>( + self: &'s Rc, + parent_el: Element, + should_bubble: bool, + ) -> Option<(&'s Rc, 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()?; + let parent_root = parent + .parent_root + .as_ref() + .expect("Not in SSR, this shouldn't be None"); + next_subtree = parent_root; + next_el = parent.mount_element.clone(); + } + Some((next_subtree, next_el)) } #[allow(clippy::needless_lifetimes)] // I don't see a way to omit the lifetimes here - fn bubble<'s>(self: &'s Rc, el: Element) -> Option<(&'s Rc, Element)> { - let parent = el.parent_element()?; - if self.host.eq(&parent) { - self.bubble_at_root() + fn start_bubbling_if_responsible<'s>( + self: &'s Rc, + event: &'s Event, + desc: &'s EventDescriptor, + ) -> Option> { + // Note: the event is not necessarily indentically the same object for all installed handlers + // hence this cache can be unreliable. + let self_is_responsible = match event.subtree_id() { + Some(responsible_tree_id) if responsible_tree_id == self.subtree_id => true, + None => false, + // 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. + Some(_) => 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`. The process is very similar + let target = event.target()?.dyn_into::().ok()?; + let should_bubble = BUBBLE_EVENTS.load(Ordering::Relaxed); + let BrandingSearchResult { + branding, + closest_branded_ancestor, + } = find_closest_branded_element(target.clone(), should_bubble)?; + // The branded element can be in a subtree that has no handler installed for the event. + // We say that the most deeply nested subtree that does have a handler installed is "responsible" + // for handling the event. + let (responsible_tree_id, bubble_start) = if branding == self.subtree_id { + // since we're currently in this handler, `self` has a handler installed and is the most + // deeply nested one. This usual case saves a look-up in the global KNOWN_ROOTS. + if self.host.eq(&target) { + // One more special case: don't handle events that get fired directly on a subtree host + // but we still want to cache this fact + (NONE_TREE_ID, closest_branded_ancestor) + } else { + (self.subtree_id, closest_branded_ancestor) + } } else { - Some((self, parent)) - } + // bubble through subtrees until we find one that has a handler installed for the event descriptor + let target_subtree = Self::find_by_id(branding) + .expect("incorrectly branded element: subtree already removed"); + if target_subtree.host.eq(&target) { + (NONE_TREE_ID, closest_branded_ancestor) + } else { + let responsible_tree = SubtreeHierarchyIterator::start_from( + &target_subtree, + &closest_branded_ancestor, + ) + .find(|(candidate, _)| { + if candidate.subtree_id == self.subtree_id { + true + } else if !self_is_responsible { + // only do this check if we aren't sure which subtree is responsible for handling + candidate.event_registry().borrow().has_any_listeners(desc) + } else { + false + } + }) + .expect("nesting error: current subtree should show up in hierarchy"); + (responsible_tree.0.subtree_id, responsible_tree.1.clone()) + } + }; + event.set_subtree_id(responsible_tree_id); // cache it for other event handlers + (responsible_tree_id == self.subtree_id) + .then(|| BubblingIterator::start_from(self, bubble_start, event, 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)} + // } - - fn run_handlers( - self: &Rc, - desc: EventDescriptor, - event: Event, - closest_target: Element, - did_bubble: bool, // did bubble to find the closest target? - ) { + /// Handle a global event firing + fn handle(self: &Rc, desc: EventDescriptor, event: Event) { let run_handler = |root: &Rc, el: &Element| { let handler = Registry::get_handler(root.event_registry(), el, &desc); if let Some(handler) = handler { handler(&event) } }; - - let should_bubble = BUBBLE_EVENTS.load(Ordering::Relaxed); - - // If we bubbled to find closest_target, respect BUBBLE_EVENTS setting - if should_bubble || !did_bubble { - run_handler(self, &closest_target); - } - - let mut current_root = self; - if should_bubble { - let mut el = closest_target; - while !event.cancel_bubble() { - let next = match current_root.bubble(el) { - Some(next) => next, - None => break, - }; - // Destructuring assignments are unstable - current_root = next.0; - el = next.1; - - run_handler(self, &el); + if let Some(bubbling_it) = self.start_bubbling_if_responsible(&event, &desc) { + test_log!("Running handler on subtree {}", self.subtree_id); + for (subtree, el) in bubbling_it { + run_handler(subtree, &el); } } } @@ -246,13 +389,8 @@ impl BSubtree { host_element: &HtmlEventTarget, parent: Option, ) -> Self { - let event_registry = Registry::new(host_element.clone()); - let root = BSubtree(Some(Rc::new(InnerBundleRoot { - host: host_element.clone(), - parent, - tree_root_id: next_root_id(), - event_registry: RefCell::new(event_registry), - }))); + let shared_inner = SubtreeData::new_ref(host_element, parent); + let root = BSubtree(Some(shared_inner)); root.brand_element(host_element); root } @@ -291,6 +429,6 @@ impl BSubtree { pub fn brand_element(&self, el: &dyn EventGrating) { let inner = self.0.as_deref().expect("Can't access listeners in SSR"); - el.set_responsible_tree_id(inner.tree_root_id); + el.set_subtree_id(inner.subtree_id); } } From 0d65e028426855bf375ef7067de599cbb937942f Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 17 Mar 2022 21:50:54 +0100 Subject: [PATCH 06/14] add button to portals/shadow dom example --- examples/portals/src/main.rs | 27 ++++++++++++++++++--- packages/yew/src/dom_bundle/subtree_root.rs | 3 ++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/examples/portals/src/main.rs b/examples/portals/src/main.rs index 7c16713a894..093f2477b06 100644 --- a/examples/portals/src/main.rs +++ b/examples/portals/src/main.rs @@ -68,11 +68,16 @@ impl Component for ShadowDOMHost { } pub struct App { - pub style_html: Html, + style_html: Html, + counter: u32, +} + +pub enum AppMessage { + IncreaseCounter, } impl Component for App { - type Message = (); + type Message = AppMessage; type Properties = (); fn create(_ctx: &Context) -> Self { @@ -85,17 +90,31 @@ impl Component for App { }, document_head.into(), ); - Self { style_html } + Self { + style_html, + counter: 0, + } } - fn view(&self, _ctx: &Context) -> Html { + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + AppMessage::IncreaseCounter => self.counter += 1, + } + true + } + + fn view(&self, ctx: &Context) -> Html { + let onclick = ctx.link().callback(|_| AppMessage::IncreaseCounter); html! { <> {self.style_html.clone()}

{"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"}

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

{format!("The button has been clicked {} times", self.counter)}

} } diff --git a/packages/yew/src/dom_bundle/subtree_root.rs b/packages/yew/src/dom_bundle/subtree_root.rs index 31ce2eb59dd..62d31cad3e2 100644 --- a/packages/yew/src/dom_bundle/subtree_root.rs +++ b/packages/yew/src/dom_bundle/subtree_root.rs @@ -155,6 +155,7 @@ impl<'tree> Iterator for BubblingIterator<'tree> { fn next(&mut self) -> Option { let candidate = self.next_el.take()?; + let candidate_parent = self.subtree; if self.event.cancel_bubble() { return None; } @@ -167,7 +168,7 @@ impl<'tree> Iterator for BubblingIterator<'tree> { self.next_el = Some(parent); } } - Some((self.subtree, candidate)) + Some((candidate_parent, candidate)) } } From 59c76ab83cdec00c1a11220901d19571fd2e0a64 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 17 Mar 2022 22:31:31 +0100 Subject: [PATCH 07/14] Update portal documentation --- examples/portals/src/main.rs | 20 +++++++++++++++++++- website/docs/advanced-topics/portals.mdx | 18 ++++++++++++++---- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/examples/portals/src/main.rs b/examples/portals/src/main.rs index 093f2477b06..30f334d6979 100644 --- a/examples/portals/src/main.rs +++ b/examples/portals/src/main.rs @@ -69,6 +69,7 @@ impl Component for ShadowDOMHost { pub struct App { style_html: Html, + title_element: Element, counter: u32, } @@ -84,6 +85,11 @@ impl Component for App { 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! { @@ -92,6 +98,7 @@ impl Component for App { ); Self { style_html, + title_element, counter: 0, } } @@ -105,6 +112,16 @@ impl Component for App { 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()} @@ -114,7 +131,8 @@ impl Component for App { {"Buttons clicked inside the shadow dom work fine."} -

{format!("The button has been clicked {} times", self.counter)}

+

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

+ {title} } } diff --git a/website/docs/advanced-topics/portals.mdx b/website/docs/advanced-topics/portals.mdx index 8551dfcc17e..8a660015726 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,15 @@ 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. + ## Further reading - [Portals example](https://github.com/yewstack/yew/tree/master/examples/portals) From 865c7303c1bf8ded9b87c9e3f9e8ed0c9c8ac553 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Tue, 22 Mar 2022 19:00:41 +0100 Subject: [PATCH 08/14] add listeners to all subtree roots this should take care of catching original events in shadow doms --- packages/yew/src/dom_bundle/btag/listeners.rs | 92 +---- packages/yew/src/dom_bundle/btag/mod.rs | 2 +- packages/yew/src/dom_bundle/mod.rs | 3 +- packages/yew/src/dom_bundle/subtree_root.rs | 330 +++++++++++------- 4 files changed, 204 insertions(+), 223 deletions(-) diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs index f8cdc03d5c3..67c2d4f383f 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -1,10 +1,9 @@ use super::Apply; -use crate::dom_bundle::{test_log, BSubtree}; -use crate::virtual_dom::{Listener, ListenerKind, Listeners}; +use crate::dom_bundle::{test_log, BSubtree, EventDescriptor}; +use crate::virtual_dom::{Listener, Listeners}; use ::wasm_bindgen::{prelude::wasm_bindgen, JsCast}; -use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase}; use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::ops::Deref; use std::rc::Rc; use web_sys::{Element, Event, EventTarget as HtmlEventTarget}; @@ -110,103 +109,24 @@ impl ListenerRegistration { } } -#[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, - - /// 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 HostHandlers { - fn new(host: HtmlEventTarget) -> Self { - Self { - host, - handling: HashSet::default(), - #[cfg(test)] - registered: Vec::default(), - } - } - - /// Ensure a descriptor has a global event handler assigned - fn ensure_handled(&mut self, root: &BSubtree, desc: EventDescriptor) { - if !self.handling.contains(&desc) { - 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, - root.event_listener(desc), - ) - }; - - // Never drop the closure as this event handler is static - #[cfg(not(test))] - cl.forget(); - #[cfg(test)] - self.registered.push((desc.kind.clone(), cl)); - - self.handling.insert(desc); - } - } -} - /// Global multiplexing event handler registry #[derive(Debug)] pub struct Registry { /// Counter for assigning new IDs id_counter: u32, - /// Registered global event handlers - global: HostHandlers, - /// Contains all registered event listeners by listener ID by_id: HashMap>>>, } impl Registry { - pub fn new(host: HtmlEventTarget) -> Self { + pub fn new() -> Self { Self { id_counter: u32::default(), - global: HostHandlers::new(host), by_id: HashMap::default(), } } - /// Check if this registry has any listeners for the given event descriptor - pub fn has_any_listeners(&self, desc: &EventDescriptor) -> bool { - self.global.handling.contains(desc) - } - /// Handle a single event, given the listening element and event descriptor. pub fn get_handler( registry: &RefCell, @@ -234,7 +154,7 @@ impl Registry { 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(root, desc.clone()); + root.ensure_handled(&desc); by_desc.entry(desc).or_default().push(l); } self.by_id.insert(id, by_desc); @@ -250,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(root, desc.clone()); + root.ensure_handled(&desc); by_desc.entry(desc).or_default().push(l); } } diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index 18bb25a264e..5143a114552 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -3,7 +3,7 @@ mod attributes; mod listeners; -pub use listeners::{EventDescriptor, Registry}; +pub use listeners::Registry; use super::{insert_node, BList, BNode, BSubtree, DomBundle, Reconcilable}; use crate::html::AnyScope; diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 523eb223e5c..12ad47eed5c 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -23,8 +23,9 @@ use self::blist::BList; use self::bnode::BNode; use self::bportal::BPortal; use self::bsuspense::BSuspense; -use self::btag::{BTag, EventDescriptor, Registry}; +use self::btag::{BTag, Registry}; use self::btext::BText; +use self::subtree_root::EventDescriptor; pub(crate) use self::bcomp::{ComponentRenderState, Mountable, PropsWrapper, Scoped}; pub(crate) use self::subtree_root::BSubtree; diff --git a/packages/yew/src/dom_bundle/subtree_root.rs b/packages/yew/src/dom_bundle/subtree_root.rs index 62d31cad3e2..8dbbe4a0311 100644 --- a/packages/yew/src/dom_bundle/subtree_root.rs +++ b/packages/yew/src/dom_bundle/subtree_root.rs @@ -1,9 +1,11 @@ //! Per-subtree state of apps -use super::{test_log, EventDescriptor, Registry}; +use super::{test_log, Registry}; +use crate::virtual_dom::{Listener, ListenerKind}; +use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase}; use std::cell::RefCell; -use std::collections::hash_map::Entry; -use std::collections::HashMap; +use std::collections::HashSet; +use std::hash::{Hash, Hasher}; use std::rc::{Rc, Weak}; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use wasm_bindgen::prelude::wasm_bindgen; @@ -61,11 +63,6 @@ fn next_root_id() -> TreeId { NEXT_ROOT_ID.fetch_add(1, Ordering::SeqCst) } -type KnownSubtrees = HashMap>; -thread_local! { - static KNOWN_ROOTS: RefCell = RefCell::default(); -} - /// Data kept per controlled subtree. [Portal] and [AppHandle] serve as /// hosts. Two controlled subtrees should never overlap. /// @@ -76,24 +73,137 @@ pub struct BSubtree( Option>, // None during SSR ); -// 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 +/// 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: Option>, + 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, - parent: Option, 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 @@ -192,49 +302,25 @@ impl<'tree> BubblingIterator<'tree> { } } -struct SubtreeHierarchyIterator<'tree> { - current: Option<(&'tree Rc, &'tree Element)>, -} - -impl<'tree> Iterator for SubtreeHierarchyIterator<'tree> { - type Item = (&'tree Rc, &'tree Element); - - fn next(&mut self) -> Option { - let next = self.current.take()?; - if let Some(parenting_info) = next.0.parent.as_ref() { - let parent_root = parenting_info - .parent_root - .as_ref() - .expect("Not in SSR, this shouldn't be None"); - self.current = Some((parent_root, &parenting_info.mount_element)); - } - Some(next) - } -} - -impl<'tree> SubtreeHierarchyIterator<'tree> { - fn start_from(subtree: &'tree Rc, el: &'tree Element) -> Self { - Self { - current: Some((subtree, el)), - } - } -} - impl SubtreeData { fn new_ref(host_element: &HtmlEventTarget, parent: Option) -> Rc { let tree_root_id = next_root_id(); - let event_registry = Registry::new(host_element.clone()); + 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(), - parent, event_registry: RefCell::new(event_registry), + global: RefCell::new(host_handlers), }); - KNOWN_ROOTS.with(|roots| { - roots - .borrow_mut() - .insert(tree_root_id, Rc::downgrade(&subtree)) - }); + subtree.app_data.borrow_mut().add_subtree(&subtree); subtree } @@ -242,22 +328,8 @@ impl SubtreeData { &self.event_registry } - fn find_by_id(tree_id: TreeId) -> Option> { - KNOWN_ROOTS.with(|roots| { - let mut roots = roots.borrow_mut(); - let subtree = match roots.entry(tree_id) { - Entry::Occupied(subtree) => subtree, - _ => return None, - }; - match subtree.get().upgrade() { - Some(subtree) => Some(subtree), - None => { - // Remove stale entry - subtree.remove(); - None - } - } - }) + fn host_handlers(&self) -> &RefCell { + &self.global } // Bubble a potential parent until it reaches an internal element @@ -275,11 +347,7 @@ impl SubtreeData { 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()?; - let parent_root = parent - .parent_root - .as_ref() - .expect("Not in SSR, this shouldn't be None"); - next_subtree = parent_root; + next_subtree = &parent.parent_root; next_el = parent.mount_element.clone(); } Some((next_subtree, next_el)) @@ -289,66 +357,50 @@ impl SubtreeData { fn start_bubbling_if_responsible<'s>( self: &'s Rc, event: &'s Event, - desc: &'s EventDescriptor, ) -> Option> { // Note: the event is not necessarily indentically the same object for all installed handlers // hence this cache can be unreliable. - let self_is_responsible = match event.subtree_id() { - Some(responsible_tree_id) if responsible_tree_id == self.subtree_id => true, - None => false, + let cached_responsible_tree_id = event.subtree_id(); + if matches!(cached_responsible_tree_id, 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. - Some(_) => return None, - }; + 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`. The process is very similar - let target = event.target()?.dyn_into::().ok()?; + // run the handling if that's `self`. + let target = event.composed_path().get(0).dyn_into::().ok()?; let should_bubble = BUBBLE_EVENTS.load(Ordering::Relaxed); - let BrandingSearchResult { - branding, - closest_branded_ancestor, - } = find_closest_branded_element(target.clone(), should_bubble)?; - // The branded element can be in a subtree that has no handler installed for the event. - // We say that the most deeply nested subtree that does have a handler installed is "responsible" - // for handling the event. - let (responsible_tree_id, bubble_start) = if branding == self.subtree_id { - // since we're currently in this handler, `self` has a handler installed and is the most - // deeply nested one. This usual case saves a look-up in the global KNOWN_ROOTS. - if self.host.eq(&target) { - // One more special case: don't handle events that get fired directly on a subtree host - // but we still want to cache this fact - (NONE_TREE_ID, closest_branded_ancestor) - } else { - (self.subtree_id, closest_branded_ancestor) - } - } else { - // bubble through subtrees until we find one that has a handler installed for the event descriptor - let target_subtree = Self::find_by_id(branding) - .expect("incorrectly branded element: subtree already removed"); - if target_subtree.host.eq(&target) { - (NONE_TREE_ID, closest_branded_ancestor) + // 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_responsible_tree_id { + (branding, target) + } else if let Some(branding) = find_closest_branded_element(target, should_bubble) { + let BrandingSearchResult { + branding, + closest_branded_ancestor, + } = branding; + event.set_subtree_id(branding); + (branding, closest_branded_ancestor) } else { - let responsible_tree = SubtreeHierarchyIterator::start_from( - &target_subtree, - &closest_branded_ancestor, - ) - .find(|(candidate, _)| { - if candidate.subtree_id == self.subtree_id { - true - } else if !self_is_responsible { - // only do this check if we aren't sure which subtree is responsible for handling - candidate.event_registry().borrow().has_any_listeners(desc) - } else { - false - } - }) - .expect("nesting error: current subtree should show up in hierarchy"); - (responsible_tree.0.subtree_id, responsible_tree.1.clone()) - } - }; - event.set_subtree_id(responsible_tree_id); // cache it for other event handlers - (responsible_tree_id == self.subtree_id) - .then(|| BubblingIterator::start_from(self, bubble_start, event, should_bubble)) + // Possible only? if bubbling is disabled + // No tree should handle this event + event.set_subtree_id(NONE_TREE_ID); + return None; + }; + if self.subtree_id != responsible_tree_id { + return None; + } + if self.host.eq(&bubbling_start) { + // One more special case: don't handle events that get fired directly on a subtree host + return None; + } + Some(BubblingIterator::start_from( + self, + bubbling_start, + event, + should_bubble, + )) // # More details: When nesting occurs // // Event listeners are installed only on the subtree roots. Still, those roots can @@ -376,13 +428,25 @@ impl SubtreeData { handler(&event) } }; - if let Some(bubbling_it) = self.start_bubbling_if_responsible(&event, &desc) { + 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 { 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 { @@ -402,11 +466,16 @@ impl BSubtree { /// 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(), + let parent_information = self.0.as_ref().map(|parent_info| ParentingInformation { + parent_root: parent_info.clone(), mount_element: mount_point, - }; - Self::do_create_root(host_element, Some(parent_information)) + }); + Self::do_create_root(host_element, parent_information) + } + /// Ensure the event described is handled on all subtrees + pub fn ensure_handled(&self, desc: &EventDescriptor) { + let inner = self.0.as_deref().expect("Can't access listeners in SSR"); + inner.app_data.borrow_mut().ensure_handled(desc); } /// Create a bundle root for ssr #[cfg(feature = "ssr")] @@ -419,15 +488,6 @@ impl BSubtree { let inner = self.0.as_deref().expect("Can't access listeners in SSR"); f(&mut *inner.event_registry().borrow_mut()) } - /// Return a closure that should be installed as an event listener on the root element for a specific - /// kind of event. - pub fn event_listener(&self, desc: EventDescriptor) -> impl 'static + FnMut(&Event) { - let inner = self.0.clone().expect("Can't access listeners in SSR"); // capture the registry - move |e: &Event| { - inner.handle(desc.clone(), e.clone()); - } - } - pub fn brand_element(&self, el: &dyn EventGrating) { let inner = self.0.as_deref().expect("Can't access listeners in SSR"); el.set_subtree_id(inner.subtree_id); From df3b8e05efec520e60604468a10013e3a026a9a7 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Tue, 22 Mar 2022 19:35:21 +0100 Subject: [PATCH 09/14] change ShadowRootMode in example to open --- examples/portals/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/portals/src/main.rs b/examples/portals/src/main.rs index 30f334d6979..20348e0401b 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") From 3cacd75af65ff84324583654c5cec0ad71e3ea62 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Tue, 22 Mar 2022 22:48:20 +0100 Subject: [PATCH 10/14] shift need not have access to the current root --- packages/yew/src/dom_bundle/bcomp.rs | 2 +- packages/yew/src/dom_bundle/blist.rs | 6 +++--- packages/yew/src/dom_bundle/bnode.rs | 14 +++++++------- packages/yew/src/dom_bundle/bportal.rs | 9 +++------ packages/yew/src/dom_bundle/bsuspense.rs | 9 ++++----- packages/yew/src/dom_bundle/btag/mod.rs | 2 +- packages/yew/src/dom_bundle/btext.rs | 2 +- packages/yew/src/dom_bundle/mod.rs | 4 ++-- packages/yew/src/dom_bundle/traits.rs | 2 +- packages/yew/src/html/component/lifecycle.rs | 3 +-- 10 files changed, 24 insertions(+), 29 deletions(-) diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index aa2d8789adb..d23b90b0adf 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -36,7 +36,7 @@ impl ReconcileTarget for BComp { self.scope.destroy_boxed(parent_to_detach); } - fn shift(&self, _next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { self.scope.shift_node(next_parent.clone(), next_sibling); } } diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs index 207e8a8b3c1..fc0caf9201a 100644 --- a/packages/yew/src/dom_bundle/blist.rs +++ b/packages/yew/src/dom_bundle/blist.rs @@ -60,7 +60,7 @@ impl<'s> NodeWriter<'s> { /// Shift a bundle into place without patching it fn shift(&self, bundle: &mut BNode) { - bundle.shift(self.root, self.parent, self.next_sibling.clone()); + bundle.shift(self.parent, self.next_sibling.clone()); } /// Patch a bundle with a new node @@ -373,9 +373,9 @@ impl ReconcileTarget for BList { } } - fn shift(&self, next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { for node in self.rev_children.iter().rev() { - node.shift(next_root, next_parent, next_sibling.clone()); + node.shift(next_parent, next_sibling.clone()); } } } diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs index 95c071dbd6a..f04928da189 100644 --- a/packages/yew/src/dom_bundle/bnode.rs +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -60,19 +60,19 @@ impl ReconcileTarget for BNode { } } - fn shift(&self, next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { match self { - Self::Tag(ref vtag) => vtag.shift(next_root, next_parent, next_sibling), - Self::Text(ref btext) => btext.shift(next_root, next_parent, next_sibling), - Self::Comp(ref bsusp) => bsusp.shift(next_root, next_parent, next_sibling), - Self::List(ref vlist) => vlist.shift(next_root, next_parent, next_sibling), + Self::Tag(ref vtag) => vtag.shift(next_parent, next_sibling), + Self::Text(ref btext) => btext.shift(next_parent, next_sibling), + Self::Comp(ref bsusp) => bsusp.shift(next_parent, next_sibling), + Self::List(ref vlist) => vlist.shift(next_parent, next_sibling), Self::Ref(ref node) => { next_parent .insert_before(node, next_sibling.get().as_ref()) .unwrap(); } - Self::Portal(ref vportal) => vportal.shift(next_root, next_parent, next_sibling), - Self::Suspense(ref vsuspense) => vsuspense.shift(next_root, next_parent, next_sibling), + Self::Portal(ref vportal) => vportal.shift(next_parent, next_sibling), + Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling), } } } diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs index eb17265a7a8..155f37c60ae 100644 --- a/packages/yew/src/dom_bundle/bportal.rs +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -26,7 +26,7 @@ impl ReconcileTarget for BPortal { self.node.detach(&self.inner_root, &self.host, false); } - fn shift(&self, _next_root: &BSubtree, _next_parent: &Element, _next_sibling: NodeRef) { + fn shift(&self, _next_parent: &Element, _next_sibling: NodeRef) { // portals have nothing in it's original place of DOM, we also do nothing. } } @@ -95,11 +95,8 @@ 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.inner_root, - &portal.host, - portal.inner_sibling.clone(), - ); + let inner_sibling = portal.inner_sibling.clone(); + portal.node.shift(&portal.host, inner_sibling); } node.reconcile_node( &portal.inner_root, diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs index 1748b9f492e..d653640d476 100644 --- a/packages/yew/src/dom_bundle/bsuspense.rs +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -41,9 +41,8 @@ impl ReconcileTarget for BSuspense { } } - fn shift(&self, next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { - self.active_node() - .shift(next_root, next_parent, next_sibling) + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + self.active_node().shift(next_parent, next_sibling) } } @@ -154,7 +153,7 @@ impl Reconcilable for VSuspense { } // Freshly suspended. Shift children into the detached parent, then add fallback to the DOM (true, None) => { - children_bundle.shift(root, &suspense.detached_parent, NodeRef::default()); + children_bundle.shift(&suspense.detached_parent, NodeRef::default()); children.reconcile_node( root, @@ -177,7 +176,7 @@ impl Reconcilable for VSuspense { .unwrap() // We just matched Some(_) .detach(root, parent, false); - children_bundle.shift(root, parent, next_sibling.clone()); + children_bundle.shift(parent, next_sibling.clone()); children.reconcile_node(root, parent_scope, parent, next_sibling, children_bundle) } } diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index ff324477c28..c7a63f38849 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -92,7 +92,7 @@ impl ReconcileTarget for BTag { } } - fn shift(&self, _next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { next_parent .insert_before(&self.reference, next_sibling.get().as_ref()) .unwrap(); diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs index 6a404196521..969ce3eef02 100644 --- a/packages/yew/src/dom_bundle/btext.rs +++ b/packages/yew/src/dom_bundle/btext.rs @@ -25,7 +25,7 @@ impl ReconcileTarget for BText { } } - fn shift(&self, _next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { let node = &self.text_node; next_parent diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 44e5864bde2..63395ef02f5 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -55,8 +55,8 @@ impl Bundle { } /// Shifts the bundle into a different position. - pub fn shift(&self, next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef) { - self.0.shift(next_root, next_parent, next_sibling); + pub fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + self.0.shift(next_parent, next_sibling); } /// Applies a virtual dom layout to current bundle. diff --git a/packages/yew/src/dom_bundle/traits.rs b/packages/yew/src/dom_bundle/traits.rs index bfff57aa4e2..a9c1639e91c 100644 --- a/packages/yew/src/dom_bundle/traits.rs +++ b/packages/yew/src/dom_bundle/traits.rs @@ -15,7 +15,7 @@ pub(super) trait ReconcileTarget { /// Move elements from one parent to another parent. /// This is for example used by `VSuspense` to preserve component state without detaching /// (which destroys component state). - fn shift(&self, next_root: &BSubtree, next_parent: &Element, next_sibling: NodeRef); + fn shift(&self, next_parent: &Element, next_sibling: NodeRef); } /// This trait provides features to update a tree by calculating a difference against another tree. diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 4bdbb59ed5a..5d5e43a6dc6 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -73,12 +73,11 @@ impl ComponentRenderState { #[cfg(feature = "csr")] Self::Render { bundle, - ref root, parent, next_sibling, .. } => { - bundle.shift(root, &next_parent, next_next_sibling.clone()); + bundle.shift(&next_parent, next_next_sibling.clone()); *parent = next_parent; *next_sibling = next_next_sibling; From f00755b65d100ffb0d5f7e7e82b0a50e6be97d8e Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Tue, 22 Mar 2022 23:02:38 +0100 Subject: [PATCH 11/14] fixup of merge --- packages/yew/src/html/component/scope.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 0d033d50b38..f48168f9e44 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -410,6 +410,7 @@ mod feat_csr { props: Rc, ) { let bundle = Bundle::new(); + node_ref.link(next_sibling.clone()); let state = ComponentRenderState::Render { bundle, root, From ac8f9804a404a390ce5dee7c987b049dfc9eec8a Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 23 Mar 2022 03:36:11 +0100 Subject: [PATCH 12/14] add shadow dom test case --- packages/yew/Cargo.toml | 8 + packages/yew/src/dom_bundle/btag/listeners.rs | 274 ++++++++++-------- .../base_component_impl-fail.stderr | 20 +- 3 files changed, 164 insertions(+), 138 deletions(-) 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/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs index 9ec034febfb..d5676316d86 100644 --- a/packages/yew/src/dom_bundle/btag/listeners.rs +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -199,12 +199,12 @@ 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::{ create_portal, html, html::TargetCast, scheduler, virtual_dom::VNode, AppHandle, Component, - Context, Html, Properties, + Context, Html, NodeRef, Properties, }; use gloo_utils::document; use wasm_bindgen::JsCast; @@ -224,10 +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; + C: Component>; } struct Comp @@ -243,7 +249,7 @@ mod tests { M: Mixin + Properties + 'static, { type Message = Message; - type Properties = M; + type Properties = MixinProps; fn create(_: &Context) -> Self { Comp { @@ -273,34 +279,48 @@ mod tests { } #[track_caller] - fn assert_count(el: &web_sys::HtmlElement, count: isize) { - assert_eq!(el.text_content(), Some(count.to_string())) + 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 + Properties + Default, { // Remove any existing elements - if let Some(el) = document().query_selector(tag).unwrap() { - el.parent_element().unwrap().remove(); + 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] @@ -311,21 +331,17 @@ mod tests { impl Mixin for Synchronous { 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 onclick = ctx.link().callback(|_| Message::Action); if state.stop_listening { html! { - {state.action} + {state.action} } } else { html! { - + {state.action} } @@ -333,20 +349,20 @@ mod tests { } } - 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); } @@ -358,7 +374,7 @@ mod tests { 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 |_| { @@ -367,7 +383,7 @@ mod tests { }); html! {
- + {state.action} @@ -376,7 +392,7 @@ mod tests { } } - let (_, el) = init::("a"); + let (_, el) = init::(); assert_count(&el, 0); @@ -404,25 +420,21 @@ mod tests { 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! { @@ -431,19 +443,17 @@ 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); } @@ -455,24 +465,17 @@ mod tests { 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! { @@ -480,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); } @@ -502,24 +503,17 @@ mod tests { 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! {
@@ -528,22 +522,20 @@ 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); } + /// 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() { - // 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 #[derive(PartialEq, Properties)] struct PortalBubbling { host: web_sys::Element, @@ -558,29 +550,18 @@ mod tests { impl Mixin for PortalBubbling { fn view(ctx: &Context, state: &State) -> Html where - C: Component, + C: Component>, { - let portal_target = ctx.props().host.clone(); - let onclick = { - let link = ctx.link().clone(); - Callback::from(move |_| { - link.send_message(Message::Action); - scheduler::start_now(); - }) - }; - let portal = create_portal( - html! { - - {state.action} - - }, - portal_target.clone(), - ); - + let portal_target = ctx.props().wrapped.host.clone(); + let onclick = ctx.link().callback(|_| Message::Action); html! { <>
- {portal} + {create_portal(html! { + + {state.action} + + }, portal_target.clone())}
{VNode::VRef(portal_target.into())} @@ -588,20 +569,64 @@ mod tests { } } - let (_, el) = init::("a"); + let (_, el) = init::(); assert_count(&el, 0); - - el.click(); + 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; @@ -609,45 +634,41 @@ mod tests { 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" { @@ -656,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/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` From e18db7380942a702d2004844bd650ef1d3162518 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Wed, 23 Mar 2022 04:50:01 +0100 Subject: [PATCH 13/14] cache invalidation & document limitations --- examples/portals/Cargo.toml | 1 + examples/portals/src/main.rs | 19 +++-- packages/yew/src/dom_bundle/subtree_root.rs | 79 ++++++++++++++------- website/docs/advanced-topics/portals.mdx | 5 ++ website/docs/concepts/html/events.mdx | 17 +++++ 5 files changed, 88 insertions(+), 33 deletions(-) diff --git a/examples/portals/Cargo.toml b/examples/portals/Cargo.toml index 308c2f8db5b..07300a4d603 100644 --- a/examples/portals/Cargo.toml +++ b/examples/portals/Cargo.toml @@ -8,6 +8,7 @@ license = "MIT OR Apache-2.0" [dependencies] yew = { path = "../../packages/yew", features = ["csr"] } gloo-utils = "0.1" +gloo-console = "*" wasm-bindgen = "0.2" [dependencies.web-sys] diff --git a/examples/portals/src/main.rs b/examples/portals/src/main.rs index 84aca7cbfd1..56af33ab20d 100644 --- a/examples/portals/src/main.rs +++ b/examples/portals/src/main.rs @@ -111,7 +111,10 @@ impl Component for App { } fn view(&self, ctx: &Context) -> Html { - let onclick = ctx.link().callback(|_| AppMessage::IncreaseCounter); + let onclick = ctx.link().callback(|ev: web_sys::MouseEvent| { + gloo_console::log!(&ev, &ev.composed_path()); + AppMessage::IncreaseCounter + }); let title = create_portal( html! { if self.counter > 0 { @@ -126,12 +129,14 @@ impl Component for App { <> {self.style_html.clone()}

{"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"}

- {"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)}

+
+ +

{"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)}

+
{title} } diff --git a/packages/yew/src/dom_bundle/subtree_root.rs b/packages/yew/src/dom_bundle/subtree_root.rs index 5105b807e38..c0329a72dbe 100644 --- a/packages/yew/src/dom_bundle/subtree_root.rs +++ b/packages/yew/src/dom_bundle/subtree_root.rs @@ -7,7 +7,7 @@ use std::cell::RefCell; use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::rc::{Rc, Weak}; -use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +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}; @@ -17,16 +17,24 @@ use web_sys::{Element, Event, EventTarget as HtmlEventTarget}; 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_root_id, structural)] + #[wasm_bindgen(method, getter = __yew_subtree_id, structural)] fn subtree_id(this: &EventTargetable) -> Option; - #[wasm_bindgen(method, setter = __yew_subtree_root_id, structural)] + #[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 { @@ -40,6 +48,12 @@ macro_rules! impl_event_grating { 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) + } } )* } @@ -53,11 +67,11 @@ impl_event_grating!( /// 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 = i32; +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: AtomicI32 = AtomicI32::new(1); +static NEXT_ROOT_ID: AtomicU32 = AtomicU32::new(1); fn next_root_id() -> TreeId { NEXT_ROOT_ID.fetch_add(1, Ordering::SeqCst) @@ -358,9 +372,21 @@ impl SubtreeData { event: &'s Event, ) -> Option> { // Note: the event is not necessarily indentically the same object for all installed handlers - // hence this cache can be unreliable. - let cached_responsible_tree_id = event.subtree_id(); - if matches!(cached_responsible_tree_id, Some(responsible_tree_id) if responsible_tree_id != self.subtree_id) + // 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. @@ -368,29 +394,30 @@ impl SubtreeData { } // 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.composed_path().get(0).dyn_into::().ok()?; + 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_responsible_tree_id { - (branding, target) - } else if let Some(branding) = find_closest_branded_element(target, should_bubble) { - let BrandingSearchResult { - branding, - closest_branded_ancestor, - } = branding; - event.set_subtree_id(branding); - (branding, closest_branded_ancestor) - } else { - // Possible only? if bubbling is disabled - // No tree should handle this event - event.set_subtree_id(NONE_TREE_ID); - return None; - }; + 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(&bubbling_start) { + if self.host.eq(&target) { // One more special case: don't handle events that get fired directly on a subtree host return None; } diff --git a/website/docs/advanced-topics/portals.mdx b/website/docs/advanced-topics/portals.mdx index 8a660015726..2a355f98ad7 100644 --- a/website/docs/advanced-topics/portals.mdx +++ b/website/docs/advanced-topics/portals.mdx @@ -54,5 +54,10 @@ 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 From e416162edf9f71316adf8c8b8026fa061ca285f0 Mon Sep 17 00:00:00 2001 From: Martin Molzer Date: Thu, 24 Mar 2022 16:58:13 +0100 Subject: [PATCH 14/14] address review comments, slight refactorings --- examples/portals/Cargo.toml | 1 - examples/portals/src/main.rs | 11 +-- packages/yew/src/dom_bundle/mod.rs | 2 +- packages/yew/src/dom_bundle/subtree_root.rs | 83 ++++++--------------- 4 files changed, 28 insertions(+), 69 deletions(-) diff --git a/examples/portals/Cargo.toml b/examples/portals/Cargo.toml index 07300a4d603..308c2f8db5b 100644 --- a/examples/portals/Cargo.toml +++ b/examples/portals/Cargo.toml @@ -8,7 +8,6 @@ license = "MIT OR Apache-2.0" [dependencies] yew = { path = "../../packages/yew", features = ["csr"] } gloo-utils = "0.1" -gloo-console = "*" wasm-bindgen = "0.2" [dependencies.web-sys] diff --git a/examples/portals/src/main.rs b/examples/portals/src/main.rs index 56af33ab20d..da751f49c28 100644 --- a/examples/portals/src/main.rs +++ b/examples/portals/src/main.rs @@ -111,10 +111,7 @@ impl Component for App { } fn view(&self, ctx: &Context) -> Html { - let onclick = ctx.link().callback(|ev: web_sys::MouseEvent| { - gloo_console::log!(&ev, &ev.composed_path()); - AppMessage::IncreaseCounter - }); + let onclick = ctx.link().callback(|_| AppMessage::IncreaseCounter); let title = create_portal( html! { if self.counter > 0 { @@ -128,16 +125,16 @@ impl Component for App { 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"}

{"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)}

- {title} } } diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs index 63395ef02f5..16df0b97db3 100644 --- a/packages/yew/src/dom_bundle/mod.rs +++ b/packages/yew/src/dom_bundle/mod.rs @@ -50,7 +50,7 @@ pub(crate) struct Bundle(BNode); impl Bundle { /// Creates a new bundle. - pub fn new() -> Self { + pub const fn new() -> Self { Self(BNode::List(BList::new())) } diff --git a/packages/yew/src/dom_bundle/subtree_root.rs b/packages/yew/src/dom_bundle/subtree_root.rs index c0329a72dbe..34b8e007bbd 100644 --- a/packages/yew/src/dom_bundle/subtree_root.rs +++ b/packages/yew/src/dom_bundle/subtree_root.rs @@ -154,7 +154,6 @@ impl HostHandlers { /// Per subtree data #[derive(Debug)] - struct SubtreeData { /// Data shared between all trees in an app app_data: Rc>, @@ -246,8 +245,9 @@ struct BrandingSearchResult { /// 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: el.subtree_id()?, + branding, closest_branded_ancestor: el, }) } else { @@ -266,53 +266,20 @@ fn find_closest_branded_element(mut el: Element, do_bubble: bool) -> Option { - event: &'tree Event, - subtree: &'tree Rc, - next_el: Option, +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); -impl<'tree> Iterator for BubblingIterator<'tree> { - type Item = (&'tree Rc, Element); - - fn next(&mut self) -> Option { - let candidate = self.next_el.take()?; - let candidate_parent = self.subtree; - if self.event.cancel_bubble() { + std::iter::successors(start, move |(subtree, element)| { + if !should_bubble { return None; } - if self.should_bubble { - if let Some((next_subtree, parent)) = candidate - .parent_element() - .and_then(|parent| self.subtree.bubble_to_inner_element(parent, true)) - { - self.subtree = next_subtree; - self.next_el = Some(parent); - } - } - Some((candidate_parent, candidate)) - } -} - -impl<'tree> BubblingIterator<'tree> { - fn start_from( - subtree: &'tree Rc, - root_or_listener: Element, - event: &'tree Event, - should_bubble: bool, - ) -> Self { - let start = match subtree.bubble_to_inner_element(root_or_listener, should_bubble) { - Some((subtree, next_el)) => (subtree, Some(next_el)), - None => (subtree, None), - }; - Self { - event, - subtree: start.0, - next_el: start.1, - should_bubble, - } - } + let parent = element.parent_element()?; + subtree.bubble_to_inner_element(parent, true) + }) } impl SubtreeData { @@ -346,12 +313,11 @@ impl SubtreeData { } // Bubble a potential parent until it reaches an internal element - #[allow(clippy::needless_lifetimes)] // I don't see a way to omit the lifetimes here - fn bubble_to_inner_element<'s>( - self: &'s Rc, + fn bubble_to_inner_element( + &self, parent_el: Element, should_bubble: bool, - ) -> Option<(&'s Rc, Element)> { + ) -> Option<(&Self, Element)> { let mut next_subtree = self; let mut next_el = parent_el; if !should_bubble && next_subtree.host.eq(&next_el) { @@ -366,11 +332,10 @@ impl SubtreeData { Some((next_subtree, next_el)) } - #[allow(clippy::needless_lifetimes)] // I don't see a way to omit the lifetimes here fn start_bubbling_if_responsible<'s>( - self: &'s Rc, + &'s self, event: &'s Event, - ) -> Option> { + ) -> 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! @@ -421,12 +386,7 @@ impl SubtreeData { // One more special case: don't handle events that get fired directly on a subtree host return None; } - Some(BubblingIterator::start_from( - self, - bubbling_start, - event, - should_bubble, - )) + 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 @@ -447,8 +407,8 @@ impl SubtreeData { // } /// Handle a global event firing - fn handle(self: &Rc, desc: EventDescriptor, event: Event) { - let run_handler = |root: &Rc, el: &Element| { + 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) @@ -457,6 +417,9 @@ impl SubtreeData { 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); } }