diff --git a/examples/portals/src/main.rs b/examples/portals/src/main.rs
index 39b46a4f7..da751f49c 100644
--- a/examples/portals/src/main.rs
+++ b/examples/portals/src/main.rs
@@ -31,7 +31,7 @@ impl Component for ShadowDOMHost {
.get()
.expect("rendered host")
.unchecked_into::()
- .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Closed))
+ .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open))
.expect("installing shadow root succeeds");
let inner_host = gloo_utils::document()
.create_element("div")
@@ -68,34 +68,73 @@ impl Component for ShadowDOMHost {
}
pub struct App {
- pub style_html: Html,
+ style_html: Html,
+ title_element: Element,
+ counter: u32,
+}
+
+pub enum AppMessage {
+ IncreaseCounter,
}
impl Component for App {
- type Message = ();
+ type Message = AppMessage;
type Properties = ();
fn create(_ctx: &Context) -> Self {
let document_head = gloo_utils::document()
.head()
.expect("head element to be present");
+ let title_element = document_head
+ .query_selector("title")
+ .expect("to find a title element")
+ .expect("to find a title element");
+ title_element.set_text_content(None); // Clear the title element
let style_html = create_portal(
html! {
},
document_head.into(),
);
- Self { style_html }
+ Self {
+ style_html,
+ title_element,
+ counter: 0,
+ }
}
- fn 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);
+ let title = create_portal(
+ html! {
+ if self.counter > 0 {
+ {format!("Clicked {} times", self.counter)}
+ } else {
+ {"Yew • Portals"}
+ }
+ },
+ self.title_element.clone(),
+ );
html! {
<>
{self.style_html.clone()}
+ {title}
{"This paragraph is colored red, and its style is mounted into "}
{"document.head"} {" with a portal"}
-
- {"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}
-
+
+
+ {"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}
+ {"Buttons clicked inside the shadow dom work fine."}
+ {"Click me!"}
+
+
{format!("The button has been clicked {} times. This is also reflected in the title of the tab!", self.counter)}
+
>
}
}
diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml
index ce95f7420..575845f7c 100644
--- a/packages/yew/Cargo.toml
+++ b/packages/yew/Cargo.toml
@@ -76,6 +76,14 @@ wasm-bindgen-futures = "0.4"
rustversion = "1"
trybuild = "1"
+[dev-dependencies.web-sys]
+version = "0.3"
+features = [
+ "ShadowRoot",
+ "ShadowRootInit",
+ "ShadowRootMode",
+]
+
[features]
ssr = ["futures", "html-escape"]
csr = []
diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/app_handle.rs
index a5cc9ca4c..06ac45012 100644
--- a/packages/yew/src/app_handle.rs
+++ b/packages/yew/src/app_handle.rs
@@ -1,5 +1,6 @@
//! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope.
+use crate::dom_bundle::BSubtree;
use crate::html::Scoped;
use crate::html::{IntoComponent, NodeRef, Scope};
use std::ops::Deref;
@@ -22,14 +23,19 @@ where
/// similarly to the `program` function in Elm. You should provide an initial model, `update`
/// function which will update the state of the model and a `view` function which
/// will render the model to a virtual DOM tree.
- pub(crate) fn mount_with_props(element: Element, props: Rc) -> Self {
- clear_element(&element);
+ pub(crate) fn mount_with_props(host: Element, props: Rc) -> Self {
+ clear_element(&host);
let app = Self {
scope: Scope::new(None),
};
-
- app.scope
- .mount_in_place(element, NodeRef::default(), NodeRef::default(), props);
+ let hosting_root = BSubtree::create_root(&host);
+ app.scope.mount_in_place(
+ hosting_root,
+ host,
+ NodeRef::default(),
+ NodeRef::default(),
+ props,
+ );
app
}
@@ -52,8 +58,8 @@ where
}
/// Removes anything from the given element.
-fn clear_element(element: &Element) {
- while let Some(child) = element.last_child() {
- element.remove_child(&child).expect("can't remove a child");
+fn clear_element(host: &Element) {
+ while let Some(child) = host.last_child() {
+ host.remove_child(&child).expect("can't remove a child");
}
}
diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs
index 4b071d2ee..d23b90b0a 100644
--- a/packages/yew/src/dom_bundle/bcomp.rs
+++ b/packages/yew/src/dom_bundle/bcomp.rs
@@ -1,8 +1,7 @@
//! This module contains the bundle implementation of a virtual component [BComp].
-use super::{BNode, Reconcilable, ReconcileTarget};
-use crate::html::AnyScope;
-use crate::html::Scoped;
+use super::{BNode, BSubtree, Reconcilable, ReconcileTarget};
+use crate::html::{AnyScope, Scoped};
use crate::virtual_dom::{Key, VComp};
use crate::NodeRef;
use std::fmt;
@@ -33,7 +32,7 @@ impl fmt::Debug for BComp {
}
impl ReconcileTarget for BComp {
- fn detach(self, _parent: &Element, parent_to_detach: bool) {
+ fn detach(self, _root: &BSubtree, _parent: &Element, parent_to_detach: bool) {
self.scope.destroy_boxed(parent_to_detach);
}
@@ -47,6 +46,7 @@ impl Reconcilable for VComp {
fn attach(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -59,6 +59,7 @@ impl Reconcilable for VComp {
} = self;
let scope = mountable.mount(
+ root,
node_ref.clone(),
parent_scope,
parent.to_owned(),
@@ -78,6 +79,7 @@ impl Reconcilable for VComp {
fn reconcile_node(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -88,14 +90,15 @@ impl Reconcilable for VComp {
BNode::Comp(ref mut bcomp)
if self.type_id == bcomp.type_id && self.key == bcomp.key =>
{
- self.reconcile(parent_scope, parent, next_sibling, bcomp)
+ self.reconcile(root, parent_scope, parent, next_sibling, bcomp)
}
- _ => self.replace(parent_scope, parent, next_sibling, bundle),
+ _ => self.replace(root, parent_scope, parent, next_sibling, bundle),
}
}
fn reconcile(
self,
+ _root: &BSubtree,
_parent_scope: &AnyScope,
_parent: &Element,
next_sibling: NodeRef,
@@ -165,22 +168,15 @@ mod tests {
#[test]
fn update_loop() {
- let document = gloo_utils::document();
- let parent_scope: AnyScope = AnyScope::test();
- let parent_element = document.create_element("div").unwrap();
+ let (root, scope, parent) = setup_parent();
let comp = html! { };
- let (_, mut bundle) = comp.attach(&parent_scope, &parent_element, NodeRef::default());
+ let (_, mut bundle) = comp.attach(&root, &scope, &parent, NodeRef::default());
scheduler::start_now();
for _ in 0..10000 {
let node = html! { };
- node.reconcile_node(
- &parent_scope,
- &parent_element,
- NodeRef::default(),
- &mut bundle,
- );
+ node.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut bundle);
scheduler::start_now();
}
}
@@ -322,27 +318,28 @@ mod tests {
}
}
- fn setup_parent() -> (AnyScope, Element) {
+ fn setup_parent() -> (BSubtree, AnyScope, Element) {
let scope = AnyScope::test();
let parent = document().create_element("div").unwrap();
+ let root = BSubtree::create_root(&parent);
document().body().unwrap().append_child(&parent).unwrap();
- (scope, parent)
+ (root, scope, parent)
}
- fn get_html(node: Html, scope: &AnyScope, parent: &Element) -> String {
+ fn get_html(node: Html, root: &BSubtree, scope: &AnyScope, parent: &Element) -> String {
// clear parent
parent.set_inner_html("");
- node.attach(scope, parent, NodeRef::default());
+ node.attach(root, scope, parent, NodeRef::default());
scheduler::start_now();
parent.inner_html()
}
#[test]
fn all_ways_of_passing_children_work() {
- let (scope, parent) = setup_parent();
+ let (root, scope, parent) = setup_parent();
let children: Vec<_> = vec!["a", "b", "c"]
.drain(..)
@@ -359,7 +356,7 @@ mod tests {
let prop_method = html! {
};
- assert_eq!(get_html(prop_method, &scope, &parent), expected_html);
+ assert_eq!(get_html(prop_method, &root, &scope, &parent), expected_html);
let children_renderer_method = html! {
@@ -367,7 +364,7 @@ mod tests {
};
assert_eq!(
- get_html(children_renderer_method, &scope, &parent),
+ get_html(children_renderer_method, &root, &scope, &parent),
expected_html
);
@@ -376,30 +373,30 @@ mod tests {
{ children.clone() }
};
- assert_eq!(get_html(direct_method, &scope, &parent), expected_html);
+ assert_eq!(
+ get_html(direct_method, &root, &scope, &parent),
+ expected_html
+ );
let for_method = html! {
{ for children }
};
- assert_eq!(get_html(for_method, &scope, &parent), expected_html);
+ assert_eq!(get_html(for_method, &root, &scope, &parent), expected_html);
}
#[test]
fn reset_node_ref() {
- let scope = AnyScope::test();
- let parent = document().create_element("div").unwrap();
-
- document().body().unwrap().append_child(&parent).unwrap();
+ let (root, scope, parent) = setup_parent();
let node_ref = NodeRef::default();
let elem = html! { };
- let (_, elem) = elem.attach(&scope, &parent, NodeRef::default());
+ let (_, elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
scheduler::start_now();
let parent_node = parent.deref();
assert_eq!(node_ref.get(), parent_node.first_child());
- elem.detach(&parent, false);
+ elem.detach(&root, &parent, false);
scheduler::start_now();
assert!(node_ref.get().is_none());
}
diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs
index edd1cfc76..fc0caf920 100644
--- a/packages/yew/src/dom_bundle/blist.rs
+++ b/packages/yew/src/dom_bundle/blist.rs
@@ -1,5 +1,5 @@
//! This module contains fragments bundles, a [BList]
-use super::{test_log, BNode};
+use super::{test_log, BNode, BSubtree};
use crate::dom_bundle::{Reconcilable, ReconcileTarget};
use crate::html::{AnyScope, NodeRef};
use crate::virtual_dom::{Key, VList, VNode, VText};
@@ -31,6 +31,7 @@ impl Deref for BList {
/// Helper struct, that keeps the position where the next element is to be placed at
#[derive(Clone)]
struct NodeWriter<'s> {
+ root: &'s BSubtree,
parent_scope: &'s AnyScope,
parent: &'s Element,
next_sibling: NodeRef,
@@ -45,7 +46,8 @@ impl<'s> NodeWriter<'s> {
self.parent.outer_html(),
self.next_sibling
);
- let (next, bundle) = node.attach(self.parent_scope, self.parent, self.next_sibling);
+ let (next, bundle) =
+ node.attach(self.root, self.parent_scope, self.parent, self.next_sibling);
test_log!(" next_position: {:?}", next);
(
Self {
@@ -70,7 +72,13 @@ impl<'s> NodeWriter<'s> {
self.next_sibling
);
// Advance the next sibling reference (from right to left)
- let next = node.reconcile_node(self.parent_scope, self.parent, self.next_sibling, bundle);
+ let next = node.reconcile_node(
+ self.root,
+ self.parent_scope,
+ self.parent,
+ self.next_sibling,
+ bundle,
+ );
test_log!(" next_position: {:?}", next);
Self {
next_sibling: next,
@@ -135,6 +143,7 @@ impl BList {
/// Diff and patch unkeyed child lists
fn apply_unkeyed(
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -142,6 +151,7 @@ impl BList {
rights: &mut Vec,
) -> NodeRef {
let mut writer = NodeWriter {
+ root,
parent_scope,
parent,
next_sibling,
@@ -151,7 +161,7 @@ impl BList {
if lefts.len() < rights.len() {
for r in rights.drain(lefts.len()..) {
test_log!("removing: {:?}", r);
- r.detach(parent, false);
+ r.detach(root, parent, false);
}
}
@@ -174,6 +184,7 @@ impl BList {
/// Optimized for node addition or removal from either end of the list and small changes in the
/// middle.
fn apply_keyed(
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -204,6 +215,7 @@ impl BList {
if matching_len_end == std::cmp::min(left_vdoms.len(), rev_bundles.len()) {
// No key changes
return Self::apply_unkeyed(
+ root,
parent_scope,
parent,
next_sibling,
@@ -215,6 +227,7 @@ impl BList {
// We partially drain the new vnodes in several steps.
let mut lefts = left_vdoms;
let mut writer = NodeWriter {
+ root,
parent_scope,
parent,
next_sibling,
@@ -336,7 +349,7 @@ impl BList {
// Step 2.3. Remove any extra rights
for KeyedEntry(_, r) in spare_bundles.drain() {
test_log!("removing: {:?}", r);
- r.detach(parent, false);
+ r.detach(root, parent, false);
}
// Step 3. Diff matching children at the start
@@ -354,9 +367,9 @@ impl BList {
}
impl ReconcileTarget for BList {
- fn detach(self, parent: &Element, parent_to_detach: bool) {
+ fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) {
for child in self.rev_children.into_iter() {
- child.detach(parent, parent_to_detach);
+ child.detach(root, parent, parent_to_detach);
}
}
@@ -372,30 +385,33 @@ impl Reconcilable for VList {
fn attach(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle) {
let mut self_ = BList::new();
- let node_ref = self.reconcile(parent_scope, parent, next_sibling, &mut self_);
+ let node_ref = self.reconcile(root, parent_scope, parent, next_sibling, &mut self_);
(node_ref, self_)
}
fn reconcile_node(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut BNode,
) -> NodeRef {
- // 'Forcefully' create a pretend the existing node is a list. Creates a
+ // 'Forcefully' pretend the existing node is a list. Creates a
// singleton list if it isn't already.
let blist = bundle.make_list();
- self.reconcile(parent_scope, parent, next_sibling, blist)
+ self.reconcile(root, parent_scope, parent, next_sibling, blist)
}
fn reconcile(
mut self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -426,9 +442,9 @@ impl Reconcilable for VList {
rights.reserve_exact(additional);
}
let first = if self.fully_keyed && blist.fully_keyed {
- BList::apply_keyed(parent_scope, parent, next_sibling, lefts, rights)
+ BList::apply_keyed(root, parent_scope, parent, next_sibling, lefts, rights)
} else {
- BList::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights)
+ BList::apply_unkeyed(root, parent_scope, parent, next_sibling, lefts, rights)
};
blist.fully_keyed = self.fully_keyed;
blist.key = self.key;
diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs
index 2401ea6f2..f04928da1 100644
--- a/packages/yew/src/dom_bundle/bnode.rs
+++ b/packages/yew/src/dom_bundle/bnode.rs
@@ -1,6 +1,6 @@
//! This module contains the bundle version of an abstract node [BNode]
-use super::{BComp, BList, BPortal, BSuspense, BTag, BText};
+use super::{BComp, BList, BPortal, BSubtree, BSuspense, BTag, BText};
use crate::dom_bundle::{Reconcilable, ReconcileTarget};
use crate::html::{AnyScope, NodeRef};
use crate::virtual_dom::{Key, VNode};
@@ -43,20 +43,20 @@ impl BNode {
impl ReconcileTarget for BNode {
/// Remove VNode from parent.
- fn detach(self, parent: &Element, parent_to_detach: bool) {
+ fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) {
match self {
- Self::Tag(vtag) => vtag.detach(parent, parent_to_detach),
- Self::Text(btext) => btext.detach(parent, parent_to_detach),
- Self::Comp(bsusp) => bsusp.detach(parent, parent_to_detach),
- Self::List(blist) => blist.detach(parent, parent_to_detach),
+ Self::Tag(vtag) => vtag.detach(root, parent, parent_to_detach),
+ Self::Text(btext) => btext.detach(root, parent, parent_to_detach),
+ Self::Comp(bsusp) => bsusp.detach(root, parent, parent_to_detach),
+ Self::List(blist) => blist.detach(root, parent, parent_to_detach),
Self::Ref(ref node) => {
// Always remove user-defined nodes to clear possible parent references of them
if parent.remove_child(node).is_err() {
console::warn!("Node not found to remove VRef");
}
}
- Self::Portal(bportal) => bportal.detach(parent, parent_to_detach),
- Self::Suspense(bsusp) => bsusp.detach(parent, parent_to_detach),
+ Self::Portal(bportal) => bportal.detach(root, parent, parent_to_detach),
+ Self::Suspense(bsusp) => bsusp.detach(root, parent, parent_to_detach),
}
}
@@ -82,25 +82,26 @@ impl Reconcilable for VNode {
fn attach(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle) {
match self {
VNode::VTag(vtag) => {
- let (node_ref, tag) = vtag.attach(parent_scope, parent, next_sibling);
+ let (node_ref, tag) = vtag.attach(root, parent_scope, parent, next_sibling);
(node_ref, tag.into())
}
VNode::VText(vtext) => {
- let (node_ref, text) = vtext.attach(parent_scope, parent, next_sibling);
+ let (node_ref, text) = vtext.attach(root, parent_scope, parent, next_sibling);
(node_ref, text.into())
}
VNode::VComp(vcomp) => {
- let (node_ref, comp) = vcomp.attach(parent_scope, parent, next_sibling);
+ let (node_ref, comp) = vcomp.attach(root, parent_scope, parent, next_sibling);
(node_ref, comp.into())
}
VNode::VList(vlist) => {
- let (node_ref, list) = vlist.attach(parent_scope, parent, next_sibling);
+ let (node_ref, list) = vlist.attach(root, parent_scope, parent, next_sibling);
(node_ref, list.into())
}
VNode::VRef(node) => {
@@ -108,11 +109,12 @@ impl Reconcilable for VNode {
(NodeRef::new(node.clone()), BNode::Ref(node))
}
VNode::VPortal(vportal) => {
- let (node_ref, portal) = vportal.attach(parent_scope, parent, next_sibling);
+ let (node_ref, portal) = vportal.attach(root, parent_scope, parent, next_sibling);
(node_ref, portal.into())
}
VNode::VSuspense(vsuspsense) => {
- let (node_ref, suspsense) = vsuspsense.attach(parent_scope, parent, next_sibling);
+ let (node_ref, suspsense) =
+ vsuspsense.attach(root, parent_scope, parent, next_sibling);
(node_ref, suspsense.into())
}
}
@@ -120,31 +122,42 @@ impl Reconcilable for VNode {
fn reconcile_node(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut BNode,
) -> NodeRef {
- self.reconcile(parent_scope, parent, next_sibling, bundle)
+ self.reconcile(root, parent_scope, parent, next_sibling, bundle)
}
fn reconcile(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut BNode,
) -> NodeRef {
match self {
- VNode::VTag(vtag) => vtag.reconcile_node(parent_scope, parent, next_sibling, bundle),
- VNode::VText(vtext) => vtext.reconcile_node(parent_scope, parent, next_sibling, bundle),
- VNode::VComp(vcomp) => vcomp.reconcile_node(parent_scope, parent, next_sibling, bundle),
- VNode::VList(vlist) => vlist.reconcile_node(parent_scope, parent, next_sibling, bundle),
+ VNode::VTag(vtag) => {
+ vtag.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
+ }
+ VNode::VText(vtext) => {
+ vtext.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
+ }
+ VNode::VComp(vcomp) => {
+ vcomp.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
+ }
+ VNode::VList(vlist) => {
+ vlist.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
+ }
VNode::VRef(node) => {
let _existing = match bundle {
BNode::Ref(ref n) if &node == n => n,
_ => {
return VNode::VRef(node).replace(
+ root,
parent_scope,
parent,
next_sibling,
@@ -155,10 +168,10 @@ impl Reconcilable for VNode {
NodeRef::new(node)
}
VNode::VPortal(vportal) => {
- vportal.reconcile_node(parent_scope, parent, next_sibling, bundle)
+ vportal.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
}
VNode::VSuspense(vsuspsense) => {
- vsuspsense.reconcile_node(parent_scope, parent, next_sibling, bundle)
+ vsuspsense.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
}
}
}
diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs
index 65fe4088a..155f37c60 100644
--- a/packages/yew/src/dom_bundle/bportal.rs
+++ b/packages/yew/src/dom_bundle/bportal.rs
@@ -1,7 +1,6 @@
//! This module contains the bundle implementation of a portal [BPortal].
-use super::test_log;
-use super::BNode;
+use super::{test_log, BNode, BSubtree};
use crate::dom_bundle::{Reconcilable, ReconcileTarget};
use crate::html::{AnyScope, NodeRef};
use crate::virtual_dom::Key;
@@ -10,7 +9,9 @@ use web_sys::Element;
/// The bundle implementation to [VPortal].
#[derive(Debug)]
-pub(super) struct BPortal {
+pub struct BPortal {
+ // The inner root
+ inner_root: BSubtree,
/// The element under which the content is inserted.
host: Element,
/// The next sibling after the inserted content
@@ -20,10 +21,9 @@ pub(super) struct BPortal {
}
impl ReconcileTarget for BPortal {
- fn detach(self, _: &Element, _parent_to_detach: bool) {
- test_log!("Detaching portal from host{:?}", self.host.outer_html());
- self.node.detach(&self.host, false);
- test_log!("Detached portal from host{:?}", self.host.outer_html());
+ fn detach(self, _root: &BSubtree, _parent: &Element, _parent_to_detach: bool) {
+ test_log!("Detaching portal from host",);
+ self.node.detach(&self.inner_root, &self.host, false);
}
fn shift(&self, _next_parent: &Element, _next_sibling: NodeRef) {
@@ -36,8 +36,9 @@ impl Reconcilable for VPortal {
fn attach(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
- _parent: &Element,
+ parent: &Element,
host_next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle) {
let Self {
@@ -45,10 +46,12 @@ impl Reconcilable for VPortal {
inner_sibling,
node,
} = self;
- let (_, inner) = node.attach(parent_scope, &host, inner_sibling.clone());
+ let inner_root = root.create_subroot(parent.clone(), &host);
+ let (_, inner) = node.attach(&inner_root, parent_scope, &host, inner_sibling.clone());
(
host_next_sibling,
BPortal {
+ inner_root,
host,
node: Box::new(inner),
inner_sibling,
@@ -58,19 +61,23 @@ impl Reconcilable for VPortal {
fn reconcile_node(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut BNode,
) -> NodeRef {
match bundle {
- BNode::Portal(portal) => self.reconcile(parent_scope, parent, next_sibling, portal),
- _ => self.replace(parent_scope, parent, next_sibling, bundle),
+ BNode::Portal(portal) => {
+ self.reconcile(root, parent_scope, parent, next_sibling, portal)
+ }
+ _ => self.replace(root, parent_scope, parent, next_sibling, bundle),
}
}
fn reconcile(
self,
+ _root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -88,11 +95,16 @@ impl Reconcilable for VPortal {
if old_host != portal.host || old_inner_sibling != portal.inner_sibling {
// Remount the inner node somewhere else instead of diffing
// Move the node, but keep the state
- portal
- .node
- .shift(&portal.host, portal.inner_sibling.clone());
+ let inner_sibling = portal.inner_sibling.clone();
+ portal.node.shift(&portal.host, inner_sibling);
}
- node.reconcile_node(parent_scope, parent, next_sibling.clone(), &mut portal.node);
+ node.reconcile_node(
+ &portal.inner_root,
+ parent_scope,
+ parent,
+ next_sibling.clone(),
+ &mut portal.node,
+ );
next_sibling
}
}
diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs
index 18d2a2b15..d653640d4 100644
--- a/packages/yew/src/dom_bundle/bsuspense.rs
+++ b/packages/yew/src/dom_bundle/bsuspense.rs
@@ -1,6 +1,6 @@
//! This module contains the bundle version of a supsense [BSuspense]
-use super::{BNode, Reconcilable, ReconcileTarget};
+use super::{BNode, BSubtree, Reconcilable, ReconcileTarget};
use crate::html::AnyScope;
use crate::virtual_dom::{Key, VSuspense};
use crate::NodeRef;
@@ -31,12 +31,13 @@ impl BSuspense {
}
impl ReconcileTarget for BSuspense {
- fn detach(self, parent: &Element, parent_to_detach: bool) {
+ fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) {
if let Some(fallback) = self.fallback_bundle {
- fallback.detach(parent, parent_to_detach);
- self.children_bundle.detach(&self.detached_parent, false);
+ fallback.detach(root, parent, parent_to_detach);
+ self.children_bundle
+ .detach(root, &self.detached_parent, false);
} else {
- self.children_bundle.detach(parent, parent_to_detach);
+ self.children_bundle.detach(root, parent, parent_to_detach);
}
}
@@ -50,6 +51,7 @@ impl Reconcilable for VSuspense {
fn attach(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -68,8 +70,9 @@ impl Reconcilable for VSuspense {
// tree while rendering fallback UI into the original place where children resides in.
if suspended {
let (_child_ref, children_bundle) =
- children.attach(parent_scope, &detached_parent, NodeRef::default());
- let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling);
+ children.attach(root, parent_scope, &detached_parent, NodeRef::default());
+ let (fallback_ref, fallback) =
+ fallback.attach(root, parent_scope, parent, next_sibling);
(
fallback_ref,
BSuspense {
@@ -80,7 +83,8 @@ impl Reconcilable for VSuspense {
},
)
} else {
- let (child_ref, children_bundle) = children.attach(parent_scope, parent, next_sibling);
+ let (child_ref, children_bundle) =
+ children.attach(root, parent_scope, parent, next_sibling);
(
child_ref,
BSuspense {
@@ -95,6 +99,7 @@ impl Reconcilable for VSuspense {
fn reconcile_node(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -103,14 +108,15 @@ impl Reconcilable for VSuspense {
match bundle {
// We only preserve the child state if they are the same suspense.
BNode::Suspense(m) if m.key == self.key => {
- self.reconcile(parent_scope, parent, next_sibling, m)
+ self.reconcile(root, parent_scope, parent, next_sibling, m)
}
- _ => self.replace(parent_scope, parent, next_sibling, bundle),
+ _ => self.replace(root, parent_scope, parent, next_sibling, bundle),
}
}
fn reconcile(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -132,30 +138,33 @@ impl Reconcilable for VSuspense {
// Both suspended, reconcile children into detached_parent, fallback into the DOM
(true, Some(fallback_bundle)) => {
children.reconcile_node(
+ root,
parent_scope,
&suspense.detached_parent,
NodeRef::default(),
children_bundle,
);
- fallback.reconcile_node(parent_scope, parent, next_sibling, fallback_bundle)
+ fallback.reconcile_node(root, parent_scope, parent, next_sibling, fallback_bundle)
}
// Not suspended, just reconcile the children into the DOM
(false, None) => {
- children.reconcile_node(parent_scope, parent, next_sibling, children_bundle)
+ children.reconcile_node(root, parent_scope, parent, next_sibling, children_bundle)
}
// Freshly suspended. Shift children into the detached parent, then add fallback to the DOM
(true, None) => {
children_bundle.shift(&suspense.detached_parent, NodeRef::default());
children.reconcile_node(
+ root,
parent_scope,
&suspense.detached_parent,
NodeRef::default(),
children_bundle,
);
// first render of fallback
- let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling);
+ let (fallback_ref, fallback) =
+ fallback.attach(root, parent_scope, parent, next_sibling);
suspense.fallback_bundle = Some(fallback);
fallback_ref
}
@@ -165,10 +174,10 @@ impl Reconcilable for VSuspense {
.fallback_bundle
.take()
.unwrap() // We just matched Some(_)
- .detach(parent, false);
+ .detach(root, parent, false);
children_bundle.shift(parent, next_sibling.clone());
- children.reconcile_node(parent_scope, parent, next_sibling, children_bundle)
+ children.reconcile_node(root, parent_scope, parent, next_sibling, children_bundle)
}
}
}
diff --git a/packages/yew/src/dom_bundle/btag/attributes.rs b/packages/yew/src/dom_bundle/btag/attributes.rs
index 761d74986..9d5ded00c 100644
--- a/packages/yew/src/dom_bundle/btag/attributes.rs
+++ b/packages/yew/src/dom_bundle/btag/attributes.rs
@@ -1,4 +1,5 @@
use super::Apply;
+use crate::dom_bundle::BSubtree;
use crate::virtual_dom::vtag::{InputFields, Value};
use crate::virtual_dom::Attributes;
use indexmap::IndexMap;
@@ -11,14 +12,14 @@ impl Apply for Value {
type Element = T;
type Bundle = Self;
- fn apply(self, el: &Self::Element) -> Self {
+ fn apply(self, _root: &BSubtree, el: &Self::Element) -> Self {
if let Some(v) = self.deref() {
el.set_value(v);
}
self
}
- fn apply_diff(self, el: &Self::Element, bundle: &mut Self) {
+ fn apply_diff(self, _root: &BSubtree, el: &Self::Element, bundle: &mut Self) {
match (self.deref(), (*bundle).deref()) {
(Some(new), Some(_)) => {
// Refresh value from the DOM. It might have changed.
@@ -62,21 +63,21 @@ impl Apply for InputFields {
type Element = InputElement;
type Bundle = Self;
- fn apply(mut self, el: &Self::Element) -> Self {
+ fn apply(mut self, root: &BSubtree, el: &Self::Element) -> Self {
// IMPORTANT! This parameter has to be set every time
// to prevent strange behaviour in the browser when the DOM changes
el.set_checked(self.checked);
- self.value = self.value.apply(el);
+ self.value = self.value.apply(root, el);
self
}
- fn apply_diff(self, el: &Self::Element, bundle: &mut Self) {
+ fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut Self) {
// IMPORTANT! This parameter has to be set every time
// to prevent strange behaviour in the browser when the DOM changes
el.set_checked(self.checked);
- self.value.apply_diff(el, &mut bundle.value);
+ self.value.apply_diff(root, el, &mut bundle.value);
}
}
@@ -186,7 +187,7 @@ impl Apply for Attributes {
type Element = Element;
type Bundle = Self;
- fn apply(self, el: &Element) -> Self {
+ fn apply(self, _root: &BSubtree, el: &Element) -> Self {
match &self {
Self::Static(arr) => {
for kv in arr.iter() {
@@ -209,7 +210,7 @@ impl Apply for Attributes {
self
}
- fn apply_diff(self, el: &Element, bundle: &mut Self) {
+ fn apply_diff(self, _root: &BSubtree, el: &Element, bundle: &mut Self) {
#[inline]
fn ptr_eq(a: &[T], b: &[T]) -> bool {
std::ptr::eq(a, b)
diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs
index 687e92106..d5676316d 100644
--- a/packages/yew/src/dom_bundle/btag/listeners.rs
+++ b/packages/yew/src/dom_bundle/btag/listeners.rs
@@ -1,42 +1,38 @@
use super::Apply;
-use crate::dom_bundle::test_log;
-use crate::virtual_dom::{Listener, ListenerKind, Listeners};
-use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase};
+use crate::dom_bundle::{test_log, BSubtree, EventDescriptor};
+use crate::virtual_dom::{Listener, Listeners};
+use ::wasm_bindgen::{prelude::wasm_bindgen, JsCast};
use std::cell::RefCell;
-use std::collections::{HashMap, HashSet};
+use std::collections::HashMap;
use std::ops::Deref;
use std::rc::Rc;
-use std::sync::atomic::{AtomicBool, Ordering};
-use wasm_bindgen::JsCast;
-use web_sys::{Element, Event};
+use web_sys::{Element, Event, EventTarget as HtmlEventTarget};
-thread_local! {
- /// Global event listener registry
- static REGISTRY: RefCell = Default::default();
-
- /// Key used to store listener id on element
- static LISTENER_ID_PROP: wasm_bindgen::JsValue = "__yew_listener_id".into();
-
- /// Cached reference to the document body
- static BODY: web_sys::HtmlElement = gloo_utils::document().body().unwrap();
+#[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);
}
-/// Bubble events during delegation
-static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true);
+/// DOM-Types that can have listeners registered on them.
+/// Uses the duck-typed interface from above in impls.
+pub trait EventListening {
+ fn listener_id(&self) -> Option;
+ fn set_listener_id(&self, id: u32);
+}
-/// Set, if events should bubble up the DOM tree, calling any matching callbacks.
-///
-/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event
-/// handling performance.
-///
-/// Note that yew uses event delegation and implements internal even bubbling for performance
-/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event
-/// handler has no effect.
-///
-/// This function should be called before any component is mounted.
-#[cfg_attr(documenting, doc(cfg(feature = "render")))]
-pub fn set_event_bubbling(bubble: bool) {
- BUBBLE_EVENTS.store(bubble, Ordering::Relaxed);
+impl EventListening for Element {
+ fn listener_id(&self) -> Option {
+ self.unchecked_ref::().listener_id()
+ }
+
+ fn set_listener_id(&self, id: u32) {
+ self.unchecked_ref::().set_listener_id(id);
+ }
}
/// An active set of listeners on an element
@@ -52,14 +48,14 @@ impl Apply for Listeners {
type Element = Element;
type Bundle = ListenerRegistration;
- fn apply(self, el: &Self::Element) -> ListenerRegistration {
+ fn apply(self, root: &BSubtree, el: &Self::Element) -> ListenerRegistration {
match self {
- Self::Pending(pending) => ListenerRegistration::register(el, &pending),
+ Self::Pending(pending) => ListenerRegistration::register(root, el, &pending),
Self::None => ListenerRegistration::NoReg,
}
}
- fn apply_diff(self, el: &Self::Element, bundle: &mut ListenerRegistration) {
+ fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut ListenerRegistration) {
use ListenerRegistration::*;
use Listeners::*;
@@ -67,10 +63,10 @@ impl Apply for Listeners {
(Pending(pending), Registered(ref id)) => {
// Reuse the ID
test_log!("reusing listeners for {}", id);
- Registry::with(|reg| reg.patch(id, &*pending));
+ root.with_listener_registry(|reg| reg.patch(root, id, &*pending));
}
(Pending(pending), bundle @ NoReg) => {
- *bundle = ListenerRegistration::register(el, &pending);
+ *bundle = ListenerRegistration::register(root, el, &pending);
test_log!(
"registering listeners for {}",
match bundle {
@@ -85,7 +81,7 @@ impl Apply for Listeners {
_ => unreachable!(),
};
test_log!("unregistering listeners for {}", id);
- Registry::with(|reg| reg.unregister(id));
+ root.with_listener_registry(|reg| reg.unregister(id));
*bundle = NoReg;
}
(None, NoReg) => {
@@ -97,116 +93,75 @@ impl Apply for Listeners {
impl ListenerRegistration {
/// Register listeners and return their handle ID
- fn register(el: &Element, pending: &[Option>]) -> Self {
- Self::Registered(Registry::with(|reg| {
- let id = reg.set_listener_id(el);
- reg.register(id, pending);
+ fn register(root: &BSubtree, el: &Element, pending: &[Option>]) -> Self {
+ Self::Registered(root.with_listener_registry(|reg| {
+ let id = reg.set_listener_id(root, el);
+ reg.register(root, id, pending);
id
}))
}
/// Remove any registered event listeners from the global registry
- pub fn unregister(&self) {
+ pub fn unregister(&self, root: &BSubtree) {
if let Self::Registered(id) = self {
- Registry::with(|r| r.unregister(id));
- }
- }
-}
-
-#[derive(Clone, Hash, Eq, PartialEq, Debug)]
-struct EventDescriptor {
- kind: ListenerKind,
- passive: bool,
-}
-
-impl From<&dyn Listener> for EventDescriptor {
- fn from(l: &dyn Listener) -> Self {
- Self {
- kind: l.kind(),
- passive: l.passive(),
- }
- }
-}
-
-/// Ensures global event handler registration.
-//
-// Separate struct to DRY, while avoiding partial struct mutability.
-#[derive(Default, Debug)]
-struct GlobalHandlers {
- /// Events with registered handlers that are possibly passive
- handling: HashSet,
-
- /// Keep track of all listeners to drop them on registry drop.
- /// The registry is never dropped in production.
- #[cfg(test)]
- registered: Vec<(ListenerKind, EventListener)>,
-}
-
-impl GlobalHandlers {
- /// Ensure a descriptor has a global event handler assigned
- fn ensure_handled(&mut self, desc: EventDescriptor) {
- if !self.handling.contains(&desc) {
- let cl = {
- let desc = desc.clone();
- BODY.with(move |body| {
- let options = EventListenerOptions {
- phase: EventListenerPhase::Capture,
- passive: desc.passive,
- };
- EventListener::new_with_options(
- body,
- desc.kind.type_name(),
- options,
- move |e: &Event| Registry::handle(desc.clone(), e.clone()),
- )
- })
- };
-
- // Never drop the closure as this event handler is static
- #[cfg(not(test))]
- cl.forget();
- #[cfg(test)]
- self.registered.push((desc.kind.clone(), cl));
-
- self.handling.insert(desc);
+ root.with_listener_registry(|r| r.unregister(id));
}
}
}
/// Global multiplexing event handler registry
-#[derive(Default, Debug)]
-struct Registry {
+#[derive(Debug)]
+pub struct Registry {
/// Counter for assigning new IDs
id_counter: u32,
- /// Registered global event handlers
- global: GlobalHandlers,
-
/// Contains all registered event listeners by listener ID
by_id: HashMap>>>,
}
impl Registry {
- /// Run f with access to global Registry
- #[inline]
- fn with(f: impl FnOnce(&mut Registry) -> R) -> R {
- REGISTRY.with(|r| f(&mut *r.borrow_mut()))
+ pub fn new() -> Self {
+ Self {
+ id_counter: u32::default(),
+ by_id: HashMap::default(),
+ }
+ }
+
+ /// Handle a single event, given the listening element and event descriptor.
+ pub fn get_handler(
+ registry: &RefCell,
+ listening: &dyn EventListening,
+ desc: &EventDescriptor,
+ ) -> Option {
+ // The tricky part is that we want to drop the reference to the registry before
+ // calling any actual listeners (since that might end up running lifecycle methods
+ // and modify the registry). So we clone the current listeners and return a closure
+ let listener_id = listening.listener_id()?;
+ let registry_ref = registry.borrow();
+ let handlers = registry_ref.by_id.get(&listener_id)?;
+ let listeners = handlers.get(desc)?.clone();
+ drop(registry_ref); // unborrow the registry, before running any listeners
+ Some(move |event: &Event| {
+ for l in listeners {
+ l.handle(event.clone());
+ }
+ })
}
/// Register all passed listeners under ID
- fn register(&mut self, id: u32, listeners: &[Option>]) {
+ fn register(&mut self, root: &BSubtree, id: u32, listeners: &[Option>]) {
let mut by_desc =
HashMap::>>::with_capacity(listeners.len());
for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
let desc = EventDescriptor::from(l.deref());
- self.global.ensure_handled(desc.clone());
+ root.ensure_handled(&desc);
by_desc.entry(desc).or_default().push(l);
}
self.by_id.insert(id, by_desc);
}
/// Patch an already registered set of handlers
- fn patch(&mut self, id: &u32, listeners: &[Option>]) {
+ fn patch(&mut self, root: &BSubtree, id: &u32, listeners: &[Option>]) {
if let Some(by_desc) = self.by_id.get_mut(id) {
// Keeping empty vectors is fine. Those don't do much and should happen rarely.
for v in by_desc.values_mut() {
@@ -215,7 +170,7 @@ impl Registry {
for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
let desc = EventDescriptor::from(l.deref());
- self.global.ensure_handled(desc.clone());
+ root.ensure_handled(&desc);
by_desc.entry(desc).or_default().push(l);
}
}
@@ -227,76 +182,30 @@ impl Registry {
}
/// Set unique listener ID onto element and return it
- fn set_listener_id(&mut self, el: &Element) -> u32 {
+ fn set_listener_id(&mut self, root: &BSubtree, el: &Element) -> u32 {
let id = self.id_counter;
self.id_counter += 1;
- LISTENER_ID_PROP.with(|prop| {
- if !js_sys::Reflect::set(el, prop, &js_sys::Number::from(id)).unwrap() {
- panic!("failed to set listener ID property");
- }
- });
+ root.brand_element(el as &HtmlEventTarget);
+ el.set_listener_id(id);
id
}
-
- /// Handle a global event firing
- fn handle(desc: EventDescriptor, event: Event) {
- let target = match event
- .target()
- .and_then(|el| el.dyn_into::().ok())
- {
- Some(el) => el,
- None => return,
- };
-
- Self::run_handlers(desc, event, target);
- }
-
- fn run_handlers(desc: EventDescriptor, event: Event, target: web_sys::Element) {
- let run_handler = |el: &web_sys::Element| {
- if let Some(l) = LISTENER_ID_PROP
- .with(|prop| js_sys::Reflect::get(el, prop).ok())
- .and_then(|v| v.dyn_into().ok())
- .and_then(|num: js_sys::Number| {
- Registry::with(|r| {
- r.by_id
- .get(&(num.value_of() as u32))
- .and_then(|s| s.get(&desc))
- .cloned()
- })
- })
- {
- for l in l {
- l.handle(event.clone());
- }
- }
- };
-
- run_handler(&target);
-
- if BUBBLE_EVENTS.load(Ordering::Relaxed) {
- let mut el = target;
- while !event.cancel_bubble() {
- el = match el.parent_element() {
- Some(el) => el,
- None => break,
- };
- run_handler(&el);
- }
- }
- }
}
-#[cfg(all(test, feature = "wasm_test"))]
+#[cfg(feature = "wasm_test")]
+#[cfg(test)]
mod tests {
use std::marker::PhantomData;
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
- use web_sys::{Event, EventInit, MouseEvent};
+ use web_sys::{Event, EventInit, HtmlElement, MouseEvent};
wasm_bindgen_test_configure!(run_in_browser);
- use crate::{html, html::TargetCast, scheduler, AppHandle, Component, Context, Html};
+ use crate::{
+ create_portal, html, html::TargetCast, scheduler, virtual_dom::VNode, AppHandle, Component,
+ Context, Html, NodeRef, Properties,
+ };
use gloo_utils::document;
use wasm_bindgen::JsCast;
use yew::Callback;
@@ -315,29 +224,16 @@ mod tests {
text: String,
}
- trait Mixin {
+ #[derive(Default, PartialEq, Properties)]
+ struct MixinProps {
+ state_ref: NodeRef,
+ wrapped: M,
+ }
+
+ trait Mixin: Properties + Sized {
fn view(ctx: &Context, state: &State) -> Html
where
- C: Component,
- {
- let link = ctx.link().clone();
- let onclick = Callback::from(move |_| {
- link.send_message(Message::Action);
- scheduler::start_now();
- });
-
- if state.stop_listening {
- html! {
- {state.action}
- }
- } else {
- html! {
-
- {state.action}
-
- }
- }
- }
+ C: Component>;
}
struct Comp
@@ -350,10 +246,10 @@ mod tests {
impl Component for Comp
where
- M: Mixin + 'static,
+ M: Mixin + Properties + 'static,
{
type Message = Message;
- type Properties = ();
+ type Properties = MixinProps;
fn create(_: &Context) -> Self {
Comp {
@@ -382,68 +278,103 @@ mod tests {
}
}
- fn assert_count(el: &web_sys::HtmlElement, count: isize) {
- assert_eq!(el.text_content(), Some(count.to_string()))
+ #[track_caller]
+ fn assert_count(el: &NodeRef, count: isize) {
+ let text = el
+ .get()
+ .expect("State ref not bound in the test case?")
+ .text_content();
+ assert_eq!(text, Some(count.to_string()))
}
- fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement {
+ #[track_caller]
+ fn click(el: &NodeRef) {
+ el.get().unwrap().dyn_into::().unwrap().click();
+ scheduler::start_now();
+ }
+
+ fn get_el_by_selector(selector: &str) -> web_sys::HtmlElement {
document()
- .query_selector(tag)
+ .query_selector(selector)
.unwrap()
.unwrap()
.dyn_into::()
.unwrap()
}
- fn init(tag: &str) -> (AppHandle>, web_sys::HtmlElement)
+ fn init() -> (AppHandle>, NodeRef)
where
- M: Mixin,
+ M: Mixin + Properties + Default,
{
- // Remove any existing listeners and elements
- super::Registry::with(|r| *r = Default::default());
- if let Some(el) = document().query_selector(tag).unwrap() {
- el.parent_element().unwrap().remove();
+ // Remove any existing elements
+ let body = document().body().unwrap();
+ while let Some(child) = body.query_selector("div#testroot").unwrap() {
+ body.remove_child(&child).unwrap();
}
let root = document().create_element("div").unwrap();
- document().body().unwrap().append_child(&root).unwrap();
- let app = crate::Renderer::>::with_root(root).render();
+ root.set_id("testroot");
+ body.append_child(&root).unwrap();
+ let props = as Component>::Properties::default();
+ let el_ref = props.state_ref.clone();
+ let app = crate::Renderer::>::with_root_and_props(root, props).render();
scheduler::start_now();
- (app, get_el_by_tag(tag))
+ (app, el_ref)
}
#[test]
fn synchronous() {
+ #[derive(Default, PartialEq, Properties)]
struct Synchronous;
- impl Mixin for Synchronous {}
+ impl Mixin for Synchronous {
+ fn view(ctx: &Context, state: &State) -> Html
+ where
+ C: Component>,
+ {
+ let onclick = ctx.link().callback(|_| Message::Action);
- let (link, el) = init::("a");
+ if state.stop_listening {
+ html! {
+ {state.action}
+ }
+ } else {
+ html! {
+
+ {state.action}
+
+ }
+ }
+ }
+ }
+
+ let (link, el) = init::();
assert_count(&el, 0);
- el.click();
+ click(&el);
assert_count(&el, 1);
- el.click();
+ click(&el);
assert_count(&el, 2);
link.send_message(Message::StopListening);
scheduler::start_now();
- el.click();
+ click(&el);
assert_count(&el, 2);
}
#[test]
async fn non_bubbling_event() {
+ #[derive(Default, PartialEq, Properties)]
struct NonBubbling;
impl Mixin for NonBubbling {
fn view(ctx: &Context, state: &State) -> Html
where
- C: Component,
+ C: Component>,
{
let link = ctx.link().clone();
let onblur = Callback::from(move |_| {
@@ -452,7 +383,7 @@ mod tests {
});
html! {
-
+
{state.action}
@@ -461,7 +392,7 @@ mod tests {
}
}
- let (_, el) = init::
("a");
+ let (_, el) = init::();
assert_count(&el, 0);
@@ -483,30 +414,27 @@ mod tests {
#[test]
fn bubbling() {
+ #[derive(Default, PartialEq, Properties)]
struct Bubbling;
impl Mixin for Bubbling {
fn view(ctx: &Context, state: &State) -> Html
where
- C: Component,
+ C: Component>,
{
if state.stop_listening {
html! {
}
} else {
- let link = ctx.link().clone();
- let cb = Callback::from(move |_| {
- link.send_message(Message::Action);
- scheduler::start_now();
- });
+ let cb = ctx.link().callback(|_| Message::Action);
html! {
@@ -515,47 +443,39 @@ mod tests {
}
}
- let (link, el) = init::("a");
+ let (link, el) = init::();
assert_count(&el, 0);
-
- el.click();
+ click(&el);
assert_count(&el, 2);
-
- el.click();
+ click(&el);
assert_count(&el, 4);
link.send_message(Message::StopListening);
scheduler::start_now();
- el.click();
+ click(&el);
assert_count(&el, 4);
}
#[test]
fn cancel_bubbling() {
+ #[derive(Default, PartialEq, Properties)]
struct CancelBubbling;
impl Mixin for CancelBubbling {
fn view(ctx: &Context, state: &State) -> Html
where
- C: Component,
+ C: Component>,
{
- let link = ctx.link().clone();
- let onclick = Callback::from(move |_| {
- link.send_message(Message::Action);
- scheduler::start_now();
- });
-
- let link = ctx.link().clone();
- let onclick2 = Callback::from(move |e: MouseEvent| {
+ let onclick = ctx.link().callback(|_| Message::Action);
+ let onclick2 = ctx.link().callback(|e: MouseEvent| {
e.stop_propagation();
- link.send_message(Message::Action);
- scheduler::start_now();
+ Message::Action
});
html! {
@@ -563,14 +483,12 @@ mod tests {
}
}
- let (_, el) = init::("a");
+ let (_, el) = init::();
assert_count(&el, 0);
-
- el.click();
+ click(&el);
assert_count(&el, 1);
-
- el.click();
+ click(&el);
assert_count(&el, 2);
}
@@ -579,29 +497,23 @@ mod tests {
// Here an event is being delivered to a DOM node which does
// _not_ have a listener but which is contained within an
// element that does and which cancels the bubble.
+ #[derive(Default, PartialEq, Properties)]
struct CancelBubbling;
impl Mixin for CancelBubbling {
fn view(ctx: &Context, state: &State) -> Html
where
- C: Component,
+ C: Component>,
{
- let link = ctx.link().clone();
- let onclick = Callback::from(move |_| {
- link.send_message(Message::Action);
- scheduler::start_now();
- });
-
- let link = ctx.link().clone();
- let onclick2 = Callback::from(move |e: MouseEvent| {
+ let onclick = ctx.link().callback(|_| Message::Action);
+ let onclick2 = ctx.link().callback(|e: MouseEvent| {
e.stop_propagation();
- link.send_message(Message::Action);
- scheduler::start_now();
+ Message::Action
});
html! {
@@ -610,65 +522,153 @@ mod tests {
}
}
- let (_, el) = init::
("a");
+ let (_, el) = init::();
assert_count(&el, 0);
-
- 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() {
+ #[derive(PartialEq, Properties)]
+ struct PortalBubbling {
+ host: web_sys::Element,
+ }
+ impl Default for PortalBubbling {
+ fn default() -> Self {
+ let host = document().create_element("div").unwrap();
+ PortalBubbling { host }
+ }
+ }
+
+ impl Mixin for PortalBubbling {
+ fn view(ctx: &Context, state: &State) -> Html
+ where
+ C: Component>,
+ {
+ let portal_target = ctx.props().wrapped.host.clone();
+ let onclick = ctx.link().callback(|_| Message::Action);
+ html! {
+ <>
+
+ {VNode::VRef(portal_target.into())}
+ >
+ }
+ }
+ }
+
+ let (_, el) = init::();
+
+ assert_count(&el, 0);
+ click(&el);
+ assert_count(&el, 1);
+ }
+
+ /// Here an event is being from inside a shadow root. It should only be caught exactly once on each handler
+ #[test]
+ fn open_shadow_dom_bubbling() {
+ use web_sys::{ShadowRootInit, ShadowRootMode};
+ #[derive(PartialEq, Properties)]
+ struct OpenShadowDom {
+ host: web_sys::Element,
+ inner_root: web_sys::Element,
+ }
+ impl Default for OpenShadowDom {
+ fn default() -> Self {
+ let host = document().create_element("div").unwrap();
+ let inner_root = document().create_element("div").unwrap();
+ let shadow = host
+ .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open))
+ .unwrap();
+ shadow.append_child(&inner_root).unwrap();
+ OpenShadowDom { host, inner_root }
+ }
+ }
+ impl Mixin for OpenShadowDom {
+ fn view(ctx: &Context, state: &State) -> Html
+ where
+ C: Component>,
+ {
+ let onclick = ctx.link().callback(|_| Message::Action);
+ let mixin = &ctx.props().wrapped;
+ html! {
+
+
+ {VNode::VRef(mixin.host.clone().into())}
+
+ }
+ }
+ }
+ let (_, el) = init::();
+
+ assert_count(&el, 0);
+ click(&el);
+ assert_count(&el, 2); // Once caught per handler
+ }
+
fn test_input_listener(make_event: impl Fn() -> E)
where
- E: JsCast + std::fmt::Debug,
+ E: Into + std::fmt::Debug,
{
+ #[derive(Default, PartialEq, Properties)]
struct Input;
impl Mixin for Input {
fn view(ctx: &Context, state: &State) -> Html
where
- C: Component,
+ C: Component>,
{
if state.stop_listening {
html! {
-
{state.text.clone()}
+
{state.text.clone()}
}
} else {
- let link = ctx.link().clone();
- let onchange = Callback::from(move |e: web_sys::Event| {
+ let onchange = ctx.link().callback(|e: web_sys::Event| {
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
- link.send_message(Message::SetText(el.value()));
- scheduler::start_now();
+ Message::SetText(el.value())
});
-
- let link = ctx.link().clone();
- let oninput = Callback::from(move |e: web_sys::InputEvent| {
+ let oninput = ctx.link().callback(|e: web_sys::InputEvent| {
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
- link.send_message(Message::SetText(el.value()));
- scheduler::start_now();
+ Message::SetText(el.value())
});
html! {
-
{state.text.clone()}
+
{state.text.clone()}
}
}
}
}
- let (link, input_el) = init:: ("input");
- let input_el = input_el.dyn_into::().unwrap();
- let p_el = get_el_by_tag("p");
+ let (link, state_ref) = init:: ();
+ let input_el = get_el_by_selector("input")
+ .dyn_into::()
+ .unwrap();
- assert_eq!(&p_el.text_content().unwrap(), "");
+ assert_eq!(&state_ref.get().unwrap().text_content().unwrap(), "");
for mut s in ["foo", "bar", "baz"].iter() {
input_el.set_value(s);
if s == &"baz" {
@@ -677,12 +677,9 @@ mod tests {
s = &"bar";
}
- input_el
- .dyn_ref::()
- .unwrap()
- .dispatch_event(&make_event().dyn_into().unwrap())
- .unwrap();
- assert_eq!(&p_el.text_content().unwrap(), s);
+ input_el.dispatch_event(&make_event().into()).unwrap();
+ scheduler::start_now();
+ assert_eq!(&state_ref.get().unwrap().text_content().unwrap(), s);
}
}
diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs
index ed902baa1..c7a63f388 100644
--- a/packages/yew/src/dom_bundle/btag/mod.rs
+++ b/packages/yew/src/dom_bundle/btag/mod.rs
@@ -3,9 +3,9 @@
mod attributes;
mod listeners;
-pub use listeners::set_event_bubbling;
+pub use listeners::Registry;
-use super::{insert_node, BList, BNode, Reconcilable, ReconcileTarget};
+use super::{insert_node, BList, BNode, BSubtree, Reconcilable, ReconcileTarget};
use crate::html::AnyScope;
use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE};
use crate::virtual_dom::{Attributes, Key, VTag};
@@ -25,10 +25,10 @@ trait Apply {
type Bundle;
/// Apply contained values to [Element](Self::Element) with no ancestor
- fn apply(self, el: &Self::Element) -> Self::Bundle;
+ fn apply(self, root: &BSubtree, el: &Self::Element) -> Self::Bundle;
/// Apply diff between [self] and `bundle` to [Element](Self::Element).
- fn apply_diff(self, el: &Self::Element, bundle: &mut Self::Bundle);
+ fn apply_diff(self, root: &BSubtree, el: &Self::Element, bundle: &mut Self::Bundle);
}
/// [BTag] fields that are specific to different [BTag] kinds.
@@ -69,14 +69,14 @@ pub(super) struct BTag {
}
impl ReconcileTarget for BTag {
- fn detach(self, parent: &Element, parent_to_detach: bool) {
- self.listeners.unregister();
+ fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) {
+ self.listeners.unregister(root);
let node = self.reference;
// recursively remove its children
if let BTagInner::Other { child_bundle, .. } = self.inner {
// This tag will be removed, so there's no point to remove any child.
- child_bundle.detach(&node, true);
+ child_bundle.detach(root, &node, true);
}
if !parent_to_detach {
let result = parent.remove_child(&node);
@@ -104,6 +104,7 @@ impl Reconcilable for VTag {
fn attach(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -118,20 +119,21 @@ impl Reconcilable for VTag {
} = self;
insert_node(&el, parent, next_sibling.get().as_ref());
- let attributes = attributes.apply(&el);
- let listeners = listeners.apply(&el);
+ let attributes = attributes.apply(root, &el);
+ let listeners = listeners.apply(root, &el);
let inner = match self.inner {
VTagInner::Input(f) => {
- let f = f.apply(el.unchecked_ref());
+ let f = f.apply(root, el.unchecked_ref());
BTagInner::Input(f)
}
VTagInner::Textarea { value } => {
- let value = value.apply(el.unchecked_ref());
+ let value = value.apply(root, el.unchecked_ref());
BTagInner::Textarea { value }
}
VTagInner::Other { children, tag } => {
- let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default());
+ let (_, child_bundle) =
+ children.attach(root, parent_scope, &el, NodeRef::default());
BTagInner::Other { child_bundle, tag }
}
};
@@ -151,6 +153,7 @@ impl Reconcilable for VTag {
fn reconcile_node(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -173,31 +176,38 @@ impl Reconcilable for VTag {
}
_ => false,
} {
- return self.reconcile(parent_scope, parent, next_sibling, ex.deref_mut());
+ return self.reconcile(
+ root,
+ parent_scope,
+ parent,
+ next_sibling,
+ ex.deref_mut(),
+ );
}
}
_ => {}
};
- self.replace(parent_scope, parent, next_sibling, bundle)
+ self.replace(root, parent_scope, parent, next_sibling, bundle)
}
fn reconcile(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
_parent: &Element,
_next_sibling: NodeRef,
tag: &mut Self::Bundle,
) -> NodeRef {
let el = &tag.reference;
- self.attributes.apply_diff(el, &mut tag.attributes);
- self.listeners.apply_diff(el, &mut tag.listeners);
+ self.attributes.apply_diff(root, el, &mut tag.attributes);
+ self.listeners.apply_diff(root, el, &mut tag.listeners);
match (self.inner, &mut tag.inner) {
(VTagInner::Input(new), BTagInner::Input(old)) => {
- new.apply_diff(el.unchecked_ref(), old);
+ new.apply_diff(root, el.unchecked_ref(), old);
}
(VTagInner::Textarea { value: new }, BTagInner::Textarea { value: old }) => {
- new.apply_diff(el.unchecked_ref(), old);
+ new.apply_diff(root, el.unchecked_ref(), old);
}
(
VTagInner::Other { children: new, .. },
@@ -205,7 +215,7 @@ impl Reconcilable for VTag {
child_bundle: old, ..
},
) => {
- new.reconcile(parent_scope, el, NodeRef::default(), old);
+ new.reconcile(root, parent_scope, el, NodeRef::default(), old);
}
// Can not happen, because we checked for tag equability above
_ => unsafe { unreachable_unchecked() },
@@ -295,8 +305,14 @@ mod tests {
wasm_bindgen_test_configure!(run_in_browser);
- fn test_scope() -> AnyScope {
- AnyScope::test()
+ fn setup_parent() -> (BSubtree, AnyScope, Element) {
+ let scope = AnyScope::test();
+ let parent = document().create_element("div").unwrap();
+ let root = BSubtree::create_root(&parent);
+
+ document().body().unwrap().append_child(&parent).unwrap();
+
+ (root, scope, parent)
}
#[test]
@@ -475,10 +491,9 @@ mod tests {
#[test]
fn supports_svg() {
+ let (root, scope, parent) = setup_parent();
let document = web_sys::window().unwrap().document().unwrap();
- let scope = test_scope();
- let div_el = document.create_element("div").unwrap();
let namespace = SVG_NAMESPACE;
let namespace = Some(namespace);
let svg_el = document.create_element_ns(namespace, "svg").unwrap();
@@ -488,17 +503,17 @@ mod tests {
let svg_node = html! { {path_node} };
let svg_tag = assert_vtag(svg_node);
- let (_, svg_tag) = svg_tag.attach(&scope, &div_el, NodeRef::default());
+ let (_, svg_tag) = svg_tag.attach(&root, &scope, &parent, NodeRef::default());
assert_namespace(&svg_tag, SVG_NAMESPACE);
let path_tag = assert_btag_ref(svg_tag.children().get(0).unwrap());
assert_namespace(path_tag, SVG_NAMESPACE);
let g_tag = assert_vtag(g_node.clone());
- let (_, g_tag) = g_tag.attach(&scope, &div_el, NodeRef::default());
+ let (_, g_tag) = g_tag.attach(&root, &scope, &parent, NodeRef::default());
assert_namespace(&g_tag, HTML_NAMESPACE);
let g_tag = assert_vtag(g_node);
- let (_, g_tag) = g_tag.attach(&scope, &svg_el, NodeRef::default());
+ let (_, g_tag) = g_tag.attach(&root, &scope, &svg_el, NodeRef::default());
assert_namespace(&g_tag, SVG_NAMESPACE);
}
@@ -594,26 +609,20 @@ mod tests {
#[test]
fn it_does_not_set_missing_class_name() {
- let scope = test_scope();
- let parent = document().create_element("div").unwrap();
-
- document().body().unwrap().append_child(&parent).unwrap();
+ let (root, scope, parent) = setup_parent();
let elem = html! {
};
- let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default());
+ let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
let vtag = assert_btag_mut(&mut elem);
// test if the className has not been set
assert!(!vtag.reference().has_attribute("class"));
}
fn test_set_class_name(gen_html: impl FnOnce() -> Html) {
- let scope = test_scope();
- let parent = document().create_element("div").unwrap();
-
- document().body().unwrap().append_child(&parent).unwrap();
+ let (root, scope, parent) = setup_parent();
let elem = gen_html();
- let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default());
+ let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
let vtag = assert_btag_mut(&mut elem);
// test if the className has been set
assert!(vtag.reference().has_attribute("class"));
@@ -631,16 +640,13 @@ mod tests {
#[test]
fn controlled_input_synced() {
- let scope = test_scope();
- let parent = document().create_element("div").unwrap();
-
- document().body().unwrap().append_child(&parent).unwrap();
+ let (root, scope, parent) = setup_parent();
let expected = "not_changed_value";
// Initial state
let elem = html! { };
- let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default());
+ let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
let vtag = assert_btag_ref(&elem);
// User input
@@ -652,7 +658,7 @@ mod tests {
let elem_vtag = assert_vtag(next_elem);
// Sync happens here
- elem_vtag.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem);
+ elem_vtag.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem);
let vtag = assert_btag_ref(&elem);
// Get new current value of the input element
@@ -667,14 +673,11 @@ mod tests {
#[test]
fn uncontrolled_input_unsynced() {
- let scope = test_scope();
- let parent = document().create_element("div").unwrap();
-
- document().body().unwrap().append_child(&parent).unwrap();
+ let (root, scope, parent) = setup_parent();
// Initial state
let elem = html! { };
- let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default());
+ let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
let vtag = assert_btag_ref(&elem);
// User input
@@ -686,7 +689,7 @@ mod tests {
let elem_vtag = assert_vtag(next_elem);
// Value should not be refreshed
- elem_vtag.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem);
+ elem_vtag.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem);
let vtag = assert_btag_ref(&elem);
// Get user value of the input element
@@ -705,10 +708,7 @@ mod tests {
#[test]
fn dynamic_tags_work() {
- let scope = test_scope();
- let parent = document().create_element("div").unwrap();
-
- document().body().unwrap().append_child(&parent).unwrap();
+ let (root, scope, parent) = setup_parent();
let elem = html! { <@{
let mut builder = String::new();
@@ -716,7 +716,7 @@ mod tests {
builder
}/> };
- let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default());
+ let (_, mut elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
let vtag = assert_btag_mut(&mut elem);
// make sure the new tag name is used internally
assert_eq!(vtag.tag(), "a");
@@ -758,36 +758,31 @@ mod tests {
#[test]
fn reset_node_ref() {
- let scope = test_scope();
- let parent = document().create_element("div").unwrap();
-
- document().body().unwrap().append_child(&parent).unwrap();
+ let (root, scope, parent) = setup_parent();
let node_ref = NodeRef::default();
let elem: VNode = html! {
};
assert_vtag_ref(&elem);
- let (_, elem) = elem.attach(&scope, &parent, NodeRef::default());
+ let (_, elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
assert_eq!(node_ref.get(), parent.first_child());
- elem.detach(&parent, false);
+ elem.detach(&root, &parent, false);
assert!(node_ref.get().is_none());
}
#[test]
fn vtag_reuse_should_reset_ancestors_node_ref() {
- let scope = test_scope();
- let parent = document().create_element("div").unwrap();
- document().body().unwrap().append_child(&parent).unwrap();
+ let (root, scope, parent) = setup_parent();
let node_ref_a = NodeRef::default();
let elem_a = html! {
};
- let (_, mut elem) = elem_a.attach(&scope, &parent, NodeRef::default());
+ let (_, mut elem) = elem_a.attach(&root, &scope, &parent, NodeRef::default());
// save the Node to check later that it has been reused.
let node_a = node_ref_a.get().unwrap();
let node_ref_b = NodeRef::default();
let elem_b = html! {
};
- elem_b.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem);
+ elem_b.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem);
let node_b = node_ref_b.get().unwrap();
@@ -800,9 +795,7 @@ mod tests {
#[test]
fn vtag_should_not_touch_newly_bound_refs() {
- let scope = test_scope();
- let parent = document().create_element("div").unwrap();
- document().body().unwrap().append_child(&parent).unwrap();
+ let (root, scope, parent) = setup_parent();
let test_ref = NodeRef::default();
let before = html! {
@@ -819,8 +812,8 @@ mod tests {
// The point of this diff is to first render the "after" div and then detach the "before" div,
// while both should be bound to the same node ref
- let (_, mut elem) = before.attach(&scope, &parent, NodeRef::default());
- after.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem);
+ let (_, mut elem) = before.attach(&root, &scope, &parent, NodeRef::default());
+ after.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem);
assert_eq!(
test_ref
diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs
index 51c54f0a4..969ce3eef 100644
--- a/packages/yew/src/dom_bundle/btext.rs
+++ b/packages/yew/src/dom_bundle/btext.rs
@@ -1,6 +1,6 @@
//! This module contains the bundle implementation of text [BText].
-use super::{insert_node, BNode, Reconcilable, ReconcileTarget};
+use super::{insert_node, BNode, BSubtree, Reconcilable, ReconcileTarget};
use crate::html::AnyScope;
use crate::virtual_dom::{AttrValue, VText};
use crate::NodeRef;
@@ -15,7 +15,7 @@ pub(super) struct BText {
}
impl ReconcileTarget for BText {
- fn detach(self, parent: &Element, parent_to_detach: bool) {
+ fn detach(self, _root: &BSubtree, parent: &Element, parent_to_detach: bool) {
if !parent_to_detach {
let result = parent.remove_child(&self.text_node);
@@ -39,6 +39,7 @@ impl Reconcilable for VText {
fn attach(
self,
+ _root: &BSubtree,
_parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -53,18 +54,21 @@ impl Reconcilable for VText {
/// Renders virtual node over existing `TextNode`, but only if value of text has changed.
fn reconcile_node(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut BNode,
) -> NodeRef {
match bundle {
- BNode::Text(btext) => self.reconcile(parent_scope, parent, next_sibling, btext),
- _ => self.replace(parent_scope, parent, next_sibling, bundle),
+ BNode::Text(btext) => self.reconcile(root, parent_scope, parent, next_sibling, btext),
+ _ => self.replace(root, parent_scope, parent, next_sibling, bundle),
}
}
+
fn reconcile(
self,
+ _root: &BSubtree,
_parent_scope: &AnyScope,
_parent: &Element,
_next_sibling: NodeRef,
diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs
index b1aa8da30..16df0b97d 100644
--- a/packages/yew/src/dom_bundle/mod.rs
+++ b/packages/yew/src/dom_bundle/mod.rs
@@ -12,11 +12,12 @@ mod bportal;
mod bsuspense;
mod btag;
mod btext;
+mod subtree_root;
+
mod traits;
mod utils;
-use gloo::utils::document;
-use web_sys::{Element, Node};
+use web_sys::Element;
use crate::html::AnyScope;
use crate::html::NodeRef;
@@ -27,13 +28,16 @@ use blist::BList;
use bnode::BNode;
use bportal::BPortal;
use bsuspense::BSuspense;
-use btag::BTag;
+use btag::{BTag, Registry};
use btext::BText;
+use subtree_root::EventDescriptor;
use traits::{Reconcilable, ReconcileTarget};
use utils::{insert_node, test_log};
#[doc(hidden)] // Publically exported from crate::events
-pub use self::btag::set_event_bubbling;
+pub use subtree_root::set_event_bubbling;
+
+pub(crate) use subtree_root::BSubtree;
/// A Bundle.
///
@@ -46,11 +50,8 @@ pub(crate) struct Bundle(BNode);
impl Bundle {
/// Creates a new bundle.
- pub fn new(parent: &Element, next_sibling: &NodeRef, node_ref: &NodeRef) -> Self {
- let placeholder: Node = document().create_text_node("").into();
- insert_node(&placeholder, parent, next_sibling.get().as_ref());
- node_ref.set(Some(placeholder.clone()));
- Self(BNode::Ref(placeholder))
+ pub const fn new() -> Self {
+ Self(BNode::List(BList::new()))
}
/// Shifts the bundle into a different position.
@@ -61,16 +62,17 @@ impl Bundle {
/// Applies a virtual dom layout to current bundle.
pub fn reconcile(
&mut self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
next_node: VNode,
) -> NodeRef {
- next_node.reconcile_node(parent_scope, parent, next_sibling, &mut self.0)
+ next_node.reconcile_node(root, parent_scope, parent, next_sibling, &mut self.0)
}
/// Detaches current bundle.
- pub fn detach(self, parent: &Element, parent_to_detach: bool) {
- self.0.detach(parent, parent_to_detach);
+ pub fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) {
+ self.0.detach(root, parent, parent_to_detach);
}
}
diff --git a/packages/yew/src/dom_bundle/subtree_root.rs b/packages/yew/src/dom_bundle/subtree_root.rs
new file mode 100644
index 000000000..34b8e007b
--- /dev/null
+++ b/packages/yew/src/dom_bundle/subtree_root.rs
@@ -0,0 +1,476 @@
+//! Per-subtree state of apps
+
+use super::{test_log, Registry};
+use crate::virtual_dom::{Listener, ListenerKind};
+use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase};
+use std::cell::RefCell;
+use std::collections::HashSet;
+use std::hash::{Hash, Hasher};
+use std::rc::{Rc, Weak};
+use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
+use wasm_bindgen::prelude::wasm_bindgen;
+use wasm_bindgen::JsCast;
+use web_sys::{Element, Event, EventTarget as HtmlEventTarget};
+
+/// DOM-Types that capture (bubbling) events. This generally includes event targets,
+/// but also subtree roots.
+pub trait EventGrating {
+ fn subtree_id(&self) -> Option;
+ fn set_subtree_id(&self, tree_id: TreeId);
+ // When caching, we key on the length of the `composed_path`. Important to check
+ // considering event retargeting!
+ fn cache_key(&self) -> Option;
+ fn set_cache_key(&self, key: u32);
+}
+
+#[wasm_bindgen]
+extern "C" {
+ // Duck-typing, not a real class on js-side. On rust-side, use impls of EventGrating below
+ type EventTargetable;
+ #[wasm_bindgen(method, getter = __yew_subtree_id, structural)]
+ fn subtree_id(this: &EventTargetable) -> Option;
+ #[wasm_bindgen(method, setter = __yew_subtree_id, structural)]
+ fn set_subtree_id(this: &EventTargetable, id: TreeId);
+ #[wasm_bindgen(method, getter = __yew_subtree_cache_key, structural)]
+ fn cache_key(this: &EventTargetable) -> Option;
+ #[wasm_bindgen(method, setter = __yew_subtree_cache_key, structural)]
+ fn set_cache_key(this: &EventTargetable, key: u32);
+}
+
+macro_rules! impl_event_grating {
+ ($($t:ty);* $(;)?) => {
+ $(
+ impl EventGrating for $t {
+ fn subtree_id(&self) -> Option {
+ self.unchecked_ref::().subtree_id()
+ }
+ fn set_subtree_id(&self, tree_id: TreeId) {
+ self.unchecked_ref::()
+ .set_subtree_id(tree_id);
+ }
+ fn cache_key(&self) -> Option {
+ self.unchecked_ref::().cache_key()
+ }
+ fn set_cache_key(&self, key: u32) {
+ self.unchecked_ref::().set_cache_key(key)
+ }
+ }
+ )*
+ }
+}
+
+impl_event_grating!(
+ HtmlEventTarget;
+ Event; // We cache the found subtree id on the event. This should speed up repeated searches
+);
+
+/// The TreeId is the additional payload attached to each listening element
+/// It identifies the host responsible for the target. Events not matching
+/// are ignored during handling
+type TreeId = u32;
+
+/// Special id for caching the fact that some event should not be handled
+static NONE_TREE_ID: TreeId = 0;
+static NEXT_ROOT_ID: AtomicU32 = AtomicU32::new(1);
+
+fn next_root_id() -> TreeId {
+ NEXT_ROOT_ID.fetch_add(1, Ordering::SeqCst)
+}
+
+/// Data kept per controlled subtree. [Portal] and [AppHandle] serve as
+/// hosts. Two controlled subtrees should never overlap.
+///
+/// [Portal]: super::bportal::BPortal
+/// [AppHandle]: super::app_handle::AppHandle
+#[derive(Debug, Clone)]
+pub struct BSubtree(Rc);
+
+/// The parent is the logical location where a subtree is mounted
+/// Used to bubble events through portals, which are physically somewhere else in the DOM tree
+/// but should bubble to logical ancestors in the virtual DOM tree
+#[derive(Debug)]
+struct ParentingInformation {
+ parent_root: Rc,
+ // Logical parent of the subtree. Might be the host element of another subtree,
+ // if mounted as a direct child, or a controlled element.
+ mount_element: Element,
+}
+
+#[derive(Clone, Hash, Eq, PartialEq, Debug)]
+pub struct EventDescriptor {
+ kind: ListenerKind,
+ passive: bool,
+}
+
+impl From<&dyn Listener> for EventDescriptor {
+ fn from(l: &dyn Listener) -> Self {
+ Self {
+ kind: l.kind(),
+ passive: l.passive(),
+ }
+ }
+}
+
+/// Ensures event handler registration.
+//
+// Separate struct to DRY, while avoiding partial struct mutability.
+#[derive(Debug)]
+struct HostHandlers {
+ /// The host element where events are registered
+ host: HtmlEventTarget,
+
+ /// Keep track of all listeners to drop them on registry drop.
+ /// The registry is never dropped in production.
+ #[cfg(test)]
+ registered: Vec<(ListenerKind, EventListener)>,
+}
+
+impl HostHandlers {
+ fn new(host: HtmlEventTarget) -> Self {
+ Self {
+ host,
+ #[cfg(test)]
+ registered: Vec::default(),
+ }
+ }
+
+ fn add_listener(&mut self, desc: &EventDescriptor, callback: impl 'static + FnMut(&Event)) {
+ let cl = {
+ let desc = desc.clone();
+ let options = EventListenerOptions {
+ phase: EventListenerPhase::Capture,
+ passive: desc.passive,
+ };
+ EventListener::new_with_options(&self.host, desc.kind.type_name(), options, callback)
+ };
+
+ // Never drop the closure as this event handler is static
+ #[cfg(not(test))]
+ cl.forget();
+ #[cfg(test)]
+ self.registered.push((desc.kind.clone(), cl));
+ }
+}
+
+/// Per subtree data
+#[derive(Debug)]
+struct SubtreeData {
+ /// Data shared between all trees in an app
+ app_data: Rc>,
+ /// Parent subtree
+ parent: Option,
+
+ subtree_id: TreeId,
+ host: HtmlEventTarget,
+ event_registry: RefCell,
+ global: RefCell,
+}
+
+#[derive(Debug)]
+struct WeakSubtree {
+ subtree_id: TreeId,
+ weak_ref: Weak,
+}
+
+impl Hash for WeakSubtree {
+ fn hash(&self, state: &mut H) {
+ self.subtree_id.hash(state)
+ }
+}
+
+impl PartialEq for WeakSubtree {
+ fn eq(&self, other: &Self) -> bool {
+ self.subtree_id == other.subtree_id
+ }
+}
+impl Eq for WeakSubtree {}
+
+/// Per tree data, shared between all subtrees in the hierarchy
+#[derive(Debug, Default)]
+struct AppData {
+ subtrees: HashSet,
+ listening: HashSet,
+}
+
+impl AppData {
+ fn add_subtree(&mut self, subtree: &Rc) {
+ for event in self.listening.iter() {
+ subtree.add_listener(event);
+ }
+ self.subtrees.insert(WeakSubtree {
+ subtree_id: subtree.subtree_id,
+ weak_ref: Rc::downgrade(subtree),
+ });
+ }
+ fn ensure_handled(&mut self, desc: &EventDescriptor) {
+ if !self.listening.insert(desc.clone()) {
+ return;
+ }
+ self.subtrees.retain(|subtree| {
+ if let Some(subtree) = subtree.weak_ref.upgrade() {
+ subtree.add_listener(desc);
+ true
+ } else {
+ false
+ }
+ })
+ }
+}
+
+/// Bubble events during delegation
+static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true);
+
+/// Set, if events should bubble up the DOM tree, calling any matching callbacks.
+///
+/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event
+/// handling performance.
+///
+/// Note that yew uses event delegation and implements internal even bubbling for performance
+/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event
+/// handler has no effect.
+///
+/// This function should be called before any component is mounted.
+#[cfg_attr(documenting, doc(cfg(feature = "csr")))]
+pub fn set_event_bubbling(bubble: bool) {
+ BUBBLE_EVENTS.store(bubble, Ordering::Relaxed);
+}
+
+struct BrandingSearchResult {
+ branding: TreeId,
+ closest_branded_ancestor: Element,
+}
+
+/// Deduce the subtree an element is part of. This already partially starts the bubbling
+/// process, as long as no listeners are encountered.
+/// Subtree roots are always branded with their own subtree id.
+fn find_closest_branded_element(mut el: Element, do_bubble: bool) -> Option {
+ if !do_bubble {
+ let branding = el.subtree_id()?;
+ Some(BrandingSearchResult {
+ branding,
+ closest_branded_ancestor: el,
+ })
+ } else {
+ let responsible_tree_id = loop {
+ if let Some(tree_id) = el.subtree_id() {
+ break tree_id;
+ }
+ el = el.parent_element()?;
+ };
+ Some(BrandingSearchResult {
+ branding: responsible_tree_id,
+ closest_branded_ancestor: el,
+ })
+ }
+}
+
+/// Iterate over all potentially listening elements in bubbling order.
+/// If bubbling is turned off, yields at most a single element.
+fn start_bubbling_from(
+ subtree: &SubtreeData,
+ root_or_listener: Element,
+ should_bubble: bool,
+) -> impl '_ + Iterator- {
+ let start = subtree.bubble_to_inner_element(root_or_listener, should_bubble);
+
+ std::iter::successors(start, move |(subtree, element)| {
+ if !should_bubble {
+ return None;
+ }
+ let parent = element.parent_element()?;
+ subtree.bubble_to_inner_element(parent, true)
+ })
+}
+
+impl SubtreeData {
+ fn new_ref(host_element: &HtmlEventTarget, parent: Option
) -> Rc {
+ let tree_root_id = next_root_id();
+ let event_registry = Registry::new();
+ let host_handlers = HostHandlers::new(host_element.clone());
+ let app_data = match parent {
+ Some(ref parent) => parent.parent_root.app_data.clone(),
+ None => Rc::default(),
+ };
+ let subtree = Rc::new(SubtreeData {
+ parent,
+ app_data,
+
+ subtree_id: tree_root_id,
+ host: host_element.clone(),
+ event_registry: RefCell::new(event_registry),
+ global: RefCell::new(host_handlers),
+ });
+ subtree.app_data.borrow_mut().add_subtree(&subtree);
+ subtree
+ }
+
+ fn event_registry(&self) -> &RefCell {
+ &self.event_registry
+ }
+
+ fn host_handlers(&self) -> &RefCell {
+ &self.global
+ }
+
+ // Bubble a potential parent until it reaches an internal element
+ fn bubble_to_inner_element(
+ &self,
+ parent_el: Element,
+ should_bubble: bool,
+ ) -> Option<(&Self, Element)> {
+ let mut next_subtree = self;
+ let mut next_el = parent_el;
+ if !should_bubble && next_subtree.host.eq(&next_el) {
+ return None;
+ }
+ while next_subtree.host.eq(&next_el) {
+ // we've reached the host, delegate to a parent if one exists
+ let parent = next_subtree.parent.as_ref()?;
+ next_subtree = &parent.parent_root;
+ next_el = parent.mount_element.clone();
+ }
+ Some((next_subtree, next_el))
+ }
+
+ fn start_bubbling_if_responsible<'s>(
+ &'s self,
+ event: &'s Event,
+ ) -> Option> {
+ // Note: the event is not necessarily indentically the same object for all installed handlers
+ // hence this cache can be unreliable. Hence the cached repsonsible_tree_id might be missing.
+ // On the other hand, due to event retargeting at shadow roots, the cache might be wrong!
+ // Keep in mind that we handle events in the capture phase, so top-down. When descending and
+ // retargeting into closed shadow-dom, the event might have been handled 'prematurely'.
+ // TODO: figure out how to prevent this and establish correct event handling for closed shadow root.
+ // Note: Other frameworks also get this wrong and dispatch such events multiple times.
+ let event_path = event.composed_path();
+ let derived_cached_key = event_path.length();
+ let cached_branding = if matches!(event.cache_key(), Some(cache_key) if cache_key == derived_cached_key)
+ {
+ event.subtree_id()
+ } else {
+ None
+ };
+ if matches!(cached_branding, Some(responsible_tree_id) if responsible_tree_id != self.subtree_id)
+ {
+ // some other handler has determined (via this function, but other `self`) a subtree that is
+ // responsible for handling this event, and it's not this subtree.
+ return None;
+ }
+ // We're tasked with finding the subtree that is reponsible with handling the event, and/or
+ // run the handling if that's `self`.
+ let target = event_path.get(0).dyn_into::().ok()?;
+ let should_bubble = BUBBLE_EVENTS.load(Ordering::Relaxed);
+ // We say that the most deeply nested subtree is "responsible" for handling the event.
+ let (responsible_tree_id, bubbling_start) = if let Some(branding) = cached_branding {
+ (branding, target.clone())
+ } else if let Some(branding) = find_closest_branded_element(target.clone(), should_bubble) {
+ let BrandingSearchResult {
+ branding,
+ closest_branded_ancestor,
+ } = branding;
+ event.set_subtree_id(branding);
+ event.set_cache_key(derived_cached_key);
+ (branding, closest_branded_ancestor)
+ } else {
+ // Possible only? if bubbling is disabled
+ // No tree should handle this event
+ event.set_subtree_id(NONE_TREE_ID);
+ event.set_cache_key(derived_cached_key);
+ return None;
+ };
+ if self.subtree_id != responsible_tree_id {
+ return None;
+ }
+ if self.host.eq(&target) {
+ // One more special case: don't handle events that get fired directly on a subtree host
+ return None;
+ }
+ Some(start_bubbling_from(self, bubbling_start, should_bubble))
+ // # More details: When nesting occurs
+ //
+ // Event listeners are installed only on the subtree roots. Still, those roots can
+ // nest. This could lead to events getting handled multiple times. We want event handling to start
+ // at the most deeply nested subtree.
+ //
+ // A nested subtree portals into an element that is controlled by the user and rendered
+ // with VNode::VRef. We get the following dom nesting:
+ //
+ // AppRoot > .. > UserControlledVRef > .. > NestedTree(PortalExit) > ..
+ // -------------- ----------------------------
+ // The underlined parts of the hierarchy are controlled by Yew.
+ //
+ // from the following virtual_dom
+ //
+ // {VNode::VRef()}
+ // {create_portal( , #portal_target)}
+ //
+ }
+ /// Handle a global event firing
+ fn handle(&self, desc: EventDescriptor, event: Event) {
+ let run_handler = |root: &Self, el: &Element| {
+ let handler = Registry::get_handler(root.event_registry(), el, &desc);
+ if let Some(handler) = handler {
+ handler(&event)
+ }
+ };
+ if let Some(bubbling_it) = self.start_bubbling_if_responsible(&event) {
+ test_log!("Running handler on subtree {}", self.subtree_id);
+ for (subtree, el) in bubbling_it {
+ if event.cancel_bubble() {
+ break;
+ }
+ run_handler(subtree, &el);
+ }
+ }
+ }
+ fn add_listener(self: &Rc, desc: &EventDescriptor) {
+ let this = self.clone();
+ let listener = {
+ let desc = desc.clone();
+ move |e: &Event| {
+ this.handle(desc.clone(), e.clone());
+ }
+ };
+ self.host_handlers()
+ .borrow_mut()
+ .add_listener(desc, listener);
+ }
+}
+
+impl BSubtree {
+ fn do_create_root(
+ host_element: &HtmlEventTarget,
+ parent: Option,
+ ) -> Self {
+ let shared_inner = SubtreeData::new_ref(host_element, parent);
+ let root = BSubtree(shared_inner);
+ root.brand_element(host_element);
+ root
+ }
+ /// Create a bundle root at the specified host element
+ pub fn create_root(host_element: &HtmlEventTarget) -> Self {
+ Self::do_create_root(host_element, None)
+ }
+ /// Create a bundle root at the specified host element, that is logically
+ /// mounted under the specified element in this tree.
+ pub fn create_subroot(&self, mount_point: Element, host_element: &HtmlEventTarget) -> Self {
+ let parent_information = ParentingInformation {
+ parent_root: self.0.clone(),
+ mount_element: mount_point,
+ };
+ Self::do_create_root(host_element, Some(parent_information))
+ }
+ /// Ensure the event described is handled on all subtrees
+ pub fn ensure_handled(&self, desc: &EventDescriptor) {
+ self.0.app_data.borrow_mut().ensure_handled(desc);
+ }
+ /// Run f with access to global Registry
+ #[inline]
+ pub fn with_listener_registry(&self, f: impl FnOnce(&mut Registry) -> R) -> R {
+ f(&mut *self.0.event_registry().borrow_mut())
+ }
+ pub fn brand_element(&self, el: &dyn EventGrating) {
+ el.set_subtree_id(self.0.subtree_id);
+ }
+}
diff --git a/packages/yew/src/dom_bundle/traits.rs b/packages/yew/src/dom_bundle/traits.rs
index a3873da9b..a9c1639e9 100644
--- a/packages/yew/src/dom_bundle/traits.rs
+++ b/packages/yew/src/dom_bundle/traits.rs
@@ -1,6 +1,5 @@
-use super::BNode;
-use crate::html::AnyScope;
-use crate::html::NodeRef;
+use super::{BNode, BSubtree};
+use crate::html::{AnyScope, NodeRef};
use web_sys::Element;
/// A Reconcile Target.
@@ -11,7 +10,7 @@ pub(super) trait ReconcileTarget {
/// Remove self from parent.
///
/// Parent to detach is `true` if the parent element will also be detached.
- fn detach(self, parent: &Element, parent_to_detach: bool);
+ fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool);
/// Move elements from one parent to another parent.
/// This is for example used by `VSuspense` to preserve component state without detaching
@@ -26,6 +25,7 @@ pub(super) trait Reconcilable {
/// Attach a virtual node to the DOM tree.
///
/// Parameters:
+ /// - `root`: bundle of the subtree root
/// - `parent_scope`: the parent `Scope` used for passing messages to the
/// parent `Component`.
/// - `parent`: the parent node in the DOM.
@@ -34,6 +34,7 @@ pub(super) trait Reconcilable {
/// Returns a reference to the newly inserted element.
fn attach(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -58,6 +59,7 @@ pub(super) trait Reconcilable {
/// Returns a reference to the newly inserted element.
fn reconcile_node(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -66,6 +68,7 @@ pub(super) trait Reconcilable {
fn reconcile(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -75,6 +78,7 @@ pub(super) trait Reconcilable {
/// Replace an existing bundle by attaching self and detaching the existing one
fn replace(
self,
+ root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
@@ -84,9 +88,9 @@ pub(super) trait Reconcilable {
Self: Sized,
Self::Bundle: Into,
{
- let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling);
+ let (self_ref, self_) = self.attach(root, parent_scope, parent, next_sibling);
let ancestor = std::mem::replace(bundle, self_.into());
- ancestor.detach(parent, false);
+ ancestor.detach(root, parent, false);
self_ref
}
}
diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs
index 92056f2a6..5d5e43a6d 100644
--- a/packages/yew/src/html/component/lifecycle.rs
+++ b/packages/yew/src/html/component/lifecycle.rs
@@ -10,7 +10,7 @@ use std::any::Any;
use std::rc::Rc;
#[cfg(feature = "csr")]
-use crate::dom_bundle::Bundle;
+use crate::dom_bundle::{BSubtree, Bundle};
#[cfg(feature = "csr")]
use crate::html::NodeRef;
#[cfg(feature = "csr")]
@@ -20,7 +20,8 @@ pub(crate) enum ComponentRenderState {
#[cfg(feature = "csr")]
Render {
bundle: Bundle,
- parent: web_sys::Element,
+ root: BSubtree,
+ parent: Element,
next_sibling: NodeRef,
node_ref: NodeRef,
},
@@ -37,12 +38,14 @@ impl std::fmt::Debug for ComponentRenderState {
#[cfg(feature = "csr")]
Self::Render {
ref bundle,
+ ref root,
ref parent,
ref next_sibling,
ref node_ref,
} => f
.debug_struct("ComponentRenderState::Render")
.field("bundle", bundle)
+ .field("root", root)
.field("parent", parent)
.field("next_sibling", next_sibling)
.field("node_ref", node_ref)
@@ -63,6 +66,32 @@ impl std::fmt::Debug for ComponentRenderState {
}
}
+#[cfg(feature = "csr")]
+impl ComponentRenderState {
+ pub(crate) fn shift(&mut self, next_parent: Element, next_next_sibling: NodeRef) {
+ match self {
+ #[cfg(feature = "csr")]
+ Self::Render {
+ bundle,
+ parent,
+ next_sibling,
+ ..
+ } => {
+ bundle.shift(&next_parent, next_next_sibling.clone());
+
+ *parent = next_parent;
+ *next_sibling = next_next_sibling;
+ }
+
+ #[cfg(feature = "ssr")]
+ Self::Ssr { .. } => {
+ #[cfg(debug_assertions)]
+ panic!("shifting is not possible during SSR");
+ }
+ }
+ }
+}
+
struct CompStateInner
where
COMP: BaseComponent,
@@ -221,9 +250,6 @@ pub(crate) enum UpdateEvent {
/// Wraps properties, node ref, and next sibling for a component
#[cfg(feature = "csr")]
Properties(Rc, NodeRef, NodeRef),
- /// Shift Scope.
- #[cfg(feature = "csr")]
- Shift(Element, NodeRef),
}
pub(crate) struct UpdateRunner {
@@ -264,32 +290,6 @@ impl Runnable for UpdateRunner {
}
}
}
-
- #[cfg(feature = "csr")]
- UpdateEvent::Shift(next_parent, next_sibling) => {
- match state.render_state {
- ComponentRenderState::Render {
- ref bundle,
- ref mut parent,
- next_sibling: ref mut current_next_sibling,
- ..
- } => {
- bundle.shift(&next_parent, next_sibling.clone());
-
- *parent = next_parent;
- *current_next_sibling = next_sibling;
- }
-
- // Shifting is not possible during SSR.
- #[cfg(feature = "ssr")]
- ComponentRenderState::Ssr { .. } => {
- #[cfg(debug_assertions)]
- panic!("shifting is not possible during SSR");
- }
- }
-
- false
- }
};
#[cfg(debug_assertions)]
@@ -331,10 +331,11 @@ impl Runnable for DestroyRunner {
ComponentRenderState::Render {
bundle,
ref parent,
+ ref root,
ref node_ref,
..
} => {
- bundle.detach(parent, self.parent_to_detach);
+ bundle.detach(root, parent, self.parent_to_detach);
node_ref.set(None);
}
@@ -429,12 +430,14 @@ impl RenderRunner {
ComponentRenderState::Render {
ref mut bundle,
ref parent,
+ ref root,
ref next_sibling,
ref node_ref,
..
} => {
let scope = state.inner.any_scope();
- let new_node_ref = bundle.reconcile(&scope, parent, next_sibling.clone(), new_root);
+ let new_node_ref =
+ bundle.reconcile(root, &scope, parent, next_sibling.clone(), new_root);
node_ref.link(new_node_ref);
let first_render = !state.has_rendered;
@@ -492,6 +495,7 @@ mod tests {
extern crate self as yew;
use super::*;
+ use crate::dom_bundle::BSubtree;
use crate::html;
use crate::html::*;
use crate::Properties;
@@ -612,12 +616,19 @@ mod tests {
fn test_lifecycle(props: Props, expected: &[&str]) {
let document = gloo_utils::document();
let scope = Scope::::new(None);
- let el = document.create_element("div").unwrap();
- let node_ref = NodeRef::default();
+ let parent = document.create_element("div").unwrap();
+ let root = BSubtree::create_root(&parent);
+
let lifecycle = props.lifecycle.clone();
lifecycle.borrow_mut().clear();
- scope.mount_in_place(el, NodeRef::default(), node_ref, Rc::new(props));
+ scope.mount_in_place(
+ root,
+ parent,
+ NodeRef::default(),
+ NodeRef::default(),
+ Rc::new(props),
+ );
crate::scheduler::start_now();
assert_eq!(&lifecycle.borrow_mut().deref()[..], expected);
diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs
index 04582114f..f48168f9e 100644
--- a/packages/yew/src/html/component/scope.rs
+++ b/packages/yew/src/html/component/scope.rs
@@ -387,7 +387,7 @@ pub(crate) use feat_csr_ssr::*;
#[cfg(feature = "csr")]
mod feat_csr {
use super::*;
- use crate::dom_bundle::Bundle;
+ use crate::dom_bundle::{BSubtree, Bundle};
use crate::html::component::lifecycle::{
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
};
@@ -403,14 +403,17 @@ mod feat_csr {
/// Mounts a component with `props` to the specified `element` in the DOM.
pub(crate) fn mount_in_place(
&self,
+ root: BSubtree,
parent: Element,
next_sibling: NodeRef,
node_ref: NodeRef,
props: Rc,
) {
- let bundle = Bundle::new(&parent, &next_sibling, &node_ref);
+ let bundle = Bundle::new();
+ node_ref.link(next_sibling.clone());
let state = ComponentRenderState::Render {
bundle,
+ root,
node_ref,
parent,
next_sibling,
@@ -486,10 +489,10 @@ mod feat_csr {
}
fn shift_node(&self, parent: Element, next_sibling: NodeRef) {
- scheduler::push_component_update(Box::new(UpdateRunner {
- state: self.state.clone(),
- event: UpdateEvent::Shift(parent, next_sibling),
- }))
+ let mut state_ref = self.state.borrow_mut();
+ if let Some(render_state) = state_ref.as_mut() {
+ render_state.render_state.shift(parent, next_sibling)
+ }
}
}
}
diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/tests/layout_tests.rs
index 682f7f596..a65096795 100644
--- a/packages/yew/src/tests/layout_tests.rs
+++ b/packages/yew/src/tests/layout_tests.rs
@@ -1,10 +1,9 @@
-use crate::dom_bundle::Bundle;
+use crate::dom_bundle::{BSubtree, Bundle};
use crate::html::AnyScope;
use crate::scheduler;
use crate::virtual_dom::VNode;
use crate::{Component, Context, Html};
use gloo::console::log;
-use web_sys::Node;
use yew::NodeRef;
struct Comp;
@@ -38,11 +37,12 @@ pub struct TestLayout<'a> {
pub fn diff_layouts(layouts: Vec>) {
let document = gloo_utils::document();
- let parent_scope: AnyScope = AnyScope::test();
+ let scope: AnyScope = AnyScope::test();
let parent_element = document.create_element("div").unwrap();
- let parent_node: Node = parent_element.clone().into();
+ let root = BSubtree::create_root(&parent_element);
+
let end_node = document.create_text_node("END");
- parent_node.append_child(&end_node).unwrap();
+ parent_element.append_child(&end_node).unwrap();
// Tests each layout independently
let next_sibling = NodeRef::new(end_node.into());
@@ -51,10 +51,8 @@ pub fn diff_layouts(layouts: Vec>) {
let vnode = layout.node.clone();
log!("Independently apply layout '{}'", layout.name);
- let node_ref = NodeRef::default();
-
- let mut bundle = Bundle::new(&parent_element, &next_sibling, &node_ref);
- bundle.reconcile(&parent_scope, &parent_element, next_sibling.clone(), vnode);
+ let mut bundle = Bundle::new();
+ bundle.reconcile(&root, &scope, &parent_element, next_sibling.clone(), vnode);
scheduler::start_now();
assert_eq!(
parent_element.inner_html(),
@@ -68,7 +66,7 @@ pub fn diff_layouts(layouts: Vec>) {
log!("Independently reapply layout '{}'", layout.name);
- bundle.reconcile(&parent_scope, &parent_element, next_sibling.clone(), vnode);
+ bundle.reconcile(&root, &scope, &parent_element, next_sibling.clone(), vnode);
scheduler::start_now();
assert_eq!(
parent_element.inner_html(),
@@ -78,7 +76,7 @@ pub fn diff_layouts(layouts: Vec>) {
);
// Detach
- bundle.detach(&parent_element, false);
+ bundle.detach(&root, &parent_element, false);
scheduler::start_now();
assert_eq!(
parent_element.inner_html(),
@@ -89,14 +87,14 @@ pub fn diff_layouts(layouts: Vec>) {
}
// Sequentially apply each layout
- let node_ref = NodeRef::default();
- let mut bundle = Bundle::new(&parent_element, &next_sibling, &node_ref);
+ let mut bundle = Bundle::new();
for layout in layouts.iter() {
let next_vnode = layout.node.clone();
log!("Sequentially apply layout '{}'", layout.name);
bundle.reconcile(
- &parent_scope,
+ &root,
+ &scope,
&parent_element,
next_sibling.clone(),
next_vnode,
@@ -117,7 +115,8 @@ pub fn diff_layouts(layouts: Vec>) {
log!("Sequentially detach layout '{}'", layout.name);
bundle.reconcile(
- &parent_scope,
+ &root,
+ &scope,
&parent_element,
next_sibling.clone(),
next_vnode,
@@ -133,7 +132,7 @@ pub fn diff_layouts(layouts: Vec>) {
}
// Detach last layout
- bundle.detach(&parent_element, false);
+ bundle.detach(&root, &parent_element, false);
scheduler::start_now();
assert_eq!(
parent_element.inner_html(),
diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs
index aebd72a36..7912238a0 100644
--- a/packages/yew/src/virtual_dom/vcomp.rs
+++ b/packages/yew/src/virtual_dom/vcomp.rs
@@ -9,6 +9,8 @@ use std::rc::Rc;
#[cfg(any(feature = "ssr", feature = "csr"))]
use crate::html::{AnyScope, Scope};
+#[cfg(feature = "csr")]
+use crate::dom_bundle::BSubtree;
#[cfg(feature = "csr")]
use crate::html::Scoped;
#[cfg(feature = "csr")]
@@ -53,6 +55,7 @@ pub(crate) trait Mountable {
#[cfg(feature = "csr")]
fn mount(
self: Box,
+ root: &BSubtree,
node_ref: NodeRef,
parent_scope: &AnyScope,
parent: Element,
@@ -91,13 +94,14 @@ impl Mountable for PropsWrapper {
#[cfg(feature = "csr")]
fn mount(
self: Box,
+ root: &BSubtree,
node_ref: NodeRef,
parent_scope: &AnyScope,
parent: Element,
next_sibling: NodeRef,
) -> Box {
let scope: Scope = Scope::new(Some(parent_scope.clone()));
- scope.mount_in_place(parent, next_sibling, node_ref, self.props);
+ scope.mount_in_place(root.clone(), parent, next_sibling, node_ref, self.props);
Box::new(scope)
}
diff --git a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr
index a7e774e1d..563ac8e34 100644
--- a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr
+++ b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr
@@ -1,12 +1,12 @@
error[E0277]: the trait bound `Comp: yew::Component` is not satisfied
- --> tests/failed_tests/base_component_impl-fail.rs:6:6
- |
-6 | impl BaseComponent for Comp {
- | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp`
- |
- = note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp`
+ --> tests/failed_tests/base_component_impl-fail.rs:6:6
+ |
+6 | impl BaseComponent for Comp {
+ | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp`
+ |
+ = note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp`
note: required by a bound in `BaseComponent`
- --> src/html/component/mod.rs
- |
- | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static {
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent`
+ --> src/html/component/mod.rs
+ |
+ | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static {
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent`
diff --git a/website/docs/advanced-topics/portals.mdx b/website/docs/advanced-topics/portals.mdx
index 8551dfcc1..2a355f98a 100644
--- a/website/docs/advanced-topics/portals.mdx
+++ b/website/docs/advanced-topics/portals.mdx
@@ -23,7 +23,7 @@ simple modal dialogue that renders its `children` into an element outside `yew`'
identified by the `id="modal_host"`.
```rust
-use yew::{html, create_portal, function_component, Children, Properties, Html};
+use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct ModalProps {
@@ -31,11 +31,11 @@ pub struct ModalProps {
pub children: Children,
}
-#[function_component(Modal)]
-fn modal(props: &ModalProps) -> Html {
+#[function_component]
+fn Modal(props: &ModalProps) -> Html {
let modal_host = gloo::utils::document()
.get_element_by_id("modal_host")
- .expect("a #modal_host element");
+ .expect("Expected to find a #modal_host element");
create_portal(
html!{ {for props.children.iter()} },
@@ -44,5 +44,20 @@ fn modal(props: &ModalProps) -> Html {
}
```
+## Event handling
+
+Events emitted on elements inside portals follow the virtual DOM when bubbling up. That is,
+if a portal is rendered as the child of an element, then an event listener on that element
+will catch events dispatched from inside the portal, even if the portal renders its contents
+in an unrelated location in the actual DOM.
+
+This allows developers to be oblivious of whether a component they consume, is implemented with
+or without portals. Events fired on its children will bubble up regardless.
+
+A known issue is that events from portals into **closed** shadow roots will be dispatched twice,
+once targeting the element inside the shadow root and once targeting the host element itself. Keep
+in mind that **open** shadow roots work fine. If this impacts you, feel free to open a bug report
+about it.
+
## Further reading
- [Portals example](https://github.com/yewstack/yew/tree/master/examples/portals)
diff --git a/website/docs/concepts/html/events.mdx b/website/docs/concepts/html/events.mdx
index 86f51f6bd..4076102b7 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