Introduce explicit internal datastructures modeling dom state (#2330)

* detach destructures now

* add failing keyed-list issue

* crude port to the new bundle infrastructure

* port over the infrastructure

the new bcomp is especially nice and lost a few unwraps owed to not having
to reserve space for a scope before rendering.
Note also that bsuspense has been slimmed a bit, storing the suspended flag
implicitly in the state.
some naming is not perfect yet and has to be adjusted still.

* mass rename: apply -> reconcile

* get rid of move_before in favor of shift

* generate id directly when creating a new scope

* bundle for text nodes

* work on naming: ancestor -> bundle

* slightly optimize list reconciler, add doccomments

* address review

* add internal documentation

* address review comments

rename fields in bsuspense
convert to gloo::events

* move even more stuff into dom_bundle to scope exports

- app_handle and layout_tests are now in there
- items are publically re-exported in crate::dom_bundle
- dom_bundle itself is private
- btag and bcomp get their own submodules
- bcomp now contains the lifecycle and scope impls

* move replace into Reconcilable

* move lifecycle and scope back into html as per review

* move back Value and InputFields into html

* actually only type-check format args in production

* fix documentation link

* move btag_impl up into containing module

* shift comps immediately

shifting the rendered Nodes does not tie into the lifecycle,
as such it can happen immediately

* use list-bundle in tag-bundle

* fix cargo make tests

* improve 05_swap benchmark

* fix a blunder where I swapped operands

* fix naming of BNode variants
This commit is contained in:
WorldSEnder 2022-03-06 04:37:07 +01:00 committed by GitHub
parent 221b4dfa51
commit 78d4204a9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 5608 additions and 4950 deletions

View File

@ -82,7 +82,7 @@ dependencies = ["test"]
[tasks.test]
private = true
command = "cargo"
args = ["test", "--all-targets", "--workspace", "--exclude", "website-test"]
args = ["test", "--all-targets"]
[tasks.doc-test-flow]
private = true

View File

@ -1,17 +1,16 @@
//! This module contains the `App` struct, which is used to bootstrap
//! a component in an isolated scope.
//! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope.
use std::ops::Deref;
use crate::html::{BaseComponent, NodeRef, Scope, Scoped};
use std::rc::Rc;
use super::{ComponentRenderState, Scoped};
use crate::html::{BaseComponent, Scope};
use crate::NodeRef;
use std::{ops::Deref, rc::Rc};
use web_sys::Element;
/// An instance of an application.
#[derive(Debug)]
pub struct AppHandle<COMP: BaseComponent> {
/// `Scope` holder
pub(crate) scope: Scope<COMP>,
scope: Scope<COMP>,
}
impl<COMP> AppHandle<COMP>
@ -27,14 +26,17 @@ where
let app = Self {
scope: Scope::new(None),
};
let node_ref = NodeRef::default();
let initial_render_state =
ComponentRenderState::new(element, NodeRef::default(), &node_ref);
app.scope
.mount_in_place(element, NodeRef::default(), NodeRef::default(), props);
.mount_in_place(initial_render_state, node_ref, props);
app
}
/// Schedule the app for destruction
pub fn destroy(mut self) {
pub fn destroy(self) {
self.scope.destroy(false)
}
}

View File

@ -0,0 +1,886 @@
//! This module contains the bundle implementation of a virtual component [BComp].
use super::{insert_node, BNode, DomBundle, Reconcilable};
use crate::html::{AnyScope, BaseComponent, Scope};
use crate::virtual_dom::{Key, VComp, VNode};
use crate::NodeRef;
#[cfg(feature = "ssr")]
use futures::channel::oneshot;
#[cfg(feature = "ssr")]
use futures::future::{FutureExt, LocalBoxFuture};
use gloo_utils::document;
use std::cell::Ref;
use std::{any::TypeId, borrow::Borrow};
use std::{fmt, rc::Rc};
use web_sys::{Element, Node};
/// A virtual component. Compare with [VComp].
pub struct BComp {
type_id: TypeId,
scope: Box<dyn Scoped>,
node_ref: NodeRef,
key: Option<Key>,
}
impl BComp {
/// Get the key of the underlying component
pub(super) fn key(&self) -> Option<&Key> {
self.key.as_ref()
}
}
impl fmt::Debug for BComp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"BComp {{ root: {:?} }}",
self.scope.as_ref().render_state(),
)
}
}
impl DomBundle for BComp {
fn detach(self, _parent: &Element, parent_to_detach: bool) {
self.scope.destroy_boxed(parent_to_detach);
}
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
self.scope.shift_node(next_parent.clone(), next_sibling);
}
}
impl Reconcilable for VComp {
type Bundle = BComp;
fn attach(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle) {
let VComp {
type_id,
mountable,
node_ref,
key,
} = self;
let scope = mountable.mount(
node_ref.clone(),
parent_scope,
parent.to_owned(),
next_sibling,
);
(
node_ref.clone(),
BComp {
type_id,
node_ref,
key,
scope,
},
)
}
fn reconcile_node(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut BNode,
) -> NodeRef {
match bundle {
// If the existing bundle is the same type, reuse it and update its properties
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.replace(parent_scope, parent, next_sibling, bundle),
}
}
fn reconcile(
self,
_parent_scope: &AnyScope,
_parent: &Element,
next_sibling: NodeRef,
bcomp: &mut Self::Bundle,
) -> NodeRef {
let VComp {
mountable,
node_ref,
key,
type_id: _,
} = self;
bcomp.key = key;
let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref.clone());
bcomp.node_ref.reuse(old_ref);
mountable.reuse(node_ref.clone(), bcomp.scope.borrow(), next_sibling);
node_ref
}
}
pub trait Mountable {
fn copy(&self) -> Box<dyn Mountable>;
fn mount(
self: Box<Self>,
node_ref: NodeRef,
parent_scope: &AnyScope,
parent: Element,
next_sibling: NodeRef,
) -> Box<dyn Scoped>;
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef);
#[cfg(feature = "ssr")]
fn render_to_string<'a>(
&'a self,
w: &'a mut String,
parent_scope: &'a AnyScope,
) -> LocalBoxFuture<'a, ()>;
}
pub struct PropsWrapper<COMP: BaseComponent> {
props: Rc<COMP::Properties>,
}
impl<COMP: BaseComponent> PropsWrapper<COMP> {
pub fn new(props: Rc<COMP::Properties>) -> Self {
Self { props }
}
}
impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
fn copy(&self) -> Box<dyn Mountable> {
let wrapper: PropsWrapper<COMP> = PropsWrapper {
props: Rc::clone(&self.props),
};
Box::new(wrapper)
}
fn mount(
self: Box<Self>,
node_ref: NodeRef,
parent_scope: &AnyScope,
parent: Element,
next_sibling: NodeRef,
) -> Box<dyn Scoped> {
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
let initial_render_state = ComponentRenderState::new(parent, next_sibling, &node_ref);
scope.mount_in_place(initial_render_state, node_ref, self.props);
Box::new(scope)
}
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) {
let scope: Scope<COMP> = scope.to_any().downcast();
scope.reuse(self.props, node_ref, next_sibling);
}
#[cfg(feature = "ssr")]
fn render_to_string<'a>(
&'a self,
w: &'a mut String,
parent_scope: &'a AnyScope,
) -> LocalBoxFuture<'a, ()> {
async move {
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
scope.render_to_string(w, self.props.clone()).await;
}
.boxed_local()
}
}
pub struct ComponentRenderState {
root_node: BNode,
/// When a component has no parent, it means that it should not be rendered.
parent: Option<Element>,
next_sibling: NodeRef,
#[cfg(feature = "ssr")]
html_sender: Option<oneshot::Sender<VNode>>,
}
impl std::fmt::Debug for ComponentRenderState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.root_node.fmt(f)
}
}
impl ComponentRenderState {
/// Prepare a place in the DOM to hold the eventual [VNode] from rendering a component
pub(crate) fn new(parent: Element, next_sibling: NodeRef, node_ref: &NodeRef) -> Self {
let placeholder = {
let placeholder: Node = document().create_text_node("").into();
insert_node(&placeholder, &parent, next_sibling.get().as_ref());
node_ref.set(Some(placeholder.clone()));
BNode::Ref(placeholder)
};
Self {
root_node: placeholder,
parent: Some(parent),
next_sibling,
#[cfg(feature = "ssr")]
html_sender: None,
}
}
/// Set up server-side rendering of a component
#[cfg(feature = "ssr")]
pub(crate) fn new_ssr(tx: oneshot::Sender<VNode>) -> Self {
use super::blist::BList;
Self {
root_node: BNode::List(BList::new()),
parent: None,
next_sibling: NodeRef::default(),
html_sender: Some(tx),
}
}
/// Reuse the render state, asserting a new next_sibling
pub(crate) fn reuse(&mut self, next_sibling: NodeRef) {
self.next_sibling = next_sibling;
}
/// Shift the rendered content to a new DOM position
pub(crate) fn shift(&mut self, new_parent: Element, next_sibling: NodeRef) {
self.root_node.shift(&new_parent, next_sibling.clone());
self.parent = Some(new_parent);
self.next_sibling = next_sibling;
}
/// Reconcile the rendered content with a new [VNode]
pub(crate) fn reconcile(&mut self, root: VNode, scope: &AnyScope) -> NodeRef {
if let Some(ref parent) = self.parent {
let next_sibling = self.next_sibling.clone();
root.reconcile_node(scope, parent, next_sibling, &mut self.root_node)
} else {
#[cfg(feature = "ssr")]
if let Some(tx) = self.html_sender.take() {
tx.send(root).unwrap();
}
NodeRef::default()
}
}
/// Detach the rendered content from the DOM
pub(crate) fn detach(self, parent_to_detach: bool) {
if let Some(ref m) = self.parent {
self.root_node.detach(m, parent_to_detach);
}
}
pub(crate) fn should_trigger_rendered(&self) -> bool {
self.parent.is_some()
}
}
pub trait Scoped {
fn to_any(&self) -> AnyScope;
/// Get the render state if it hasn't already been destroyed
fn render_state(&self) -> Option<Ref<'_, ComponentRenderState>>;
/// Shift the node associated with this scope to a new place
fn shift_node(&self, parent: Element, next_sibling: NodeRef);
/// Process an event to destroy a component
fn destroy(self, parent_to_detach: bool);
fn destroy_boxed(self: Box<Self>, parent_to_detach: bool);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dom_bundle::{DomBundle, Reconcilable};
use crate::scheduler;
use crate::{
html,
virtual_dom::{Key, VChild, VNode},
Children, Component, Context, Html, NodeRef, Properties,
};
use gloo_utils::document;
use std::ops::Deref;
use web_sys::Element;
use web_sys::Node;
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
struct Comp;
#[derive(Clone, PartialEq, Properties)]
struct Props {
#[prop_or_default]
field_1: u32,
#[prop_or_default]
field_2: u32,
}
impl Component for Comp {
type Message = ();
type Properties = Props;
fn create(_: &Context<Self>) -> Self {
Comp
}
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
unimplemented!();
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! { <div/> }
}
}
#[test]
fn update_loop() {
let document = gloo_utils::document();
let parent_scope: AnyScope = AnyScope::test();
let parent_element = document.create_element("div").unwrap();
let comp = html! { <Comp></Comp> };
let (_, mut bundle) = comp.attach(&parent_scope, &parent_element, 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,
);
scheduler::start_now();
}
}
#[test]
fn set_properties_to_component() {
html! {
<Comp />
};
html! {
<Comp field_1=1 />
};
html! {
<Comp field_2=2 />
};
html! {
<Comp field_1=1 field_2=2 />
};
let props = Props {
field_1: 1,
field_2: 1,
};
html! {
<Comp ..props />
};
}
#[test]
fn set_component_key() {
let test_key: Key = "test".to_string().into();
let check_key = |vnode: VNode| {
assert_eq!(vnode.key(), Some(&test_key));
};
let props = Props {
field_1: 1,
field_2: 1,
};
let props_2 = props.clone();
check_key(html! { <Comp key={test_key.clone()} /> });
check_key(html! { <Comp key={test_key.clone()} field_1=1 /> });
check_key(html! { <Comp field_1=1 key={test_key.clone()} /> });
check_key(html! { <Comp key={test_key.clone()} ..props /> });
check_key(html! { <Comp key={test_key.clone()} ..props_2 /> });
}
#[test]
fn set_component_node_ref() {
let test_node: Node = document().create_text_node("test").into();
let test_node_ref = NodeRef::new(test_node);
let check_node_ref = |vnode: VNode| {
let vcomp = match vnode {
VNode::VComp(vcomp) => vcomp,
_ => unreachable!("should be a vcomp"),
};
assert_eq!(vcomp.node_ref, test_node_ref);
};
let props = Props {
field_1: 1,
field_2: 1,
};
let props_2 = props.clone();
check_node_ref(html! { <Comp ref={test_node_ref.clone()} /> });
check_node_ref(html! { <Comp ref={test_node_ref.clone()} field_1=1 /> });
check_node_ref(html! { <Comp field_1=1 ref={test_node_ref.clone()} /> });
check_node_ref(html! { <Comp ref={test_node_ref.clone()} ..props /> });
check_node_ref(html! { <Comp ref={test_node_ref.clone()} ..props_2 /> });
}
#[test]
fn vchild_partialeq() {
let vchild1: VChild<Comp> = VChild::new(
Props {
field_1: 1,
field_2: 1,
},
NodeRef::default(),
None,
);
let vchild2: VChild<Comp> = VChild::new(
Props {
field_1: 1,
field_2: 1,
},
NodeRef::default(),
None,
);
let vchild3: VChild<Comp> = VChild::new(
Props {
field_1: 2,
field_2: 2,
},
NodeRef::default(),
None,
);
assert_eq!(vchild1, vchild2);
assert_ne!(vchild1, vchild3);
assert_ne!(vchild2, vchild3);
}
#[derive(Clone, Properties, PartialEq)]
pub struct ListProps {
pub children: Children,
}
pub struct List;
impl Component for List {
type Message = ();
type Properties = ListProps;
fn create(_: &Context<Self>) -> Self {
Self
}
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
unimplemented!();
}
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
unimplemented!();
}
fn view(&self, ctx: &Context<Self>) -> Html {
let item_iter = ctx
.props()
.children
.iter()
.map(|item| html! {<li>{ item }</li>});
html! {
<ul>{ for item_iter }</ul>
}
}
}
fn setup_parent() -> (AnyScope, Element) {
let scope = AnyScope::test();
let parent = document().create_element("div").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
(scope, parent)
}
fn get_html(node: Html, scope: &AnyScope, parent: &Element) -> String {
// clear parent
parent.set_inner_html("");
node.attach(scope, parent, NodeRef::default());
scheduler::start_now();
parent.inner_html()
}
#[test]
fn all_ways_of_passing_children_work() {
let (scope, parent) = setup_parent();
let children: Vec<_> = vec!["a", "b", "c"]
.drain(..)
.map(|text| html! {<span>{ text }</span>})
.collect();
let children_renderer = Children::new(children.clone());
let expected_html = "\
<ul>\
<li><span>a</span></li>\
<li><span>b</span></li>\
<li><span>c</span></li>\
</ul>";
let prop_method = html! {
<List children={children_renderer.clone()} />
};
assert_eq!(get_html(prop_method, &scope, &parent), expected_html);
let children_renderer_method = html! {
<List>
{ children_renderer }
</List>
};
assert_eq!(
get_html(children_renderer_method, &scope, &parent),
expected_html
);
let direct_method = html! {
<List>
{ children.clone() }
</List>
};
assert_eq!(get_html(direct_method, &scope, &parent), expected_html);
let for_method = html! {
<List>
{ for children }
</List>
};
assert_eq!(get_html(for_method, &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 node_ref = NodeRef::default();
let elem = html! { <Comp ref={node_ref.clone()}></Comp> };
let (_, elem) = elem.attach(&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);
scheduler::start_now();
assert!(node_ref.get().is_none());
}
}
#[cfg(test)]
mod layout_tests {
extern crate self as yew;
use crate::html;
use crate::tests::layout_tests::{diff_layouts, TestLayout};
use crate::{Children, Component, Context, Html, Properties};
use std::marker::PhantomData;
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
struct Comp<T> {
_marker: PhantomData<T>,
}
#[derive(Properties, Clone, PartialEq)]
struct CompProps {
#[prop_or_default]
children: Children,
}
impl<T: 'static> Component for Comp<T> {
type Message = ();
type Properties = CompProps;
fn create(_: &Context<Self>) -> Self {
Comp {
_marker: PhantomData::default(),
}
}
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
unimplemented!();
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>{ ctx.props().children.clone() }</>
}
}
}
struct A;
struct B;
#[test]
fn diff() {
let layout1 = TestLayout {
name: "1",
node: html! {
<Comp<A>>
<Comp<B>></Comp<B>>
{"C"}
</Comp<A>>
},
expected: "C",
};
let layout2 = TestLayout {
name: "2",
node: html! {
<Comp<A>>
{"A"}
</Comp<A>>
},
expected: "A",
};
let layout3 = TestLayout {
name: "3",
node: html! {
<Comp<B>>
<Comp<A>></Comp<A>>
{"B"}
</Comp<B>>
},
expected: "B",
};
let layout4 = TestLayout {
name: "4",
node: html! {
<Comp<B>>
<Comp<A>>{"A"}</Comp<A>>
{"B"}
</Comp<B>>
},
expected: "AB",
};
let layout5 = TestLayout {
name: "5",
node: html! {
<Comp<B>>
<>
<Comp<A>>
{"A"}
</Comp<A>>
</>
{"B"}
</Comp<B>>
},
expected: "AB",
};
let layout6 = TestLayout {
name: "6",
node: html! {
<Comp<B>>
<>
<Comp<A>>
{"A"}
</Comp<A>>
{"B"}
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout7 = TestLayout {
name: "7",
node: html! {
<Comp<B>>
<>
<Comp<A>>
{"A"}
</Comp<A>>
<Comp<A>>
{"B"}
</Comp<A>>
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout8 = TestLayout {
name: "8",
node: html! {
<Comp<B>>
<>
<Comp<A>>
{"A"}
</Comp<A>>
<Comp<A>>
<Comp<A>>
{"B"}
</Comp<A>>
</Comp<A>>
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout9 = TestLayout {
name: "9",
node: html! {
<Comp<B>>
<>
<>
{"A"}
</>
<Comp<A>>
<Comp<A>>
{"B"}
</Comp<A>>
</Comp<A>>
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout10 = TestLayout {
name: "10",
node: html! {
<Comp<B>>
<>
<Comp<A>>
<Comp<A>>
{"A"}
</Comp<A>>
</Comp<A>>
<>
{"B"}
</>
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout11 = TestLayout {
name: "11",
node: html! {
<Comp<B>>
<>
<>
<Comp<A>>
<Comp<A>>
{"A"}
</Comp<A>>
{"B"}
</Comp<A>>
</>
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout12 = TestLayout {
name: "12",
node: html! {
<Comp<B>>
<>
<Comp<A>></Comp<A>>
<>
<Comp<A>>
<>
<Comp<A>>
{"A"}
</Comp<A>>
<></>
<Comp<A>>
<Comp<A>></Comp<A>>
<></>
{"B"}
<></>
<Comp<A>></Comp<A>>
</Comp<A>>
</>
</Comp<A>>
<></>
</>
<Comp<A>></Comp<A>>
</>
{"C"}
<Comp<A>></Comp<A>>
<></>
</Comp<B>>
},
expected: "ABC",
};
diff_layouts(vec![
layout1, layout2, layout3, layout4, layout5, layout6, layout7, layout8, layout9,
layout10, layout11, layout12,
]);
}
#[test]
fn component_with_children() {
#[derive(Properties, PartialEq)]
struct Props {
children: Children,
}
struct ComponentWithChildren;
impl Component for ComponentWithChildren {
type Message = ();
type Properties = Props;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<ul>
{ for ctx.props().children.iter().map(|child| html! { <li>{ child }</li> }) }
</ul>
}
}
}
let layout = TestLayout {
name: "13",
node: html! {
<ComponentWithChildren>
if true {
<span>{ "hello" }</span>
<span>{ "world" }</span>
} else {
<span>{ "goodbye" }</span>
<span>{ "world" }</span>
}
</ComponentWithChildren>
},
expected: "<ul><li><span>hello</span><span>world</span></li></ul>",
};
diff_layouts(vec![layout]);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,254 @@
//! This module contains the bundle version of an abstract node [BNode]
use super::{BComp, BList, BPortal, BSuspense, BTag, BText};
use crate::dom_bundle::{DomBundle, Reconcilable};
use crate::html::{AnyScope, NodeRef};
use crate::virtual_dom::{Key, VNode};
use gloo::console;
use std::fmt;
use web_sys::{Element, Node};
/// The bundle implementation to [VNode].
pub enum BNode {
/// A bind between `VTag` and `Element`.
Tag(Box<BTag>),
/// A bind between `VText` and `TextNode`.
Text(BText),
/// A bind between `VComp` and `Element`.
Comp(BComp),
/// A holder for a list of other nodes.
List(BList),
/// A portal to another part of the document
Portal(BPortal),
/// A holder for any `Node` (necessary for replacing node).
Ref(Node),
/// A suspendible document fragment.
Suspense(Box<BSuspense>),
}
impl BNode {
/// Get the key of the underlying node
pub(super) fn key(&self) -> Option<&Key> {
match self {
Self::Comp(bsusp) => bsusp.key(),
Self::List(blist) => blist.key(),
Self::Ref(_) => None,
Self::Tag(btag) => btag.key(),
Self::Text(_) => None,
Self::Portal(bportal) => bportal.key(),
Self::Suspense(bsusp) => bsusp.key(),
}
}
}
impl DomBundle for BNode {
/// Remove VNode from parent.
fn detach(self, 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::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),
}
}
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
match self {
Self::Tag(ref vtag) => vtag.shift(next_parent, next_sibling),
Self::Text(ref btext) => btext.shift(next_parent, next_sibling),
Self::Comp(ref bsusp) => bsusp.shift(next_parent, next_sibling),
Self::List(ref vlist) => vlist.shift(next_parent, next_sibling),
Self::Ref(ref node) => {
next_parent
.insert_before(node, next_sibling.get().as_ref())
.unwrap();
}
Self::Portal(ref vportal) => vportal.shift(next_parent, next_sibling),
Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling),
}
}
}
impl Reconcilable for VNode {
type Bundle = BNode;
fn attach(
self,
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);
(node_ref, tag.into())
}
VNode::VText(vtext) => {
let (node_ref, text) = vtext.attach(parent_scope, parent, next_sibling);
(node_ref, text.into())
}
VNode::VComp(vcomp) => {
let (node_ref, comp) = vcomp.attach(parent_scope, parent, next_sibling);
(node_ref, comp.into())
}
VNode::VList(vlist) => {
let (node_ref, list) = vlist.attach(parent_scope, parent, next_sibling);
(node_ref, list.into())
}
VNode::VRef(node) => {
super::insert_node(&node, parent, next_sibling.get().as_ref());
(NodeRef::new(node.clone()), BNode::Ref(node))
}
VNode::VPortal(vportal) => {
let (node_ref, portal) = vportal.attach(parent_scope, parent, next_sibling);
(node_ref, portal.into())
}
VNode::VSuspense(vsuspsense) => {
let (node_ref, suspsense) = vsuspsense.attach(parent_scope, parent, next_sibling);
(node_ref, suspsense.into())
}
}
}
fn reconcile_node(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut BNode,
) -> NodeRef {
self.reconcile(parent_scope, parent, next_sibling, bundle)
}
fn reconcile(
self,
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::VRef(node) => {
let _existing = match bundle {
BNode::Ref(ref n) if &node == n => n,
_ => {
return VNode::VRef(node).replace(
parent_scope,
parent,
next_sibling,
bundle,
);
}
};
NodeRef::new(node)
}
VNode::VPortal(vportal) => {
vportal.reconcile_node(parent_scope, parent, next_sibling, bundle)
}
VNode::VSuspense(vsuspsense) => {
vsuspsense.reconcile_node(parent_scope, parent, next_sibling, bundle)
}
}
}
}
impl From<BText> for BNode {
#[inline]
fn from(btext: BText) -> Self {
Self::Text(btext)
}
}
impl From<BList> for BNode {
#[inline]
fn from(blist: BList) -> Self {
Self::List(blist)
}
}
impl From<BTag> for BNode {
#[inline]
fn from(btag: BTag) -> Self {
Self::Tag(Box::new(btag))
}
}
impl From<BComp> for BNode {
#[inline]
fn from(bcomp: BComp) -> Self {
Self::Comp(bcomp)
}
}
impl From<BPortal> for BNode {
#[inline]
fn from(bportal: BPortal) -> Self {
Self::Portal(bportal)
}
}
impl From<BSuspense> for BNode {
#[inline]
fn from(bsusp: BSuspense) -> Self {
Self::Suspense(Box::new(bsusp))
}
}
impl fmt::Debug for BNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Tag(ref vtag) => vtag.fmt(f),
Self::Text(ref btext) => btext.fmt(f),
Self::Comp(ref bsusp) => bsusp.fmt(f),
Self::List(ref vlist) => vlist.fmt(f),
Self::Ref(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)),
Self::Portal(ref vportal) => vportal.fmt(f),
Self::Suspense(ref bsusp) => bsusp.fmt(f),
}
}
}
#[cfg(test)]
mod layout_tests {
use super::*;
use crate::tests::layout_tests::{diff_layouts, TestLayout};
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
#[test]
fn diff() {
let document = gloo_utils::document();
let vref_node_1 = VNode::VRef(document.create_element("i").unwrap().into());
let vref_node_2 = VNode::VRef(document.create_element("b").unwrap().into());
let layout1 = TestLayout {
name: "1",
node: vref_node_1,
expected: "<i></i>",
};
let layout2 = TestLayout {
name: "2",
node: vref_node_2,
expected: "<b></b>",
};
diff_layouts(vec![layout1, layout2]);
}
}

