Scoped event handlers (#2510)

* implement event handling with multiple subtree roots
* add listeners to all subtree roots
* move host element to Registry
* add BSubtree argument
* surface level internal API for BSubtree
* cache invalidation & document limitations
* Update portal documentation
* Add test case for hierarchical event bubbling
* add shadow dom test case
* add button to portals/shadow dom example
* change ShadowRootMode in example to open

BSubtree controls the element where listeners are registered.
 we have create_root and create_ssr

Async event dispatching is surprisingly complicated.
Make sure to see #2510 for details, comments and discussion

takes care of catching original events in shadow doms
This commit is contained in:
WorldSEnder 2022-03-25 17:09:15 +01:00 committed by GitHub
parent bbb7ded83e
commit ee6a67e3ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1195 additions and 569 deletions

View File

@ -31,7 +31,7 @@ impl Component for ShadowDOMHost {
.get() .get()
.expect("rendered host") .expect("rendered host")
.unchecked_into::<Element>() .unchecked_into::<Element>()
.attach_shadow(&ShadowRootInit::new(ShadowRootMode::Closed)) .attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open))
.expect("installing shadow root succeeds"); .expect("installing shadow root succeeds");
let inner_host = gloo_utils::document() let inner_host = gloo_utils::document()
.create_element("div") .create_element("div")
@ -68,34 +68,73 @@ impl Component for ShadowDOMHost {
} }
pub struct App { pub struct App {
pub style_html: Html, style_html: Html,
title_element: Element,
counter: u32,
}
pub enum AppMessage {
IncreaseCounter,
} }
impl Component for App { impl Component for App {
type Message = (); type Message = AppMessage;
type Properties = (); type Properties = ();
fn create(_ctx: &Context<Self>) -> Self { fn create(_ctx: &Context<Self>) -> Self {
let document_head = gloo_utils::document() let document_head = gloo_utils::document()
.head() .head()
.expect("head element to be present"); .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( let style_html = create_portal(
html! { html! {
<style>{"p { color: red; }"}</style> <style>{"p { color: red; }"}</style>
}, },
document_head.into(), document_head.into(),
); );
Self { style_html } Self {
style_html,
title_element,
counter: 0,
}
} }
fn view(&self, _ctx: &Context<Self>) -> Html { fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
AppMessage::IncreaseCounter => self.counter += 1,
}
true
}
fn view(&self, ctx: &Context<Self>) -> 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! { html! {
<> <>
{self.style_html.clone()} {self.style_html.clone()}
{title}
<p>{"This paragraph is colored red, and its style is mounted into "}<pre>{"document.head"}</pre>{" with a portal"}</p> <p>{"This paragraph is colored red, and its style is mounted into "}<pre>{"document.head"}</pre>{" with a portal"}</p>
<ShadowDOMHost> <div>
<p>{"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}</p> <ShadowDOMHost>
</ShadowDOMHost> <p>{"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}</p>
<span>{"Buttons clicked inside the shadow dom work fine."}</span>
<button {onclick}>{"Click me!"}</button>
</ShadowDOMHost>
<p>{format!("The button has been clicked {} times. This is also reflected in the title of the tab!", self.counter)}</p>
</div>
</> </>
} }
} }

View File

@ -76,6 +76,14 @@ wasm-bindgen-futures = "0.4"
rustversion = "1" rustversion = "1"
trybuild = "1" trybuild = "1"
[dev-dependencies.web-sys]
version = "0.3"
features = [
"ShadowRoot",
"ShadowRootInit",
"ShadowRootMode",
]
[features] [features]
ssr = ["futures", "html-escape"] ssr = ["futures", "html-escape"]
csr = [] csr = []

View File

@ -1,5 +1,6 @@
//! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope. //! [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::Scoped;
use crate::html::{IntoComponent, NodeRef, Scope}; use crate::html::{IntoComponent, NodeRef, Scope};
use std::ops::Deref; use std::ops::Deref;
@ -22,14 +23,19 @@ where
/// similarly to the `program` function in Elm. You should provide an initial model, `update` /// 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 /// function which will update the state of the model and a `view` function which
/// will render the model to a virtual DOM tree. /// will render the model to a virtual DOM tree.
pub(crate) fn mount_with_props(element: Element, props: Rc<ICOMP::Properties>) -> Self { pub(crate) fn mount_with_props(host: Element, props: Rc<ICOMP::Properties>) -> Self {
clear_element(&element); clear_element(&host);
let app = Self { let app = Self {
scope: Scope::new(None), scope: Scope::new(None),
}; };
let hosting_root = BSubtree::create_root(&host);
app.scope app.scope.mount_in_place(
.mount_in_place(element, NodeRef::default(), NodeRef::default(), props); hosting_root,
host,
NodeRef::default(),
NodeRef::default(),
props,
);
app app
} }
@ -52,8 +58,8 @@ where
} }
/// Removes anything from the given element. /// Removes anything from the given element.
fn clear_element(element: &Element) { fn clear_element(host: &Element) {
while let Some(child) = element.last_child() { while let Some(child) = host.last_child() {
element.remove_child(&child).expect("can't remove a child"); host.remove_child(&child).expect("can't remove a child");
} }
} }

View File

