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()
.expect("rendered host")
.unchecked_into::<Element>()
.attach_shadow(&ShadowRootInit::new(ShadowRootMode::Closed))
.attach_shadow(&ShadowRootInit::new(ShadowRootMode::Open))
.expect("installing shadow root succeeds");
let inner_host = gloo_utils::document()
.create_element("div")
@ -68,34 +68,73 @@ impl Component for ShadowDOMHost {
}
pub struct App {
pub style_html: Html,
style_html: Html,
title_element: Element,
counter: u32,
}
pub enum AppMessage {
IncreaseCounter,
}
impl Component for App {
type Message = ();
type Message = AppMessage;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
let document_head = gloo_utils::document()
.head()
.expect("head element to be present");
let title_element = document_head
.query_selector("title")
.expect("to find a title element")
.expect("to find a title element");
title_element.set_text_content(None); // Clear the title element
let style_html = create_portal(
html! {
<style>{"p { color: red; }"}</style>
},
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! {
<>
{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>
<ShadowDOMHost>
<p>{"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}</p>
</ShadowDOMHost>
<div>
<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"
trybuild = "1"
[dev-dependencies.web-sys]
version = "0.3"
features = [
"ShadowRoot",
"ShadowRootInit",
"ShadowRootMode",
]
[features]
ssr = ["futures", "html-escape"]
csr = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ simple modal dialogue that renders its `children` into an element outside `yew`'
identified by the `id="modal_host"`.
```rust
use yew::{html, create_portal, function_component, Children, Properties, Html};
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct ModalProps {
@ -31,11 +31,11 @@ pub struct ModalProps {
pub children: Children,
}
#[function_component(Modal)]
fn modal(props: &ModalProps) -> Html {
#[function_component]
fn Modal(props: &ModalProps) -> Html {
let modal_host = gloo::utils::document()
.get_element_by_id("modal_host")
.expect("a #modal_host element");
.expect("Expected to find a #modal_host element");
create_portal(
html!{ {for props.children.iter()} },
@ -44,5 +44,20 @@ fn modal(props: &ModalProps) -> Html {
}
```
## Event handling
Events emitted on elements inside portals follow the virtual DOM when bubbling up. That is,
if a portal is rendered as the child of an element, then an event listener on that element
will catch events dispatched from inside the portal, even if the portal renders its contents
in an unrelated location in the actual DOM.
This allows developers to be oblivious of whether a component they consume, is implemented with
or without portals. Events fired on its children will bubble up regardless.
A known issue is that events from portals into **closed** shadow roots will be dispatched twice,
once targeting the element inside the shadow root and once targeting the host element itself. Keep
in mind that **open** shadow roots work fine. If this impacts you, feel free to open a bug report
about it.
## Further reading
- [Portals example](https://github.com/yewstack/yew/tree/master/examples/portals)

View File

@ -135,6 +135,23 @@ listens for `click` events.
| `ontransitionrun` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) |
| `ontransitionstart` | [TransitionEvent](https://docs.rs/web-sys/latest/web_sys/struct.TransitionEvent.html) |
## Event bubbling
Events dispatched by Yew follow the virtual DOM hierarchy when bubbling up to listeners. Currently, only the bubbling phase
is supported for listeners. Note that the virtual DOM hierarchy is most often, but not always, identical to the actual
DOM hierarchy. The distinction is important when working with [portals](../../advanced-topics/portals.mdx) and other
more advanced techniques. The intuition for well implemented components should be that events bubble from children
to parents, so that the hierarchy in your coded `html!` is the one observed by event handlers.
If you are not interested in event bubbling, you can turn it off by calling
```rust
yew::set_event_bubbling(false);
```
*before* starting your app. This speeds up event handling, but some components may break from not receiving events they expect.
Use this with care!
## Typed event target
:::caution