View File

@ -0,0 +1,190 @@
//! This module contains the bundle implementation of a portal [BPortal].
use super::test_log;
use super::BNode;
use crate::dom_bundle::{DomBundle, Reconcilable};
use crate::html::{AnyScope, NodeRef};
use crate::virtual_dom::Key;
use crate::virtual_dom::VPortal;
use web_sys::Element;
/// The bundle implementation to [VPortal].
#[derive(Debug)]
pub struct BPortal {
/// The element under which the content is inserted.
host: Element,
/// The next sibling after the inserted content
inner_sibling: NodeRef,
/// The inserted node
node: Box<BNode>,
}
impl DomBundle 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 shift(&self, _next_parent: &Element, _next_sibling: NodeRef) {
// portals have nothing in it's original place of DOM, we also do nothing.
}
}
impl Reconcilable for VPortal {
type Bundle = BPortal;
fn attach(
self,
parent_scope: &AnyScope,
_parent: &Element,
host_next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle) {
let Self {
host,
inner_sibling,
node,
} = self;
let (_, inner) = node.attach(parent_scope, &host, inner_sibling.clone());
(
host_next_sibling,
BPortal {
host,
node: Box::new(inner),
inner_sibling,
},
)
}
fn reconcile_node(
self,
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),
}
}
fn reconcile(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
portal: &mut Self::Bundle,
) -> NodeRef {
let Self {
host,
inner_sibling,
node,
} = self;
let old_host = std::mem::replace(&mut portal.host, host);
let old_inner_sibling = std::mem::replace(&mut portal.inner_sibling, inner_sibling);
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());
}
node.reconcile_node(parent_scope, parent, next_sibling.clone(), &mut portal.node);
next_sibling
}
}
impl BPortal {
/// Get the key of the underlying portal
pub(super) fn key(&self) -> Option<&Key> {
self.node.key()
}
}
#[cfg(test)]
mod layout_tests {
extern crate self as yew;
use crate::html;
use crate::tests::layout_tests::{diff_layouts, TestLayout};
use crate::virtual_dom::VNode;
use yew::virtual_dom::VPortal;
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
#[test]
fn diff() {
let mut layouts = vec![];
let first_target = gloo_utils::document().create_element("i").unwrap();
let second_target = gloo_utils::document().create_element("o").unwrap();
let target_with_child = gloo_utils::document().create_element("i").unwrap();
let target_child = gloo_utils::document().create_element("s").unwrap();
target_with_child.append_child(&target_child).unwrap();
layouts.push(TestLayout {
name: "Portal - first target",
node: html! {
<div>
{VNode::VRef(first_target.clone().into())}
{VNode::VRef(second_target.clone().into())}
{VNode::VPortal(VPortal::new(
html! { {"PORTAL"} },
first_target.clone(),
))}
{"AFTER"}
</div>
},
expected: "<div><i>PORTAL</i><o></o>AFTER</div>",
});
layouts.push(TestLayout {
name: "Portal - second target",
node: html! {
<div>
{VNode::VRef(first_target.clone().into())}
{VNode::VRef(second_target.clone().into())}
{VNode::VPortal(VPortal::new(
html! { {"PORTAL"} },
second_target.clone(),
))}
{"AFTER"}
</div>
},
expected: "<div><i></i><o>PORTAL</o>AFTER</div>",
});
layouts.push(TestLayout {
name: "Portal - replaced by text",
node: html! {
<div>
{VNode::VRef(first_target.clone().into())}
{VNode::VRef(second_target.clone().into())}
{"FOO"}
{"AFTER"}
</div>
},
expected: "<div><i></i><o></o>FOOAFTER</div>",
});
layouts.push(TestLayout {
name: "Portal - next sibling",
node: html! {
<div>
{VNode::VRef(target_with_child.clone().into())}
{VNode::VPortal(VPortal::new_before(
html! { {"PORTAL"} },
target_with_child.clone(),
Some(target_child.clone().into()),
))}
</div>
},
expected: "<div><i>PORTAL<s></s></i></div>",
});
diff_layouts(layouts)
}
}

View File

@ -0,0 +1,178 @@
//! This module contains the bundle version of a supsense [BSuspense]
use super::{BNode, DomBundle, Reconcilable};
use crate::html::AnyScope;
use crate::virtual_dom::{Key, VSuspense};
use crate::NodeRef;
use web_sys::Element;
/// The bundle implementation to [VSuspense]
#[derive(Debug)]
pub struct BSuspense {
children_bundle: BNode,
/// The supsense is suspended if fallback contains [Some] bundle
fallback_bundle: Option<BNode>,
detached_parent: Element,
key: Option<Key>,
}
impl BSuspense {
/// Get the key of the underlying suspense
pub(super) fn key(&self) -> Option<&Key> {
self.key.as_ref()
}
/// Get the bundle node that actually shows up in the dom
fn active_node(&self) -> &BNode {
self.fallback_bundle
.as_ref()
.unwrap_or(&self.children_bundle)
}
}
impl DomBundle for BSuspense {
fn detach(self, 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);
} else {
self.children_bundle.detach(parent, parent_to_detach);
}
}
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
self.active_node().shift(next_parent, next_sibling)
}
}
impl Reconcilable for VSuspense {
type Bundle = BSuspense;
fn attach(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle) {
let VSuspense {
children,
fallback,
detached_parent,
suspended,
key,
} = self;
let detached_parent = detached_parent.expect("no detached parent?");
// When it's suspended, we render children into an element that is detached from the dom
// 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);
(
fallback_ref,
BSuspense {
children_bundle,
fallback_bundle: Some(fallback),
detached_parent,
key,
},
)
} else {
let (child_ref, children_bundle) = children.attach(parent_scope, parent, next_sibling);
(
child_ref,
BSuspense {
children_bundle,
fallback_bundle: None,
detached_parent,
key,
},
)
}
}
fn reconcile_node(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut BNode,
) -> NodeRef {
match bundle {
// We only preserve the child state if they are the same suspense.
BNode::Suspense(m)
if m.key == self.key
&& self.detached_parent.as_ref() == Some(&m.detached_parent) =>
{
self.reconcile(parent_scope, parent, next_sibling, m)
}
_ => self.replace(parent_scope, parent, next_sibling, bundle),
}
}
fn reconcile(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
suspense: &mut Self::Bundle,
) -> NodeRef {
let VSuspense {
children,
fallback,
detached_parent,
suspended,
key: _,
} = self;
let detached_parent = detached_parent.expect("no detached parent?");
let children_bundle = &mut suspense.children_bundle;
// no need to update key & detached_parent
// When it's suspended, we render children into an element that is detached from the dom
// tree while rendering fallback UI into the original place where children resides in.
match (suspended, &mut suspense.fallback_bundle) {
// Both suspended, reconcile children into detached_parent, fallback into the DOM
(true, Some(fallback_bundle)) => {
children.reconcile_node(
parent_scope,
&detached_parent,
NodeRef::default(),
children_bundle,
);
fallback.reconcile_node(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)
}
// Freshly suspended. Shift children into the detached parent, then add fallback to the DOM
(true, None) => {
children_bundle.shift(&detached_parent, NodeRef::default());
children.reconcile_node(
parent_scope,
&detached_parent,
NodeRef::default(),
children_bundle,
);
// first render of fallback
let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling);
suspense.fallback_bundle = Some(fallback);
fallback_ref
}
// Freshly unsuspended. Detach fallback from the DOM, then shift children into it.
(false, Some(_)) => {
suspense
.fallback_bundle
.take()
.unwrap() // We just matched Some(_)
.detach(parent, false);
children_bundle.shift(parent, next_sibling.clone());
children.reconcile_node(parent_scope, parent, next_sibling, children_bundle)
}
}
}
}

View File