@ -1,8 +1,7 @@
//! This module contains the bundle implementation of a virtual component [BComp]. //! This module contains the bundle implementation of a virtual component [BComp].
use super::{BNode, Reconcilable, ReconcileTarget}; use super::{BNode, BSubtree, Reconcilable, ReconcileTarget};
use crate::html::AnyScope; use crate::html::{AnyScope, Scoped};
use crate::html::Scoped;
use crate::virtual_dom::{Key, VComp}; use crate::virtual_dom::{Key, VComp};
use crate::NodeRef; use crate::NodeRef;
use std::fmt; use std::fmt;
@ -33,7 +32,7 @@ impl fmt::Debug for BComp {
} }
impl ReconcileTarget 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); self.scope.destroy_boxed(parent_to_detach);
} }
@ -47,6 +46,7 @@ impl Reconcilable for VComp {
fn attach( fn attach(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -59,6 +59,7 @@ impl Reconcilable for VComp {
} = self; } = self;
let scope = mountable.mount( let scope = mountable.mount(
root,
node_ref.clone(), node_ref.clone(),
parent_scope, parent_scope,
parent.to_owned(), parent.to_owned(),
@ -78,6 +79,7 @@ impl Reconcilable for VComp {
fn reconcile_node( fn reconcile_node(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -88,14 +90,15 @@ impl Reconcilable for VComp {
BNode::Comp(ref mut bcomp) BNode::Comp(ref mut bcomp)
if self.type_id == bcomp.type_id && self.key == bcomp.key => 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( fn reconcile(
self, self,
_root: &BSubtree,
_parent_scope: &AnyScope, _parent_scope: &AnyScope,
_parent: &Element, _parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -165,22 +168,15 @@ mod tests {
#[test] #[test]
fn update_loop() { fn update_loop() {
let document = gloo_utils::document(); let (root, scope, parent) = setup_parent();
let parent_scope: AnyScope = AnyScope::test();
let parent_element = document.create_element("div").unwrap();
let comp = html! { <Comp></Comp> }; let comp = html! { <Comp></Comp> };
let (_, mut bundle) = comp.attach(&parent_scope, &parent_element, NodeRef::default()); let (_, mut bundle) = comp.attach(&root, &scope, &parent, NodeRef::default());
scheduler::start_now(); scheduler::start_now();
for _ in 0..10000 { for _ in 0..10000 {
let node = html! { <Comp></Comp> }; let node = html! { <Comp></Comp> };
node.reconcile_node( node.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut bundle);
&parent_scope,
&parent_element,
NodeRef::default(),
&mut bundle,
);
scheduler::start_now(); 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 scope = AnyScope::test();
let parent = document().create_element("div").unwrap(); let parent = document().create_element("div").unwrap();
let root = BSubtree::create_root(&parent);
document().body().unwrap().append_child(&parent).unwrap(); 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 // clear parent
parent.set_inner_html(""); parent.set_inner_html("");
node.attach(scope, parent, NodeRef::default()); node.attach(root, scope, parent, NodeRef::default());
scheduler::start_now(); scheduler::start_now();
parent.inner_html() parent.inner_html()
} }
#[test] #[test]
fn all_ways_of_passing_children_work() { 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"] let children: Vec<_> = vec!["a", "b", "c"]
.drain(..) .drain(..)
@ -359,7 +356,7 @@ mod tests {
let prop_method = html! { let prop_method = html! {
<List children={children_renderer.clone()} /> <List children={children_renderer.clone()} />
}; };
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! { let children_renderer_method = html! {
<List> <List>
@ -367,7 +364,7 @@ mod tests {
</List> </List>
}; };
assert_eq!( assert_eq!(
get_html(children_renderer_method, &scope, &parent), get_html(children_renderer_method, &root, &scope, &parent),
expected_html expected_html
); );
@ -376,30 +373,30 @@ mod tests {
{ children.clone() } { children.clone() }
</List> </List>
}; };
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! { let for_method = html! {
<List> <List>
{ for children } { for children }
</List> </List>
}; };
assert_eq!(get_html(for_method, &scope, &parent), expected_html); assert_eq!(get_html(for_method, &root, &scope, &parent), expected_html);
} }
#[test] #[test]
fn reset_node_ref() { fn reset_node_ref() {
let scope = AnyScope::test(); let (root, scope, parent) = setup_parent();
let parent = document().create_element("div").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
let node_ref = NodeRef::default(); let node_ref = NodeRef::default();
let elem = html! { <Comp ref={node_ref.clone()}></Comp> }; let elem = html! { <Comp ref={node_ref.clone()}></Comp> };
let (_, elem) = elem.attach(&scope, &parent, NodeRef::default()); let (_, elem) = elem.attach(&root, &scope, &parent, NodeRef::default());
scheduler::start_now(); scheduler::start_now();
let parent_node = parent.deref(); let parent_node = parent.deref();
assert_eq!(node_ref.get(), parent_node.first_child()); assert_eq!(node_ref.get(), parent_node.first_child());
elem.detach(&parent, false); elem.detach(&root, &parent, false);
scheduler::start_now(); scheduler::start_now();
assert!(node_ref.get().is_none()); assert!(node_ref.get().is_none());
} }

View File

@ -1,5 +1,5 @@
//! This module contains fragments bundles, a [BList] //! 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::dom_bundle::{Reconcilable, ReconcileTarget};
use crate::html::{AnyScope, NodeRef}; use crate::html::{AnyScope, NodeRef};
use crate::virtual_dom::{Key, VList, VNode, VText}; 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 /// Helper struct, that keeps the position where the next element is to be placed at
#[derive(Clone)] #[derive(Clone)]
struct NodeWriter<'s> { struct NodeWriter<'s> {
root: &'s BSubtree,
parent_scope: &'s AnyScope, parent_scope: &'s AnyScope,
parent: &'s Element, parent: &'s Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -45,7 +46,8 @@ impl<'s> NodeWriter<'s> {
self.parent.outer_html(), self.parent.outer_html(),
self.next_sibling 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); test_log!(" next_position: {:?}", next);
( (
Self { Self {
@ -70,7 +72,13 @@ impl<'s> NodeWriter<'s> {
self.next_sibling self.next_sibling
); );
// Advance the next sibling reference (from right to left) // 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); test_log!(" next_position: {:?}", next);
Self { Self {
next_sibling: next, next_sibling: next,
@ -135,6 +143,7 @@ impl BList {
/// Diff and patch unkeyed child lists /// Diff and patch unkeyed child lists
fn apply_unkeyed( fn apply_unkeyed(
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -142,6 +151,7 @@ impl BList {
rights: &mut Vec<BNode>, rights: &mut Vec<BNode>,
) -> NodeRef { ) -> NodeRef {
let mut writer = NodeWriter { let mut writer = NodeWriter {
root,
parent_scope, parent_scope,
parent, parent,
next_sibling, next_sibling,
@ -151,7 +161,7 @@ impl BList {
if lefts.len() < rights.len() { if lefts.len() < rights.len() {
for r in rights.drain(lefts.len()..) { for r in rights.drain(lefts.len()..) {
test_log!("removing: {:?}", r); 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 /// Optimized for node addition or removal from either end of the list and small changes in the
/// middle. /// middle.
fn apply_keyed( fn apply_keyed(
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -204,6 +215,7 @@ impl BList {
if matching_len_end == std::cmp::min(left_vdoms.len(), rev_bundles.len()) { if matching_len_end == std::cmp::min(left_vdoms.len(), rev_bundles.len()) {
// No key changes // No key changes
return Self::apply_unkeyed( return Self::apply_unkeyed(
root,
parent_scope, parent_scope,
parent, parent,
next_sibling, next_sibling,
@ -215,6 +227,7 @@ impl BList {
// We partially drain the new vnodes in several steps. // We partially drain the new vnodes in several steps.
let mut lefts = left_vdoms; let mut lefts = left_vdoms;
let mut writer = NodeWriter { let mut writer = NodeWriter {
root,
parent_scope, parent_scope,
parent, parent,
next_sibling, next_sibling,
@ -336,7 +349,7 @@ impl BList {
// Step 2.3. Remove any extra rights // Step 2.3. Remove any extra rights
for KeyedEntry(_, r) in spare_bundles.drain() { for KeyedEntry(_, r) in spare_bundles.drain() {
test_log!("removing: {:?}", r); test_log!("removing: {:?}", r);
r.detach(parent, false); r.detach(root, parent, false);
} }
// Step 3. Diff matching children at the start // Step 3. Diff matching children at the start
@ -354,9 +367,9 @@ impl BList {
} }
impl ReconcileTarget for 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() { 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( fn attach(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle) { ) -> (NodeRef, Self::Bundle) {
let mut self_ = BList::new(); 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_) (node_ref, self_)
} }
fn reconcile_node( fn reconcile_node(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
bundle: &mut BNode, bundle: &mut BNode,
) -> NodeRef { ) -> 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. // singleton list if it isn't already.
let blist = bundle.make_list(); let blist = bundle.make_list();
self.reconcile(parent_scope, parent, next_sibling, blist) self.reconcile(root, parent_scope, parent, next_sibling, blist)
} }
fn reconcile( fn reconcile(
mut self, mut self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -426,9 +442,9 @@ impl Reconcilable for VList {
rights.reserve_exact(additional); rights.reserve_exact(additional);
} }
let first = if self.fully_keyed && blist.fully_keyed { 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 { } 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.fully_keyed = self.fully_keyed;
blist.key = self.key; blist.key = self.key;

View File

@ -1,6 +1,6 @@
//! This module contains the bundle version of an abstract node [BNode] //! 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::dom_bundle::{Reconcilable, ReconcileTarget};
use crate::html::{AnyScope, NodeRef}; use crate::html::{AnyScope, NodeRef};
use crate::virtual_dom::{Key, VNode}; use crate::virtual_dom::{Key, VNode};
@ -43,20 +43,20 @@ impl BNode {
impl ReconcileTarget for BNode { impl ReconcileTarget for BNode {
/// Remove VNode from parent. /// 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 { match self {
Self::Tag(vtag) => vtag.detach(parent, parent_to_detach), Self::Tag(vtag) => vtag.detach(root, parent, parent_to_detach),
Self::Text(btext) => btext.detach(parent, parent_to_detach), Self::Text(btext) => btext.detach(root, parent, parent_to_detach),
Self::Comp(bsusp) => bsusp.detach(parent, parent_to_detach), Self::Comp(bsusp) => bsusp.detach(root, parent, parent_to_detach),
Self::List(blist) => blist.detach(parent, parent_to_detach), Self::List(blist) => blist.detach(root, parent, parent_to_detach),
Self::Ref(ref node) => { Self::Ref(ref node) => {
// Always remove user-defined nodes to clear possible parent references of them // Always remove user-defined nodes to clear possible parent references of them
if parent.remove_child(node).is_err() { if parent.remove_child(node).is_err() {
console::warn!("Node not found to remove VRef"); console::warn!("Node not found to remove VRef");
} }
} }
Self::Portal(bportal) => bportal.detach(parent, parent_to_detach), Self::Portal(bportal) => bportal.detach(root, parent, parent_to_detach),
Self::Suspense(bsusp) => bsusp.detach(parent, parent_to_detach), Self::Suspense(bsusp) => bsusp.detach(root, parent, parent_to_detach),
} }
} }
@ -82,25 +82,26 @@ impl Reconcilable for VNode {
fn attach( fn attach(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle) { ) -> (NodeRef, Self::Bundle) {
match self { match self {
VNode::VTag(vtag) => { 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()) (node_ref, tag.into())
} }
VNode::VText(vtext) => { 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()) (node_ref, text.into())
} }
VNode::VComp(vcomp) => { 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()) (node_ref, comp.into())
} }
VNode::VList(vlist) => { 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()) (node_ref, list.into())
} }
VNode::VRef(node) => { VNode::VRef(node) => {
@ -108,11 +109,12 @@ impl Reconcilable for VNode {
(NodeRef::new(node.clone()), BNode::Ref(node)) (NodeRef::new(node.clone()), BNode::Ref(node))
} }
VNode::VPortal(vportal) => { 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()) (node_ref, portal.into())
} }
VNode::VSuspense(vsuspsense) => { 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()) (node_ref, suspsense.into())
} }
} }
@ -120,31 +122,42 @@ impl Reconcilable for VNode {
fn reconcile_node( fn reconcile_node(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
bundle: &mut BNode, bundle: &mut BNode,
) -> NodeRef { ) -> NodeRef {
self.reconcile(parent_scope, parent, next_sibling, bundle) self.reconcile(root, parent_scope, parent, next_sibling, bundle)
} }
fn reconcile( fn reconcile(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
bundle: &mut BNode, bundle: &mut BNode,
) -> NodeRef { ) -> NodeRef {
match self { match self {
VNode::VTag(vtag) => vtag.reconcile_node(parent_scope, parent, next_sibling, bundle), VNode::VTag(vtag) => {
VNode::VText(vtext) => vtext.reconcile_node(parent_scope, parent, next_sibling, bundle), vtag.reconcile_node(root, 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::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) => { VNode::VRef(node) => {
let _existing = match bundle { let _existing = match bundle {
BNode::Ref(ref n) if &node == n => n, BNode::Ref(ref n) if &node == n => n,
_ => { _ => {
return VNode::VRef(node).replace( return VNode::VRef(node).replace(
root,
parent_scope, parent_scope,
parent, parent,
next_sibling, next_sibling,
@ -155,10 +168,10 @@ impl Reconcilable for VNode {
NodeRef::new(node) NodeRef::new(node)
} }
VNode::VPortal(vportal) => { 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) => { VNode::VSuspense(vsuspsense) => {
vsuspsense.reconcile_node(parent_scope, parent, next_sibling, bundle) vsuspsense.reconcile_node(root, parent_scope, parent, next_sibling, bundle)
} }
} }
} }

View File

@ -1,7 +1,6 @@
//! This module contains the bundle implementation of a portal [BPortal]. //! This module contains the bundle implementation of a portal [BPortal].
use super::test_log; use super::{test_log, BNode, BSubtree};
use super::BNode;
use crate::dom_bundle::{Reconcilable, ReconcileTarget}; use crate::dom_bundle::{Reconcilable, ReconcileTarget};
use crate::html::{AnyScope, NodeRef}; use crate::html::{AnyScope, NodeRef};
use crate::virtual_dom::Key; use crate::virtual_dom::Key;
@ -10,7 +9,9 @@ use web_sys::Element;
/// The bundle implementation to [VPortal]. /// The bundle implementation to [VPortal].
#[derive(Debug)] #[derive(Debug)]
pub(super) struct BPortal { pub struct BPortal {
// The inner root
inner_root: BSubtree,
/// The element under which the content is inserted. /// The element under which the content is inserted.
host: Element, host: Element,
/// The next sibling after the inserted content /// The next sibling after the inserted content
@ -20,10 +21,9 @@ pub(super) struct BPortal {
} }
impl ReconcileTarget for BPortal { impl ReconcileTarget for BPortal {
fn detach(self, _: &Element, _parent_to_detach: bool) { fn detach(self, _root: &BSubtree, _parent: &Element, _parent_to_detach: bool) {
test_log!("Detaching portal from host{:?}", self.host.outer_html()); test_log!("Detaching portal from host",);
self.node.detach(&self.host, false); self.node.detach(&self.inner_root, &self.host, false);
test_log!("Detached portal from host{:?}", self.host.outer_html());
} }
fn shift(&self, _next_parent: &Element, _next_sibling: NodeRef) { fn shift(&self, _next_parent: &Element, _next_sibling: NodeRef) {
@ -36,8 +36,9 @@ impl Reconcilable for VPortal {
fn attach( fn attach(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
_parent: &Element, parent: &Element,
host_next_sibling: NodeRef, host_next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle) { ) -> (NodeRef, Self::Bundle) {
let Self { let Self {
@ -45,10 +46,12 @@ impl Reconcilable for VPortal {
inner_sibling, inner_sibling,
node, node,
} = self; } = 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, host_next_sibling,
BPortal { BPortal {
inner_root,
host, host,
node: Box::new(inner), node: Box::new(inner),
inner_sibling, inner_sibling,
@ -58,19 +61,23 @@ impl Reconcilable for VPortal {
fn reconcile_node( fn reconcile_node(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
bundle: &mut BNode, bundle: &mut BNode,
) -> NodeRef { ) -> NodeRef {
match bundle { match bundle {
BNode::Portal(portal) => self.reconcile(parent_scope, parent, next_sibling, portal), BNode::Portal(portal) => {
_ => self.replace(parent_scope, parent, next_sibling, bundle), self.reconcile(root, parent_scope, parent, next_sibling, portal)
}
_ => self.replace(root, parent_scope, parent, next_sibling, bundle),
} }
} }
fn reconcile( fn reconcile(
self, self,
_root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -88,11 +95,16 @@ impl Reconcilable for VPortal {
if old_host != portal.host || old_inner_sibling != portal.inner_sibling { if old_host != portal.host || old_inner_sibling != portal.inner_sibling {
// Remount the inner node somewhere else instead of diffing // Remount the inner node somewhere else instead of diffing
// Move the node, but keep the state // Move the node, but keep the state
portal let inner_sibling = portal.inner_sibling.clone();
.node portal.node.shift(&portal.host, inner_sibling);
.shift(&portal.host, portal.inner_sibling.clone());
} }
node.reconcile_node(parent_scope, parent, next_sibling.clone(), &mut portal.node); node.reconcile_node(
&portal.inner_root,
parent_scope,
parent,
next_sibling.clone(),
&mut portal.node,
);
next_sibling next_sibling
} }
} }

View File

@ -1,6 +1,6 @@
//! This module contains the bundle version of a supsense [BSuspense] //! 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::html::AnyScope;
use crate::virtual_dom::{Key, VSuspense}; use crate::virtual_dom::{Key, VSuspense};
use crate::NodeRef; use crate::NodeRef;
@ -31,12 +31,13 @@ impl BSuspense {
} }
impl ReconcileTarget for 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 { if let Some(fallback) = self.fallback_bundle {
fallback.detach(parent, parent_to_detach); fallback.detach(root, parent, parent_to_detach);
self.children_bundle.detach(&self.detached_parent, false); self.children_bundle
.detach(root, &self.detached_parent, false);
} else { } 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( fn attach(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -68,8 +70,9 @@ impl Reconcilable for VSuspense {
// tree while rendering fallback UI into the original place where children resides in. // tree while rendering fallback UI into the original place where children resides in.
if suspended { if suspended {
let (_child_ref, children_bundle) = let (_child_ref, children_bundle) =
children.attach(parent_scope, &detached_parent, NodeRef::default()); children.attach(root, parent_scope, &detached_parent, NodeRef::default());
let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling); let (fallback_ref, fallback) =
fallback.attach(root, parent_scope, parent, next_sibling);
( (
fallback_ref, fallback_ref,
BSuspense { BSuspense {
@ -80,7 +83,8 @@ impl Reconcilable for VSuspense {
}, },
) )
} else { } 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, child_ref,
BSuspense { BSuspense {
@ -95,6 +99,7 @@ impl Reconcilable for VSuspense {
fn reconcile_node( fn reconcile_node(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -103,14 +108,15 @@ impl Reconcilable for VSuspense {
match bundle { match bundle {
// We only preserve the child state if they are the same suspense. // We only preserve the child state if they are the same suspense.
BNode::Suspense(m) if m.key == self.key => { 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( fn reconcile(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -132,30 +138,33 @@ impl Reconcilable for VSuspense {
// Both suspended, reconcile children into detached_parent, fallback into the DOM // Both suspended, reconcile children into detached_parent, fallback into the DOM
(true, Some(fallback_bundle)) => { (true, Some(fallback_bundle)) => {
children.reconcile_node( children.reconcile_node(
root,
parent_scope, parent_scope,
&suspense.detached_parent, &suspense.detached_parent,
NodeRef::default(), NodeRef::default(),
children_bundle, 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 // Not suspended, just reconcile the children into the DOM
(false, None) => { (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 // Freshly suspended. Shift children into the detached parent, then add fallback to the DOM
(true, None) => { (true, None) => {
children_bundle.shift(&suspense.detached_parent, NodeRef::default()); children_bundle.shift(&suspense.detached_parent, NodeRef::default());
children.reconcile_node( children.reconcile_node(
root,
parent_scope, parent_scope,
&suspense.detached_parent, &suspense.detached_parent,
NodeRef::default(), NodeRef::default(),
children_bundle, children_bundle,
); );
// first render of fallback // 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); suspense.fallback_bundle = Some(fallback);
fallback_ref fallback_ref
} }
@ -165,10 +174,10 @@ impl Reconcilable for VSuspense {
.fallback_bundle .fallback_bundle
.take() .take()
.unwrap() // We just matched Some(_) .unwrap() // We just matched Some(_)
.detach(parent, false); .detach(root, parent, false);
children_bundle.shift(parent, next_sibling.clone()); 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)
} }
} }
} }

View File

@ -1,4 +1,5 @@
use super::Apply; use super::Apply;
use crate::dom_bundle::BSubtree;
use crate::virtual_dom::vtag::{InputFields, Value}; use crate::virtual_dom::vtag::{InputFields, Value};
use crate::virtual_dom::Attributes; use crate::virtual_dom::Attributes;
use indexmap::IndexMap; use indexmap::IndexMap;
@ -11,14 +12,14 @@ impl<T: AccessValue> Apply for Value<T> {
type Element = T; type Element = T;
type Bundle = Self; 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() { if let Some(v) = self.deref() {
el.set_value(v); el.set_value(v);
} }
self 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()) { match (self.deref(), (*bundle).deref()) {
(Some(new), Some(_)) => { (Some(new), Some(_)) => {
// Refresh value from the DOM. It might have changed. // Refresh value from the DOM. It might have changed.
@ -62,21 +63,21 @@ impl Apply for InputFields {
type Element = InputElement; type Element = InputElement;
type Bundle = Self; 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 // IMPORTANT! This parameter has to be set every time
// to prevent strange behaviour in the browser when the DOM changes // to prevent strange behaviour in the browser when the DOM changes
el.set_checked(self.checked); el.set_checked(self.checked);
self.value = self.value.apply(el); self.value = self.value.apply(root, el);
self 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 // IMPORTANT! This parameter has to be set every time
// to prevent strange behaviour in the browser when the DOM changes // to prevent strange behaviour in the browser when the DOM changes
el.set_checked(self.checked); 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 Element = Element;
type Bundle = Self; type Bundle = Self;
fn apply(self, el: &Element) -> Self { fn apply(self, _root: &BSubtree, el: &Element) -> Self {
match &self { match &self {
Self::Static(arr) => { Self::Static(arr) => {
for kv in arr.iter() { for kv in arr.iter() {
@ -209,7 +210,7 @@ impl Apply for Attributes {
self self
} }
fn apply_diff(self, el: &Element, bundle: &mut Self) { fn apply_diff(self, _root: &BSubtree, el: &Element, bundle: &mut Self) {
#[inline] #[inline]
fn ptr_eq<T>(a: &[T], b: &[T]) -> bool { fn ptr_eq<T>(a: &[T], b: &[T]) -> bool {
std::ptr::eq(a, b) std::ptr::eq(a, b)

View File

@ -1,42 +1,38 @@
use super::Apply; use super::Apply;
use crate::dom_bundle::test_log; use crate::dom_bundle::{test_log, BSubtree, EventDescriptor};
use crate::virtual_dom::{Listener, ListenerKind, Listeners}; use crate::virtual_dom::{Listener, Listeners};
use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase}; use ::wasm_bindgen::{prelude::wasm_bindgen, JsCast};
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::{HashMap, HashSet}; use std::collections::HashMap;
use std::ops::Deref; use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering}; use web_sys::{Element, Event, EventTarget as HtmlEventTarget};
use wasm_bindgen::JsCast;
use web_sys::{Element, Event};
thread_local! { #[wasm_bindgen]
/// Global event listener registry extern "C" {
static REGISTRY: RefCell<Registry> = Default::default(); // Duck-typing, not a real class on js-side. On rust-side, use impls of EventTarget below
type EventTargetable;
/// Key used to store listener id on element #[wasm_bindgen(method, getter = __yew_listener_id, structural)]
static LISTENER_ID_PROP: wasm_bindgen::JsValue = "__yew_listener_id".into(); fn listener_id(this: &EventTargetable) -> Option<u32>;
#[wasm_bindgen(method, setter = __yew_listener_id, structural)]
/// Cached reference to the document body fn set_listener_id(this: &EventTargetable, id: u32);
static BODY: web_sys::HtmlElement = gloo_utils::document().body().unwrap();
} }
/// Bubble events during delegation /// DOM-Types that can have listeners registered on them.
static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true); /// Uses the duck-typed interface from above in impls.
pub trait EventListening {
fn listener_id(&self) -> Option<u32>;
fn set_listener_id(&self, id: u32);
}
/// Set, if events should bubble up the DOM tree, calling any matching callbacks. impl EventListening for Element {
/// fn listener_id(&self) -> Option<u32> {
/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event self.unchecked_ref::<EventTargetable>().listener_id()
/// handling performance. }
///
/// Note that yew uses event delegation and implements internal even bubbling for performance fn set_listener_id(&self, id: u32) {
/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event self.unchecked_ref::<EventTargetable>().set_listener_id(id);
/// 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);
} }
/// An active set of listeners on an element /// An active set of listeners on an element
@ -52,14 +48,14 @@ impl Apply for Listeners {
type Element = Element; type Element = Element;
type Bundle = ListenerRegistration; type Bundle = ListenerRegistration;
fn apply(self, el: &Self::Element) -> ListenerRegistration { fn apply(self, root: &BSubtree, el: &Self::Element) -> ListenerRegistration {
match self { match self {
Self::Pending(pending) => ListenerRegistration::register(el, &pending), Self::Pending(pending) => ListenerRegistration::register(root, el, &pending),
Self::None => ListenerRegistration::NoReg, 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 ListenerRegistration::*;
use Listeners::*; use Listeners::*;
@ -67,10 +63,10 @@ impl Apply for Listeners {
(Pending(pending), Registered(ref id)) => { (Pending(pending), Registered(ref id)) => {
// Reuse the ID // Reuse the ID
test_log!("reusing listeners for {}", 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) => { (Pending(pending), bundle @ NoReg) => {
*bundle = ListenerRegistration::register(el, &pending); *bundle = ListenerRegistration::register(root, el, &pending);
test_log!( test_log!(
"registering listeners for {}", "registering listeners for {}",
match bundle { match bundle {
@ -85,7 +81,7 @@ impl Apply for Listeners {
_ => unreachable!(), _ => unreachable!(),
}; };
test_log!("unregistering listeners for {}", id); test_log!("unregistering listeners for {}", id);
Registry::with(|reg| reg.unregister(id)); root.with_listener_registry(|reg| reg.unregister(id));
*bundle = NoReg; *bundle = NoReg;
} }
(None, NoReg) => { (None, NoReg) => {
@ -97,116 +93,75 @@ impl Apply for Listeners {
impl ListenerRegistration { impl ListenerRegistration {
/// Register listeners and return their handle ID /// Register listeners and return their handle ID
fn register(el: &Element, pending: &[Option<Rc<dyn Listener>>]) -> Self { fn register(root: &BSubtree, el: &Element, pending: &[Option<Rc<dyn Listener>>]) -> Self {
Self::Registered(Registry::with(|reg| { Self::Registered(root.with_listener_registry(|reg| {
let id = reg.set_listener_id(el); let id = reg.set_listener_id(root, el);
reg.register(id, pending); reg.register(root, id, pending);
id id
})) }))
} }
/// Remove any registered event listeners from the global registry /// 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 { if let Self::Registered(id) = self {
Registry::with(|r| r.unregister(id)); root.with_listener_registry(|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<EventDescriptor>,
/// Keep track of all listeners to drop them on registry drop.
/// The registry is never dropped in production.
#[cfg(test)]
registered: Vec<(ListenerKind, EventListener)>,
}
impl GlobalHandlers {
/// Ensure a descriptor has a global event handler assigned
fn ensure_handled(&mut self, desc: EventDescriptor) {
if !self.handling.contains(&desc) {
let cl = {
let desc = desc.clone();
BODY.with(move |body| {
let options = EventListenerOptions {
phase: EventListenerPhase::Capture,
passive: desc.passive,
};
EventListener::new_with_options(
body,
desc.kind.type_name(),
options,
move |e: &Event| Registry::handle(desc.clone(), e.clone()),
)
})
};
// Never drop the closure as this event handler is static
#[cfg(not(test))]
cl.forget();
#[cfg(test)]
self.registered.push((desc.kind.clone(), cl));
self.handling.insert(desc);
} }
} }
} }
/// Global multiplexing event handler registry /// Global multiplexing event handler registry
#[derive(Default, Debug)] #[derive(Debug)]
struct Registry { pub struct Registry {
/// Counter for assigning new IDs /// Counter for assigning new IDs
id_counter: u32, id_counter: u32,
/// Registered global event handlers
global: GlobalHandlers,
/// Contains all registered event listeners by listener ID /// Contains all registered event listeners by listener ID
by_id: HashMap<u32, HashMap<EventDescriptor, Vec<Rc<dyn Listener>>>>, by_id: HashMap<u32, HashMap<EventDescriptor, Vec<Rc<dyn Listener>>>>,
} }
impl Registry { impl Registry {
/// Run f with access to global Registry pub fn new() -> Self {
#[inline] Self {
fn with<R>(f: impl FnOnce(&mut Registry) -> R) -> R { id_counter: u32::default(),
REGISTRY.with(|r| f(&mut *r.borrow_mut())) by_id: HashMap::default(),
}
}
/// Handle a single event, given the listening element and event descriptor.
pub fn get_handler(
registry: &RefCell<Registry>,
listening: &dyn EventListening,
desc: &EventDescriptor,
) -> Option<impl FnOnce(&Event)> {
// 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 /// Register all passed listeners under ID
fn register(&mut self, id: u32, listeners: &[Option<Rc<dyn Listener>>]) { fn register(&mut self, root: &BSubtree, id: u32, listeners: &[Option<Rc<dyn Listener>>]) {
let mut by_desc = let mut by_desc =
HashMap::<EventDescriptor, Vec<Rc<dyn Listener>>>::with_capacity(listeners.len()); HashMap::<EventDescriptor, Vec<Rc<dyn Listener>>>::with_capacity(listeners.len());
for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() { for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
let desc = EventDescriptor::from(l.deref()); let desc = EventDescriptor::from(l.deref());
self.global.ensure_handled(desc.clone()); root.ensure_handled(&desc);
by_desc.entry(desc).or_default().push(l); by_desc.entry(desc).or_default().push(l);
} }
self.by_id.insert(id, by_desc); self.by_id.insert(id, by_desc);
} }
/// Patch an already registered set of handlers /// Patch an already registered set of handlers
fn patch(&mut self, id: &u32, listeners: &[Option<Rc<dyn Listener>>]) { fn patch(&mut self, root: &BSubtree, id: &u32, listeners: &[Option<Rc<dyn Listener>>]) {
if let Some(by_desc) = self.by_id.get_mut(id) { 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. // Keeping empty vectors is fine. Those don't do much and should happen rarely.
for v in by_desc.values_mut() { 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() { for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
let desc = EventDescriptor::from(l.deref()); let desc = EventDescriptor::from(l.deref());
self.global.ensure_handled(desc.clone()); root.ensure_handled(&desc);
by_desc.entry(desc).or_default().push(l); by_desc.entry(desc).or_default().push(l);
} }
} }
@ -227,76 +182,30 @@ impl Registry {
} }
/// Set unique listener ID onto element and return it /// 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; let id = self.id_counter;
self.id_counter += 1; self.id_counter += 1;
LISTENER_ID_PROP.with(|prop| { root.brand_element(el as &HtmlEventTarget);
if !js_sys::Reflect::set(el, prop, &js_sys::Number::from(id)).unwrap() { el.set_listener_id(id);
panic!("failed to set listener ID property");
}
});
id id
} }
/// Handle a global event firing
fn handle(desc: EventDescriptor, event: Event) {
let target = match event
.target()
.and_then(|el| el.dyn_into::<web_sys::Element>().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 { mod tests {
use std::marker::PhantomData; use std::marker::PhantomData;
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; 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); 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 gloo_utils::document;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use yew::Callback; use yew::Callback;
@ -315,29 +224,16 @@ mod tests {
text: String, text: String,
} }
trait Mixin { #[derive(Default, PartialEq, Properties)]
struct MixinProps<M: Properties> {
state_ref: NodeRef,
wrapped: M,
}
trait Mixin: Properties + Sized {
fn view<C>(ctx: &Context<C>, state: &State) -> Html fn view<C>(ctx: &Context<C>, state: &State) -> Html
where where
C: Component<Message = Message>, C: Component<Message = Message, Properties = MixinProps<Self>>;
{
let link = ctx.link().clone();
let onclick = Callback::from(move |_| {
link.send_message(Message::Action);
scheduler::start_now();
});
if state.stop_listening {
html! {
<a>{state.action}</a>
}
} else {
html! {
<a {onclick}>
{state.action}
</a>
}
}
}
} }
struct Comp<M> struct Comp<M>
@ -350,10 +246,10 @@ mod tests {
impl<M> Component for Comp<M> impl<M> Component for Comp<M>
where where
M: Mixin + 'static, M: Mixin + Properties + 'static,
{ {
type Message = Message; type Message = Message;
type Properties = (); type Properties = MixinProps<M>;
fn create(_: &Context<Self>) -> Self { fn create(_: &Context<Self>) -> Self {
Comp { Comp {
@ -382,68 +278,103 @@ mod tests {
} }
} }
fn assert_count(el: &web_sys::HtmlElement, count: isize) { #[track_caller]
assert_eq!(el.text_content(), Some(count.to_string())) fn assert_count(el: &NodeRef, count: isize) {
let text = el
.get()
.expect("State ref not bound in the test case?")
.text_content();
assert_eq!(text, Some(count.to_string()))
} }
fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement { #[track_caller]
fn click(el: &NodeRef) {
el.get().unwrap().dyn_into::<HtmlElement>().unwrap().click();
scheduler::start_now();
}
fn get_el_by_selector(selector: &str) -> web_sys::HtmlElement {
document() document()
.query_selector(tag) .query_selector(selector)
.unwrap() .unwrap()
.unwrap() .unwrap()
.dyn_into::<web_sys::HtmlElement>() .dyn_into::<web_sys::HtmlElement>()
.unwrap() .unwrap()
} }
fn init<M>(tag: &str) -> (AppHandle<Comp<M>>, web_sys::HtmlElement) fn init<M>() -> (AppHandle<Comp<M>>, NodeRef)
where where
M: Mixin, M: Mixin + Properties + Default,
{ {
// Remove any existing listeners and elements // Remove any existing elements
super::Registry::with(|r| *r = Default::default()); let body = document().body().unwrap();
if let Some(el) = document().query_selector(tag).unwrap() { while let Some(child) = body.query_selector("div#testroot").unwrap() {
el.parent_element().unwrap().remove(); body.remove_child(&child).unwrap();
} }
let root = document().create_element("div").unwrap(); let root = document().create_element("div").unwrap();
document().body().unwrap().append_child(&root).unwrap(); root.set_id("testroot");
let app = crate::Renderer::<Comp<M>>::with_root(root).render(); body.append_child(&root).unwrap();
let props = <Comp<M> as Component>::Properties::default();
let el_ref = props.state_ref.clone();
let app = crate::Renderer::<Comp<M>>::with_root_and_props(root, props).render();
scheduler::start_now(); scheduler::start_now();
(app, get_el_by_tag(tag)) (app, el_ref)
} }
#[test] #[test]
fn synchronous() { fn synchronous() {
#[derive(Default, PartialEq, Properties)]
struct Synchronous; struct Synchronous;
impl Mixin for Synchronous {} impl Mixin for Synchronous {
fn view<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message, Properties = MixinProps<Self>>,
{
let onclick = ctx.link().callback(|_| Message::Action);
let (link, el) = init::<Synchronous>("a"); if state.stop_listening {
html! {
<a ref={&ctx.props().state_ref}>{state.action}</a>
}
} else {
html! {
<a {onclick} ref={&ctx.props().state_ref}>
{state.action}
</a>
}
}
}
}
let (link, el) = init::<Synchronous>();
assert_count(&el, 0); assert_count(&el, 0);
el.click(); click(&el);
assert_count(&el, 1); assert_count(&el, 1);
el.click(); click(&el);
assert_count(&el, 2); assert_count(&el, 2);
link.send_message(Message::StopListening); link.send_message(Message::StopListening);
scheduler::start_now(); scheduler::start_now();
el.click(); click(&el);
assert_count(&el, 2); assert_count(&el, 2);
} }
#[test] #[test]
async fn non_bubbling_event() { async fn non_bubbling_event() {
#[derive(Default, PartialEq, Properties)]
struct NonBubbling; struct NonBubbling;
impl Mixin for NonBubbling { impl Mixin for NonBubbling {
fn view<C>(ctx: &Context<C>, state: &State) -> Html fn view<C>(ctx: &Context<C>, state: &State) -> Html
where where
C: Component<Message = Message>, C: Component<Message = Message, Properties = MixinProps<Self>>,
{ {
let link = ctx.link().clone(); let link = ctx.link().clone();
let onblur = Callback::from(move |_| { let onblur = Callback::from(move |_| {
@ -452,7 +383,7 @@ mod tests {
}); });
html! { html! {
<div> <div>
<a> <a ref={&ctx.props().state_ref}>
<input id="input" {onblur} type="text" /> <input id="input" {onblur} type="text" />
{state.action} {state.action}
</a> </a>
@ -461,7 +392,7 @@ mod tests {
} }
} }
let (_, el) = init::<NonBubbling>("a"); let (_, el) = init::<NonBubbling>();
assert_count(&el, 0); assert_count(&el, 0);
@ -483,30 +414,27 @@ mod tests {
#[test] #[test]
fn bubbling() { fn bubbling() {
#[derive(Default, PartialEq, Properties)]
struct Bubbling; struct Bubbling;
impl Mixin for Bubbling { impl Mixin for Bubbling {
fn view<C>(ctx: &Context<C>, state: &State) -> Html fn view<C>(ctx: &Context<C>, state: &State) -> Html
where where
C: Component<Message = Message>, C: Component<Message = Message, Properties = MixinProps<Self>>,
{ {
if state.stop_listening { if state.stop_listening {
html! { html! {
<div> <div>
<a> <a ref={&ctx.props().state_ref}>
{state.action} {state.action}
</a> </a>
</div> </div>
} }
} else { } else {
let link = ctx.link().clone(); let cb = ctx.link().callback(|_| Message::Action);
let cb = Callback::from(move |_| {
link.send_message(Message::Action);
scheduler::start_now();
});
html! { html! {
<div onclick={cb.clone()}> <div onclick={cb.clone()}>
<a onclick={cb}> <a onclick={cb} ref={&ctx.props().state_ref}>
{state.action} {state.action}
</a> </a>
</div> </div>
@ -515,47 +443,39 @@ mod tests {
} }
} }
let (link, el) = init::<Bubbling>("a"); let (link, el) = init::<Bubbling>();
assert_count(&el, 0); assert_count(&el, 0);
click(&el);
el.click();
assert_count(&el, 2); assert_count(&el, 2);
click(&el);
el.click();
assert_count(&el, 4); assert_count(&el, 4);
link.send_message(Message::StopListening); link.send_message(Message::StopListening);
scheduler::start_now(); scheduler::start_now();
el.click(); click(&el);
assert_count(&el, 4); assert_count(&el, 4);
} }
#[test] #[test]
fn cancel_bubbling() { fn cancel_bubbling() {
#[derive(Default, PartialEq, Properties)]
struct CancelBubbling; struct CancelBubbling;
impl Mixin for CancelBubbling { impl Mixin for CancelBubbling {
fn view<C>(ctx: &Context<C>, state: &State) -> Html fn view<C>(ctx: &Context<C>, state: &State) -> Html
where where
C: Component<Message = Message>, C: Component<Message = Message, Properties = MixinProps<Self>>,
{ {
let link = ctx.link().clone(); let onclick = ctx.link().callback(|_| Message::Action);
let onclick = Callback::from(move |_| { let onclick2 = ctx.link().callback(|e: MouseEvent| {
link.send_message(Message::Action);
scheduler::start_now();
});
let link = ctx.link().clone();
let onclick2 = Callback::from(move |e: MouseEvent| {
e.stop_propagation(); e.stop_propagation();
link.send_message(Message::Action); Message::Action
scheduler::start_now();
}); });
html! { html! {
<div onclick={onclick}> <div onclick={onclick}>
<a onclick={onclick2}> <a onclick={onclick2} ref={&ctx.props().state_ref}>
{state.action} {state.action}
</a> </a>
</div> </div>
@ -563,14 +483,12 @@ mod tests {
} }
} }
let (_, el) = init::<CancelBubbling>("a"); let (_, el) = init::<CancelBubbling>();
assert_count(&el, 0); assert_count(&el, 0);
click(&el);
el.click();
assert_count(&el, 1); assert_count(&el, 1);
click(&el);
el.click();
assert_count(&el, 2); assert_count(&el, 2);
} }
@ -579,29 +497,23 @@ mod tests {
// Here an event is being delivered to a DOM node which does // Here an event is being delivered to a DOM node which does
// _not_ have a listener but which is contained within an // _not_ have a listener but which is contained within an
// element that does and which cancels the bubble. // element that does and which cancels the bubble.
#[derive(Default, PartialEq, Properties)]
struct CancelBubbling; struct CancelBubbling;
impl Mixin for CancelBubbling { impl Mixin for CancelBubbling {
fn view<C>(ctx: &Context<C>, state: &State) -> Html fn view<C>(ctx: &Context<C>, state: &State) -> Html
where where
C: Component<Message = Message>, C: Component<Message = Message, Properties = MixinProps<Self>>,
{ {
let link = ctx.link().clone(); let onclick = ctx.link().callback(|_| Message::Action);
let onclick = Callback::from(move |_| { let onclick2 = ctx.link().callback(|e: MouseEvent| {
link.send_message(Message::Action);
scheduler::start_now();
});
let link = ctx.link().clone();
let onclick2 = Callback::from(move |e: MouseEvent| {
e.stop_propagation(); e.stop_propagation();
link.send_message(Message::Action); Message::Action
scheduler::start_now();
}); });
html! { html! {
<div onclick={onclick}> <div onclick={onclick}>
<div onclick={onclick2}> <div onclick={onclick2}>
<a> <a ref={&ctx.props().state_ref}>
{state.action} {state.action}
</a> </a>
</div> </div>
@ -610,65 +522,153 @@ mod tests {
} }
} }
let (_, el) = init::<CancelBubbling>("a"); let (_, el) = init::<CancelBubbling>();
assert_count(&el, 0); assert_count(&el, 0);
click(&el);
el.click();
assert_count(&el, 1); assert_count(&el, 1);
click(&el);
el.click();
assert_count(&el, 2); 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<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message, Properties = MixinProps<Self>>,
{
let portal_target = ctx.props().wrapped.host.clone();
let onclick = ctx.link().callback(|_| Message::Action);
html! {
<>
<div onclick={onclick}>
{create_portal(html! {
<a ref={&ctx.props().state_ref}>
{state.action}
</a>
}, portal_target.clone())}
</div>
{VNode::VRef(portal_target.into())}
</>
}
}
}
let (_, el) = init::<PortalBubbling>();
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<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message, Properties = MixinProps<Self>>,
{
let onclick = ctx.link().callback(|_| Message::Action);
let mixin = &ctx.props().wrapped;
html! {
<div onclick={onclick.clone()}>
<div {onclick}>
{create_portal(html! {
<a ref={&ctx.props().state_ref}>
{state.action}
</a>
}, mixin.inner_root.clone())}
</div>
{VNode::VRef(mixin.host.clone().into())}
</div>
}
}
}
let (_, el) = init::<OpenShadowDom>();
assert_count(&el, 0);
click(&el);
assert_count(&el, 2); // Once caught per handler
}
fn test_input_listener<E>(make_event: impl Fn() -> E) fn test_input_listener<E>(make_event: impl Fn() -> E)
where where
E: JsCast + std::fmt::Debug, E: Into<Event> + std::fmt::Debug,
{ {
#[derive(Default, PartialEq, Properties)]
struct Input; struct Input;
impl Mixin for Input { impl Mixin for Input {
fn view<C>(ctx: &Context<C>, state: &State) -> Html fn view<C>(ctx: &Context<C>, state: &State) -> Html
where where
C: Component<Message = Message>, C: Component<Message = Message, Properties = MixinProps<Self>>,
{ {
if state.stop_listening { if state.stop_listening {
html! { html! {
<div> <div>
<input type="text" /> <input type="text" />
<p>{state.text.clone()}</p> <p ref={&ctx.props().state_ref}>{state.text.clone()}</p>
</div> </div>
} }
} else { } else {
let link = ctx.link().clone(); let onchange = ctx.link().callback(|e: web_sys::Event| {
let onchange = Callback::from(move |e: web_sys::Event| {
let el: web_sys::HtmlInputElement = e.target_unchecked_into(); let el: web_sys::HtmlInputElement = e.target_unchecked_into();
link.send_message(Message::SetText(el.value())); Message::SetText(el.value())
scheduler::start_now();
}); });
let oninput = ctx.link().callback(|e: web_sys::InputEvent| {
let link = ctx.link().clone();
let oninput = Callback::from(move |e: web_sys::InputEvent| {
let el: web_sys::HtmlInputElement = e.target_unchecked_into(); let el: web_sys::HtmlInputElement = e.target_unchecked_into();
link.send_message(Message::SetText(el.value())); Message::SetText(el.value())
scheduler::start_now();
}); });
html! { html! {
<div> <div>
<input type="text" {onchange} {oninput} /> <input type="text" {onchange} {oninput} />
<p>{state.text.clone()}</p> <p ref={&ctx.props().state_ref}>{state.text.clone()}</p>
</div> </div>
} }
} }
} }
} }
let (link, input_el) = init::<Input>("input"); let (link, state_ref) = init::<Input>();
let input_el = input_el.dyn_into::<web_sys::HtmlInputElement>().unwrap(); let input_el = get_el_by_selector("input")
let p_el = get_el_by_tag("p"); .dyn_into::<web_sys::HtmlInputElement>()
.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() { for mut s in ["foo", "bar", "baz"].iter() {
input_el.set_value(s); input_el.set_value(s);
if s == &"baz" { if s == &"baz" {
@ -677,12 +677,9 @@ mod tests {
s = &"bar"; s = &"bar";
} }
input_el input_el.dispatch_event(&make_event().into()).unwrap();
.dyn_ref::<web_sys::EventTarget>() scheduler::start_now();
.unwrap() assert_eq!(&state_ref.get().unwrap().text_content().unwrap(), s);
.dispatch_event(&make_event().dyn_into().unwrap())
.unwrap();
assert_eq!(&p_el.text_content().unwrap(), s);
} }
} }

View File

@ -3,9 +3,9 @@
mod attributes; mod attributes;
mod listeners; 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::html::AnyScope;
use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE}; use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE};
use crate::virtual_dom::{Attributes, Key, VTag}; use crate::virtual_dom::{Attributes, Key, VTag};
@ -25,10 +25,10 @@ trait Apply {
type Bundle; type Bundle;
/// Apply contained values to [Element](Self::Element) with no ancestor /// 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). /// 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. /// [BTag] fields that are specific to different [BTag] kinds.
@ -69,14 +69,14 @@ pub(super) struct BTag {
} }
impl ReconcileTarget for BTag { impl ReconcileTarget for BTag {
fn detach(self, parent: &Element, parent_to_detach: bool) { fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) {
self.listeners.unregister(); self.listeners.unregister(root);
let node = self.reference; let node = self.reference;
// recursively remove its children // recursively remove its children
if let BTagInner::Other { child_bundle, .. } = self.inner { if let BTagInner::Other { child_bundle, .. } = self.inner {
// This tag will be removed, so there's no point to remove any child. // 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 { if !parent_to_detach {
let result = parent.remove_child(&node); let result = parent.remove_child(&node);
@ -104,6 +104,7 @@ impl Reconcilable for VTag {
fn attach( fn attach(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -118,20 +119,21 @@ impl Reconcilable for VTag {
} = self; } = self;
insert_node(&el, parent, next_sibling.get().as_ref()); insert_node(&el, parent, next_sibling.get().as_ref());
let attributes = attributes.apply(&el); let attributes = attributes.apply(root, &el);
let listeners = listeners.apply(&el); let listeners = listeners.apply(root, &el);
let inner = match self.inner { let inner = match self.inner {
VTagInner::Input(f) => { VTagInner::Input(f) => {
let f = f.apply(el.unchecked_ref()); let f = f.apply(root, el.unchecked_ref());
BTagInner::Input(f) BTagInner::Input(f)
} }
VTagInner::Textarea { value } => { VTagInner::Textarea { value } => {
let value = value.apply(el.unchecked_ref()); let value = value.apply(root, el.unchecked_ref());
BTagInner::Textarea { value } BTagInner::Textarea { value }
} }
VTagInner::Other { children, tag } => { 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 } BTagInner::Other { child_bundle, tag }
} }
}; };
@ -151,6 +153,7 @@ impl Reconcilable for VTag {
fn reconcile_node( fn reconcile_node(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -173,31 +176,38 @@ impl Reconcilable for VTag {
} }
_ => false, _ => 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( fn reconcile(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
_parent: &Element, _parent: &Element,
_next_sibling: NodeRef, _next_sibling: NodeRef,
tag: &mut Self::Bundle, tag: &mut Self::Bundle,
) -> NodeRef { ) -> NodeRef {
let el = &tag.reference; let el = &tag.reference;
self.attributes.apply_diff(el, &mut tag.attributes); self.attributes.apply_diff(root, el, &mut tag.attributes);
self.listeners.apply_diff(el, &mut tag.listeners); self.listeners.apply_diff(root, el, &mut tag.listeners);
match (self.inner, &mut tag.inner) { match (self.inner, &mut tag.inner) {
(VTagInner::Input(new), BTagInner::Input(old)) => { (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 }) => { (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, .. }, VTagInner::Other { children: new, .. },
@ -205,7 +215,7 @@ impl Reconcilable for VTag {
child_bundle: old, .. 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 // Can not happen, because we checked for tag equability above
_ => unsafe { unreachable_unchecked() }, _ => unsafe { unreachable_unchecked() },
@ -295,8 +305,14 @@ mod tests {
wasm_bindgen_test_configure!(run_in_browser); wasm_bindgen_test_configure!(run_in_browser);
fn test_scope() -> AnyScope { fn setup_parent() -> (BSubtree, AnyScope, Element) {
AnyScope::test() 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] #[test]
@ -475,10 +491,9 @@ mod tests {
#[test] #[test]
fn supports_svg() { fn supports_svg() {
let (root, scope, parent) = setup_parent();
let document = web_sys::window().unwrap().document().unwrap(); 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 = SVG_NAMESPACE;
let namespace = Some(namespace); let namespace = Some(namespace);
let svg_el = document.create_element_ns(namespace, "svg").unwrap(); let svg_el = document.create_element_ns(namespace, "svg").unwrap();
@ -488,17 +503,17 @@ mod tests {
let svg_node = html! { <svg>{path_node}</svg> }; let svg_node = html! { <svg>{path_node}</svg> };
let svg_tag = assert_vtag(svg_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); assert_namespace(&svg_tag, SVG_NAMESPACE);
let path_tag = assert_btag_ref(svg_tag.children().get(0).unwrap()); let path_tag = assert_btag_ref(svg_tag.children().get(0).unwrap());
assert_namespace(path_tag, SVG_NAMESPACE); assert_namespace(path_tag, SVG_NAMESPACE);
let g_tag = assert_vtag(g_node.clone()); 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); assert_namespace(&g_tag, HTML_NAMESPACE);
let g_tag = assert_vtag(g_node); 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); assert_namespace(&g_tag, SVG_NAMESPACE);
} }
@ -594,26 +609,20 @@ mod tests {
#[test] #[test]
fn it_does_not_set_missing_class_name() { fn it_does_not_set_missing_class_name() {
let scope = test_scope(); let (root, scope, parent) = setup_parent();
let parent = document().create_element("div").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
let elem = html! { <div></div> }; let elem = html! { <div></div> };
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); let vtag = assert_btag_mut(&mut elem);
// test if the className has not been set // test if the className has not been set
assert!(!vtag.reference().has_attribute("class")); assert!(!vtag.reference().has_attribute("class"));
} }
fn test_set_class_name(gen_html: impl FnOnce() -> Html) { fn test_set_class_name(gen_html: impl FnOnce() -> Html) {
let scope = test_scope(); let (root, scope, parent) = setup_parent();
let parent = document().create_element("div").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
let elem = gen_html(); 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); let vtag = assert_btag_mut(&mut elem);
// test if the className has been set // test if the className has been set
assert!(vtag.reference().has_attribute("class")); assert!(vtag.reference().has_attribute("class"));
@ -631,16 +640,13 @@ mod tests {
#[test] #[test]
fn controlled_input_synced() { fn controlled_input_synced() {
let scope = test_scope(); let (root, scope, parent) = setup_parent();
let parent = document().create_element("div").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
let expected = "not_changed_value"; let expected = "not_changed_value";
// Initial state // Initial state
let elem = html! { <input value={expected} /> }; let elem = html! { <input value={expected} /> };
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); let vtag = assert_btag_ref(&elem);
// User input // User input
@ -652,7 +658,7 @@ mod tests {
let elem_vtag = assert_vtag(next_elem); let elem_vtag = assert_vtag(next_elem);
// Sync happens here // 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); let vtag = assert_btag_ref(&elem);
// Get new current value of the input element // Get new current value of the input element
@ -667,14 +673,11 @@ mod tests {
#[test] #[test]
fn uncontrolled_input_unsynced() { fn uncontrolled_input_unsynced() {
let scope = test_scope(); let (root, scope, parent) = setup_parent();
let parent = document().create_element("div").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
// Initial state // Initial state
let elem = html! { <input /> }; let elem = html! { <input /> };
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); let vtag = assert_btag_ref(&elem);
// User input // User input
@ -686,7 +689,7 @@ mod tests {
let elem_vtag = assert_vtag(next_elem); let elem_vtag = assert_vtag(next_elem);
// Value should not be refreshed // 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); let vtag = assert_btag_ref(&elem);
// Get user value of the input element // Get user value of the input element
@ -705,10 +708,7 @@ mod tests {
#[test] #[test]
fn dynamic_tags_work() { fn dynamic_tags_work() {
let scope = test_scope(); let (root, scope, parent) = setup_parent();
let parent = document().create_element("div").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
let elem = html! { <@{ let elem = html! { <@{
let mut builder = String::new(); let mut builder = String::new();
@ -716,7 +716,7 @@ mod tests {
builder 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); let vtag = assert_btag_mut(&mut elem);
// make sure the new tag name is used internally // make sure the new tag name is used internally
assert_eq!(vtag.tag(), "a"); assert_eq!(vtag.tag(), "a");
@ -758,36 +758,31 @@ mod tests {
#[test] #[test]
fn reset_node_ref() { fn reset_node_ref() {
let scope = test_scope(); let (root, scope, parent) = setup_parent();
let parent = document().create_element("div").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
let node_ref = NodeRef::default(); let node_ref = NodeRef::default();
let elem: VNode = html! { <div ref={node_ref.clone()}></div> }; let elem: VNode = html! { <div ref={node_ref.clone()}></div> };
assert_vtag_ref(&elem); 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()); assert_eq!(node_ref.get(), parent.first_child());
elem.detach(&parent, false); elem.detach(&root, &parent, false);
assert!(node_ref.get().is_none()); assert!(node_ref.get().is_none());
} }
#[test] #[test]
fn vtag_reuse_should_reset_ancestors_node_ref() { fn vtag_reuse_should_reset_ancestors_node_ref() {
let scope = test_scope(); let (root, scope, parent) = setup_parent();
let parent = document().create_element("div").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
let node_ref_a = NodeRef::default(); let node_ref_a = NodeRef::default();
let elem_a = html! { <div id="a" ref={node_ref_a.clone()} /> }; let elem_a = html! { <div id="a" ref={node_ref_a.clone()} /> };
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. // save the Node to check later that it has been reused.
let node_a = node_ref_a.get().unwrap(); let node_a = node_ref_a.get().unwrap();
let node_ref_b = NodeRef::default(); let node_ref_b = NodeRef::default();
let elem_b = html! { <div id="b" ref={node_ref_b.clone()} /> }; let elem_b = html! { <div id="b" ref={node_ref_b.clone()} /> };
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(); let node_b = node_ref_b.get().unwrap();
@ -800,9 +795,7 @@ mod tests {
#[test] #[test]
fn vtag_should_not_touch_newly_bound_refs() { fn vtag_should_not_touch_newly_bound_refs() {
let scope = test_scope(); let (root, scope, parent) = setup_parent();
let parent = document().create_element("div").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
let test_ref = NodeRef::default(); let test_ref = NodeRef::default();
let before = html! { 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, // 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 // while both should be bound to the same node ref
let (_, mut elem) = before.attach(&scope, &parent, NodeRef::default()); let (_, mut elem) = before.attach(&root, &scope, &parent, NodeRef::default());
after.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); after.reconcile_node(&root, &scope, &parent, NodeRef::default(), &mut elem);
assert_eq!( assert_eq!(
test_ref test_ref

View File

@ -1,6 +1,6 @@
//! This module contains the bundle implementation of text [BText]. //! 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::html::AnyScope;
use crate::virtual_dom::{AttrValue, VText}; use crate::virtual_dom::{AttrValue, VText};
use crate::NodeRef; use crate::NodeRef;
@ -15,7 +15,7 @@ pub(super) struct BText {
} }
impl ReconcileTarget for 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 { if !parent_to_detach {
let result = parent.remove_child(&self.text_node); let result = parent.remove_child(&self.text_node);
@ -39,6 +39,7 @@ impl Reconcilable for VText {
fn attach( fn attach(
self, self,
_root: &BSubtree,
_parent_scope: &AnyScope, _parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, 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. /// Renders virtual node over existing `TextNode`, but only if value of text has changed.
fn reconcile_node( fn reconcile_node(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
bundle: &mut BNode, bundle: &mut BNode,
) -> NodeRef { ) -> NodeRef {
match bundle { match bundle {
BNode::Text(btext) => self.reconcile(parent_scope, parent, next_sibling, btext), BNode::Text(btext) => self.reconcile(root, parent_scope, parent, next_sibling, btext),
_ => self.replace(parent_scope, parent, next_sibling, bundle), _ => self.replace(root, parent_scope, parent, next_sibling, bundle),
} }
} }
fn reconcile( fn reconcile(
self, self,
_root: &BSubtree,
_parent_scope: &AnyScope, _parent_scope: &AnyScope,
_parent: &Element, _parent: &Element,
_next_sibling: NodeRef, _next_sibling: NodeRef,

View File

@ -12,11 +12,12 @@ mod bportal;
mod bsuspense; mod bsuspense;
mod btag; mod btag;
mod btext; mod btext;
mod subtree_root;
mod traits; mod traits;
mod utils; mod utils;
use gloo::utils::document; use web_sys::Element;
use web_sys::{Element, Node};
use crate::html::AnyScope; use crate::html::AnyScope;
use crate::html::NodeRef; use crate::html::NodeRef;
@ -27,13 +28,16 @@ use blist::BList;
use bnode::BNode; use bnode::BNode;
use bportal::BPortal; use bportal::BPortal;
use bsuspense::BSuspense; use bsuspense::BSuspense;
use btag::BTag; use btag::{BTag, Registry};
use btext::BText; use btext::BText;
use subtree_root::EventDescriptor;
use traits::{Reconcilable, ReconcileTarget}; use traits::{Reconcilable, ReconcileTarget};
use utils::{insert_node, test_log}; use utils::{insert_node, test_log};
#[doc(hidden)] // Publically exported from crate::events #[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. /// A Bundle.
/// ///
@ -46,11 +50,8 @@ pub(crate) struct Bundle(BNode);
impl Bundle { impl Bundle {
/// Creates a new bundle. /// Creates a new bundle.
pub fn new(parent: &Element, next_sibling: &NodeRef, node_ref: &NodeRef) -> Self { pub const fn new() -> Self {
let placeholder: Node = document().create_text_node("").into(); Self(BNode::List(BList::new()))
insert_node(&placeholder, parent, next_sibling.get().as_ref());
node_ref.set(Some(placeholder.clone()));
Self(BNode::Ref(placeholder))
} }
/// Shifts the bundle into a different position. /// Shifts the bundle into a different position.
@ -61,16 +62,17 @@ impl Bundle {
/// Applies a virtual dom layout to current bundle. /// Applies a virtual dom layout to current bundle.
pub fn reconcile( pub fn reconcile(
&mut self, &mut self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
next_node: VNode, next_node: VNode,
) -> NodeRef { ) -> 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. /// Detaches current bundle.
pub fn detach(self, parent: &Element, parent_to_detach: bool) { pub fn detach(self, root: &BSubtree, parent: &Element, parent_to_detach: bool) {
self.0.detach(parent, parent_to_detach); self.0.detach(root, parent, parent_to_detach);
} }
} }

View File

@ -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<TreeId>;
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<u32>;
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<TreeId>;
#[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<u32>;
#[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<TreeId> {
self.unchecked_ref::<EventTargetable>().subtree_id()
}
fn set_subtree_id(&self, tree_id: TreeId) {
self.unchecked_ref::<EventTargetable>()
.set_subtree_id(tree_id);
}
fn cache_key(&self) -> Option<u32> {
self.unchecked_ref::<EventTargetable>().cache_key()
}
fn set_cache_key(&self, key: u32) {
self.unchecked_ref::<EventTargetable>().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<SubtreeData>);
/// 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<SubtreeData>,
// 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<RefCell<AppData>>,
/// Parent subtree
parent: Option<ParentingInformation>,
subtree_id: TreeId,
host: HtmlEventTarget,
event_registry: RefCell<Registry>,
global: RefCell<HostHandlers>,
}
#[derive(Debug)]
struct WeakSubtree {
subtree_id: TreeId,
weak_ref: Weak<SubtreeData>,
}
impl Hash for WeakSubtree {
fn hash<H: Hasher>(&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<WeakSubtree>,
listening: HashSet<EventDescriptor>,
}
impl AppData {
fn add_subtree(&mut self, subtree: &Rc<SubtreeData>) {
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<BrandingSearchResult> {
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<Item = (&'_ SubtreeData, Element)> {
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<ParentingInformation>) -> Rc<Self> {
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<Registry> {
&self.event_registry
}
fn host_handlers(&self) -> &RefCell<HostHandlers> {
&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<impl 's + Iterator<Item = (&'s SubtreeData, Element)>> {
// 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::<Element>().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
// <AppRoot>
// {VNode::VRef(<div><div id="portal_target" /></div>)}
// {create_portal(<NestedTree />, #portal_target)}
// </AppRoot>
}
/// 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<Self>, 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<ParentingInformation>,
) -> 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<R>(&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);
}
}

View File

@ -1,6 +1,5 @@
use super::BNode; use super::{BNode, BSubtree};
use crate::html::AnyScope; use crate::html::{AnyScope, NodeRef};
use crate::html::NodeRef;
use web_sys::Element; use web_sys::Element;
/// A Reconcile Target. /// A Reconcile Target.
@ -11,7 +10,7 @@ pub(super) trait ReconcileTarget {
/// Remove self from parent. /// Remove self from parent.
/// ///
/// Parent to detach is `true` if the parent element will also be detached. /// 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. /// Move elements from one parent to another parent.
/// This is for example used by `VSuspense` to preserve component state without detaching /// 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. /// Attach a virtual node to the DOM tree.
/// ///
/// Parameters: /// Parameters:
/// - `root`: bundle of the subtree root
/// - `parent_scope`: the parent `Scope` used for passing messages to the /// - `parent_scope`: the parent `Scope` used for passing messages to the
/// parent `Component`. /// parent `Component`.
/// - `parent`: the parent node in the DOM. /// - `parent`: the parent node in the DOM.
@ -34,6 +34,7 @@ pub(super) trait Reconcilable {
/// Returns a reference to the newly inserted element. /// Returns a reference to the newly inserted element.
fn attach( fn attach(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -58,6 +59,7 @@ pub(super) trait Reconcilable {
/// Returns a reference to the newly inserted element. /// Returns a reference to the newly inserted element.
fn reconcile_node( fn reconcile_node(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -66,6 +68,7 @@ pub(super) trait Reconcilable {
fn reconcile( fn reconcile(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -75,6 +78,7 @@ pub(super) trait Reconcilable {
/// Replace an existing bundle by attaching self and detaching the existing one /// Replace an existing bundle by attaching self and detaching the existing one
fn replace( fn replace(
self, self,
root: &BSubtree,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: &Element, parent: &Element,
next_sibling: NodeRef, next_sibling: NodeRef,
@ -84,9 +88,9 @@ pub(super) trait Reconcilable {
Self: Sized, Self: Sized,
Self::Bundle: Into<BNode>, Self::Bundle: Into<BNode>,
{ {
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()); let ancestor = std::mem::replace(bundle, self_.into());
ancestor.detach(parent, false); ancestor.detach(root, parent, false);
self_ref self_ref
} }
} }

View File

@ -10,7 +10,7 @@ use std::any::Any;
use std::rc::Rc; use std::rc::Rc;
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
use crate::dom_bundle::Bundle; use crate::dom_bundle::{BSubtree, Bundle};
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
use crate::html::NodeRef; use crate::html::NodeRef;
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
@ -20,7 +20,8 @@ pub(crate) enum ComponentRenderState {
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
Render { Render {
bundle: Bundle, bundle: Bundle,
parent: web_sys::Element, root: BSubtree,
parent: Element,
next_sibling: NodeRef, next_sibling: NodeRef,
node_ref: NodeRef, node_ref: NodeRef,
}, },
@ -37,12 +38,14 @@ impl std::fmt::Debug for ComponentRenderState {
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
Self::Render { Self::Render {
ref bundle, ref bundle,
ref root,
ref parent, ref parent,
ref next_sibling, ref next_sibling,
ref node_ref, ref node_ref,
} => f } => f
.debug_struct("ComponentRenderState::Render") .debug_struct("ComponentRenderState::Render")
.field("bundle", bundle) .field("bundle", bundle)
.field("root", root)
.field("parent", parent) .field("parent", parent)
.field("next_sibling", next_sibling) .field("next_sibling", next_sibling)
.field("node_ref", node_ref) .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<COMP> struct CompStateInner<COMP>
where where
COMP: BaseComponent, COMP: BaseComponent,
@ -221,9 +250,6 @@ pub(crate) enum UpdateEvent {
/// Wraps properties, node ref, and next sibling for a component /// Wraps properties, node ref, and next sibling for a component
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
Properties(Rc<dyn Any>, NodeRef, NodeRef), Properties(Rc<dyn Any>, NodeRef, NodeRef),
/// Shift Scope.
#[cfg(feature = "csr")]
Shift(Element, NodeRef),
} }
pub(crate) struct UpdateRunner { 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)] #[cfg(debug_assertions)]
@ -331,10 +331,11 @@ impl Runnable for DestroyRunner {
ComponentRenderState::Render { ComponentRenderState::Render {
bundle, bundle,
ref parent, ref parent,
ref root,
ref node_ref, ref node_ref,
.. ..
} => { } => {
bundle.detach(parent, self.parent_to_detach); bundle.detach(root, parent, self.parent_to_detach);
node_ref.set(None); node_ref.set(None);
} }
@ -429,12 +430,14 @@ impl RenderRunner {
ComponentRenderState::Render { ComponentRenderState::Render {
ref mut bundle, ref mut bundle,
ref parent, ref parent,
ref root,
ref next_sibling, ref next_sibling,
ref node_ref, ref node_ref,
.. ..
} => { } => {
let scope = state.inner.any_scope(); 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); node_ref.link(new_node_ref);
let first_render = !state.has_rendered; let first_render = !state.has_rendered;
@ -492,6 +495,7 @@ mod tests {
extern crate self as yew; extern crate self as yew;
use super::*; use super::*;
use crate::dom_bundle::BSubtree;
use crate::html; use crate::html;
use crate::html::*; use crate::html::*;
use crate::Properties; use crate::Properties;
@ -612,12 +616,19 @@ mod tests {
fn test_lifecycle(props: Props, expected: &[&str]) { fn test_lifecycle(props: Props, expected: &[&str]) {
let document = gloo_utils::document(); let document = gloo_utils::document();
let scope = Scope::<Comp>::new(None); let scope = Scope::<Comp>::new(None);
let el = document.create_element("div").unwrap(); let parent = document.create_element("div").unwrap();
let node_ref = NodeRef::default(); let root = BSubtree::create_root(&parent);
let lifecycle = props.lifecycle.clone(); let lifecycle = props.lifecycle.clone();
lifecycle.borrow_mut().clear(); 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(); crate::scheduler::start_now();
assert_eq!(&lifecycle.borrow_mut().deref()[..], expected); assert_eq!(&lifecycle.borrow_mut().deref()[..], expected);

View File

@ -387,7 +387,7 @@ pub(crate) use feat_csr_ssr::*;
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
mod feat_csr { mod feat_csr {
use super::*; use super::*;
use crate::dom_bundle::Bundle; use crate::dom_bundle::{BSubtree, Bundle};
use crate::html::component::lifecycle::{ use crate::html::component::lifecycle::{
ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner, ComponentRenderState, CreateRunner, DestroyRunner, RenderRunner,
}; };
@ -403,14 +403,17 @@ mod feat_csr {
/// Mounts a component with `props` to the specified `element` in the DOM. /// Mounts a component with `props` to the specified `element` in the DOM.
pub(crate) fn mount_in_place( pub(crate) fn mount_in_place(
&self, &self,
root: BSubtree,
parent: Element, parent: Element,
next_sibling: NodeRef, next_sibling: NodeRef,
node_ref: NodeRef, node_ref: NodeRef,
props: Rc<COMP::Properties>, props: Rc<COMP::Properties>,
) { ) {
let bundle = Bundle::new(&parent, &next_sibling, &node_ref); let bundle = Bundle::new();
node_ref.link(next_sibling.clone());
let state = ComponentRenderState::Render { let state = ComponentRenderState::Render {
bundle, bundle,
root,
node_ref, node_ref,
parent, parent,
next_sibling, next_sibling,
@ -486,10 +489,10 @@ mod feat_csr {
} }
fn shift_node(&self, parent: Element, next_sibling: NodeRef) { fn shift_node(&self, parent: Element, next_sibling: NodeRef) {
scheduler::push_component_update(Box::new(UpdateRunner { let mut state_ref = self.state.borrow_mut();
state: self.state.clone(), if let Some(render_state) = state_ref.as_mut() {
event: UpdateEvent::Shift(parent, next_sibling), render_state.render_state.shift(parent, next_sibling)
})) }
} }
} }
} }

View File

@ -1,10 +1,9 @@
use crate::dom_bundle::Bundle; use crate::dom_bundle::{BSubtree, Bundle};
use crate::html::AnyScope; use crate::html::AnyScope;
use crate::scheduler; use crate::scheduler;
use crate::virtual_dom::VNode; use crate::virtual_dom::VNode;
use crate::{Component, Context, Html}; use crate::{Component, Context, Html};
use gloo::console::log; use gloo::console::log;
use web_sys::Node;
use yew::NodeRef; use yew::NodeRef;
struct Comp; struct Comp;
@ -38,11 +37,12 @@ pub struct TestLayout<'a> {
pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) { pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
let document = gloo_utils::document(); 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_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"); 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 // Tests each layout independently
let next_sibling = NodeRef::new(end_node.into()); let next_sibling = NodeRef::new(end_node.into());
@ -51,10 +51,8 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
let vnode = layout.node.clone(); let vnode = layout.node.clone();
log!("Independently apply layout '{}'", layout.name); log!("Independently apply layout '{}'", layout.name);
let node_ref = NodeRef::default(); let mut bundle = Bundle::new();
bundle.reconcile(&root, &scope, &parent_element, next_sibling.clone(), vnode);
let mut bundle = Bundle::new(&parent_element, &next_sibling, &node_ref);
bundle.reconcile(&parent_scope, &parent_element, next_sibling.clone(), vnode);
scheduler::start_now(); scheduler::start_now();
assert_eq!( assert_eq!(
parent_element.inner_html(), parent_element.inner_html(),
@ -68,7 +66,7 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
log!("Independently reapply layout '{}'", layout.name); 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(); scheduler::start_now();
assert_eq!( assert_eq!(
parent_element.inner_html(), parent_element.inner_html(),
@ -78,7 +76,7 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
); );
// Detach // Detach
bundle.detach(&parent_element, false); bundle.detach(&root, &parent_element, false);
scheduler::start_now(); scheduler::start_now();
assert_eq!( assert_eq!(
parent_element.inner_html(), parent_element.inner_html(),
@ -89,14 +87,14 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
} }
// Sequentially apply each layout // Sequentially apply each layout
let node_ref = NodeRef::default(); let mut bundle = Bundle::new();
let mut bundle = Bundle::new(&parent_element, &next_sibling, &node_ref);
for layout in layouts.iter() { for layout in layouts.iter() {
let next_vnode = layout.node.clone(); let next_vnode = layout.node.clone();
log!("Sequentially apply layout '{}'", layout.name); log!("Sequentially apply layout '{}'", layout.name);
bundle.reconcile( bundle.reconcile(
&parent_scope, &root,
&scope,
&parent_element, &parent_element,
next_sibling.clone(), next_sibling.clone(),
next_vnode, next_vnode,
@ -117,7 +115,8 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
log!("Sequentially detach layout '{}'", layout.name); log!("Sequentially detach layout '{}'", layout.name);
bundle.reconcile( bundle.reconcile(
&parent_scope, &root,
&scope,
&parent_element, &parent_element,
next_sibling.clone(), next_sibling.clone(),
next_vnode, next_vnode,
@ -133,7 +132,7 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
} }
// Detach last layout // Detach last layout
bundle.detach(&parent_element, false); bundle.detach(&root, &parent_element, false);
scheduler::start_now(); scheduler::start_now();
assert_eq!( assert_eq!(
parent_element.inner_html(), parent_element.inner_html(),

View File

@ -9,6 +9,8 @@ use std::rc::Rc;
#[cfg(any(feature = "ssr", feature = "csr"))] #[cfg(any(feature = "ssr", feature = "csr"))]
use crate::html::{AnyScope, Scope}; use crate::html::{AnyScope, Scope};
#[cfg(feature = "csr")]
use crate::dom_bundle::BSubtree;
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
use crate::html::Scoped; use crate::html::Scoped;
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
@ -53,6 +55,7 @@ pub(crate) trait Mountable {
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
fn mount( fn mount(
self: Box<Self>, self: Box<Self>,
root: &BSubtree,
node_ref: NodeRef, node_ref: NodeRef,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: Element, parent: Element,
@ -91,13 +94,14 @@ impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
#[cfg(feature = "csr")] #[cfg(feature = "csr")]
fn mount( fn mount(
self: Box<Self>, self: Box<Self>,
root: &BSubtree,
node_ref: NodeRef, node_ref: NodeRef,
parent_scope: &AnyScope, parent_scope: &AnyScope,
parent: Element, parent: Element,
next_sibling: NodeRef, next_sibling: NodeRef,
) -> Box<dyn Scoped> { ) -> Box<dyn Scoped> {
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone())); let scope: Scope<COMP> = 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) Box::new(scope)
} }

View File

@ -1,12 +1,12 @@
error[E0277]: the trait bound `Comp: yew::Component` is not satisfied error[E0277]: the trait bound `Comp: yew::Component` is not satisfied
--> tests/failed_tests/base_component_impl-fail.rs:6:6 --> tests/failed_tests/base_component_impl-fail.rs:6:6
| |
6 | impl BaseComponent for Comp { 6 | impl BaseComponent for Comp {
| ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented 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 because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp`
note: required by a bound in `BaseComponent` note: required by a bound in `BaseComponent`
--> src/html/component/mod.rs --> src/html/component/mod.rs
| |
| pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static { | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent`

View File

@ -23,7 +23,7 @@ simple modal dialogue that renders its `children` into an element outside `yew`'
identified by the `id="modal_host"`. identified by the `id="modal_host"`.
```rust ```rust
use yew::{html, create_portal, function_component, Children, Properties, Html}; use yew::prelude::*;
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct ModalProps { pub struct ModalProps {
@ -31,11 +31,11 @@ pub struct ModalProps {
pub children: Children, pub children: Children,
} }
#[function_component(Modal)] #[function_component]
fn modal(props: &ModalProps) -> Html { fn Modal(props: &ModalProps) -> Html {
let modal_host = gloo::utils::document() let modal_host = gloo::utils::document()
.get_element_by_id("modal_host") .get_element_by_id("modal_host")
.expect("a #modal_host element"); .expect("Expected to find a #modal_host element");
create_portal( create_portal(
html!{ {for props.children.iter()} }, 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 ## Further reading
- [Portals example](https://github.com/yewstack/yew/tree/master/examples/portals) - [Portals example](https://github.com/yewstack/yew/tree/master/examples/portals)

View File

@ -135,6 +135,23 @@ listens for `click` events.
| `ontransitionrun` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) | | `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) | | `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 ## Typed event target
:::caution :::caution