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! { };
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! {
@@ -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! {
<>
+ }
+ }
+ }
+ 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);
}
}