@ -0,0 +1,275 @@
use super::Apply;
use crate::virtual_dom::vtag::{InputFields, Value};
use crate::virtual_dom::Attributes;
use indexmap::IndexMap;
use std::collections::HashMap;
use std::iter;
use std::ops::Deref;
use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
impl<T: AccessValue> Apply for Value<T> {
type Element = T;
type Bundle = Self;
fn apply(self, 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) {
match (self.deref(), (*bundle).deref()) {
(Some(new), Some(_)) => {
// Refresh value from the DOM. It might have changed.
if new.as_ref() != el.value() {
el.set_value(new);
}
}
(Some(new), None) => el.set_value(new),
(None, Some(_)) => el.set_value(""),
(None, None) => (),
}
}
}
macro_rules! impl_access_value {
($( $type:ty )*) => {
$(
impl AccessValue for $type {
#[inline]
fn value(&self) -> String {
<$type>::value(&self)
}
#[inline]
fn set_value(&self, v: &str) {
<$type>::set_value(&self, v)
}
}
)*
};
}
impl_access_value! {InputElement TextAreaElement}
/// Able to have its value read or set
pub trait AccessValue {
fn value(&self) -> String;
fn set_value(&self, v: &str);
}
impl Apply for InputFields {
type Element = InputElement;
type Bundle = Self;
fn apply(mut self, 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
}
fn apply_diff(self, 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);
}
}
impl Attributes {
#[cold]
fn apply_diff_index_maps<'a, A, B>(
el: &Element,
// this makes it possible to diff `&'a IndexMap<_, A>` and `IndexMap<_, &'a A>`.
mut new_iter: impl Iterator<Item = (&'static str, &'a str)>,
new: &IndexMap<&'static str, A>,
old: &IndexMap<&'static str, B>,
) where
A: AsRef<str>,
B: AsRef<str>,
{
let mut old_iter = old.iter();
loop {
match (new_iter.next(), old_iter.next()) {
(Some((new_key, new_value)), Some((old_key, old_value))) => {
if new_key != *old_key {
break;
}
if new_value != old_value.as_ref() {
Self::set_attribute(el, new_key, new_value);
}
}
// new attributes
(Some(attr), None) => {
for (key, value) in iter::once(attr).chain(new_iter) {
match old.get(key) {
Some(old_value) => {
if value != old_value.as_ref() {
Self::set_attribute(el, key, value);
}
}
None => {
Self::set_attribute(el, key, value);
}
}
}
break;
}
// removed attributes
(None, Some(attr)) => {
for (key, _) in iter::once(attr).chain(old_iter) {
if !new.contains_key(key) {
Self::remove_attribute(el, key);
}
}
break;
}
(None, None) => break,
}
}
}
/// Convert [Attributes] pair to [HashMap]s and patch changes to `el`.
/// Works with any [Attributes] variants.
#[cold]
fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) {
fn collect<'a>(src: &'a Attributes) -> HashMap<&'static str, &'a str> {
use Attributes::*;
match src {
Static(arr) => (*arr).iter().map(|[k, v]| (*k, *v)).collect(),
Dynamic { keys, values } => keys
.iter()
.zip(values.iter())
.filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref())))
.collect(),
IndexMap(m) => m.iter().map(|(k, v)| (*k, v.as_ref())).collect(),
}
}
let new = collect(new);
let old = collect(old);
// Update existing or set new
for (k, new) in new.iter() {
if match old.get(k) {
Some(old) => old != new,
None => true,
} {
el.set_attribute(k, new).unwrap();
}
}
// Remove missing
for k in old.keys() {
if !new.contains_key(k) {
Self::remove_attribute(el, k);
}
}
}
fn set_attribute(el: &Element, key: &str, value: &str) {
el.set_attribute(key, value).expect("invalid attribute key")
}
fn remove_attribute(el: &Element, key: &str) {
el.remove_attribute(key)
.expect("could not remove attribute")
}
}
impl Apply for Attributes {
type Element = Element;
type Bundle = Self;
fn apply(self, el: &Element) -> Self {
match &self {
Self::Static(arr) => {
for kv in arr.iter() {
Self::set_attribute(el, kv[0], kv[1]);
}
}
Self::Dynamic { keys, values } => {
for (k, v) in keys.iter().zip(values.iter()) {
if let Some(v) = v {
Self::set_attribute(el, k, v)
}
}
}
Self::IndexMap(m) => {
for (k, v) in m.iter() {
Self::set_attribute(el, k, v)
}
}
}
self
}
fn apply_diff(self, el: &Element, bundle: &mut Self) {
#[inline]
fn ptr_eq<T>(a: &[T], b: &[T]) -> bool {
std::ptr::eq(a, b)
}
let ancestor = std::mem::replace(bundle, self);
let bundle = &*bundle; // reborrow it immutably from here
match (bundle, ancestor) {
// Hot path
(Self::Static(new), Self::Static(old)) if ptr_eq(new, old) => (),
// Hot path
(
Self::Dynamic {
keys: new_k,
values: new_v,
},
Self::Dynamic {
keys: old_k,
values: old_v,
},
) if ptr_eq(new_k, old_k) => {
// Double zipping does not optimize well, so use asserts and unsafe instead
assert!(new_k.len() == new_v.len());
assert!(new_k.len() == old_v.len());
for i in 0..new_k.len() {
macro_rules! key {
() => {
unsafe { new_k.get_unchecked(i) }
};
}
macro_rules! set {
($new:expr) => {
Self::set_attribute(el, key!(), $new)
};
}
match unsafe { (new_v.get_unchecked(i), old_v.get_unchecked(i)) } {
(Some(new), Some(old)) => {
if new != old {
set!(new);
}
}
(Some(new), None) => set!(new),
(None, Some(_)) => {
Self::remove_attribute(el, key!());
}
(None, None) => (),
}
}
}
// For VTag's constructed outside the html! macro
(Self::IndexMap(new), Self::IndexMap(ref old)) => {
let new_iter = new.iter().map(|(k, v)| (*k, v.as_ref()));
Self::apply_diff_index_maps(el, new_iter, new, old);
}
// Cold path. Happens only with conditional swapping and reordering of `VTag`s with the
// same tag and no keys.
(new, ref ancestor) => {
Self::apply_diff_as_maps(el, new, ancestor);
}
}
}
}

View File

