mirror of https://github.com/yewstack/yew
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:
parent
bbb7ded83e
commit
ee6a67e3ea
|
@ -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>
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue