Skip to content

Commit

Permalink
Scoped event handlers (#2510)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
WorldSEnder authored Mar 25, 2022
1 parent bbb7ded commit ee6a67e
Show file tree
Hide file tree
Showing 22 changed files with 1,195 additions and 569 deletions.
55 changes: 47 additions & 8 deletions examples/portals/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 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 {
fn view(&self, ctx: &Context<Self>) -> Html {
let onclick = ctx.link().callback(|_| AppMessage::IncreaseCounter);
let title = create_portal(
html! {
if self.counter > 0 {
{format!("Clicked {} times", self.counter)}
} else {
{"Yew • Portals"}
}
},
self.title_element.clone(),
);
html! {
<>
{self.style_html.clone()}
{title}
<p>{"This paragraph is colored red, and its style is mounted into "}<pre>{"document.head"}</pre>{" with a portal"}</p>
<ShadowDOMHost>
<p>{"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}</p>
</ShadowDOMHost>
<div>
<ShadowDOMHost>
<p>{"This paragraph is rendered in a shadow dom and thus not affected by the surrounding styling context"}</p>
<span>{"Buttons clicked inside the shadow dom work fine."}</span>
<button {onclick}>{"Click me!"}</button>
</ShadowDOMHost>
<p>{format!("The button has been clicked {} times. This is also reflected in the title of the tab!", self.counter)}</p>
</div>
</>
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/yew/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
22 changes: 14 additions & 8 deletions packages/yew/src/app_handle.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
}
Expand All @@ -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");
}
}
59 changes: 28 additions & 31 deletions packages/yew/src/dom_bundle/bcomp.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}

Expand All @@ -47,6 +46,7 @@ impl Reconcilable for VComp {

fn attach(
self,
root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
Expand All @@ -59,6 +59,7 @@ impl Reconcilable for VComp {
} = self;

let scope = mountable.mount(
root,
node_ref.clone(),
parent_scope,
parent.to_owned(),
Expand All @@ -78,6 +79,7 @@ impl Reconcilable for VComp {

fn reconcile_node(
self,
root: &BSubtree,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
Expand All @@ -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,
Expand Down Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -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(..)
Expand All @@ -359,15 +356,15 @@ 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>
{ children_renderer }
</List>
};
assert_eq!(
get_html(children_renderer_method, &scope, &parent),
get_html(children_renderer_method, &root, &scope, &parent),
expected_html
);

Expand All @@ -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());
}
Expand Down
Loading

1 comment on commit ee6a67e

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yew master branch benchmarks (Lower is better)

Benchmark suite Current: ee6a67e Previous: bbb7ded Ratio
yew-struct-keyed 01_run1k 214.97 283.168 0.76
yew-struct-keyed 02_replace1k 218.0205 287.736 0.76
yew-struct-keyed 03_update10th1k_x16 396.078 392.2815 1.01
yew-struct-keyed 04_select1k 81.9455 83.3 0.98
yew-struct-keyed 05_swap1k 102.1965 111.2725 0.92
yew-struct-keyed 06_remove-one-1k 34.729 36.055499999999995 0.96
yew-struct-keyed 07_create10k 2490.689 3179.486 0.78
yew-struct-keyed 08_create1k-after1k_x2 475.3195 628.8935 0.76
yew-struct-keyed 09_clear1k_x8 176.929 269.11 0.66
yew-struct-keyed 21_ready-memory 0.9634475708007812 0.9634475708007812 1
yew-struct-keyed 22_run-memory 1.5024986267089844 1.45648193359375 1.03
yew-struct-keyed 23_update5-memory 1.4602203369140625 1.4602203369140625 1
yew-struct-keyed 24_run5-memory 1.5095291137695312 1.5095291137695312 1
yew-struct-keyed 25_run-clear-memory 1.1272430419921875 1.1272430419921875 1
yew-struct-keyed 31_startup-ci 1736.664 1884.03 0.92
yew-struct-keyed 32_startup-bt 38.01 41.218 0.92
yew-struct-keyed 34_startup-totalbytes 330.5556640625 330.5556640625 1

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.