@ -0,0 +1,709 @@
use super::Apply;
use crate::dom_bundle::test_log;
use crate::virtual_dom::{Listener, ListenerKind, Listeners};
use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use wasm_bindgen::JsCast;
use web_sys::{Element, Event};
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();
}
/// 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.
pub fn set_event_bubbling(bubble: bool) {
BUBBLE_EVENTS.store(bubble, Ordering::Relaxed);
}
/// An active set of listeners on an element
#[derive(Debug)]
pub(super) enum ListenerRegistration {
/// No listeners registered.
NoReg,
/// Added to global registry by ID
Registered(u32),
}
impl Apply for Listeners {
type Element = Element;
type Bundle = ListenerRegistration;
fn apply(self, el: &Self::Element) -> ListenerRegistration {
match self {
Self::Pending(pending) => ListenerRegistration::register(el, &pending),
Self::None => ListenerRegistration::NoReg,
}
}
fn apply_diff(self, el: &Self::Element, bundle: &mut ListenerRegistration) {
use ListenerRegistration::*;
use Listeners::*;
match (self, bundle) {
(Pending(pending), Registered(ref id)) => {
// Reuse the ID
test_log!("reusing listeners for {}", id);
Registry::with(|reg| reg.patch(id, &*pending));
}
(Pending(pending), bundle @ NoReg) => {
*bundle = ListenerRegistration::register(el, &pending);
test_log!(
"registering listeners for {}",
match bundle {
ListenerRegistration::Registered(id) => id,
_ => unreachable!(),
}
);
}
(None, bundle @ Registered(_)) => {
let id = match bundle {
ListenerRegistration::Registered(ref id) => id,
_ => unreachable!(),
};
test_log!("unregistering listeners for {}", id);
Registry::with(|reg| reg.unregister(id));
*bundle = NoReg;
}
(None, NoReg) => {
test_log!("{}", &"unchanged empty 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);
id
}))
}
/// Remove any registered event listeners from the global registry
pub(super) fn unregister(&self) {
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);
}
}
}
/// Global multiplexing event handler registry
#[derive(Default, Debug)]
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()))
}
/// Register all passed listeners under ID
fn register(&mut self, 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());
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>>]) {
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() {
v.clear()
}
for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
let desc = EventDescriptor::from(l.deref());
self.global.ensure_handled(desc.clone());
by_desc.entry(desc).or_default().push(l);
}
}
}
/// Unregister any existing listeners for ID
fn unregister(&mut self, id: &u32) {
self.by_id.remove(id);
}
/// Set unique listener ID onto element and return it
fn set_listener_id(&mut self, 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");
}
});
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"))]
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};
wasm_bindgen_test_configure!(run_in_browser);
use crate::{html, html::TargetCast, scheduler, AppHandle, Component, Context, Html};
use gloo_utils::document;
use wasm_bindgen::JsCast;
use yew::Callback;
#[derive(Clone)]
enum Message {
Action,
StopListening,
SetText(String),
}
#[derive(Default)]
struct State {
stop_listening: bool,
action: u32,
text: String,
}
trait Mixin {
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>
}
}
}
}
struct Comp<M>
where
M: Mixin + 'static,
{
state: State,
pd: PhantomData<M>,
}
impl<M> Component for Comp<M>
where
M: Mixin + 'static,
{
type Message = Message;
type Properties = ();
fn create(_: &Context<Self>) -> Self {
Comp {
state: Default::default(),
pd: PhantomData,
}
}
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Message::Action => {
self.state.action += 1;
}
Message::StopListening => {
self.state.stop_listening = true;
}
Message::SetText(s) => {
self.state.text = s;
}
};
true
}
fn view(&self, ctx: &Context<Self>) -> crate::Html {
M::view(ctx, &self.state)
}
}
fn assert_count(el: &web_sys::HtmlElement, count: isize) {
assert_eq!(el.text_content(), Some(count.to_string()))
}
fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement {
document()
.query_selector(tag)
.unwrap()
.unwrap()
.dyn_into::<web_sys::HtmlElement>()
.unwrap()
}
fn init<M>(tag: &str) -> (AppHandle<Comp<M>>, web_sys::HtmlElement)
where
M: Mixin,
{
// 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();
}
let root = document().create_element("div").unwrap();
document().body().unwrap().append_child(&root).unwrap();
let app = crate::start_app_in_element::<Comp<M>>(root);
scheduler::start_now();
(app, get_el_by_tag(tag))
}
#[test]
fn synchronous() {
struct Synchronous;
impl Mixin for Synchronous {}
let (link, el) = init::<Synchronous>("a");
assert_count(&el, 0);
el.click();
assert_count(&el, 1);
el.click();
assert_count(&el, 2);
link.send_message(Message::StopListening);
scheduler::start_now();
el.click();
assert_count(&el, 2);
}
#[test]
async fn non_bubbling_event() {
struct NonBubbling;
impl Mixin for NonBubbling {
fn view<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message>,
{
let link = ctx.link().clone();
let onblur = Callback::from(move |_| {
link.send_message(Message::Action);
scheduler::start_now();
});
html! {
<div>
<a>
<input id="input" {onblur} type="text" />
{state.action}
</a>
</div>
}
}
}
let (_, el) = init::<NonBubbling>("a");
assert_count(&el, 0);
let input = document().get_element_by_id("input").unwrap();
input
.dispatch_event(
&Event::new_with_event_init_dict("blur", &{
let mut dict = EventInit::new();
dict.bubbles(false);
dict
})
.unwrap(),
)
.unwrap();
assert_count(&el, 1);
}
#[test]
fn bubbling() {
struct Bubbling;
impl Mixin for Bubbling {
fn view<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message>,
{
if state.stop_listening {
html! {
<div>
<a>
{state.action}
</a>
</div>
}
} else {
let link = ctx.link().clone();
let cb = Callback::from(move |_| {
link.send_message(Message::Action);
scheduler::start_now();
});
html! {
<div onclick={cb.clone()}>
<a onclick={cb}>
{state.action}
</a>
</div>
}
}
}
}
let (link, el) = init::<Bubbling>("a");
assert_count(&el, 0);
el.click();
assert_count(&el, 2);
el.click();
assert_count(&el, 4);
link.send_message(Message::StopListening);
scheduler::start_now();
el.click();
assert_count(&el, 4);
}
#[test]
fn cancel_bubbling() {
struct CancelBubbling;
impl Mixin for CancelBubbling {
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();
});
let link = ctx.link().clone();
let onclick2 = Callback::from(move |e: MouseEvent| {
e.stop_propagation();
link.send_message(Message::Action);
scheduler::start_now();
});
html! {
<div onclick={onclick}>
<a onclick={onclick2}>
{state.action}
</a>
</div>
}
}
}
let (_, el) = init::<CancelBubbling>("a");
assert_count(&el, 0);
el.click();
assert_count(&el, 1);
el.click();
assert_count(&el, 2);
}
#[test]
fn cancel_bubbling_nested() {
// 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.
struct CancelBubbling;
impl Mixin for CancelBubbling {
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();
});
let link = ctx.link().clone();
let onclick2 = Callback::from(move |e: MouseEvent| {
e.stop_propagation();
link.send_message(Message::Action);
scheduler::start_now();
});
html! {
<div onclick={onclick}>
<div onclick={onclick2}>
<a>
{state.action}
</a>
</div>
</div>
}
}
}
let (_, el) = init::<CancelBubbling>("a");
assert_count(&el, 0);
el.click();
assert_count(&el, 1);
el.click();
assert_count(&el, 2);
}
fn test_input_listener<E>(make_event: impl Fn() -> E)
where
E: JsCast + std::fmt::Debug,
{
struct Input;
impl Mixin for Input {
fn view<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message>,
{
if state.stop_listening {
html! {
<div>
<input type="text" />
<p>{state.text.clone()}</p>
</div>
}
} else {
let link = ctx.link().clone();
let onchange = Callback::from(move |e: web_sys::Event| {
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
link.send_message(Message::SetText(el.value()));
scheduler::start_now();
});
let link = ctx.link().clone();
let oninput = Callback::from(move |e: web_sys::InputEvent| {
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
link.send_message(Message::SetText(el.value()));
scheduler::start_now();
});
html! {
<div>
<input type="text" {onchange} {oninput} />
<p>{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");
assert_eq!(&p_el.text_content().unwrap(), "");
for mut s in ["foo", "bar", "baz"].iter() {
input_el.set_value(s);
if s == &"baz" {
link.send_message(Message::StopListening);
scheduler::start_now();
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);
}
}
#[test]
fn oninput() {
test_input_listener(|| {
web_sys::InputEvent::new_with_event_init_dict(
"input",
web_sys::InputEventInit::new().bubbles(true),
)
.unwrap()
})
}
#[test]
fn onchange() {
test_input_listener(|| {
web_sys::Event::new_with_event_init_dict(
"change",
web_sys::EventInit::new().bubbles(true),
)
.unwrap()
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,163 @@
//! This module contains the bundle implementation of text [BText].
use super::{insert_node, BNode, DomBundle, Reconcilable};
use crate::html::AnyScope;
use crate::virtual_dom::{AttrValue, VText};
use crate::NodeRef;
use gloo::console;
use gloo_utils::document;
use web_sys::{Element, Text as TextNode};
/// The bundle implementation to [VText]
pub struct BText {
text: AttrValue,
text_node: TextNode,
}
impl DomBundle for BText {
fn detach(self, parent: &Element, parent_to_detach: bool) {
if !parent_to_detach {
let result = parent.remove_child(&self.text_node);
if result.is_err() {
console::warn!("Node not found to remove VText");
}
}
}
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
let node = &self.text_node;
next_parent
.insert_before(node, next_sibling.get().as_ref())
.unwrap();
}
}
impl Reconcilable for VText {
type Bundle = BText;
fn attach(
self,
_parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle) {
let Self { text } = self;
let text_node = document().create_text_node(&text);
insert_node(&text_node, parent, next_sibling.get().as_ref());
let node_ref = NodeRef::new(text_node.clone().into());
(node_ref, BText { text, text_node })
}
/// Renders virtual node over existing `TextNode`, but only if value of text has changed.
fn reconcile_node(
self,
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),
}
}
fn reconcile(
self,
_parent_scope: &AnyScope,
_parent: &Element,
_next_sibling: NodeRef,
btext: &mut Self::Bundle,
) -> NodeRef {
let Self { text } = self;
let ancestor_text = std::mem::replace(&mut btext.text, text);
if btext.text != ancestor_text {
btext.text_node.set_node_value(Some(&btext.text));
}
NodeRef::new(btext.text_node.clone().into())
}
}
impl std::fmt::Debug for BText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BText {{ text: \"{}\" }}", self.text)
}
}
#[cfg(test)]
mod test {
extern crate self as yew;
use crate::html;
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
#[test]
fn text_as_root() {
html! {
"Text Node As Root"
};
html! {
{ "Text Node As Root" }
};
}
}
#[cfg(test)]
mod layout_tests {
extern crate self as yew;
use crate::html;
use crate::tests::layout_tests::{diff_layouts, TestLayout};
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
#[test]
fn diff() {
let layout1 = TestLayout {
name: "1",
node: html! { "a" },
expected: "a",
};
let layout2 = TestLayout {
name: "2",
node: html! { "b" },
expected: "b",
};
let layout3 = TestLayout {
name: "3",
node: html! {
<>
{"a"}
{"b"}
</>
},
expected: "ab",
};
let layout4 = TestLayout {
name: "4",
node: html! {
<>
{"b"}
{"a"}
</>
},
expected: "ba",
};
diff_layouts(vec![layout1, layout2, layout3, layout4]);
}
}

View File

@ -0,0 +1,151 @@
//! Realizing a virtual dom on the actual DOM
//!
//! A bundle, borrowed from the mathematical meaning, is any structure over some base space.
//! In our case, the base space is the virtual dom we're trying to render.
//! In order to efficiently implement updates, and diffing, additional information has to be
//! kept around. This information is carried in the bundle.
mod app_handle;
mod bcomp;
mod blist;
mod bnode;
mod bportal;
mod bsuspense;
mod btag;
mod btext;
#[cfg(test)]
mod tests;
use self::bcomp::BComp;
use self::blist::BList;
use self::bnode::BNode;
use self::bportal::BPortal;
use self::bsuspense::BSuspense;
use self::btag::BTag;
use self::btext::BText;
pub(crate) use self::bcomp::{ComponentRenderState, Mountable, PropsWrapper, Scoped};
#[doc(hidden)] // Publically exported from crate::app_handle
pub use self::app_handle::AppHandle;
#[doc(hidden)] // Publically exported from crate::events
pub use self::btag::set_event_bubbling;
#[cfg(test)]
#[doc(hidden)] // Publically exported from crate::tests
pub use self::tests::layout_tests;
use crate::html::AnyScope;
use crate::NodeRef;
use web_sys::{Element, Node};
trait DomBundle {
/// 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);
/// Move elements from one parent to another parent.
/// This is for example used by `VSuspense` to preserve component state without detaching
/// (which destroys component state).
fn shift(&self, next_parent: &Element, next_sibling: NodeRef);
}
/// This trait provides features to update a tree by calculating a difference against another tree.
trait Reconcilable {
type Bundle: DomBundle;
/// Attach a virtual node to the DOM tree.
///
/// Parameters:
/// - `parent_scope`: the parent `Scope` used for passing messages to the
/// parent `Component`.
/// - `parent`: the parent node in the DOM.
/// - `next_sibling`: to find where to put the node.
///
/// Returns a reference to the newly inserted element.
fn attach(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
) -> (NodeRef, Self::Bundle);
/// Scoped diff apply to other tree.
///
/// Virtual rendering for the node. It uses parent node and existing
/// children (virtual and DOM) to check the difference and apply patches to
/// the actual DOM representation.
///
/// Parameters:
/// - `parent_scope`: the parent `Scope` used for passing messages to the
/// parent `Component`.
/// - `parent`: the parent node in the DOM.
/// - `next_sibling`: the next sibling, used to efficiently find where to
/// put the node.
/// - `bundle`: the node that this node will be replacing in the DOM. This
/// method will remove the `bundle` from the `parent` if it is of the wrong
/// kind, and otherwise reuse it.
///
/// Returns a reference to the newly inserted element.
fn reconcile_node(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut BNode,
) -> NodeRef;
fn reconcile(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut Self::Bundle,
) -> NodeRef;
/// Replace an existing bundle by attaching self and detaching the existing one
fn replace(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut BNode,
) -> NodeRef
where
Self: Sized,
Self::Bundle: Into<BNode>,
{
let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling);
let ancestor = std::mem::replace(bundle, self_.into());
ancestor.detach(parent, false);
self_ref
}
}
/// Insert a concrete [Node] into the DOM
fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) {
match next_sibling {
Some(next_sibling) => parent
.insert_before(node, Some(next_sibling))
.expect("failed to insert tag before next sibling"),
None => parent.append_child(node).expect("failed to append child"),
};
}
#[cfg(all(test, feature = "wasm_test", verbose_tests))]
macro_rules! test_log {
($fmt:literal, $($arg:expr),* $(,)?) => {
::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*);
};
}
#[cfg(not(all(test, feature = "wasm_test", verbose_tests)))]
macro_rules! test_log {
($fmt:literal, $($arg:expr),* $(,)?) => {
// Only type-check the format expression, do not run any side effects
let _ = || { std::format_args!(concat!("\t ", $fmt), $($arg),*); };
};
}
/// Log an operation during tests for debugging purposes
/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate.
pub(self) use test_log;

View File

@ -1,6 +1,7 @@
use crate::html::{AnyScope, Scope};
use crate::dom_bundle::{BNode, DomBundle, Reconcilable};
use crate::html::AnyScope;
use crate::scheduler;
use crate::virtual_dom::{VDiff, VNode, VText};
use crate::virtual_dom::VNode;
use crate::{Component, Context, Html};
use gloo::console::log;
use web_sys::Node;
@ -37,21 +38,20 @@ pub struct TestLayout<'a> {
pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
let document = gloo_utils::document();
let parent_scope: AnyScope = Scope::<Comp>::new(None).into();
let parent_scope: AnyScope = AnyScope::test();
let parent_element = document.create_element("div").unwrap();
let parent_node: Node = parent_element.clone().into();
let end_node = document.create_text_node("END");
parent_node.append_child(&end_node).unwrap();
let mut empty_node: VNode = VText::new("").into();
// Tests each layout independently
let next_sibling = NodeRef::new(end_node.into());
for layout in layouts.iter() {
// Apply the layout
let mut node = layout.node.clone();
let vnode = layout.node.clone();
log!("Independently apply layout '{}'", layout.name);
node.apply(&parent_scope, &parent_element, next_sibling.clone(), None);
let (_, mut bundle) = vnode.attach(&parent_scope, &parent_element, next_sibling.clone());
scheduler::start_now();
assert_eq!(
parent_element.inner_html(),
@ -61,15 +61,15 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
);
// Diff with no changes
let mut node_clone = layout.node.clone();
let vnode = layout.node.clone();
log!("Independently reapply layout '{}'", layout.name);
node_clone.apply(
vnode.reconcile_node(
&parent_scope,
&parent_element,
next_sibling.clone(),
Some(node),
&mut bundle,
);
scheduler::start_now();
assert_eq!(
@ -80,12 +80,7 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
);
// Detach
empty_node.clone().apply(
&parent_scope,
&parent_element,
next_sibling.clone(),
Some(node_clone),
);
bundle.detach(&parent_element, false);
scheduler::start_now();
assert_eq!(
parent_element.inner_html(),
@ -96,16 +91,16 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
}
// Sequentially apply each layout
let mut ancestor: Option<VNode> = None;
let mut bundle: Option<BNode> = None;
for layout in layouts.iter() {
let mut next_node = layout.node.clone();
let next_vnode = layout.node.clone();
log!("Sequentially apply layout '{}'", layout.name);
next_node.apply(
next_vnode.reconcile_sequentially(
&parent_scope,
&parent_element,
next_sibling.clone(),
ancestor,
&mut bundle,
);
scheduler::start_now();
assert_eq!(
@ -114,19 +109,18 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
"Sequential apply failed for layout '{}'",
layout.name,
);
ancestor = Some(next_node);
}
// Sequentially detach each layout
for layout in layouts.into_iter().rev() {
let mut next_node = layout.node.clone();
let next_vnode = layout.node.clone();
log!("Sequentially detach layout '{}'", layout.name);
next_node.apply(
next_vnode.reconcile_sequentially(
&parent_scope,
&parent_element,
next_sibling.clone(),
ancestor,
&mut bundle,
);
scheduler::start_now();
assert_eq!(
@ -135,11 +129,12 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
"Sequential detach failed for layout '{}'",
layout.name,
);
ancestor = Some(next_node);
}
// Detach last layout
empty_node.apply(&parent_scope, &parent_element, next_sibling, ancestor);
if let Some(bundle) = bundle {
bundle.detach(&parent_element, false);
}
scheduler::start_now();
assert_eq!(
parent_element.inner_html(),

View File

@ -0,0 +1,26 @@
pub mod layout_tests;
use super::Reconcilable;
use crate::virtual_dom::VNode;
use crate::{dom_bundle::BNode, html::AnyScope, NodeRef};
use web_sys::Element;
impl VNode {
fn reconcile_sequentially(
self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
bundle: &mut Option<BNode>,
) -> NodeRef {
match bundle {
None => {
let (self_ref, node) = self.attach(parent_scope, parent, next_sibling);
*bundle = Some(node);
self_ref
}
Some(bundle) => self.reconcile_node(parent_scope, parent, next_sibling, bundle),
}
}
}

View File

@ -1,19 +1,16 @@
//! Component lifecycle module
use super::{AnyScope, BaseComponent, Scope};
use crate::html::{RenderError, RenderResult};
use super::scope::{AnyScope, Scope};
use super::BaseComponent;
use crate::dom_bundle::ComponentRenderState;
use crate::html::RenderError;
use crate::scheduler::{self, Runnable, Shared};
use crate::suspense::{Suspense, Suspension};
use crate::virtual_dom::{VDiff, VNode};
use crate::Callback;
use crate::{Context, NodeRef};
#[cfg(feature = "ssr")]
use futures::channel::oneshot;
use crate::{Callback, Context, HtmlResult, NodeRef};
use std::any::Any;
use std::rc::Rc;
use web_sys::Element;
pub(crate) struct CompStateInner<COMP>
pub struct CompStateInner<COMP>
where
COMP: BaseComponent,
{
@ -26,8 +23,8 @@ where
///
/// Mostly a thin wrapper that passes the context to a component's lifecycle
/// methods.
pub(crate) trait Stateful {
fn view(&self) -> RenderResult<VNode>;
pub trait Stateful {
fn view(&self) -> HtmlResult;
fn rendered(&mut self, first_render: bool);
fn destroy(&mut self);
@ -44,7 +41,7 @@ impl<COMP> Stateful for CompStateInner<COMP>
where
COMP: BaseComponent,
{
fn view(&self) -> RenderResult<VNode> {
fn view(&self) -> HtmlResult {
self.component.view(&self.context)
}
@ -93,23 +90,15 @@ where
}
}
pub(crate) struct ComponentState {
pub(crate) inner: Box<dyn Stateful>,
pub struct ComponentState {
pub(super) inner: Box<dyn Stateful>,
pub(crate) root_node: VNode,
/// When a component has no parent, it means that it should not be rendered.
parent: Option<Element>,
next_sibling: NodeRef,
pub(super) render_state: ComponentRenderState,
node_ref: NodeRef,
has_rendered: bool,
suspension: Option<Suspension>,
#[cfg(feature = "ssr")]
html_sender: Option<oneshot::Sender<VNode>>,
// Used for debug logging
#[cfg(debug_assertions)]
pub(crate) vcomp_id: usize,
@ -117,13 +106,10 @@ pub(crate) struct ComponentState {
impl ComponentState {
pub(crate) fn new<COMP: BaseComponent>(
parent: Option<Element>,
next_sibling: NodeRef,
root_node: VNode,
initial_render_state: ComponentRenderState,
node_ref: NodeRef,
scope: Scope<COMP>,
props: Rc<COMP::Properties>,
#[cfg(feature = "ssr")] html_sender: Option<oneshot::Sender<VNode>>,
) -> Self {
#[cfg(debug_assertions)]
let vcomp_id = scope.vcomp_id;
@ -136,31 +122,22 @@ impl ComponentState {
Self {
inner,
root_node,
parent,
next_sibling,
render_state: initial_render_state,
node_ref,
suspension: None,
has_rendered: false,
#[cfg(feature = "ssr")]
html_sender,
#[cfg(debug_assertions)]
vcomp_id,
}
}
}
pub(crate) struct CreateRunner<COMP: BaseComponent> {
pub(crate) parent: Option<Element>,
pub(crate) next_sibling: NodeRef,
pub(crate) placeholder: VNode,
pub(crate) node_ref: NodeRef,
pub(crate) props: Rc<COMP::Properties>,
pub(crate) scope: Scope<COMP>,
#[cfg(feature = "ssr")]
pub(crate) html_sender: Option<oneshot::Sender<VNode>>,
pub struct CreateRunner<COMP: BaseComponent> {
pub initial_render_state: ComponentRenderState,
pub node_ref: NodeRef,
pub props: Rc<COMP::Properties>,
pub scope: Scope<COMP>,
}
impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
@ -168,34 +145,28 @@ impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
let mut current_state = self.scope.state.borrow_mut();
if current_state.is_none() {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(self.scope.vcomp_id, "create");
super::log_event(self.scope.vcomp_id, "create");
*current_state = Some(ComponentState::new(
self.parent,
self.next_sibling,
self.placeholder,
self.initial_render_state,
self.node_ref,
self.scope.clone(),
self.props,
#[cfg(feature = "ssr")]
self.html_sender,
));
}
}
}
pub(crate) enum UpdateEvent {
pub enum UpdateEvent {
/// Drain messages for a component.
Message,
/// Wraps properties, node ref, and next sibling for a component.
Properties(Rc<dyn Any>, NodeRef, NodeRef),
/// Shift Scope.
Shift(Element, NodeRef),
}
pub(crate) struct UpdateRunner {
pub(crate) state: Shared<Option<ComponentState>>,
pub(crate) event: UpdateEvent,
pub struct UpdateRunner {
pub state: Shared<Option<ComponentState>>,
pub event: UpdateEvent,
}
impl Runnable for UpdateRunner {
@ -207,27 +178,15 @@ impl Runnable for UpdateRunner {
// When components are updated, a new node ref could have been passed in
state.node_ref = node_ref;
// When components are updated, their siblings were likely also updated
state.next_sibling = next_sibling;
state.render_state.reuse(next_sibling);
// Only trigger changed if props were changed
state.inner.props_changed(props)
}
UpdateEvent::Shift(parent, next_sibling) => {
state.root_node.shift(
state.parent.as_ref().unwrap(),
&parent,
next_sibling.clone(),
);
state.parent = Some(parent);
state.next_sibling = next_sibling;
false
}
};
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(
super::log_event(
state.vcomp_id,
format!("update(schedule_render={})", schedule_render),
);
@ -245,46 +204,38 @@ impl Runnable for UpdateRunner {
}
}
pub(crate) struct DestroyRunner {
pub(crate) state: Shared<Option<ComponentState>>,
pub(crate) parent_to_detach: bool,
pub struct DestroyRunner {
pub state: Shared<Option<ComponentState>>,
pub parent_to_detach: bool,
}
impl Runnable for DestroyRunner {
fn run(self: Box<Self>) {
if let Some(mut state) = self.state.borrow_mut().take() {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "destroy");
super::log_event(state.vcomp_id, "destroy");
state.inner.destroy();
if let Some(ref m) = state.parent {
state.root_node.detach(m, self.parent_to_detach);
state.node_ref.set(None);
}
state.render_state.detach(self.parent_to_detach);
state.node_ref.set(None);
}
}
}
pub(crate) struct RenderRunner {
pub(crate) state: Shared<Option<ComponentState>>,
pub struct RenderRunner {
pub state: Shared<Option<ComponentState>>,
}
impl Runnable for RenderRunner {
fn run(self: Box<Self>) {
if let Some(state) = self.state.borrow_mut().as_mut() {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "render");
super::log_event(state.vcomp_id, "render");
match state.inner.view() {
Ok(m) => {
Ok(root) => {
// Currently not suspended, we remove any previous suspension and update
// normally.
let mut root = m;
if state.parent.is_some() {
std::mem::swap(&mut root, &mut state.root_node);
}
if let Some(m) = state.suspension.take() {
let comp_scope = state.inner.any_scope();
@ -294,15 +245,11 @@ impl Runnable for RenderRunner {
suspense.resume(m);
}
if let Some(ref m) = state.parent {
let ancestor = Some(root);
let new_root = &mut state.root_node;
let scope = state.inner.any_scope();
let next_sibling = state.next_sibling.clone();
let node = new_root.apply(&scope, m, next_sibling, ancestor);
state.node_ref.link(node);
let scope = state.inner.any_scope();
let node = state.render_state.reconcile(root, &scope);
state.node_ref.link(node);
if state.render_state.should_trigger_rendered() {
let first_render = !state.has_rendered;
state.has_rendered = true;
@ -314,11 +261,6 @@ impl Runnable for RenderRunner {
},
first_render,
);
} else {
#[cfg(feature = "ssr")]
if let Some(tx) = state.html_sender.take() {
tx.send(root).unwrap();
}
}
}
@ -372,8 +314,8 @@ impl Runnable for RenderRunner {
}
}
pub(crate) struct RenderedRunner {
pub(crate) state: Shared<Option<ComponentState>>,
struct RenderedRunner {
state: Shared<Option<ComponentState>>,
first_render: bool,
}
@ -381,9 +323,9 @@ impl Runnable for RenderedRunner {
fn run(self: Box<Self>) {
if let Some(state) = self.state.borrow_mut().as_mut() {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "rendered");
super::log_event(state.vcomp_id, "rendered");
if state.suspension.is_none() && state.parent.is_some() {
if state.suspension.is_none() {
state.inner.rendered(self.first_render);
}
}
@ -394,10 +336,13 @@ impl Runnable for RenderedRunner {
mod tests {
extern crate self as yew;
use crate::dom_bundle::ComponentRenderState;
use crate::html;
use crate::html::*;
use crate::Properties;
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
@ -515,10 +460,12 @@ mod tests {
let document = gloo_utils::document();
let scope = Scope::<Comp>::new(None);
let el = document.create_element("div").unwrap();
let node_ref = NodeRef::default();
let render_state = ComponentRenderState::new(el, NodeRef::default(), &node_ref);
let lifecycle = props.lifecycle.clone();
lifecycle.borrow_mut().clear();
scope.mount_in_place(el, NodeRef::default(), NodeRef::default(), Rc::new(props));
scope.mount_in_place(render_state, node_ref, Rc::new(props));
crate::scheduler::start_now();
assert_eq!(&lifecycle.borrow_mut().deref()[..], expected);

View File

@ -8,16 +8,52 @@ mod scope;
use super::{Html, HtmlResult, IntoHtmlResult};
pub use children::*;
pub use properties::*;
pub(crate) use scope::Scoped;
pub use scope::{AnyScope, Scope, SendAsMessage};
use std::rc::Rc;
#[cfg(debug_assertions)]
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(debug_assertions)]
thread_local! {
static EVENT_HISTORY: std::cell::RefCell<std::collections::HashMap<usize, Vec<String>>>
= Default::default();
static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
}
/// Push [Component] event to lifecycle debugging registry
#[cfg(debug_assertions)]
pub(crate) fn log_event(vcomp_id: usize, event: impl ToString) {
EVENT_HISTORY.with(|h| {
h.borrow_mut()
.entry(vcomp_id)
.or_default()
.push(event.to_string())
});
}
/// Get [Component] event log from lifecycle debugging registry
#[cfg(debug_assertions)]
#[allow(dead_code)]
pub(crate) fn get_event_log(vcomp_id: usize) -> Vec<String> {
EVENT_HISTORY.with(|h| {
h.borrow()
.get(&vcomp_id)
.map(|l| (*l).clone())
.unwrap_or_default()
})
}
#[cfg(debug_assertions)]
pub(crate) fn next_id() -> usize {
COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed))
}
/// The [`Component`]'s context. This contains component's [`Scope`] and and props and
/// is passed to every lifecycle method.
#[derive(Debug)]
pub struct Context<COMP: BaseComponent> {
pub(crate) scope: Scope<COMP>,
pub(crate) props: Rc<COMP::Properties>,
scope: Scope<COMP>,
props: Rc<COMP::Properties>,
}
impl<COMP: BaseComponent> Context<COMP> {

View File

@ -9,17 +9,16 @@ use super::{
};
use crate::callback::Callback;
use crate::context::{ContextHandle, ContextProvider};
use crate::dom_bundle::{ComponentRenderState, Scoped};
use crate::html::NodeRef;
use crate::scheduler::{self, Shared};
use crate::virtual_dom::{insert_node, VNode};
use gloo_utils::document;
use std::any::TypeId;
use std::cell::{Ref, RefCell};
use std::marker::PhantomData;
use std::ops::Deref;
use std::rc::Rc;
use std::{fmt, iter};
use web_sys::{Element, Node};
use web_sys::Element;
#[derive(Debug)]
pub(crate) struct MsgQueue<Msg>(Shared<Vec<Msg>>);
@ -65,9 +64,6 @@ pub struct AnyScope {
type_id: TypeId,
parent: Option<Rc<AnyScope>>,
state: Shared<Option<ComponentState>>,
#[cfg(debug_assertions)]
pub(crate) vcomp_id: usize,
}
impl fmt::Debug for AnyScope {
@ -82,9 +78,6 @@ impl<COMP: BaseComponent> From<Scope<COMP>> for AnyScope {
type_id: TypeId::of::<COMP>(),
parent: scope.parent,
state: scope.state,
#[cfg(debug_assertions)]
vcomp_id: scope.vcomp_id,
}
}
}
@ -96,9 +89,6 @@ impl AnyScope {
type_id: TypeId::of::<()>(),
parent: None,
state: Rc::new(RefCell::new(None)),
#[cfg(debug_assertions)]
vcomp_id: 0,
}
}
@ -163,44 +153,41 @@ impl AnyScope {
}
}
pub(crate) trait Scoped {
fn to_any(&self) -> AnyScope;
fn root_vnode(&self) -> Option<Ref<'_, VNode>>;
fn destroy(&mut self, parent_to_detach: bool);
fn shift_node(&self, parent: Element, next_sibling: NodeRef);
}
impl<COMP: BaseComponent> Scoped for Scope<COMP> {
fn to_any(&self) -> AnyScope {
self.clone().into()
}
fn root_vnode(&self) -> Option<Ref<'_, VNode>> {
fn render_state(&self) -> Option<Ref<'_, ComponentRenderState>> {
let state_ref = self.state.borrow();
// check that component hasn't been destroyed
state_ref.as_ref()?;
Some(Ref::map(state_ref, |state_ref| {
&state_ref.as_ref().unwrap().root_node
&state_ref.as_ref().unwrap().render_state
}))
}
/// Process an event to destroy a component
fn destroy(&mut self, parent_to_detach: bool) {
fn destroy(self, parent_to_detach: bool) {
scheduler::push_component_destroy(DestroyRunner {
state: self.state.clone(),
state: self.state,
parent_to_detach,
});
// Not guaranteed to already have the scheduler started
scheduler::start();
}
fn destroy_boxed(self: Box<Self>, parent_to_detach: bool) {
self.destroy(parent_to_detach)
}
fn shift_node(&self, parent: Element, next_sibling: NodeRef) {
scheduler::push_component_update(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)
}
}
}
@ -258,14 +245,12 @@ impl<COMP: BaseComponent> Scope<COMP> {
})
}
/// Crate a scope with an optional parent scope
pub(crate) fn new(parent: Option<AnyScope>) -> Self {
let parent = parent.map(Rc::new);
let state = Rc::new(RefCell::new(None));
let pending_messages = MsgQueue::new();
#[cfg(debug_assertions)]
let vcomp_id = parent.as_ref().map(|p| p.vcomp_id).unwrap_or_default();
Scope {
_marker: PhantomData,
pending_messages,
@ -273,37 +258,23 @@ impl<COMP: BaseComponent> Scope<COMP> {
parent,
#[cfg(debug_assertions)]
vcomp_id,
vcomp_id: super::next_id(),
}
}
/// Mounts a component with `props` to the specified `element` in the DOM.
pub(crate) fn mount_in_place(
&self,
parent: Element,
next_sibling: NodeRef,
initial_render_state: ComponentRenderState,
node_ref: NodeRef,
props: Rc<COMP::Properties>,
) {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(self.vcomp_id, "create placeholder");
let placeholder = {
let placeholder: Node = document().create_text_node("").into();
insert_node(&placeholder, &parent, next_sibling.get().as_ref());
node_ref.set(Some(placeholder.clone()));
VNode::VRef(placeholder)
};
scheduler::push_component_create(
CreateRunner {
parent: Some(parent),
next_sibling,
placeholder,
initial_render_state,
node_ref,
props,
scope: self.clone(),
#[cfg(feature = "ssr")]
html_sender: None,
},
RenderRunner {
state: self.state.clone(),
@ -320,7 +291,7 @@ impl<COMP: BaseComponent> Scope<COMP> {
next_sibling: NodeRef,
) {
#[cfg(debug_assertions)]
crate::virtual_dom::vcomp::log_event(self.vcomp_id, "reuse");
super::log_event(self.vcomp_id, "reuse");
self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling));
}
@ -346,6 +317,9 @@ impl<COMP: BaseComponent> Scope<COMP> {
}
/// Send a batch of messages to the component.
///
/// This is slightly more efficient than calling [`send_message`](Self::send_message)
/// in a loop.
pub fn send_message_batch(&self, mut messages: Vec<COMP::Message>) {
let msg_len = messages.len();
@ -410,24 +384,11 @@ mod feat_ssr {
use futures::channel::oneshot;
impl<COMP: BaseComponent> Scope<COMP> {
pub(crate) async fn render_to_string(&self, w: &mut String, props: Rc<COMP::Properties>) {
pub(crate) async fn render_to_string(self, w: &mut String, props: Rc<COMP::Properties>) {
let (tx, rx) = oneshot::channel();
let initial_render_state = ComponentRenderState::new_ssr(tx);
scheduler::push_component_create(
CreateRunner {
parent: None,
next_sibling: NodeRef::default(),
placeholder: VNode::default(),
node_ref: NodeRef::default(),
props,
scope: self.clone(),
html_sender: Some(tx),
},
RenderRunner {
state: self.state.clone(),
},
);
scheduler::start();
self.mount_in_place(initial_render_state, NodeRef::default(), props);
let html = rx.await.unwrap();
@ -442,6 +403,7 @@ mod feat_ssr {
}
}
}
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
mod feat_io {

View File

@ -263,9 +263,9 @@ pub mod macros {
pub use crate::props;
}
mod app_handle;
pub mod callback;
pub mod context;
mod dom_bundle;
pub mod functional;
pub mod html;
mod io_coop;
@ -274,18 +274,21 @@ mod sealed;
#[cfg(feature = "ssr")]
mod server_renderer;
pub mod suspense;
#[cfg(test)]
pub mod tests;
pub mod utils;
pub mod virtual_dom;
#[cfg(feature = "ssr")]
pub use server_renderer::*;
#[cfg(test)]
pub mod tests {
pub use crate::dom_bundle::layout_tests;
}
/// The module that contains all events available in the framework.
pub mod events {
pub use crate::html::TargetCast;
pub use crate::virtual_dom::listeners::set_event_bubbling;
pub use crate::dom_bundle::set_event_bubbling;
#[doc(no_inline)]
pub use web_sys::{
@ -294,7 +297,7 @@ pub mod events {
};
}
pub use crate::app_handle::AppHandle;
pub use crate::dom_bundle::AppHandle;
use web_sys::Element;
use crate::html::BaseComponent;
@ -324,7 +327,7 @@ where
COMP: BaseComponent,
COMP::Properties: Default,
{
start_app_with_props_in_element(element, COMP::Properties::default())
start_app_with_props_in_element::<COMP>(element, COMP::Properties::default())
}
/// Starts an yew app mounted to the body of the document.
@ -334,7 +337,7 @@ where
COMP: BaseComponent,
COMP::Properties: Default,
{
start_app_with_props(COMP::Properties::default())
start_app_with_props::<COMP>(COMP::Properties::default())
}
/// The main entry point of a Yew application. This function does the
@ -356,7 +359,7 @@ pub fn start_app_with_props<COMP>(props: COMP::Properties) -> AppHandle<COMP>
where
COMP: BaseComponent,
{
start_app_with_props_in_element(
start_app_with_props_in_element::<COMP>(
gloo_utils::document()
.body()
.expect("no body node found")
@ -374,9 +377,9 @@ where
/// use yew::prelude::*;
/// ```
pub mod prelude {
pub use crate::app_handle::AppHandle;
pub use crate::callback::Callback;
pub use crate::context::{ContextHandle, ContextProvider};
pub use crate::dom_bundle::AppHandle;
pub use crate::events::*;
pub use crate::html::{
create_portal, BaseComponent, Children, ChildrenWithProps, Classes, Component, Context,

View File

@ -3,8 +3,8 @@ use super::*;
use crate::html::Scope;
/// A Yew Server-side Renderer.
#[cfg_attr(documenting, doc(cfg(feature = "ssr")))]
#[derive(Debug)]
#[cfg_attr(documenting, doc(cfg(feature = "ssr")))]
pub struct ServerRenderer<COMP>
where
COMP: BaseComponent,

View File

@ -1 +0,0 @@
pub mod layout_tests;

View File

@ -1,40 +1,4 @@
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
ops::Deref,
rc::Rc,
sync::atomic::{AtomicBool, Ordering},
};
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{Element, Event};
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();
}
/// 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.
pub fn set_event_bubbling(bubble: bool) {
BUBBLE_EVENTS.store(bubble, Ordering::Relaxed);
}
use std::rc::Rc;
/// The [Listener] trait is an universal implementation of an event listener
/// which is used to bind Rust-listener to JS-listener (DOM).
@ -75,10 +39,10 @@ macro_rules! gen_listener_kinds {
}
impl ListenerKind {
pub fn type_name(&self) -> &str {
pub fn type_name(&self) -> std::borrow::Cow<'static, str> {
match self {
Self::other(type_name) => type_name.as_ref(),
kind => &kind.as_ref()[2..],
Self::other(type_name) => type_name.clone(),
$( Self::$kind => stringify!($kind)[2..].into(), )*
}
}
}
@ -200,90 +164,16 @@ pub enum Listeners {
/// Distinct from `Pending` with an empty slice to avoid an allocation.
None,
/// Added to global registry by ID
Registered(u32),
/// Not yet added to the element or registry
Pending(Box<[Option<Rc<dyn Listener>>]>),
}
impl Listeners {
/// 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);
id
}))
}
/// Remove any registered event listeners from the global registry
pub(super) fn unregister(&self) {
if let Self::Registered(id) = self {
Registry::with(|r| r.unregister(id));
}
}
}
impl super::Apply for Listeners {
type Element = Element;
fn apply(&mut self, el: &Self::Element) {
if let Self::Pending(pending) = self {
*self = Self::register(el, pending);
}
}
fn apply_diff(&mut self, el: &Self::Element, ancestor: Self) {
use Listeners::*;
match (std::mem::take(self), ancestor) {
(Pending(pending), Registered(id)) => {
// Reuse the ID
Registry::with(|reg| reg.patch(&id, &*pending));
*self = Registered(id);
}
(Pending(pending), None) => {
*self = Self::register(el, &pending);
}
(None, Registered(id)) => {
Registry::with(|reg| reg.unregister(&id));
}
_ => (),
};
}
}
impl PartialEq for Listeners {
fn eq(&self, rhs: &Self) -> bool {
use Listeners::*;
match (self, rhs) {
(None, None) => true,
(Registered(lhs), Registered(rhs)) => lhs == rhs,
(Registered(registered_id), Pending(pending))
| (Pending(pending), Registered(registered_id)) => {
use std::option::Option::None;
Registry::with(|reg| match reg.by_id.get(registered_id) {
Some(reg) => {
if reg.len() != pending.len() {
return false;
}
pending.iter().filter_map(|l| l.as_ref()).all(|l| {
match reg.get(&EventDescriptor::from(l.deref())) {
Some(reg) => reg.iter().any(|reg| {
#[allow(clippy::vtable_address_comparisons)]
Rc::ptr_eq(reg, l)
}),
None => false,
}
})
}
None => false,
})
}
(Pending(lhs), Pending(rhs)) => {
if lhs.len() != rhs.len() {
false
@ -303,7 +193,7 @@ impl PartialEq for Listeners {
})
}
}
_ => false,
(None, Pending(pending)) | (Pending(pending), None) => pending.len() == 0,
}
}
}
@ -311,7 +201,7 @@ impl PartialEq for Listeners {
impl Clone for Listeners {
fn clone(&self) -> Self {
match self {
Self::None | Self::Registered(_) => Self::None,
Self::None => Self::None,
Self::Pending(v) => Self::Pending(v.clone()),
}
}
@ -322,623 +212,3 @@ impl Default for Listeners {
Self::None
}
}
#[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)]
#[allow(clippy::type_complexity)]
registered: Vec<(ListenerKind, Closure<dyn Fn(web_sys::Event)>)>,
}
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 = BODY.with(|body| {
let cl = Closure::wrap(Box::new({
let desc = desc.clone();
move |e: Event| Registry::handle(desc.clone(), e)
}) as Box<dyn Fn(Event)>);
AsRef::<web_sys::EventTarget>::as_ref(body)
.add_event_listener_with_callback_and_add_event_listener_options(
desc.kind.type_name(),
cl.as_ref().unchecked_ref(),
&{
let mut opts = web_sys::AddEventListenerOptions::new();
opts.capture(true);
// We need to explicitly set passive to override any browser defaults
opts.passive(desc.passive);
opts
},
)
.map_err(|e| format!("could not register global listener: {:?}", e))
.unwrap();
cl
});
// 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);
}
}
}
// Enable resetting between tests
#[cfg(test)]
impl Drop for GlobalHandlers {
fn drop(&mut self) {
BODY.with(|body| {
for (kind, cl) in std::mem::take(&mut self.registered) {
AsRef::<web_sys::EventTarget>::as_ref(body)
.remove_event_listener_with_callback(
kind.type_name(),
cl.as_ref().unchecked_ref(),
)
.unwrap();
}
});
}
}
/// Global multiplexing event handler registry
#[derive(Default, Debug)]
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()))
}
/// Register all passed listeners under ID
fn register(&mut self, 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());
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>>]) {
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() {
v.clear()
}
for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
let desc = EventDescriptor::from(l.deref());
self.global.ensure_handled(desc.clone());
by_desc.entry(desc).or_default().push(l);
}
}
}
/// Unregister any existing listeners for ID
fn unregister(&mut self, id: &u32) {
self.by_id.remove(id);
}
/// Set unique listener ID onto element and return it
fn set_listener_id(&mut self, 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");
}
});
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"))]
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};
wasm_bindgen_test_configure!(run_in_browser);
use crate::{html, html::TargetCast, scheduler, AppHandle, Component, Context, Html};
use gloo_utils::document;
use wasm_bindgen::JsCast;
use yew::Callback;
#[derive(Clone)]
enum Message {
Action,
StopListening,
SetText(String),
}
#[derive(Default)]
struct State {
stop_listening: bool,
action: u32,
text: String,
}
trait Mixin {
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>
}
}
}
}
struct Comp<M>
where
M: Mixin + 'static,
{
state: State,
pd: PhantomData<M>,
}
impl<M> Component for Comp<M>
where
M: Mixin + 'static,
{
type Message = Message;
type Properties = ();
fn create(_: &Context<Self>) -> Self {
Comp {
state: Default::default(),
pd: PhantomData,
}
}
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Message::Action => {
self.state.action += 1;
}
Message::StopListening => {
self.state.stop_listening = true;
}
Message::SetText(s) => {
self.state.text = s;
}
};
true
}
fn view(&self, ctx: &Context<Self>) -> crate::Html {
M::view(ctx, &self.state)
}
}
fn assert_count(el: &web_sys::HtmlElement, count: isize) {
assert_eq!(el.text_content(), Some(count.to_string()))
}
fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement {
document()
.query_selector(tag)
.unwrap()
.unwrap()
.dyn_into::<web_sys::HtmlElement>()
.unwrap()
}
fn init<M>(tag: &str) -> (AppHandle<Comp<M>>, web_sys::HtmlElement)
where
M: Mixin,
{
// 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();
}
let root = document().create_element("div").unwrap();
document().body().unwrap().append_child(&root).unwrap();
let app = crate::start_app_in_element::<Comp<M>>(root);
scheduler::start_now();
(app, get_el_by_tag(tag))
}
#[test]
fn synchronous() {
struct Synchronous;
impl Mixin for Synchronous {}
let (link, el) = init::<Synchronous>("a");
assert_count(&el, 0);
el.click();
assert_count(&el, 1);
el.click();
assert_count(&el, 2);
link.send_message(Message::StopListening);
scheduler::start_now();
el.click();
assert_count(&el, 2);
}
#[test]
async fn non_bubbling_event() {
struct NonBubbling;
impl Mixin for NonBubbling {
fn view<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message>,
{
let link = ctx.link().clone();
let onblur = Callback::from(move |_| {
link.send_message(Message::Action);
scheduler::start_now();
});
html! {
<div>
<a>
<input id="input" {onblur} type="text" />
{state.action}
</a>
</div>
}
}
}
let (_, el) = init::<NonBubbling>("a");
assert_count(&el, 0);
let input = document().get_element_by_id("input").unwrap();
input
.dispatch_event(
&Event::new_with_event_init_dict("blur", &{
let mut dict = EventInit::new();
dict.bubbles(false);
dict
})
.unwrap(),
)
.unwrap();
assert_count(&el, 1);
}
#[test]
fn bubbling() {
struct Bubbling;
impl Mixin for Bubbling {
fn view<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message>,
{
if state.stop_listening {
html! {
<div>
<a>
{state.action}
</a>
</div>
}
} else {
let link = ctx.link().clone();
let cb = Callback::from(move |_| {
link.send_message(Message::Action);
scheduler::start_now();
});
html! {
<div onclick={cb.clone()}>
<a onclick={cb}>
{state.action}
</a>
</div>
}
}
}
}
let (link, el) = init::<Bubbling>("a");
assert_count(&el, 0);
el.click();
assert_count(&el, 2);
el.click();
assert_count(&el, 4);
link.send_message(Message::StopListening);
scheduler::start_now();
el.click();
assert_count(&el, 4);
}
#[test]
fn cancel_bubbling() {
struct CancelBubbling;
impl Mixin for CancelBubbling {
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();
});
let link = ctx.link().clone();
let onclick2 = Callback::from(move |e: MouseEvent| {
e.stop_propagation();
link.send_message(Message::Action);
scheduler::start_now();
});
html! {
<div onclick={onclick}>
<a onclick={onclick2}>
{state.action}
</a>
</div>
}
}
}
let (_, el) = init::<CancelBubbling>("a");
assert_count(&el, 0);
el.click();
assert_count(&el, 1);
el.click();
assert_count(&el, 2);
}
#[test]
fn cancel_bubbling_nested() {
// 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.
struct CancelBubbling;
impl Mixin for CancelBubbling {
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();
});
let link = ctx.link().clone();
let onclick2 = Callback::from(move |e: MouseEvent| {
e.stop_propagation();
link.send_message(Message::Action);
scheduler::start_now();
});
html! {
<div onclick={onclick}>
<div onclick={onclick2}>
<a>
{state.action}
</a>
</div>
</div>
}
}
}
let (_, el) = init::<CancelBubbling>("a");
assert_count(&el, 0);
el.click();
assert_count(&el, 1);
el.click();
assert_count(&el, 2);
}
fn test_input_listener<E>(make_event: impl Fn() -> E)
where
E: JsCast + std::fmt::Debug,
{
struct Input;
impl Mixin for Input {
fn view<C>(ctx: &Context<C>, state: &State) -> Html
where
C: Component<Message = Message>,
{
if state.stop_listening {
html! {
<div>
<input type="text" />
<p>{state.text.clone()}</p>
</div>
}
} else {
let link = ctx.link().clone();
let onchange = Callback::from(move |e: web_sys::Event| {
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
link.send_message(Message::SetText(el.value()));
scheduler::start_now();
});
let link = ctx.link().clone();
let oninput = Callback::from(move |e: web_sys::InputEvent| {
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
link.send_message(Message::SetText(el.value()));
scheduler::start_now();
});
html! {
<div>
<input type="text" {onchange} {oninput} />
<p>{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");
assert_eq!(&p_el.text_content().unwrap(), "");
for mut s in ["foo", "bar", "baz"].iter() {
input_el.set_value(s);
if s == &"baz" {
link.send_message(Message::StopListening);
scheduler::start_now();
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);
}
}
#[test]
fn oninput() {
test_input_listener(|| {
web_sys::InputEvent::new_with_event_init_dict(
"input",
web_sys::InputEventInit::new().bubbles(true),
)
.unwrap()
})
}
#[test]
fn onchange() {
test_input_listener(|| {
web_sys::Event::new_with_event_init_dict(
"change",
web_sys::EventInit::new().bubbles(true),
)
.unwrap()
})
}
}

View File

@ -19,12 +19,6 @@ pub mod vtag;
#[doc(hidden)]
pub mod vtext;
use crate::html::{AnyScope, NodeRef};
use indexmap::IndexMap;
use std::borrow::Cow;
use std::{collections::HashMap, fmt, hint::unreachable_unchecked, iter};
use web_sys::{Element, Node};
#[doc(inline)]
pub use self::key::Key;
#[doc(inline)]
@ -43,9 +37,13 @@ pub use self::vsuspense::VSuspense;
pub use self::vtag::VTag;
#[doc(inline)]
pub use self::vtext::VText;
use indexmap::IndexMap;
use std::borrow::Cow;
use std::fmt::Formatter;
use std::ops::Deref;
use std::rc::Rc;
use std::{fmt, hint::unreachable_unchecked};
/// Attribute value
#[derive(Debug)]
@ -181,18 +179,6 @@ mod tests_attr_value {
}
}
/// Applies contained changes to DOM [Element]
trait Apply {
/// [Element] type to apply the changes to
type Element;
/// Apply contained values to [Element] with no ancestor
fn apply(&mut self, el: &Self::Element);
/// Apply diff between [self] and `ancestor` to [Element].
fn apply_diff(&mut self, el: &Self::Element, ancestor: Self);
}
/// A collection of attributes for an element
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum Attributes {
@ -271,194 +257,6 @@ impl Attributes {
}
}
}
#[cold]
fn apply_diff_index_maps<'a, A, B>(
el: &Element,
// this makes it possible to diff `&'a IndexMap<_, A>` and `IndexMap<_, &'a A>`.
mut new_iter: impl Iterator<Item = (&'static str, &'a str)>,
new: &IndexMap<&'static str, A>,
old: &IndexMap<&'static str, B>,
) where
A: AsRef<str>,
B: AsRef<str>,
{
let mut old_iter = old.iter();
loop {
match (new_iter.next(), old_iter.next()) {
(Some((new_key, new_value)), Some((old_key, old_value))) => {
if new_key != *old_key {
break;
}
if new_value != old_value.as_ref() {
Self::set_attribute(el, new_key, new_value);
}
}
// new attributes
(Some(attr), None) => {
for (key, value) in iter::once(attr).chain(new_iter) {
match old.get(key) {
Some(old_value) => {
if value != old_value.as_ref() {
Self::set_attribute(el, key, value);
}
}
None => {
Self::set_attribute(el, key, value);
}
}
}
break;
}
// removed attributes
(None, Some(attr)) => {
for (key, _) in iter::once(attr).chain(old_iter) {
if !new.contains_key(key) {
Self::remove_attribute(el, key);
}
}
break;
}
(None, None) => break,
}
}
}
/// Convert [Attributes] pair to [HashMap]s and patch changes to `el`.
/// Works with any [Attributes] variants.
#[cold]
fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) {
fn collect<'a>(src: &'a Attributes) -> HashMap<&'static str, &'a str> {
use Attributes::*;
match src {
Static(arr) => (*arr).iter().map(|[k, v]| (*k, *v)).collect(),
Dynamic { keys, values } => keys
.iter()
.zip(values.iter())
.filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref())))
.collect(),
IndexMap(m) => m.iter().map(|(k, v)| (*k, v.as_ref())).collect(),
}
}
let new = collect(new);
let old = collect(old);
// Update existing or set new
for (k, new) in new.iter() {
if match old.get(k) {
Some(old) => old != new,
None => true,
} {
el.set_attribute(k, new).unwrap();
}
}
// Remove missing
for k in old.keys() {
if !new.contains_key(k) {
Self::remove_attribute(el, k);
}
}
}
fn set_attribute(el: &Element, key: &str, value: &str) {
el.set_attribute(key, value).expect("invalid attribute key")
}
fn remove_attribute(el: &Element, key: &str) {
el.remove_attribute(key)
.expect("could not remove attribute")
}
}
impl Apply for Attributes {
type Element = Element;
fn apply(&mut self, el: &Element) {
match self {
Self::Static(arr) => {
for kv in arr.iter() {
Self::set_attribute(el, kv[0], kv[1]);
}
}
Self::Dynamic { keys, values } => {
for (k, v) in keys.iter().zip(values.iter()) {
if let Some(v) = v {
Self::set_attribute(el, k, v)
}
}
}
Self::IndexMap(m) => {
for (k, v) in m.iter() {
Self::set_attribute(el, k, v)
}
}
}
}
fn apply_diff(&mut self, el: &Element, ancestor: Self) {
#[inline]
fn ptr_eq<T>(a: &[T], b: &[T]) -> bool {
a.as_ptr() == b.as_ptr()
}
match (self, ancestor) {
// Hot path
(Self::Static(new), Self::Static(old)) if ptr_eq(new, old) => (),
// Hot path
(
Self::Dynamic {
keys: new_k,
values: new_v,
},
Self::Dynamic {
keys: old_k,
values: old_v,
},
) if ptr_eq(new_k, old_k) => {
// Double zipping does not optimize well, so use asserts and unsafe instead
assert!(new_k.len() == new_v.len());
assert!(new_k.len() == old_v.len());
for i in 0..new_k.len() {
macro_rules! key {
() => {
unsafe { new_k.get_unchecked(i) }
};
}
macro_rules! set {
($new:expr) => {
Self::set_attribute(el, key!(), $new)
};
}
match unsafe { (new_v.get_unchecked(i), old_v.get_unchecked(i)) } {
(Some(new), Some(old)) => {
if new != old {
set!(new);
}
}
(Some(new), None) => set!(new),
(None, Some(_)) => {
Self::remove_attribute(el, key!());
}
(None, None) => (),
}
}
}
// For VTag's constructed outside the html! macro
(Self::IndexMap(new), Self::IndexMap(old)) => {
let new_iter = new.iter().map(|(k, v)| (*k, v.as_ref()));
Self::apply_diff_index_maps(el, new_iter, new, &old);
}
// Cold path. Happens only with conditional swapping and reordering of `VTag`s with the
// same tag and no keys.
(new, ancestor) => {
Self::apply_diff_as_maps(el, new, &ancestor);
}
}
}
}
impl From<IndexMap<&'static str, AttrValue>> for Attributes {
@ -473,66 +271,6 @@ impl Default for Attributes {
}
}
// TODO(#938): What about implementing `VDiff` for `Element`?
// It would make it possible to include ANY element into the tree.
// `Ace` editor embedding for example?
/// This trait provides features to update a tree by calculating a difference against another tree.
pub(crate) trait VDiff {
/// Remove self from parent.
///
/// Parent to detach is `true` if the parent element will also be detached.
fn detach(&mut self, parent: &Element, parent_to_detach: bool);
/// Move elements from one parent to another parent.
/// This is currently only used by `VSuspense` to preserve component state without detaching
/// (which destroys component state).
/// Prefer `detach` then apply if possible.
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef);
/// Scoped diff apply to other tree.
///
/// Virtual rendering for the node. It uses parent node and existing
/// children (virtual and DOM) to check the difference and apply patches to
/// the actual DOM representation.
///
/// Parameters:
/// - `parent_scope`: the parent `Scope` used for passing messages to the
/// parent `Component`.
/// - `parent`: the parent node in the DOM.
/// - `next_sibling`: the next sibling, used to efficiently find where to
/// put the node.
/// - `ancestor`: the node that this node will be replacing in the DOM. This
/// method will _always_ remove the `ancestor` from the `parent`.
///
/// Returns a reference to the newly inserted element.
///
/// ### Internal Behavior Notice:
///
/// Note that these modify the DOM by modifying the reference that _already_
/// exists on the `ancestor`. If `self.reference` exists (which it
/// _shouldn't_) this method will panic.
///
/// The exception to this is obviously `VRef` which simply uses the inner
/// `Node` directly (always removes the `Node` that exists).
fn apply(
&mut self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
ancestor: Option<VNode>,
) -> NodeRef;
}
pub(crate) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) {
match next_sibling {
Some(next_sibling) => parent
.insert_before(node, Some(next_sibling))
.expect("failed to insert tag before next sibling"),
None => parent.append_child(node).expect("failed to append child"),
};
}
#[cfg(all(test, feature = "wasm_bench"))]
mod benchmarks {
use super::*;

View File

@ -1,77 +1,41 @@
//! This module contains the implementation of a virtual component (`VComp`).
use super::{Key, VDiff, VNode};
use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped};
#[cfg(feature = "ssr")]
use futures::future::{FutureExt, LocalBoxFuture};
use super::Key;
use crate::dom_bundle::{Mountable, PropsWrapper};
use crate::html::{BaseComponent, NodeRef};
use std::any::TypeId;
use std::borrow::Borrow;
use std::fmt;
use std::ops::Deref;
use std::rc::Rc;
#[cfg(debug_assertions)]
use std::sync::atomic::{AtomicUsize, Ordering};
use web_sys::Element;
#[cfg(debug_assertions)]
thread_local! {
static EVENT_HISTORY: std::cell::RefCell<std::collections::HashMap<usize, Vec<String>>>
= Default::default();
static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
}
/// Push [VComp] event to lifecycle debugging registry
#[cfg(debug_assertions)]
pub(crate) fn log_event(vcomp_id: usize, event: impl ToString) {
EVENT_HISTORY.with(|h| {
h.borrow_mut()
.entry(vcomp_id)
.or_default()
.push(event.to_string())
});
}
/// Get [VComp] event log from lifecycle debugging registry
#[cfg(debug_assertions)]
pub(crate) fn get_event_log(vcomp_id: usize) -> Vec<String> {
EVENT_HISTORY.with(|h| {
h.borrow()
.get(&vcomp_id)
.map(|l| (*l).clone())
.unwrap_or_default()
})
}
thread_local! {}
/// A virtual component.
pub struct VComp {
type_id: TypeId,
scope: Option<Box<dyn Scoped>>,
mountable: Option<Box<dyn Mountable>>,
pub(crate) type_id: TypeId,
pub(crate) mountable: Box<dyn Mountable>,
pub(crate) node_ref: NodeRef,
pub(crate) key: Option<Key>,
}
#[cfg(debug_assertions)]
pub(crate) id: usize,
impl fmt::Debug for VComp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VComp")
.field("type_id", &self.type_id)
.field("node_ref", &self.node_ref)
.field("mountable", &"..")
.field("key", &self.key)
.finish()
}
}
impl Clone for VComp {
fn clone(&self) -> Self {
if self.scope.is_some() {
panic!("Mounted components are not allowed to be cloned!");
}
#[cfg(debug_assertions)]
log_event(self.id, "clone");
Self {
type_id: self.type_id,
scope: None,
mountable: self.mountable.as_ref().map(|m| m.copy()),
mountable: self.mountable.copy(),
node_ref: self.node_ref.clone(),
key: self.key.clone(),
#[cfg(debug_assertions)]
id: self.id,
}
}
}
@ -136,155 +100,10 @@ impl VComp {
VComp {
type_id: TypeId::of::<COMP>(),
node_ref,
mountable: Some(Box::new(PropsWrapper::<COMP>::new(props))),
scope: None,
mountable: Box::new(PropsWrapper::<COMP>::new(props)),
key,
#[cfg(debug_assertions)]
id: Self::next_id(),
}
}
pub(crate) fn root_vnode(&self) -> Option<impl Deref<Target = VNode> + '_> {
self.scope.as_ref().and_then(|scope| scope.root_vnode())
}
/// Take ownership of [Box<dyn Scoped>] or panic with error message, if component is not mounted
#[inline]
fn take_scope(&mut self) -> Box<dyn Scoped> {
self.scope.take().unwrap_or_else(|| {
#[cfg(not(debug_assertions))]
panic!("no scope; VComp should be mounted");
#[cfg(debug_assertions)]
panic!(
"no scope; VComp should be mounted after: {:?}",
get_event_log(self.id)
);
})
}
#[cfg(debug_assertions)]
pub(crate) fn next_id() -> usize {
COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed))
}
}
trait Mountable {
fn copy(&self) -> Box<dyn Mountable>;
fn mount(
self: Box<Self>,
node_ref: NodeRef,
parent_scope: &AnyScope,
parent: Element,
next_sibling: NodeRef,
) -> Box<dyn Scoped>;
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef);
#[cfg(feature = "ssr")]
fn render_to_string<'a>(
&'a self,
w: &'a mut String,
parent_scope: &'a AnyScope,
) -> LocalBoxFuture<'a, ()>;
}
struct PropsWrapper<COMP: BaseComponent> {
props: Rc<COMP::Properties>,
}
impl<COMP: BaseComponent> PropsWrapper<COMP> {
fn new(props: Rc<COMP::Properties>) -> Self {
Self { props }
}
}
impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
fn copy(&self) -> Box<dyn Mountable> {
let wrapper: PropsWrapper<COMP> = PropsWrapper {
props: Rc::clone(&self.props),
};
Box::new(wrapper)
}
fn mount(
self: Box<Self>,
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);
Box::new(scope)
}
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) {
let scope: Scope<COMP> = scope.to_any().downcast();
scope.reuse(self.props, node_ref, next_sibling);
}
#[cfg(feature = "ssr")]
fn render_to_string<'a>(
&'a self,
w: &'a mut String,
parent_scope: &'a AnyScope,
) -> LocalBoxFuture<'a, ()> {
async move {
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
scope.render_to_string(w, self.props.clone()).await;
}
.boxed_local()
}
}
impl VDiff for VComp {
fn detach(&mut self, _parent: &Element, parent_to_detach: bool) {
self.take_scope().destroy(parent_to_detach);
}
fn shift(&self, _previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
let scope = self.scope.as_ref().unwrap();
scope.shift_node(next_parent.clone(), next_sibling);
}
fn apply(
&mut self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
ancestor: Option<VNode>,
) -> NodeRef {
let mountable = self
.mountable
.take()
.expect("VComp has already been mounted");
if let Some(mut ancestor) = ancestor {
if let VNode::VComp(ref mut vcomp) = &mut ancestor {
// If the ancestor is the same type, reuse it and update its properties
if self.type_id == vcomp.type_id && self.key == vcomp.key {
self.node_ref.reuse(vcomp.node_ref.clone());
let scope = vcomp.take_scope();
mountable.reuse(self.node_ref.clone(), scope.borrow(), next_sibling);
self.scope = Some(scope);
return vcomp.node_ref.clone();
}
}
ancestor.detach(parent, false);
}
self.scope = Some(mountable.mount(
self.node_ref.clone(),
parent_scope,
parent.to_owned(),
next_sibling,
));
self.node_ref.clone()
}
}
impl PartialEq for VComp {
@ -293,12 +112,6 @@ impl PartialEq for VComp {
}
}
impl fmt::Debug for VComp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "VComp {{ root: {:?} }}", self.root_vnode().as_deref())
}
}
impl<COMP: BaseComponent> fmt::Debug for VChild<COMP> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("VChild<_>")
@ -308,608 +121,18 @@ impl<COMP: BaseComponent> fmt::Debug for VChild<COMP> {
#[cfg(feature = "ssr")]
mod feat_ssr {
use super::*;
use crate::html::AnyScope;
impl VComp {
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
self.mountable
.as_ref()
.map(|m| m.copy())
.unwrap()
.render_to_string(w, parent_scope)
.await;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scheduler;
use crate::{html, Children, Component, Context, Html, NodeRef, Properties};
use gloo_utils::document;
use web_sys::Node;
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
struct Comp;
#[derive(Clone, PartialEq, Properties)]
struct Props {
#[prop_or_default]
field_1: u32,
#[prop_or_default]
field_2: u32,
}
impl Component for Comp {
type Message = ();
type Properties = Props;
fn create(_: &Context<Self>) -> Self {
Comp
}
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
unimplemented!();
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! { <div/> }
}
}
#[test]
fn update_loop() {
let document = gloo_utils::document();
let parent_scope: AnyScope = crate::html::Scope::<Comp>::new(None).into();
let parent_element = document.create_element("div").unwrap();
let mut ancestor = html! { <Comp></Comp> };
ancestor.apply(&parent_scope, &parent_element, NodeRef::default(), None);
scheduler::start_now();
for _ in 0..10000 {
let mut node = html! { <Comp></Comp> };
node.apply(
&parent_scope,
&parent_element,
NodeRef::default(),
Some(ancestor),
);
scheduler::start_now();
ancestor = node;
}
}
#[test]
fn set_properties_to_component() {
html! {
<Comp />
};
html! {
<Comp field_1=1 />
};
html! {
<Comp field_2=2 />
};
html! {
<Comp field_1=1 field_2=2 />
};
let props = Props {
field_1: 1,
field_2: 1,
};
html! {
<Comp ..props />
};
}
#[test]
fn set_component_key() {
let test_key: Key = "test".to_string().into();
let check_key = |vnode: VNode| {
assert_eq!(vnode.key().as_ref(), Some(&test_key));
};
let props = Props {
field_1: 1,
field_2: 1,
};
let props_2 = props.clone();
check_key(html! { <Comp key={test_key.clone()} /> });
check_key(html! { <Comp key={test_key.clone()} field_1=1 /> });
check_key(html! { <Comp field_1=1 key={test_key.clone()} /> });
check_key(html! { <Comp key={test_key.clone()} ..props /> });
check_key(html! { <Comp key={test_key.clone()} ..props_2 /> });
}
#[test]
fn set_component_node_ref() {
let test_node: Node = document().create_text_node("test").into();
let test_node_ref = NodeRef::new(test_node);
let check_node_ref = |vnode: VNode| {
assert_eq!(vnode.unchecked_first_node(), test_node_ref.get().unwrap());
};
let props = Props {
field_1: 1,
field_2: 1,
};
let props_2 = props.clone();
check_node_ref(html! { <Comp ref={test_node_ref.clone()} /> });
check_node_ref(html! { <Comp ref={test_node_ref.clone()} field_1=1 /> });
check_node_ref(html! { <Comp field_1=1 ref={test_node_ref.clone()} /> });
check_node_ref(html! { <Comp ref={test_node_ref.clone()} ..props /> });
check_node_ref(html! { <Comp ref={test_node_ref.clone()} ..props_2 /> });
}
#[test]
fn vchild_partialeq() {
let vchild1: VChild<Comp> = VChild::new(
Props {
field_1: 1,
field_2: 1,
},
NodeRef::default(),
None,
);
let vchild2: VChild<Comp> = VChild::new(
Props {
field_1: 1,
field_2: 1,
},
NodeRef::default(),
None,
);
let vchild3: VChild<Comp> = VChild::new(
Props {
field_1: 2,
field_2: 2,
},
NodeRef::default(),
None,
);
assert_eq!(vchild1, vchild2);
assert_ne!(vchild1, vchild3);
assert_ne!(vchild2, vchild3);
}
#[derive(Clone, Properties, PartialEq)]
pub struct ListProps {
pub children: Children,
}
pub struct List;
impl Component for List {
type Message = ();
type Properties = ListProps;
fn create(_: &Context<Self>) -> Self {
Self
}
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
unimplemented!();
}
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
unimplemented!();
}
fn view(&self, ctx: &Context<Self>) -> Html {
let item_iter = ctx
.props()
.children
.iter()
.map(|item| html! {<li>{ item }</li>});
html! {
<ul>{ for item_iter }</ul>
}
}
}
use super::{AnyScope, Element};
fn setup_parent() -> (AnyScope, Element) {
let scope = AnyScope::test();
let parent = document().create_element("div").unwrap();
document().body().unwrap().append_child(&parent).unwrap();
(scope, parent)
}
fn get_html(mut node: Html, scope: &AnyScope, parent: &Element) -> String {
// clear parent
parent.set_inner_html("");
node.apply(scope, parent, NodeRef::default(), None);
scheduler::start_now();
parent.inner_html()
}
#[test]
fn all_ways_of_passing_children_work() {
let (scope, parent) = setup_parent();
let children: Vec<_> = vec!["a", "b", "c"]
.drain(..)
.map(|text| html! {<span>{ text }</span>})
.collect();
let children_renderer = Children::new(children.clone());
let expected_html = "\
<ul>\
<li><span>a</span></li>\
<li><span>b</span></li>\
<li><span>c</span></li>\
</ul>";
let prop_method = html! {
<List children={children_renderer.clone()} />
};
assert_eq!(get_html(prop_method, &scope, &parent), expected_html);
let children_renderer_method = html! {
<List>
{ children_renderer }
</List>
};
assert_eq!(
get_html(children_renderer_method, &scope, &parent),
expected_html
);
let direct_method = html! {
<List>
{ children.clone() }
</List>
};
assert_eq!(get_html(direct_method, &scope, &parent), expected_html);
let for_method = html! {
<List>
{ for children }
</List>
};
assert_eq!(get_html(for_method, &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 node_ref = NodeRef::default();
let mut elem: VNode = html! { <Comp ref={node_ref.clone()}></Comp> };
elem.apply(&scope, &parent, NodeRef::default(), None);
scheduler::start_now();
let parent_node = parent.deref();
assert_eq!(node_ref.get(), parent_node.first_child());
elem.detach(&parent, false);
scheduler::start_now();
assert!(node_ref.get().is_none());
}
}
#[cfg(test)]
mod layout_tests {
extern crate self as yew;
use crate::html;
use crate::tests::layout_tests::{diff_layouts, TestLayout};
use crate::{Children, Component, Context, Html, Properties};
use std::marker::PhantomData;
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
wasm_bindgen_test_configure!(run_in_browser);
struct Comp<T> {
_marker: PhantomData<T>,
}
#[derive(Properties, Clone, PartialEq)]
struct CompProps {
#[prop_or_default]
children: Children,
}
impl<T: 'static> Component for Comp<T> {
type Message = ();
type Properties = CompProps;
fn create(_: &Context<Self>) -> Self {
Comp {
_marker: PhantomData::default(),
}
}
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
unimplemented!();
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>{ ctx.props().children.clone() }</>
}
}
}
struct A;
struct B;
#[test]
fn diff() {
let layout1 = TestLayout {
name: "1",
node: html! {
<Comp<A>>
<Comp<B>></Comp<B>>
{"C"}
</Comp<A>>
},
expected: "C",
};
let layout2 = TestLayout {
name: "2",
node: html! {
<Comp<A>>
{"A"}
</Comp<A>>
},
expected: "A",
};
let layout3 = TestLayout {
name: "3",
node: html! {
<Comp<B>>
<Comp<A>></Comp<A>>
{"B"}
</Comp<B>>
},
expected: "B",
};
let layout4 = TestLayout {
name: "4",
node: html! {
<Comp<B>>
<Comp<A>>{"A"}</Comp<A>>
{"B"}
</Comp<B>>
},
expected: "AB",
};
let layout5 = TestLayout {
name: "5",
node: html! {
<Comp<B>>
<>
<Comp<A>>
{"A"}
</Comp<A>>
</>
{"B"}
</Comp<B>>
},
expected: "AB",
};
let layout6 = TestLayout {
name: "6",
node: html! {
<Comp<B>>
<>
<Comp<A>>
{"A"}
</Comp<A>>
{"B"}
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout7 = TestLayout {
name: "7",
node: html! {
<Comp<B>>
<>
<Comp<A>>
{"A"}
</Comp<A>>
<Comp<A>>
{"B"}
</Comp<A>>
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout8 = TestLayout {
name: "8",
node: html! {
<Comp<B>>
<>
<Comp<A>>
{"A"}
</Comp<A>>
<Comp<A>>
<Comp<A>>
{"B"}
</Comp<A>>
</Comp<A>>
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout9 = TestLayout {
name: "9",
node: html! {
<Comp<B>>
<>
<>
{"A"}
</>
<Comp<A>>
<Comp<A>>
{"B"}
</Comp<A>>
</Comp<A>>
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout10 = TestLayout {
name: "10",
node: html! {
<Comp<B>>
<>
<Comp<A>>
<Comp<A>>
{"A"}
</Comp<A>>
</Comp<A>>
<>
{"B"}
</>
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout11 = TestLayout {
name: "11",
node: html! {
<Comp<B>>
<>
<>
<Comp<A>>
<Comp<A>>
{"A"}
</Comp<A>>
{"B"}
</Comp<A>>
</>
</>
{"C"}
</Comp<B>>
},
expected: "ABC",
};
let layout12 = TestLayout {
name: "12",
node: html! {
<Comp<B>>
<>
<Comp<A>></Comp<A>>
<>
<Comp<A>>
<>
<Comp<A>>
{"A"}
</Comp<A>>
<></>
<Comp<A>>
<Comp<A>></Comp<A>>
<></>
{"B"}
<></>
<Comp<A>></Comp<A>>
</Comp<A>>
</>
</Comp<A>>
<></>
</>
<Comp<A>></Comp<A>>
</>
{"C"}
<Comp<A>></Comp<A>>
<></>
</Comp<B>>
},
expected: "ABC",
};
diff_layouts(vec![
layout1, layout2, layout3, layout4, layout5, layout6, layout7, layout8, layout9,
layout10, layout11, layout12,
]);
}
#[test]
fn component_with_children() {
#[derive(Properties, PartialEq)]
struct Props {
children: Children,
}
struct ComponentWithChildren;
impl Component for ComponentWithChildren {
type Message = ();
type Properties = Props;
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<ul>
{ for ctx.props().children.iter().map(|child| html! { <li>{ child }</li> }) }
</ul>
}
}
}
let layout = TestLayout {
name: "13",
node: html! {
<ComponentWithChildren>
if true {
<span>{ "hello" }</span>
<span>{ "world" }</span>
} else {
<span>{ "goodbye" }</span>
<span>{ "world" }</span>
}
</ComponentWithChildren>
},
expected: "<ul><li><span>hello</span><span>world</span></li></ul>",
};
diff_layouts(vec![layout]);
}
}
#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))]
mod ssr_tests {
use tokio::test;

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,11 @@
//! This module contains the implementation of abstract virtual node.
use super::{Key, VChild, VComp, VDiff, VList, VPortal, VSuspense, VTag, VText};
use crate::html::{AnyScope, BaseComponent, NodeRef};
use gloo::console;
use super::{Key, VChild, VComp, VList, VPortal, VSuspense, VTag, VText};
use crate::html::BaseComponent;
use std::cmp::PartialEq;
use std::fmt;
use std::iter::FromIterator;
use wasm_bindgen::JsCast;
use web_sys::{Element, Node};
use web_sys::Node;
/// Bind virtual element to a DOM reference.
#[derive(Clone)]
@ -30,177 +27,21 @@ pub enum VNode {
}
impl VNode {
pub fn key(&self) -> Option<Key> {
pub fn key(&self) -> Option<&Key> {
match self {
VNode::VComp(vcomp) => vcomp.key.clone(),
VNode::VList(vlist) => vlist.key.clone(),
VNode::VComp(vcomp) => vcomp.key.as_ref(),
VNode::VList(vlist) => vlist.key.as_ref(),
VNode::VRef(_) => None,
VNode::VTag(vtag) => vtag.key.clone(),
VNode::VTag(vtag) => vtag.key.as_ref(),
VNode::VText(_) => None,
VNode::VPortal(vportal) => vportal.node.key(),
VNode::VSuspense(vsuspense) => vsuspense.key.clone(),
VNode::VSuspense(vsuspense) => vsuspense.key.as_ref(),
}
}
/// Returns true if the [VNode] has a key without needlessly cloning the key.
/// Returns true if the [VNode] has a key.
pub fn has_key(&self) -> bool {
match self {
VNode::VComp(vcomp) => vcomp.key.is_some(),
VNode::VList(vlist) => vlist.key.is_some(),
VNode::VRef(_) | VNode::VText(_) => false,
VNode::VTag(vtag) => vtag.key.is_some(),
VNode::VPortal(vportal) => vportal.node.has_key(),
VNode::VSuspense(vsuspense) => vsuspense.key.is_some(),
}
}
/// Returns the first DOM node if available
pub(crate) fn first_node(&self) -> Option<Node> {
match self {
VNode::VTag(vtag) => vtag.reference().cloned().map(JsCast::unchecked_into),
VNode::VText(vtext) => vtext
.reference
.as_ref()
.cloned()
.map(JsCast::unchecked_into),
VNode::VComp(vcomp) => vcomp.node_ref.get(),
VNode::VList(vlist) => vlist.get(0).and_then(VNode::first_node),
VNode::VRef(node) => Some(node.clone()),
VNode::VPortal(vportal) => vportal.next_sibling(),
VNode::VSuspense(vsuspense) => vsuspense.first_node(),
}
}
/// Returns the first DOM node that is used to designate the position of the virtual DOM node.
pub(crate) fn unchecked_first_node(&self) -> Node {
match self {
VNode::VTag(vtag) => vtag
.reference()
.expect("VTag is not mounted")
.clone()
.into(),
VNode::VText(vtext) => {
let text_node = vtext.reference.as_ref().expect("VText is not mounted");
text_node.clone().into()
}
VNode::VComp(vcomp) => vcomp.node_ref.get().unwrap_or_else(|| {
#[cfg(not(debug_assertions))]
panic!("no node_ref; VComp should be mounted");
#[cfg(debug_assertions)]
panic!(
"no node_ref; VComp should be mounted after: {:?}",
crate::virtual_dom::vcomp::get_event_log(vcomp.id),
);
}),
VNode::VList(vlist) => vlist
.get(0)
.expect("VList is not mounted")
.unchecked_first_node(),
VNode::VRef(node) => node.clone(),
VNode::VPortal(_) => panic!("portals have no first node, they are empty inside"),
VNode::VSuspense(vsuspense) => {
vsuspense.first_node().expect("VSuspense is not mounted")
}
}
}
pub(crate) fn move_before(&self, parent: &Element, next_sibling: &Option<Node>) {
match self {
VNode::VList(vlist) => {
for node in vlist.iter() {
node.move_before(parent, next_sibling);
}
}
VNode::VComp(vcomp) => {
vcomp
.root_vnode()
.expect("VComp has no root vnode")
.move_before(parent, next_sibling);
}
VNode::VPortal(_) => {} // no need to move portals
_ => super::insert_node(&self.unchecked_first_node(), parent, next_sibling.as_ref()),
};
}
}
impl VDiff for VNode {
/// Remove VNode from parent.
fn detach(&mut self, parent: &Element, parent_to_detach: bool) {
match *self {
VNode::VTag(ref mut vtag) => vtag.detach(parent, parent_to_detach),
VNode::VText(ref mut vtext) => vtext.detach(parent, parent_to_detach),
VNode::VComp(ref mut vcomp) => vcomp.detach(parent, parent_to_detach),
VNode::VList(ref mut vlist) => vlist.detach(parent, parent_to_detach),
VNode::VRef(ref node) => {
if parent.remove_child(node).is_err() {
console::warn!("Node not found to remove VRef");
}
}
VNode::VPortal(ref mut vportal) => vportal.detach(parent, parent_to_detach),
VNode::VSuspense(ref mut vsuspense) => vsuspense.detach(parent, parent_to_detach),
}
}
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
match *self {
VNode::VTag(ref vtag) => vtag.shift(previous_parent, next_parent, next_sibling),
VNode::VText(ref vtext) => vtext.shift(previous_parent, next_parent, next_sibling),
VNode::VComp(ref vcomp) => vcomp.shift(previous_parent, next_parent, next_sibling),
VNode::VList(ref vlist) => vlist.shift(previous_parent, next_parent, next_sibling),
VNode::VRef(ref node) => {
previous_parent.remove_child(node).unwrap();
next_parent
.insert_before(node, next_sibling.get().as_ref())
.unwrap();
}
VNode::VPortal(ref vportal) => {
vportal.shift(previous_parent, next_parent, next_sibling)
}
VNode::VSuspense(ref vsuspense) => {
vsuspense.shift(previous_parent, next_parent, next_sibling)
}
}
}
fn apply(
&mut self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
ancestor: Option<VNode>,
) -> NodeRef {
match *self {
VNode::VTag(ref mut vtag) => vtag.apply(parent_scope, parent, next_sibling, ancestor),
VNode::VText(ref mut vtext) => {
vtext.apply(parent_scope, parent, next_sibling, ancestor)
}
VNode::VComp(ref mut vcomp) => {
vcomp.apply(parent_scope, parent, next_sibling, ancestor)
}
VNode::VList(ref mut vlist) => {
vlist.apply(parent_scope, parent, next_sibling, ancestor)
}
VNode::VRef(ref mut node) => {
if let Some(mut ancestor) = ancestor {
// We always remove VRef in case it's meant to be used somewhere else.
if let VNode::VRef(n) = &ancestor {
if node == n {
return NodeRef::new(node.clone());
}
}
ancestor.detach(parent, false);
}
super::insert_node(node, parent, next_sibling.get().as_ref());
NodeRef::new(node.clone())
}
VNode::VPortal(ref mut vportal) => {
vportal.apply(parent_scope, parent, next_sibling, ancestor)
}
VNode::VSuspense(ref mut vsuspense) => {
vsuspense.apply(parent_scope, parent, next_sibling, ancestor)
}
}
self.key().is_some()
}
}
@ -245,6 +86,13 @@ impl From<VSuspense> for VNode {
}
}
impl From<VPortal> for VNode {
#[inline]
fn from(vportal: VPortal) -> Self {
VNode::VPortal(vportal)
}
}
impl<COMP> From<VChild<COMP>> for VNode
where
COMP: BaseComponent,
@ -299,9 +147,9 @@ impl PartialEq for VNode {
#[cfg(feature = "ssr")]
mod feat_ssr {
use futures::future::{FutureExt, LocalBoxFuture};
use super::*;
use crate::html::AnyScope;
use futures::future::{FutureExt, LocalBoxFuture};
impl VNode {
// Boxing is needed here, due to: https://rust-lang.github.io/async-book/07_workarounds/04_recursion.html
@ -335,36 +183,3 @@ mod feat_ssr {
}
}
}
#[cfg(test)]
mod layout_tests {
use super::*;
use crate::tests::layout_tests::{diff_layouts, TestLayout};
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
#[test]
fn diff() {
let document = gloo_utils::document();
let vref_node_1 = VNode::VRef(document.create_element("i").unwrap().into());
let vref_node_2 = VNode::VRef(document.create_element("b").unwrap().into());
let layout1 = TestLayout {
name: "1",
node: vref_node_1,
expected: "<i></i>",
};
let layout2 = TestLayout {
name: "2",
node: vref_node_2,
expected: "<b></b>",
};
diff_layouts(vec![layout1, layout2]);
}
}

View File

@ -1,74 +1,17 @@
//! This module contains the implementation of a portal `VPortal`.
use super::{VDiff, VNode};
use crate::html::{AnyScope, NodeRef};
use super::VNode;
use crate::html::NodeRef;
use web_sys::{Element, Node};
#[derive(Debug, Clone)]
pub struct VPortal {
/// The element under which the content is inserted.
pub host: Element,
/// The next sibling after the inserted content
pub next_sibling: NodeRef,
/// The next sibling after the inserted content. Most be a child of `host`.
pub inner_sibling: NodeRef,
/// The inserted node
pub node: Box<VNode>,
/// The next sibling after the portal. Set when rendered
sibling_ref: NodeRef,
}
impl VDiff for VPortal {
fn detach(&mut self, _: &Element, _parent_to_detach: bool) {
self.node.detach(&self.host, false);
self.sibling_ref.set(None);
}
fn shift(&self, _previous_parent: &Element, _next_parent: &Element, _next_sibling: NodeRef) {
// portals have nothing in it's original place of DOM, we also do nothing.
}
fn apply(
&mut self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
ancestor: Option<VNode>,
) -> NodeRef {
let inner_ancestor = match ancestor {
Some(VNode::VPortal(old_portal)) => {
let VPortal {
host: old_host,
next_sibling: old_sibling,
mut node,
..
} = old_portal;
if old_host != self.host {
// Remount the inner node somewhere else instead of diffing
node.detach(&old_host, false);
None
} else if old_sibling != self.next_sibling {
// Move the node, but keep the state
node.move_before(&self.host, &self.next_sibling.get());
Some(*node)
} else {
Some(*node)
}
}
Some(mut node) => {
node.detach(parent, false);
None
}
None => None,
};
self.node.apply(
parent_scope,
&self.host,
self.next_sibling.clone(),
inner_ancestor,
);
self.sibling_ref = next_sibling.clone();
next_sibling
}
}
impl VPortal {
@ -76,114 +19,22 @@ impl VPortal {
pub fn new(content: VNode, host: Element) -> Self {
Self {
host,
next_sibling: NodeRef::default(),
inner_sibling: NodeRef::default(),
node: Box::new(content),
sibling_ref: NodeRef::default(),
}
}
/// Creates a [VPortal] rendering `content` in the DOM hierarchy under `host`.
/// If `next_sibling` is given, the content is inserted before that [Node].
/// The parent of `next_sibling`, if given, must be `host`.
pub fn new_before(content: VNode, host: Element, next_sibling: Option<Node>) -> Self {
pub fn new_before(content: VNode, host: Element, inner_sibling: Option<Node>) -> Self {
Self {
host,
next_sibling: {
inner_sibling: {
let sib_ref = NodeRef::default();
sib_ref.set(next_sibling);
sib_ref.set(inner_sibling);
sib_ref
},
node: Box::new(content),
sibling_ref: NodeRef::default(),
}
}
/// Returns the [Node] following this [VPortal], if this [VPortal]
/// has already been mounted in the DOM.
pub fn next_sibling(&self) -> Option<Node> {
self.sibling_ref.get()
}
}
#[cfg(test)]
mod layout_tests {
extern crate self as yew;
use crate::html;
use crate::tests::layout_tests::{diff_layouts, TestLayout};
use crate::virtual_dom::VNode;
use yew::virtual_dom::VPortal;
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
#[test]
fn diff() {
let mut layouts = vec![];
let first_target = gloo_utils::document().create_element("i").unwrap();
let second_target = gloo_utils::document().create_element("o").unwrap();
let target_with_child = gloo_utils::document().create_element("i").unwrap();
let target_child = gloo_utils::document().create_element("s").unwrap();
target_with_child.append_child(&target_child).unwrap();
layouts.push(TestLayout {
name: "Portal - first target",
node: html! {
<div>
{VNode::VRef(first_target.clone().into())}
{VNode::VRef(second_target.clone().into())}
{VNode::VPortal(VPortal::new(
html! { {"PORTAL"} },
first_target.clone(),
))}
{"AFTER"}
</div>
},
expected: "<div><i>PORTAL</i><o></o>AFTER</div>",
});
layouts.push(TestLayout {
name: "Portal - second target",
node: html! {
<div>
{VNode::VRef(first_target.clone().into())}
{VNode::VRef(second_target.clone().into())}
{VNode::VPortal(VPortal::new(
html! { {"PORTAL"} },
second_target.clone(),
))}
{"AFTER"}
</div>
},
expected: "<div><i></i><o>PORTAL</o>AFTER</div>",
});
layouts.push(TestLayout {
name: "Portal - replaced by text",
node: html! {
<div>
{VNode::VRef(first_target.clone().into())}
{VNode::VRef(second_target.clone().into())}
{"FOO"}
{"AFTER"}
</div>
},
expected: "<div><i></i><o></o>FOOAFTER</div>",
});
layouts.push(TestLayout {
name: "Portal - next sibling",
node: html! {
<div>
{VNode::VRef(target_with_child.clone().into())}
{VNode::VPortal(VPortal::new_before(
html! { {"PORTAL"} },
target_with_child.clone(),
Some(target_child.clone().into()),
))}
</div>
},
expected: "<div><i>PORTAL<s></s></i></div>",
});
diff_layouts(layouts)
}
}

View File

@ -1,22 +1,17 @@
use super::{Key, VDiff, VNode};
use crate::html::{AnyScope, NodeRef};
use web_sys::{Element, Node};
use super::{Key, VNode};
use web_sys::Element;
/// This struct represents a suspendable DOM fragment.
#[derive(Clone, Debug, PartialEq)]
pub struct VSuspense {
/// Child nodes.
children: Box<VNode>,
pub(crate) children: Box<VNode>,
/// Fallback nodes when suspended.
fallback: Box<VNode>,
pub(crate) fallback: Box<VNode>,
/// The element to attach to when children is not attached to DOM
detached_parent: Option<Element>,
pub(crate) detached_parent: Option<Element>,
/// Whether the current status is suspended.
suspended: bool,
pub(crate) suspended: bool,
/// The Key.
pub(crate) key: Option<Key>,
}
@ -37,122 +32,12 @@ impl VSuspense {
key,
}
}
pub(crate) fn first_node(&self) -> Option<Node> {
if self.suspended {
self.fallback.first_node()
} else {
self.children.first_node()
}
}
}
impl VDiff for VSuspense {
fn detach(&mut self, parent: &Element, parent_to_detach: bool) {
if self.suspended {
self.fallback.detach(parent, parent_to_detach);
if let Some(ref m) = self.detached_parent {
self.children.detach(m, false);
}
} else {
self.children.detach(parent, parent_to_detach);
}
}
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
if self.suspended {
self.fallback
.shift(previous_parent, next_parent, next_sibling);
} else {
self.children
.shift(previous_parent, next_parent, next_sibling);
}
}
fn apply(
&mut self,
parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
ancestor: Option<VNode>,
) -> NodeRef {
let detached_parent = self.detached_parent.as_ref().expect("no detached parent?");
let (already_suspended, children_ancestor, fallback_ancestor) = match ancestor {
Some(VNode::VSuspense(mut m)) => {
// We only preserve the child state if they are the same suspense.
if m.key != self.key || self.detached_parent != m.detached_parent {
m.detach(parent, false);
(false, None, None)
} else {
(m.suspended, Some(*m.children), Some(*m.fallback))
}
}
Some(mut m) => {
m.detach(parent, false);
(false, None, None)
}
None => (false, None, None),
};
// When it's suspended, we render children into an element that is detached from the dom
// tree while rendering fallback UI into the original place where children resides in.
match (self.suspended, already_suspended) {
(true, true) => {
self.children.apply(
parent_scope,
detached_parent,
NodeRef::default(),
children_ancestor,
);
self.fallback
.apply(parent_scope, parent, next_sibling, fallback_ancestor)
}
(false, false) => {
self.children
.apply(parent_scope, parent, next_sibling, children_ancestor)
}
(true, false) => {
children_ancestor.as_ref().unwrap().shift(
parent,
detached_parent,
NodeRef::default(),
);
self.children.apply(
parent_scope,
detached_parent,
NodeRef::default(),
children_ancestor,
);
// first render of fallback, ancestor needs to be None.
self.fallback
.apply(parent_scope, parent, next_sibling, None)
}
(false, true) => {
fallback_ancestor.unwrap().detach(parent, false);
children_ancestor.as_ref().unwrap().shift(
detached_parent,
parent,
next_sibling.clone(),
);
self.children
.apply(parent_scope, parent, next_sibling, children_ancestor)
}
}
}
}
#[cfg(feature = "ssr")]
mod feat_ssr {
use super::*;
use crate::html::AnyScope;
impl VSuspense {
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,7 @@
//! This module contains the implementation of a virtual text node `VText`.
use super::{AttrValue, VDiff, VNode};
use crate::html::{AnyScope, NodeRef};
use gloo::console;
use gloo_utils::document;
use super::AttrValue;
use std::cmp::PartialEq;
use web_sys::{Element, Text as TextNode};
/// A type for a virtual
/// [`TextNode`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode)
@ -14,17 +10,24 @@ use web_sys::{Element, Text as TextNode};
pub struct VText {
/// Contains a text of the node.
pub text: AttrValue,
/// A reference to the `TextNode`.
pub reference: Option<TextNode>,
}
impl VText {
/// Creates new virtual text node with a content.
pub fn new(text: impl Into<AttrValue>) -> Self {
VText {
text: text.into(),
reference: None,
}
VText { text: text.into() }
}
}
impl std::fmt::Debug for VText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "VText {{ text: \"{}\" }}", self.text)
}
}
impl PartialEq for VText {
fn eq(&self, other: &VText) -> bool {
self.text == other.text
}
}
@ -39,163 +42,6 @@ mod feat_ssr {
}
}
impl std::fmt::Debug for VText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"VText {{ text: \"{}\", reference: {} }}",
self.text,
match &self.reference {
Some(_) => "Some(...)",
None => "None",
}
)
}
}
impl VDiff for VText {
/// Remove VText from parent.
fn detach(&mut self, parent: &Element, parent_to_detach: bool) {
let node = self
.reference
.take()
.expect("tried to remove not rendered VText from DOM");
if !parent_to_detach {
let result = parent.remove_child(&node);
if result.is_err() {
console::warn!("Node not found to remove VText");
}
}
}
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
let node = self
.reference
.as_ref()
.expect("tried to shift not rendered VTag from DOM");
previous_parent.remove_child(node).unwrap();
next_parent
.insert_before(node, next_sibling.get().as_ref())
.unwrap();
}
/// Renders virtual node over existing `TextNode`, but only if value of text has changed.
fn apply(
&mut self,
_parent_scope: &AnyScope,
parent: &Element,
next_sibling: NodeRef,
ancestor: Option<VNode>,
) -> NodeRef {
if let Some(mut ancestor) = ancestor {
if let VNode::VText(mut vtext) = ancestor {
self.reference = vtext.reference.take();
let text_node = self
.reference
.clone()
.expect("Rendered VText nodes should have a ref");
if self.text != vtext.text {
text_node.set_node_value(Some(&self.text));
}
return NodeRef::new(text_node.into());
}
ancestor.detach(parent, false);
}
let text_node = document().create_text_node(&self.text);
super::insert_node(&text_node, parent, next_sibling.get().as_ref());
self.reference = Some(text_node.clone());
NodeRef::new(text_node.into())
}
}
impl PartialEq for VText {
fn eq(&self, other: &VText) -> bool {
self.text == other.text
}
}
#[cfg(test)]
mod test {
extern crate self as yew;
use crate::html;
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
#[test]
fn text_as_root() {
html! {
"Text Node As Root"
};
html! {
{ "Text Node As Root" }
};
}
}
#[cfg(test)]
mod layout_tests {
extern crate self as yew;
use crate::html;
use crate::tests::layout_tests::{diff_layouts, TestLayout};
#[cfg(feature = "wasm_test")]
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
#[cfg(feature = "wasm_test")]
wasm_bindgen_test_configure!(run_in_browser);
#[test]
fn diff() {
let layout1 = TestLayout {
name: "1",
node: html! { "a" },
expected: "a",
};
let layout2 = TestLayout {
name: "2",
node: html! { "b" },
expected: "b",
};
let layout3 = TestLayout {
name: "3",
node: html! {
<>
{"a"}
{"b"}
</>
},
expected: "ab",
};
let layout4 = TestLayout {
name: "4",
node: html! {
<>
{"b"}
{"a"}
</>
},
expected: "ba",
};
diff_layouts(vec![layout1, layout2, layout3, layout4]);
}
}
#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))]
mod ssr_tests {
use tokio::test;

View File

@ -0,0 +1,5 @@
# Extends the root Makefile.toml
[tasks.doc-test]
command = "echo"
args = ["No doctests run for benchmark-hooks"]

View File

@ -0,0 +1,5 @@
# Extends the root Makefile.toml
[tasks.doc-test]
command = "echo"
args = ["No doctests run for benchmark-struct"]

View File

@ -0,0 +1,5 @@
# Extends the root Makefile.toml
[tasks.doc-test]
command = "echo"
args = ["No doctests run for process-benchmark-results"]