diff --git a/Makefile.toml b/Makefile.toml index 3e62f00fcb8..56d8503140f 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -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 diff --git a/packages/yew/src/app_handle.rs b/packages/yew/src/dom_bundle/app_handle.rs similarity index 71% rename from packages/yew/src/app_handle.rs rename to packages/yew/src/dom_bundle/app_handle.rs index 5badcd8c750..771eb1b9d8e 100644 --- a/packages/yew/src/app_handle.rs +++ b/packages/yew/src/dom_bundle/app_handle.rs @@ -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 { /// `Scope` holder - pub(crate) scope: Scope, + scope: Scope, } impl AppHandle @@ -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) } } diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs new file mode 100644 index 00000000000..fe2d1ccabf0 --- /dev/null +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -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, + node_ref: NodeRef, + key: Option, +} + +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; + fn mount( + self: Box, + node_ref: NodeRef, + parent_scope: &AnyScope, + parent: Element, + next_sibling: NodeRef, + ) -> Box; + fn reuse(self: Box, 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 { + props: Rc, +} + +impl PropsWrapper { + pub fn new(props: Rc) -> Self { + Self { props } + } +} + +impl Mountable for PropsWrapper { + fn copy(&self) -> Box { + let wrapper: PropsWrapper = PropsWrapper { + props: Rc::clone(&self.props), + }; + Box::new(wrapper) + } + + fn mount( + self: Box, + node_ref: NodeRef, + parent_scope: &AnyScope, + parent: Element, + next_sibling: NodeRef, + ) -> Box { + let scope: Scope = 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, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { + let scope: Scope = 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 = 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, + next_sibling: NodeRef, + + #[cfg(feature = "ssr")] + html_sender: Option>, +} + +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) -> 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>; + /// 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, 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 { + Comp + } + + fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { + unimplemented!(); + } + + fn view(&self, _ctx: &Context) -> Html { + html! {
} + } + } + + #[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! { }; + let (_, mut bundle) = comp.attach(&parent_scope, &parent_element, NodeRef::default()); + scheduler::start_now(); + + for _ in 0..10000 { + let node = html! { }; + node.reconcile_node( + &parent_scope, + &parent_element, + NodeRef::default(), + &mut bundle, + ); + scheduler::start_now(); + } + } + + #[test] + fn set_properties_to_component() { + html! { + + }; + + html! { + + }; + + html! { + + }; + + html! { + + }; + + let props = Props { + field_1: 1, + field_2: 1, + }; + + html! { + + }; + } + + #[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! { }); + check_key(html! { }); + check_key(html! { }); + check_key(html! { }); + check_key(html! { }); + } + + #[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! { }); + check_node_ref(html! { }); + check_node_ref(html! { }); + check_node_ref(html! { }); + check_node_ref(html! { }); + } + + #[test] + fn vchild_partialeq() { + let vchild1: VChild = VChild::new( + Props { + field_1: 1, + field_2: 1, + }, + NodeRef::default(), + None, + ); + + let vchild2: VChild = VChild::new( + Props { + field_1: 1, + field_2: 1, + }, + NodeRef::default(), + None, + ); + + let vchild3: VChild = 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 + } + fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { + unimplemented!(); + } + fn changed(&mut self, _ctx: &Context) -> bool { + unimplemented!(); + } + fn view(&self, ctx: &Context) -> Html { + let item_iter = ctx + .props() + .children + .iter() + .map(|item| html! {
  • { item }
  • }); + html! { +
      { for item_iter }
    + } + } + } + + 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! {{ text }}) + .collect(); + let children_renderer = Children::new(children.clone()); + let expected_html = "\ +
      \ +
    • a
    • \ +
    • b
    • \ +
    • c
    • \ +
    "; + + let prop_method = html! { + + }; + assert_eq!(get_html(prop_method, &scope, &parent), expected_html); + + let children_renderer_method = html! { + + { children_renderer } + + }; + assert_eq!( + get_html(children_renderer_method, &scope, &parent), + expected_html + ); + + let direct_method = html! { + + { children.clone() } + + }; + assert_eq!(get_html(direct_method, &scope, &parent), expected_html); + + let for_method = html! { + + { for children } + + }; + 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! { }; + 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 { + _marker: PhantomData, + } + + #[derive(Properties, Clone, PartialEq)] + struct CompProps { + #[prop_or_default] + children: Children, + } + + impl Component for Comp { + type Message = (); + type Properties = CompProps; + + fn create(_: &Context) -> Self { + Comp { + _marker: PhantomData::default(), + } + } + + fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { + unimplemented!(); + } + + fn view(&self, ctx: &Context) -> Html { + html! { + <>{ ctx.props().children.clone() } + } + } + } + + struct A; + struct B; + + #[test] + fn diff() { + let layout1 = TestLayout { + name: "1", + node: html! { + > + >> + {"C"} + > + }, + expected: "C", + }; + + let layout2 = TestLayout { + name: "2", + node: html! { + > + {"A"} + > + }, + expected: "A", + }; + + let layout3 = TestLayout { + name: "3", + node: html! { + > + >> + {"B"} + > + }, + expected: "B", + }; + + let layout4 = TestLayout { + name: "4", + node: html! { + > + >{"A"}> + {"B"} + > + }, + expected: "AB", + }; + + let layout5 = TestLayout { + name: "5", + node: html! { + > + <> + > + {"A"} + > + + {"B"} + > + }, + expected: "AB", + }; + + let layout6 = TestLayout { + name: "6", + node: html! { + > + <> + > + {"A"} + > + {"B"} + + {"C"} + > + }, + expected: "ABC", + }; + + let layout7 = TestLayout { + name: "7", + node: html! { + > + <> + > + {"A"} + > + > + {"B"} + > + + {"C"} + > + }, + expected: "ABC", + }; + + let layout8 = TestLayout { + name: "8", + node: html! { + > + <> + > + {"A"} + > + > + > + {"B"} + > + > + + {"C"} + > + }, + expected: "ABC", + }; + + let layout9 = TestLayout { + name: "9", + node: html! { + > + <> + <> + {"A"} + + > + > + {"B"} + > + > + + {"C"} + > + }, + expected: "ABC", + }; + + let layout10 = TestLayout { + name: "10", + node: html! { + > + <> + > + > + {"A"} + > + > + <> + {"B"} + + + {"C"} + > + }, + expected: "ABC", + }; + + let layout11 = TestLayout { + name: "11", + node: html! { + > + <> + <> + > + > + {"A"} + > + {"B"} + > + + + {"C"} + > + }, + expected: "ABC", + }; + + let layout12 = TestLayout { + name: "12", + node: html! { + > + <> + >> + <> + > + <> + > + {"A"} + > + <> + > + >> + <> + {"B"} + <> + >> + > + + > + <> + + >> + + {"C"} + >> + <> + > + }, + 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 + } + + fn view(&self, ctx: &Context) -> Html { + html! { +
      + { for ctx.props().children.iter().map(|child| html! {
    • { child }
    • }) } +
    + } + } + } + + let layout = TestLayout { + name: "13", + node: html! { + + if true { + { "hello" } + { "world" } + } else { + { "goodbye" } + { "world" } + } + + }, + expected: "
    • helloworld
    ", + }; + + diff_layouts(vec![layout]); + } +} diff --git a/packages/yew/src/dom_bundle/blist.rs b/packages/yew/src/dom_bundle/blist.rs new file mode 100644 index 00000000000..21e66fc60ec --- /dev/null +++ b/packages/yew/src/dom_bundle/blist.rs @@ -0,0 +1,1382 @@ +//! This module contains fragments bundles, a [BList] +use super::{test_log, BNode}; +use crate::dom_bundle::{DomBundle, Reconcilable}; +use crate::html::{AnyScope, NodeRef}; +use crate::virtual_dom::{Key, VList, VNode, VText}; +use std::borrow::Borrow; +use std::cmp::Ordering; +use std::collections::HashSet; +use std::hash::Hash; +use std::ops::Deref; +use web_sys::Element; + +/// This struct represents a mounted [VList] +#[derive(Debug)] +pub struct BList { + /// The reverse (render order) list of child [BNode]s + rev_children: Vec, + /// All [BNode]s in the BList have keys + fully_keyed: bool, + key: Option, +} + +impl Deref for BList { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.rev_children + } +} + +/// Helper struct, that keeps the position where the next element is to be placed at +#[derive(Clone)] +struct NodeWriter<'s> { + parent_scope: &'s AnyScope, + parent: &'s Element, + next_sibling: NodeRef, +} + +impl<'s> NodeWriter<'s> { + /// Write a new node that has no ancestor + fn add(self, node: VNode) -> (Self, BNode) { + test_log!("adding: {:?}", node); + test_log!( + " parent={:?}, next_sibling={:?}", + self.parent.outer_html(), + self.next_sibling + ); + let (next, bundle) = node.attach(self.parent_scope, self.parent, self.next_sibling); + test_log!(" next_position: {:?}", next); + ( + Self { + next_sibling: next, + ..self + }, + bundle, + ) + } + + /// Shift a bundle into place without patching it + fn shift(&self, bundle: &mut BNode) { + bundle.shift(self.parent, self.next_sibling.clone()); + } + + /// Patch a bundle with a new node + fn patch(self, node: VNode, bundle: &mut BNode) -> Self { + test_log!("patching: {:?} -> {:?}", bundle, node); + test_log!( + " parent={:?}, next_sibling={:?}", + self.parent.outer_html(), + self.next_sibling + ); + // Advance the next sibling reference (from right to left) + let next = node.reconcile_node(self.parent_scope, self.parent, self.next_sibling, bundle); + test_log!(" next_position: {:?}", next); + Self { + next_sibling: next, + ..self + } + } +} +/// Helper struct implementing [Eq] and [Hash] by only looking at a node's key +struct KeyedEntry(usize, BNode); +impl Borrow for KeyedEntry { + fn borrow(&self) -> &Key { + self.1.key().expect("unkeyed child in fully keyed list") + } +} +impl Hash for KeyedEntry { + fn hash(&self, state: &mut H) { + >::borrow(self).hash(state) + } +} +impl PartialEq for KeyedEntry { + fn eq(&self, other: &Self) -> bool { + >::borrow(self) == >::borrow(other) + } +} +impl Eq for KeyedEntry {} + +impl BNode { + /// Assert that a bundle node is a list, or convert it to a list with a single child + fn make_list(&mut self) -> &mut BList { + match self { + Self::List(blist) => blist, + self_ => { + let b = std::mem::replace(self_, BNode::List(BList::new())); + let self_list = match self_ { + BNode::List(blist) => blist, + _ => unreachable!("just been set to the variant"), + }; + let key = b.key().cloned(); + self_list.rev_children.push(b); + self_list.fully_keyed = key.is_some(); + self_list.key = key; + self_list + } + } + } +} + +impl BList { + /// Create a new empty [BList] + pub(super) const fn new() -> BList { + BList { + rev_children: vec![], + fully_keyed: true, + key: None, + } + } + + /// Get the key of the underlying fragment + pub(super) fn key(&self) -> Option<&Key> { + self.key.as_ref() + } + + /// Diff and patch unkeyed child lists + fn apply_unkeyed( + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + lefts: Vec, + rights: &mut Vec, + ) -> NodeRef { + let mut writer = NodeWriter { + parent_scope, + parent, + next_sibling, + }; + + // Remove extra nodes + if lefts.len() < rights.len() { + for r in rights.drain(lefts.len()..) { + test_log!("removing: {:?}", r); + r.detach(parent, false); + } + } + + let mut lefts_it = lefts.into_iter().rev(); + for (r, l) in rights.iter_mut().zip(&mut lefts_it) { + writer = writer.patch(l, r); + } + + // Add missing nodes + for l in lefts_it { + let (next_writer, el) = writer.add(l); + rights.push(el); + writer = next_writer; + } + writer.next_sibling + } + + /// Diff and patch fully keyed child lists. + /// + /// Optimized for node addition or removal from either end of the list and small changes in the + /// middle. + fn apply_keyed( + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + left_vdoms: Vec, + rev_bundles: &mut Vec, + ) -> NodeRef { + macro_rules! key { + ($v:expr) => { + $v.key().expect("unkeyed child in fully keyed list") + }; + } + /// Find the first differing key in 2 iterators + fn matching_len<'a, 'b>( + a: impl Iterator, + b: impl Iterator, + ) -> usize { + a.zip(b).take_while(|(a, b)| a == b).count() + } + + // Find first key mismatch from the back + let matching_len_end = matching_len( + left_vdoms.iter().map(|v| key!(v)).rev(), + rev_bundles.iter().map(|v| key!(v)), + ); + + // If there is no key mismatch, apply the unkeyed approach + // Corresponds to adding or removing items from the back of the list + if matching_len_end == std::cmp::min(left_vdoms.len(), rev_bundles.len()) { + // No key changes + return Self::apply_unkeyed( + parent_scope, + parent, + next_sibling, + left_vdoms, + rev_bundles, + ); + } + + // We partially drain the new vnodes in several steps. + let mut lefts = left_vdoms; + let mut writer = NodeWriter { + parent_scope, + parent, + next_sibling, + }; + // Step 1. Diff matching children at the end + let lefts_to = lefts.len() - matching_len_end; + for (l, r) in lefts + .drain(lefts_to..) + .rev() + .zip(rev_bundles[..matching_len_end].iter_mut()) + { + writer = writer.patch(l, r); + } + + // Step 2. Diff matching children in the middle, that is between the first and last key mismatch + // Find first key mismatch from the front + let matching_len_start = matching_len( + lefts.iter().map(|v| key!(v)), + rev_bundles.iter().map(|v| key!(v)).rev(), + ); + + // Step 2.1. Splice out the existing middle part and build a lookup by key + let rights_to = rev_bundles.len() - matching_len_start; + let mut spliced_middle = + rev_bundles.splice(matching_len_end..rights_to, std::iter::empty()); + let mut spare_bundles: HashSet = + HashSet::with_capacity((matching_len_end..rights_to).len()); + for (idx, r) in (&mut spliced_middle).enumerate() { + spare_bundles.insert(KeyedEntry(idx, r)); + } + + // Step 2.2. Put the middle part back together in the new key order + let mut replacements: Vec = Vec::with_capacity((matching_len_start..lefts_to).len()); + // The goal is to shift as few nodes as possible. + + // We handle runs of in-order nodes. When we encounter one out-of-order, we decide whether: + // - to shift all nodes in the current run to the position after the node before of the run, or to + // - "commit" to the current run, shift all nodes before the end of the run that we might + // encounter in the future, and then start a new run. + // Example of a run: + // barrier_idx --v v-- end_idx + // spliced_middle [ ... , M , N , C , D , E , F , G , ... ] (original element order) + // ^---^-----------^ the nodes that are part of the current run + // v start_writer + // replacements [ ... , M , C , D , G ] (new element order) + // ^-- start_idx + let mut barrier_idx = 0; // nodes from spliced_middle[..barrier_idx] are shifted unconditionally + struct RunInformation<'a> { + start_writer: NodeWriter<'a>, + start_idx: usize, + end_idx: usize, + } + let mut current_run: Option> = None; + + for l in lefts + .drain(matching_len_start..) // lefts_to.. has been drained + .rev() + { + let ancestor = spare_bundles.take(key!(l)); + // Check if we need to shift or commit a run + if let Some(run) = current_run.as_mut() { + if let Some(KeyedEntry(idx, _)) = ancestor { + // If there are only few runs, this is a cold path + if idx < run.end_idx { + // Have to decide whether to shift or commit the current run. A few calculations: + // A perfect estimate of the amount of nodes we have to shift if we move this run: + let run_length = replacements.len() - run.start_idx; + // A very crude estimate of the amount of nodes we will have to shift if we commit the run: + // Note nodes of the current run should not be counted here! + let estimated_skipped_nodes = run.end_idx - idx.max(barrier_idx); + // double run_length to counteract that the run is part of the estimated_skipped_nodes + if 2 * run_length > estimated_skipped_nodes { + // less work to commit to this run + barrier_idx = 1 + run.end_idx; + } else { + // Less work to shift this run + for r in replacements[run.start_idx..].iter_mut().rev() { + run.start_writer.shift(r); + } + } + current_run = None; + } + } + } + let bundle = if let Some(KeyedEntry(idx, mut r_bundle)) = ancestor { + match current_run.as_mut() { + // hot path + // We know that idx >= run.end_idx, so this node doesn't need to shift + Some(run) => run.end_idx = idx, + None => match idx.cmp(&barrier_idx) { + // peep hole optimization, don't start a run as the element is already where it should be + Ordering::Equal => barrier_idx += 1, + // shift the node unconditionally, don't start a run + Ordering::Less => writer.shift(&mut r_bundle), + // start a run + Ordering::Greater => { + current_run = Some(RunInformation { + start_writer: writer.clone(), + start_idx: replacements.len(), + end_idx: idx, + }) + } + }, + } + writer = writer.patch(l, &mut r_bundle); + r_bundle + } else { + // Even if there is an active run, we don't have to modify it + let (next_writer, bundle) = writer.add(l); + writer = next_writer; + bundle + }; + replacements.push(bundle); + } + // drop the splice iterator and immediately replace the range with the reordered elements + drop(spliced_middle); + rev_bundles.splice(matching_len_end..matching_len_end, replacements); + + // Step 2.3. Remove any extra rights + for KeyedEntry(_, r) in spare_bundles.drain() { + test_log!("removing: {:?}", r); + r.detach(parent, false); + } + + // Step 3. Diff matching children at the start + let rights_to = rev_bundles.len() - matching_len_start; + for (l, r) in lefts + .drain(..) // matching_len_start.. has been drained already + .rev() + .zip(rev_bundles[rights_to..].iter_mut()) + { + writer = writer.patch(l, r); + } + + writer.next_sibling + } +} + +impl DomBundle for BList { + fn detach(self, parent: &Element, parent_to_detach: bool) { + for child in self.rev_children.into_iter() { + child.detach(parent, parent_to_detach); + } + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + for node in self.rev_children.iter().rev() { + node.shift(next_parent, next_sibling.clone()); + } + } +} + +impl Reconcilable for VList { + type Bundle = BList; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let mut self_ = BList::new(); + let node_ref = self.reconcile(parent_scope, parent, next_sibling, &mut self_); + (node_ref, self_) + } + + fn reconcile_node( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut BNode, + ) -> NodeRef { + // 'Forcefully' create a pretend the existing node is a list. Creates a + // singleton list if it isn't already. + let blist = bundle.make_list(); + self.reconcile(parent_scope, parent, next_sibling, blist) + } + + fn reconcile( + mut self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + blist: &mut BList, + ) -> NodeRef { + // Here, we will try to diff the previous list elements with the new + // ones we want to insert. For that, we will use two lists: + // - lefts: new elements to render in the DOM + // - rights: previously rendered elements. + // + // The left items are known since we want to insert them + // (self.children). For the right ones, we will look at the bundle, + // i.e. the current DOM list element that we want to replace with self. + + if self.children.is_empty() { + // Without a placeholder the next element becomes first + // and corrupts the order of rendering + // We use empty text element to stake out a place + self.add_child(VText::new("").into()); + } + + let lefts = self.children; + let rights = &mut blist.rev_children; + test_log!("lefts: {:?}", lefts); + test_log!("rights: {:?}", rights); + + if let Some(additional) = lefts.len().checked_sub(rights.len()) { + rights.reserve_exact(additional); + } + let first = if self.fully_keyed && blist.fully_keyed { + BList::apply_keyed(parent_scope, parent, next_sibling, lefts, rights) + } else { + BList::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights) + }; + blist.fully_keyed = self.fully_keyed; + blist.key = self.key; + test_log!("result: {:?}", rights); + first + } +} + +#[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"} + {"b"} + <> + {"c"} + {"d"} + + {"e"} + + }, + expected: "abcde", + }; + + let layout2 = TestLayout { + name: "2", + node: html! { + <> + {"a"} + {"b"} + <> + {"e"} + {"f"} + + }, + expected: "abef", + }; + + let layout3 = TestLayout { + name: "3", + node: html! { + <> + {"a"} + <> + {"b"} + {"e"} + + }, + expected: "abe", + }; + + let layout4 = TestLayout { + name: "4", + node: html! { + <> + {"a"} + <> + {"c"} + {"d"} + + {"b"} + {"e"} + + }, + expected: "acdbe", + }; + + diff_layouts(vec![layout1, layout2, layout3, layout4]); + } +} + +#[cfg(test)] +mod layout_tests_keys { + extern crate self as yew; + + use crate::html; + use crate::tests::layout_tests::{diff_layouts, TestLayout}; + use crate::virtual_dom::VNode; + use crate::{Children, Component, Context, Html, Properties}; + 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(Properties, Clone, PartialEq)] + struct CountingCompProps { + id: usize, + #[prop_or(false)] + can_change: bool, + } + + impl Component for Comp { + type Message = (); + type Properties = CountingCompProps; + + fn create(_: &Context) -> Self { + Comp {} + } + + fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { + unimplemented!(); + } + + fn view(&self, ctx: &Context) -> Html { + html! {

    { ctx.props().id }

    } + } + } + + #[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() + } + + fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { + unimplemented!(); + } + + fn view(&self, ctx: &Context) -> Html { + html! { <>{ for ctx.props().children.iter() } } + } + } + + #[test] + fn diff() { + let mut layouts = vec![]; + + let vref_node: Node = gloo_utils::document().create_element("i").unwrap().into(); + layouts.push(TestLayout { + name: "All VNode types as children", + node: html! { + <> + {"a"} + + {"c"} + {"d"} + + + {"foo"} + {"bar"} + + {VNode::VRef(vref_node)} + + }, + expected: "acd

    0

    foobar", + }); + + layouts.extend(vec![ + TestLayout { + name: "Inserting into VList first child - before", + node: html! { + <> + + + +

    + + }, + expected: "

    ", + }, + TestLayout { + name: "Inserting into VList first child - after", + node: html! { + <> + + + + +

    + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "No matches - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "No matches - after", + node: html! { + <> + +

    + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Append - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "Append - after", + node: html! { + <> + + +

    + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Prepend - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "Prepend - after", + node: html! { + <> +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Delete first - before", + node: html! { + <> + + +

    + + }, + expected: "

    ", + }, + TestLayout { + name: "Delete first - after", + node: html! { + <> + +

    + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Delete last - before", + node: html! { + <> + + +

    + + }, + expected: "

    ", + }, + TestLayout { + name: "Delete last - after", + node: html! { + <> + + + + }, + expected: "", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Delete last and change node type - before", + node: html! { + <> + + +

    + + }, + expected: "

    ", + }, + TestLayout { + name: "Delete last - after", + node: html! { + <> + + + + + }, + expected: "", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Delete middle - before", + node: html! { + <> + + +

    + + + }, + expected: "

    ", + }, + TestLayout { + name: "Delete middle - after", + node: html! { + <> + + +

    + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Delete middle and change node type - before", + node: html! { + <> + + +

    + + + }, + expected: "

    ", + }, + TestLayout { + name: "Delete middle and change node type- after", + node: html! { + <> + + +

    + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Reverse - before", + node: html! { + <> + + +

    + + + }, + expected: "

    ", + }, + TestLayout { + name: "Reverse - after", + node: html! { + <> + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Reverse and change node type - before", + node: html! { + <> + + + + + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Reverse and change node type - after", + node: html! { + <> + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap 1&2 - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 1&2 - after", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap 1&2 and change node type - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 1&2 and change node type - after", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "test - before", + node: html! { + <> + + +

    + + + + + +

    + + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 4&5 - after", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap 4&5 - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 4&5 - after", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap 1&5 - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 1&5 - after", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Move 2 after 4 - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Move 2 after 4 - after", + node: html! { + <> + +

    + + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap 1,2 <-> 3,4 - before", + node: html! { + <> + + +

    + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap 1,2 <-> 3,4 - after", + node: html! { + <> +

    + + + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap lists - before", + node: html! { + <> + + + + + + + + + + }, + expected: "", + }, + TestLayout { + name: "Swap lists - after", + node: html! { + <> + + + + + + + + + + }, + expected: "", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Swap lists with in-between - before", + node: html! { + <> + + + + +

    + + + + + + }, + expected: "

    ", + }, + TestLayout { + name: "Swap lists with in-between - after", + node: html! { + <> + + + + +

    + + + + + + }, + expected: "

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Insert VComp front - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "Insert VComp front - after", + node: html! { + <> + + + + + }, + expected: "

    0

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Insert VComp middle - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "Insert VComp middle - after", + node: html! { + <> + + + + + }, + expected: "

    0

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Insert VComp back - before", + node: html! { + <> + + + + }, + expected: "", + }, + TestLayout { + name: "Insert VComp back - after", + node: html! { + <> + + + + + }, + expected: "

    0

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Reverse VComp children - before", + node: html! { + <> + + + + + }, + expected: "

    1

    2

    3

    ", + }, + TestLayout { + name: "Reverse VComp children - after", + node: html! { + <> + + + + + }, + expected: "

    3

    2

    1

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Reverse VComp children with children - before", + node: html! { + <> +

    {"11"}

    {"12"}

    +

    {"21"}

    {"22"}

    +

    {"31"}

    {"32"}

    + + }, + expected: "

    11

    12

    21

    22

    31

    32

    ", + }, + TestLayout { + name: "Reverse VComp children with children - after", + node: html! { + <> +

    {"31"}

    {"32"}

    +

    {"21"}

    {"22"}

    +

    {"11"}

    {"12"}

    + + }, + expected: "

    31

    32

    21

    22

    11

    12

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Complex component update - before", + node: html! { + + + + + }, + expected: "

    1

    2

    ", + }, + TestLayout { + name: "Complex component update - after", + node: html! { + + + + + +

    {"2"}

    +
    +
    + }, + expected: "

    1

    2

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Reorder VComp children with children - before", + node: html! { + <> +

    {"1"}

    +

    {"3"}

    +

    {"5"}

    +

    {"2"}

    +

    {"4"}

    +

    {"6"}

    + + }, + expected: "

    1

    3

    5

    2

    4

    6

    ", + }, + TestLayout { + name: "Reorder VComp children with children - after", + node: html! { + <> + + + + + + + + }, + expected: "

    6

    5

    4

    3

    2

    1

    ", + }, + ]); + + layouts.extend(vec![ + TestLayout { + name: "Replace and reorder components - before", + node: html! { + +

    {"1"}

    +

    {"2"}

    +

    {"3"}

    +
    + }, + expected: "

    1

    2

    3

    ", + }, + TestLayout { + name: "Replace and reorder components - after", + node: html! { + + + + + + }, + expected: "

    3

    2

    1

    ", + }, + ]); + + diff_layouts(layouts); + } +} diff --git a/packages/yew/src/dom_bundle/bnode.rs b/packages/yew/src/dom_bundle/bnode.rs new file mode 100644 index 00000000000..0e80563fd30 --- /dev/null +++ b/packages/yew/src/dom_bundle/bnode.rs @@ -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), + /// 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), +} + +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 for BNode { + #[inline] + fn from(btext: BText) -> Self { + Self::Text(btext) + } +} + +impl From for BNode { + #[inline] + fn from(blist: BList) -> Self { + Self::List(blist) + } +} + +impl From for BNode { + #[inline] + fn from(btag: BTag) -> Self { + Self::Tag(Box::new(btag)) + } +} + +impl From for BNode { + #[inline] + fn from(bcomp: BComp) -> Self { + Self::Comp(bcomp) + } +} + +impl From for BNode { + #[inline] + fn from(bportal: BPortal) -> Self { + Self::Portal(bportal) + } +} + +impl From 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: "", + }; + + let layout2 = TestLayout { + name: "2", + node: vref_node_2, + expected: "", + }; + + diff_layouts(vec![layout1, layout2]); + } +} diff --git a/packages/yew/src/dom_bundle/bportal.rs b/packages/yew/src/dom_bundle/bportal.rs new file mode 100644 index 00000000000..a5c6d769181 --- /dev/null +++ b/packages/yew/src/dom_bundle/bportal.rs @@ -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, +} + +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! { +
    + {VNode::VRef(first_target.clone().into())} + {VNode::VRef(second_target.clone().into())} + {VNode::VPortal(VPortal::new( + html! { {"PORTAL"} }, + first_target.clone(), + ))} + {"AFTER"} +
    + }, + expected: "
    PORTALAFTER
    ", + }); + layouts.push(TestLayout { + name: "Portal - second target", + node: html! { +
    + {VNode::VRef(first_target.clone().into())} + {VNode::VRef(second_target.clone().into())} + {VNode::VPortal(VPortal::new( + html! { {"PORTAL"} }, + second_target.clone(), + ))} + {"AFTER"} +
    + }, + expected: "
    PORTALAFTER
    ", + }); + layouts.push(TestLayout { + name: "Portal - replaced by text", + node: html! { +
    + {VNode::VRef(first_target.clone().into())} + {VNode::VRef(second_target.clone().into())} + {"FOO"} + {"AFTER"} +
    + }, + expected: "
    FOOAFTER
    ", + }); + layouts.push(TestLayout { + name: "Portal - next sibling", + node: html! { +
    + {VNode::VRef(target_with_child.clone().into())} + {VNode::VPortal(VPortal::new_before( + html! { {"PORTAL"} }, + target_with_child.clone(), + Some(target_child.clone().into()), + ))} +
    + }, + expected: "
    PORTAL
    ", + }); + + diff_layouts(layouts) + } +} diff --git a/packages/yew/src/dom_bundle/bsuspense.rs b/packages/yew/src/dom_bundle/bsuspense.rs new file mode 100644 index 00000000000..0781b512e7d --- /dev/null +++ b/packages/yew/src/dom_bundle/bsuspense.rs @@ -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, + detached_parent: Element, + key: Option, +} + +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) + } + } + } +} diff --git a/packages/yew/src/dom_bundle/btag/attributes.rs b/packages/yew/src/dom_bundle/btag/attributes.rs new file mode 100644 index 00000000000..cdec0630e6b --- /dev/null +++ b/packages/yew/src/dom_bundle/btag/attributes.rs @@ -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 Apply for Value { + 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, + new: &IndexMap<&'static str, A>, + old: &IndexMap<&'static str, B>, + ) where + A: AsRef, + B: AsRef, + { + 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(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); + } + } + } +} diff --git a/packages/yew/src/dom_bundle/btag/listeners.rs b/packages/yew/src/dom_bundle/btag/listeners.rs new file mode 100644 index 00000000000..66f14363b3f --- /dev/null +++ b/packages/yew/src/dom_bundle/btag/listeners.rs @@ -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 = 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>]) -> 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, + + /// 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>>>, +} + +impl Registry { + /// Run f with access to global Registry + #[inline] + fn with(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>]) { + let mut by_desc = + HashMap::>>::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>]) { + 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::().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(ctx: &Context, state: &State) -> Html + where + C: Component, + { + let link = ctx.link().clone(); + let onclick = Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }); + + if state.stop_listening { + html! { + {state.action} + } + } else { + html! { + + {state.action} + + } + } + } + } + + struct Comp + where + M: Mixin + 'static, + { + state: State, + pd: PhantomData, + } + + impl Component for Comp + where + M: Mixin + 'static, + { + type Message = Message; + type Properties = (); + + fn create(_: &Context) -> Self { + Comp { + state: Default::default(), + pd: PhantomData, + } + } + + fn update(&mut self, _: &Context, 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) -> 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::() + .unwrap() + } + + fn init(tag: &str) -> (AppHandle>, 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::>(root); + scheduler::start_now(); + + (app, get_el_by_tag(tag)) + } + + #[test] + fn synchronous() { + struct Synchronous; + + impl Mixin for Synchronous {} + + let (link, el) = init::("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(ctx: &Context, state: &State) -> Html + where + C: Component, + { + let link = ctx.link().clone(); + let onblur = Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }); + html! { + + } + } + } + + let (_, el) = init::("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(ctx: &Context, state: &State) -> Html + where + C: Component, + { + if state.stop_listening { + html! { + + } + } else { + let link = ctx.link().clone(); + let cb = Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }); + html! { + + } + } + } + } + + let (link, el) = init::("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(ctx: &Context, state: &State) -> Html + where + C: Component, + { + 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! { + + } + } + } + + let (_, el) = init::("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(ctx: &Context, state: &State) -> Html + where + C: Component, + { + 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! { + + } + } + } + + let (_, el) = init::("a"); + + assert_count(&el, 0); + + el.click(); + assert_count(&el, 1); + + el.click(); + assert_count(&el, 2); + } + + fn test_input_listener(make_event: impl Fn() -> E) + where + E: JsCast + std::fmt::Debug, + { + struct Input; + + impl Mixin for Input { + fn view(ctx: &Context, state: &State) -> Html + where + C: Component, + { + if state.stop_listening { + html! { +
    + +

    {state.text.clone()}

    +
    + } + } 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! { +
    + +

    {state.text.clone()}

    +
    + } + } + } + } + + let (link, input_el) = init::("input"); + let input_el = input_el.dyn_into::().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::() + .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() + }) + } +} diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs new file mode 100644 index 00000000000..1d172c1f87d --- /dev/null +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -0,0 +1,1078 @@ +//! This module contains the bundle implementation of a tag [BTag] + +mod attributes; +mod listeners; + +pub use listeners::set_event_bubbling; + +use super::{insert_node, BList, BNode, DomBundle, Reconcilable}; +use crate::html::AnyScope; +use crate::virtual_dom::vtag::{InputFields, VTagInner, Value, SVG_NAMESPACE}; +use crate::virtual_dom::{Attributes, Key, VTag}; +use crate::NodeRef; +use gloo::console; +use gloo_utils::document; +use listeners::ListenerRegistration; +use std::ops::DerefMut; +use std::{borrow::Cow, hint::unreachable_unchecked}; +use wasm_bindgen::JsCast; +use web_sys::{Element, HtmlTextAreaElement as TextAreaElement}; + +/// Applies contained changes to DOM [web_sys::Element] +trait Apply { + /// [web_sys::Element] subtype to apply the changes to + type Element; + type Bundle; + + /// Apply contained values to [Element](Self::Element) with no ancestor + fn apply(self, el: &Self::Element) -> Self::Bundle; + + /// Apply diff between [self] and `bundle` to [Element](Self::Element). + fn apply_diff(self, el: &Self::Element, bundle: &mut Self::Bundle); +} + +/// [BTag] fields that are specific to different [BTag] kinds. +/// Decreases the memory footprint of [BTag] by avoiding impossible field and value combinations. +#[derive(Debug)] +enum BTagInner { + /// Fields specific to + /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) + Input(InputFields), + /// Fields specific to + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + Textarea { + /// Contains a value of an + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + value: Value, + }, + /// Fields for all other kinds of [VTag]s + Other { + /// A tag of the element. + tag: Cow<'static, str>, + /// List of child nodes + child_bundle: BList, + }, +} + +/// The bundle implementation to [VTag] +#[derive(Debug)] +pub struct BTag { + /// [BTag] fields that are specific to different [BTag] kinds. + inner: BTagInner, + listeners: ListenerRegistration, + attributes: Attributes, + /// A reference to the DOM [`Element`]. + reference: Element, + /// A node reference used for DOM access in Component lifecycle methods + node_ref: NodeRef, + key: Option, +} + +impl DomBundle for BTag { + fn detach(self, parent: &Element, parent_to_detach: bool) { + self.listeners.unregister(); + + let node = self.reference; + // recursively remove its children + if let BTagInner::Other { child_bundle, .. } = self.inner { + // This tag will be removed, so there's no point to remove any child. + child_bundle.detach(&node, true); + } + if !parent_to_detach { + let result = parent.remove_child(&node); + + if result.is_err() { + console::warn!("Node not found to remove VTag"); + } + } + // It could be that the ref was already reused when rendering another element. + // Only unset the ref it still belongs to our node + if self.node_ref.get().as_ref() == Some(&node) { + self.node_ref.set(None); + } + } + + fn shift(&self, next_parent: &Element, next_sibling: NodeRef) { + next_parent + .insert_before(&self.reference, next_sibling.get().as_ref()) + .unwrap(); + } +} + +impl Reconcilable for VTag { + type Bundle = BTag; + + fn attach( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + ) -> (NodeRef, Self::Bundle) { + let el = self.create_element(parent); + let Self { + listeners, + attributes, + node_ref, + key, + .. + } = self; + insert_node(&el, parent, next_sibling.get().as_ref()); + + let attributes = attributes.apply(&el); + let listeners = listeners.apply(&el); + + let inner = match self.inner { + VTagInner::Input(f) => { + let f = f.apply(el.unchecked_ref()); + BTagInner::Input(f) + } + VTagInner::Textarea { value } => { + let value = value.apply(el.unchecked_ref()); + BTagInner::Textarea { value } + } + VTagInner::Other { children, tag } => { + let (_, child_bundle) = children.attach(parent_scope, &el, NodeRef::default()); + BTagInner::Other { child_bundle, tag } + } + }; + node_ref.set(Some(el.clone().into())); + ( + node_ref.clone(), + BTag { + inner, + listeners, + reference: el, + attributes, + key, + node_ref, + }, + ) + } + + fn reconcile_node( + self, + parent_scope: &AnyScope, + parent: &Element, + next_sibling: NodeRef, + bundle: &mut BNode, + ) -> NodeRef { + // This kind of branching patching routine reduces branch predictor misses and the need to + // unpack the enums (including `Option`s) all the time, resulting in a more streamlined + // patching flow + match bundle { + // If the ancestor is a tag of the same type, don't recreate, keep the + // old tag and update its attributes and children. + BNode::Tag(ex) if self.key == ex.key => { + if match (&self.inner, &ex.inner) { + (VTagInner::Input(_), BTagInner::Input(_)) => true, + (VTagInner::Textarea { .. }, BTagInner::Textarea { .. }) => true, + (VTagInner::Other { tag: l, .. }, BTagInner::Other { tag: r, .. }) + if l == r => + { + true + } + _ => false, + } { + return self.reconcile(parent_scope, parent, next_sibling, ex.deref_mut()); + } + } + _ => {} + }; + self.replace(parent_scope, parent, next_sibling, bundle) + } + + fn reconcile( + self, + parent_scope: &AnyScope, + _parent: &Element, + _next_sibling: NodeRef, + tag: &mut Self::Bundle, + ) -> NodeRef { + let el = &tag.reference; + self.attributes.apply_diff(el, &mut tag.attributes); + self.listeners.apply_diff(el, &mut tag.listeners); + + match (self.inner, &mut tag.inner) { + (VTagInner::Input(new), BTagInner::Input(old)) => { + new.apply_diff(el.unchecked_ref(), old); + } + (VTagInner::Textarea { value: new }, BTagInner::Textarea { value: old }) => { + new.apply_diff(el.unchecked_ref(), old); + } + ( + VTagInner::Other { children: new, .. }, + BTagInner::Other { + child_bundle: old, .. + }, + ) => { + new.reconcile(parent_scope, el, NodeRef::default(), old); + } + // Can not happen, because we checked for tag equability above + _ => unsafe { unreachable_unchecked() }, + } + + tag.key = self.key; + + if self.node_ref != tag.node_ref && tag.node_ref.get().as_ref() == Some(el) { + tag.node_ref.set(None); + } + if self.node_ref != tag.node_ref { + tag.node_ref = self.node_ref; + tag.node_ref.set(Some(el.clone().into())); + } + + tag.node_ref.clone() + } +} + +impl VTag { + fn create_element(&self, parent: &Element) -> Element { + let tag = self.tag(); + if tag == "svg" + || parent + .namespace_uri() + .map_or(false, |ns| ns == SVG_NAMESPACE) + { + let namespace = Some(SVG_NAMESPACE); + document() + .create_element_ns(namespace, tag) + .expect("can't create namespaced element for vtag") + } else { + document() + .create_element(tag) + .expect("can't create element for vtag") + } + } +} + +impl BTag { + /// Get the key of the underlying tag + pub(super) fn key(&self) -> Option<&Key> { + self.key.as_ref() + } + + #[cfg(test)] + fn reference(&self) -> &Element { + &self.reference + } + + #[cfg(test)] + fn children(&self) -> &[BNode] { + match &self.inner { + BTagInner::Other { child_bundle, .. } => child_bundle, + _ => &[], + } + } + + #[cfg(test)] + fn tag(&self) -> &str { + match &self.inner { + BTagInner::Input { .. } => "input", + BTagInner::Textarea { .. } => "textarea", + BTagInner::Other { tag, .. } => tag.as_ref(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::dom_bundle::{BNode, DomBundle, Reconcilable}; + use crate::html; + use crate::html::AnyScope; + use crate::virtual_dom::vtag::{HTML_NAMESPACE, SVG_NAMESPACE}; + use crate::virtual_dom::{AttrValue, VNode, VTag}; + use crate::{Html, NodeRef}; + use gloo_utils::document; + use wasm_bindgen::JsCast; + use web_sys::HtmlInputElement as InputElement; + + #[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); + + fn test_scope() -> AnyScope { + AnyScope::test() + } + + #[test] + fn it_compares_tags() { + let a = html! { +
    + }; + + let b = html! { +
    + }; + + let c = html! { +

    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_text() { + let a = html! { +
    { "correct" }
    + }; + + let b = html! { +
    { "correct" }
    + }; + + let c = html! { +
    { "incorrect" }
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_attributes_static() { + let a = html! { +
    + }; + + let b = html! { +
    + }; + + let c = html! { +
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_attributes_dynamic() { + let a = html! { +
    + }; + + let b = html! { +
    + }; + + let c = html! { +
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_children() { + let a = html! { +
    +

    +
    + }; + + let b = html! { +
    +

    +
    + }; + + let c = html! { +
    + +
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_classes_static() { + let a = html! { +
    + }; + + let b = html! { +
    + }; + + let c = html! { +
    + }; + + let d = html! { +
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + assert_ne!(a, d); + } + + #[test] + fn it_compares_classes_dynamic() { + let a = html! { +
    + }; + + let b = html! { +
    + }; + + let c = html! { +
    + }; + + let d = html! { +
    + }; + + assert_eq!(a, b); + assert_ne!(a, c); + assert_ne!(a, d); + } + + fn assert_vtag(node: VNode) -> VTag { + if let VNode::VTag(vtag) = node { + return *vtag; + } + panic!("should be vtag"); + } + + fn assert_btag_ref(node: &BNode) -> &BTag { + if let BNode::Tag(vtag) = node { + return vtag; + } + panic!("should be btag"); + } + + fn assert_vtag_ref(node: &VNode) -> &VTag { + if let VNode::VTag(vtag) = node { + return vtag; + } + panic!("should be vtag"); + } + + fn assert_btag_mut(node: &mut BNode) -> &mut BTag { + if let BNode::Tag(btag) = node { + return btag; + } + panic!("should be btag"); + } + + fn assert_namespace(vtag: &BTag, namespace: &'static str) { + assert_eq!(vtag.reference().namespace_uri().unwrap(), namespace); + } + + #[test] + fn supports_svg() { + let document = web_sys::window().unwrap().document().unwrap(); + + let scope = test_scope(); + let div_el = document.create_element("div").unwrap(); + let namespace = SVG_NAMESPACE; + let namespace = Some(namespace); + let svg_el = document.create_element_ns(namespace, "svg").unwrap(); + + let g_node = html! { }; + let path_node = html! { }; + let svg_node = html! { {path_node} }; + + let svg_tag = assert_vtag(svg_node); + let (_, svg_tag) = svg_tag.attach(&scope, &div_el, NodeRef::default()); + assert_namespace(&svg_tag, SVG_NAMESPACE); + let path_tag = assert_btag_ref(svg_tag.children().get(0).unwrap()); + assert_namespace(path_tag, SVG_NAMESPACE); + + let g_tag = assert_vtag(g_node.clone()); + let (_, g_tag) = g_tag.attach(&scope, &div_el, NodeRef::default()); + assert_namespace(&g_tag, HTML_NAMESPACE); + + let g_tag = assert_vtag(g_node); + let (_, g_tag) = g_tag.attach(&scope, &svg_el, NodeRef::default()); + assert_namespace(&g_tag, SVG_NAMESPACE); + } + + #[test] + fn it_compares_values() { + let a = html! { + + }; + + let b = html! { + + }; + + let c = html! { + + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_kinds() { + let a = html! { + + }; + + let b = html! { + + }; + + let c = html! { + + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_compares_checked() { + let a = html! { + + }; + + let b = html! { + + }; + + let c = html! { + + }; + + assert_eq!(a, b); + assert_ne!(a, c); + } + + #[test] + fn it_allows_aria_attributes() { + let a = html! { +

    + + +

    +

    + }; + if let VNode::VTag(vtag) = a { + assert_eq!( + vtag.attributes + .iter() + .find(|(k, _)| k == &"aria-controls") + .map(|(_, v)| v), + Some("it-works") + ); + } else { + panic!("vtag expected"); + } + } + + #[test] + fn it_does_not_set_missing_class_name() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + let elem = html! {
    }; + let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_btag_mut(&mut elem); + // test if the className has not been set + assert!(!vtag.reference().has_attribute("class")); + } + + fn test_set_class_name(gen_html: impl FnOnce() -> Html) { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + let elem = gen_html(); + let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_btag_mut(&mut elem); + // test if the className has been set + assert!(vtag.reference().has_attribute("class")); + } + + #[test] + fn it_sets_class_name_static() { + test_set_class_name(|| html! {
    }); + } + + #[test] + fn it_sets_class_name_dynamic() { + test_set_class_name(|| html! {
    }); + } + + #[test] + fn controlled_input_synced() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + let expected = "not_changed_value"; + + // Initial state + let elem = html! { }; + let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_btag_ref(&elem); + + // User input + let input_ref = &vtag.reference(); + let input = input_ref.dyn_ref::(); + input.unwrap().set_value("User input"); + + let next_elem = html! { }; + let elem_vtag = assert_vtag(next_elem); + + // Sync happens here + elem_vtag.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); + let vtag = assert_btag_ref(&elem); + + // Get new current value of the input element + let input_ref = &vtag.reference(); + let input = input_ref.dyn_ref::().unwrap(); + + let current_value = input.value(); + + // check whether not changed virtual dom value has been set to the input element + assert_eq!(current_value, expected); + } + + #[test] + fn uncontrolled_input_unsynced() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + // Initial state + let elem = html! { }; + let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_btag_ref(&elem); + + // User input + let input_ref = &vtag.reference(); + let input = input_ref.dyn_ref::(); + input.unwrap().set_value("User input"); + + let next_elem = html! { }; + let elem_vtag = assert_vtag(next_elem); + + // Value should not be refreshed + elem_vtag.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); + let vtag = assert_btag_ref(&elem); + + // Get user value of the input element + let input_ref = &vtag.reference(); + let input = input_ref.dyn_ref::().unwrap(); + + let current_value = input.value(); + + // check whether not changed virtual dom value has been set to the input element + assert_eq!(current_value, "User input"); + + // Need to remove the element to clean up the dirty state of the DOM. Failing this causes + // event listener tests to fail. + parent.remove(); + } + + #[test] + fn dynamic_tags_work() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + let elem = html! { <@{ + let mut builder = String::new(); + builder.push('a'); + builder + }/> }; + + let (_, mut elem) = Reconcilable::attach(elem, &scope, &parent, NodeRef::default()); + let vtag = assert_btag_mut(&mut elem); + // make sure the new tag name is used internally + assert_eq!(vtag.tag(), "a"); + + // Element.tagName is always in the canonical upper-case form. + assert_eq!(vtag.reference().tag_name(), "A"); + } + + #[test] + fn dynamic_tags_handle_value_attribute() { + let div_el = html! { + <@{"div"} value="Hello"/> + }; + let div_vtag = assert_vtag_ref(&div_el); + assert!(div_vtag.value().is_none()); + let v: Option<&str> = div_vtag + .attributes + .iter() + .find(|(k, _)| k == &"value") + .map(|(_, v)| AsRef::as_ref(v)); + assert_eq!(v, Some("Hello")); + + let input_el = html! { + <@{"input"} value="World"/> + }; + let input_vtag = assert_vtag_ref(&input_el); + assert_eq!(input_vtag.value(), Some(&AttrValue::Static("World"))); + assert!(!input_vtag.attributes.iter().any(|(k, _)| k == "value")); + } + + #[test] + fn dynamic_tags_handle_weird_capitalization() { + let el = html! { + <@{"tExTAREa"}/> + }; + let vtag = assert_vtag_ref(&el); + assert_eq!(vtag.tag(), "textarea"); + } + + #[test] + fn reset_node_ref() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + + document().body().unwrap().append_child(&parent).unwrap(); + + let node_ref = NodeRef::default(); + let elem: VNode = html! {
    }; + assert_vtag_ref(&elem); + let (_, elem) = elem.attach(&scope, &parent, NodeRef::default()); + assert_eq!(node_ref.get(), parent.first_child()); + elem.detach(&parent, false); + assert!(node_ref.get().is_none()); + } + + #[test] + fn vtag_reuse_should_reset_ancestors_node_ref() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + document().body().unwrap().append_child(&parent).unwrap(); + + let node_ref_a = NodeRef::default(); + let elem_a = html! {
    }; + let (_, mut elem) = elem_a.attach(&scope, &parent, NodeRef::default()); + + // save the Node to check later that it has been reused. + let node_a = node_ref_a.get().unwrap(); + + let node_ref_b = NodeRef::default(); + let elem_b = html! {
    }; + elem_b.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); + + let node_b = node_ref_b.get().unwrap(); + + assert_eq!(node_a, node_b, "VTag should have reused the element"); + assert!( + node_ref_a.get().is_none(), + "node_ref_a should have been reset when the element was reused." + ); + } + + #[test] + fn vtag_should_not_touch_newly_bound_refs() { + let scope = test_scope(); + let parent = document().create_element("div").unwrap(); + document().body().unwrap().append_child(&parent).unwrap(); + + let test_ref = NodeRef::default(); + let before = html! { + <> +
    + + }; + let after = html! { + <> +
    +
    + + }; + // The point of this diff is to first render the "after" div and then detach the "before" div, + // while both should be bound to the same node ref + + let (_, mut elem) = before.attach(&scope, &parent, NodeRef::default()); + after.reconcile_node(&scope, &parent, NodeRef::default(), &mut elem); + + assert_eq!( + test_ref + .get() + .unwrap() + .dyn_ref::() + .unwrap() + .outer_html(), + "
    " + ); + } +} + +#[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"} +
    • +
    • + {"b"} +
    • +
    + }, + expected: "
    • a
    • b
    ", + }; + + let layout2 = TestLayout { + name: "2", + node: html! { +
      +
    • + {"a"} +
    • +
    • + {"b"} +
    • +
    • + {"d"} +
    • +
    + }, + expected: "
    • a
    • b
    • d
    ", + }; + + let layout3 = TestLayout { + name: "3", + node: html! { +
      +
    • + {"a"} +
    • +
    • + {"b"} +
    • +
    • + {"c"} +
    • +
    • + {"d"} +
    • +
    + }, + expected: "
    • a
    • b
    • c
    • d
    ", + }; + + let layout4 = TestLayout { + name: "4", + node: html! { +
      +
    • + <> + {"a"} + +
    • +
    • + {"b"} +
    • + {"c"} +
    • +
    • + {"d"} +
    • + +
    + }, + expected: "
    • a
    • b
    • c
    • d
    ", + }; + + diff_layouts(vec![layout1, layout2, layout3, layout4]); + } +} + +#[cfg(test)] +mod tests_without_browser { + use crate::html; + + #[test] + fn html_if_bool() { + assert_eq!( + html! { + if true { +
    + } + }, + html! {
    }, + ); + assert_eq!( + html! { + if false { +
    + } else { +
    + } + }, + html! { +
    + }, + ); + assert_eq!( + html! { + if false { +
    + } + }, + html! {}, + ); + + // non-root tests + assert_eq!( + html! { +
    + if true { +
    + } +
    + }, + html! { +
    +
    +
    + }, + ); + assert_eq!( + html! { +
    + if false { +
    + } else { +
    + } +
    + }, + html! { +
    +
    +
    + }, + ); + assert_eq!( + html! { +
    + if false { +
    + } +
    + }, + html! { +
    + <> +
    + }, + ); + } + + #[test] + fn html_if_option() { + let option_foo = Some("foo"); + let none: Option<&'static str> = None; + assert_eq!( + html! { + if let Some(class) = option_foo { +
    + } + }, + html! {
    }, + ); + assert_eq!( + html! { + if let Some(class) = none { +
    + } else { +
    + } + }, + html! {
    }, + ); + assert_eq!( + html! { + if let Some(class) = none { +
    + } + }, + html! {}, + ); + + // non-root tests + assert_eq!( + html! { +
    + if let Some(class) = option_foo { +
    + } +
    + }, + html! {
    }, + ); + assert_eq!( + html! { +
    + if let Some(class) = none { +
    + } else { +
    + } +
    + }, + html! {
    }, + ); + assert_eq!( + html! { +
    + if let Some(class) = none { +
    + } +
    + }, + html! {
    <>
    }, + ); + } +} diff --git a/packages/yew/src/dom_bundle/btext.rs b/packages/yew/src/dom_bundle/btext.rs new file mode 100644 index 00000000000..af152955daf --- /dev/null +++ b/packages/yew/src/dom_bundle/btext.rs @@ -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]); + } +} diff --git a/packages/yew/src/dom_bundle/mod.rs b/packages/yew/src/dom_bundle/mod.rs new file mode 100644 index 00000000000..a1f0b596a14 --- /dev/null +++ b/packages/yew/src/dom_bundle/mod.rs @@ -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, + { + 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; diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/dom_bundle/tests/layout_tests.rs similarity index 76% rename from packages/yew/src/tests/layout_tests.rs rename to packages/yew/src/dom_bundle/tests/layout_tests.rs index 744651bafcc..d0ef714a5fc 100644 --- a/packages/yew/src/tests/layout_tests.rs +++ b/packages/yew/src/dom_bundle/tests/layout_tests.rs @@ -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>) { let document = gloo_utils::document(); - let parent_scope: AnyScope = Scope::::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>) { ); // 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>) { ); // 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>) { } // Sequentially apply each layout - let mut ancestor: Option = None; + let mut bundle: Option = 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>) { "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>) { "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(), diff --git a/packages/yew/src/dom_bundle/tests/mod.rs b/packages/yew/src/dom_bundle/tests/mod.rs new file mode 100644 index 00000000000..1208f4409c5 --- /dev/null +++ b/packages/yew/src/dom_bundle/tests/mod.rs @@ -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, + ) -> 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), + } + } +} diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 15595b91de0..ed833c4d46e 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -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 +pub struct CompStateInner 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; +pub trait Stateful { + fn view(&self) -> HtmlResult; fn rendered(&mut self, first_render: bool); fn destroy(&mut self); @@ -44,7 +41,7 @@ impl Stateful for CompStateInner where COMP: BaseComponent, { - fn view(&self) -> RenderResult { + fn view(&self) -> HtmlResult { self.component.view(&self.context) } @@ -93,23 +90,15 @@ where } } -pub(crate) struct ComponentState { - pub(crate) inner: Box, +pub struct ComponentState { + pub(super) inner: Box, - pub(crate) root_node: VNode, - - /// When a component has no parent, it means that it should not be rendered. - parent: Option, - - next_sibling: NodeRef, + pub(super) render_state: ComponentRenderState, node_ref: NodeRef, has_rendered: bool, suspension: Option, - #[cfg(feature = "ssr")] - html_sender: Option>, - // 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( - parent: Option, - next_sibling: NodeRef, - root_node: VNode, + initial_render_state: ComponentRenderState, node_ref: NodeRef, scope: Scope, props: Rc, - #[cfg(feature = "ssr")] html_sender: Option>, ) -> 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 { - pub(crate) parent: Option, - pub(crate) next_sibling: NodeRef, - pub(crate) placeholder: VNode, - pub(crate) node_ref: NodeRef, - pub(crate) props: Rc, - pub(crate) scope: Scope, - #[cfg(feature = "ssr")] - pub(crate) html_sender: Option>, +pub struct CreateRunner { + pub initial_render_state: ComponentRenderState, + pub node_ref: NodeRef, + pub props: Rc, + pub scope: Scope, } impl Runnable for CreateRunner { @@ -168,34 +145,28 @@ impl Runnable for CreateRunner { 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, NodeRef, NodeRef), - /// Shift Scope. - Shift(Element, NodeRef), } -pub(crate) struct UpdateRunner { - pub(crate) state: Shared>, - pub(crate) event: UpdateEvent, +pub struct UpdateRunner { + pub state: Shared>, + 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>, - pub(crate) parent_to_detach: bool, +pub struct DestroyRunner { + pub state: Shared>, + pub parent_to_detach: bool, } impl Runnable for DestroyRunner { fn run(self: Box) { 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>, +pub struct RenderRunner { + pub state: Shared>, } impl Runnable for RenderRunner { fn run(self: Box) { 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>, +struct RenderedRunner { + state: Shared>, first_render: bool, } @@ -381,9 +323,9 @@ impl Runnable for RenderedRunner { fn run(self: Box) { 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::::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); diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index 3528c7c9470..74443781962 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -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>> + = 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 { + 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 { - pub(crate) scope: Scope, - pub(crate) props: Rc, + scope: Scope, + props: Rc, } impl Context { diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 6229f3b3a57..89529c9d1e2 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -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(Shared>); @@ -65,9 +64,6 @@ pub struct AnyScope { type_id: TypeId, parent: Option>, state: Shared>, - - #[cfg(debug_assertions)] - pub(crate) vcomp_id: usize, } impl fmt::Debug for AnyScope { @@ -82,9 +78,6 @@ impl From> for AnyScope { type_id: TypeId::of::(), 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>; - fn destroy(&mut self, parent_to_detach: bool); - fn shift_node(&self, parent: Element, next_sibling: NodeRef); -} - impl Scoped for Scope { fn to_any(&self) -> AnyScope { self.clone().into() } - fn root_vnode(&self) -> Option> { + fn render_state(&self) -> Option> { 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, 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 Scope { }) } + /// Crate a scope with an optional parent scope pub(crate) fn new(parent: Option) -> 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 Scope { 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, ) { - #[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 Scope { 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 Scope { } /// 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) { let msg_len = messages.len(); @@ -410,24 +384,11 @@ mod feat_ssr { use futures::channel::oneshot; impl Scope { - pub(crate) async fn render_to_string(&self, w: &mut String, props: Rc) { + pub(crate) async fn render_to_string(self, w: &mut String, props: Rc) { 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 { diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index e0885153ca5..e614ff2a77e 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -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::(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::Properties::default()) } /// The main entry point of a Yew application. This function does the @@ -356,7 +359,7 @@ pub fn start_app_with_props(props: COMP::Properties) -> AppHandle where COMP: BaseComponent, { - start_app_with_props_in_element( + start_app_with_props_in_element::( 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, diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index 9e5cd5fe1cb..91bf4c95212 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -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 where COMP: BaseComponent, diff --git a/packages/yew/src/tests/mod.rs b/packages/yew/src/tests/mod.rs deleted file mode 100644 index 7c8881b072f..00000000000 --- a/packages/yew/src/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod layout_tests; diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs index 29c0db7adf4..caa0943d599 100644 --- a/packages/yew/src/virtual_dom/listeners.rs +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -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 = 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>]>), } -impl Listeners { - /// Register listeners and return their handle ID - fn register(el: &Element, pending: &[Option>]) -> 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, - - /// 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)>, -} - -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); - AsRef::::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::::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>>>, -} - -impl Registry { - /// Run f with access to global Registry - #[inline] - fn with(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>]) { - let mut by_desc = - HashMap::>>::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>]) { - 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::().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(ctx: &Context, state: &State) -> Html - where - C: Component, - { - let link = ctx.link().clone(); - let onclick = Callback::from(move |_| { - link.send_message(Message::Action); - scheduler::start_now(); - }); - - if state.stop_listening { - html! { - {state.action} - } - } else { - html! { - - {state.action} - - } - } - } - } - - struct Comp - where - M: Mixin + 'static, - { - state: State, - pd: PhantomData, - } - - impl Component for Comp - where - M: Mixin + 'static, - { - type Message = Message; - type Properties = (); - - fn create(_: &Context) -> Self { - Comp { - state: Default::default(), - pd: PhantomData, - } - } - - fn update(&mut self, _: &Context, 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) -> 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::() - .unwrap() - } - - fn init(tag: &str) -> (AppHandle>, 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::>(root); - scheduler::start_now(); - - (app, get_el_by_tag(tag)) - } - - #[test] - fn synchronous() { - struct Synchronous; - - impl Mixin for Synchronous {} - - let (link, el) = init::("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(ctx: &Context, state: &State) -> Html - where - C: Component, - { - let link = ctx.link().clone(); - let onblur = Callback::from(move |_| { - link.send_message(Message::Action); - scheduler::start_now(); - }); - html! { - - } - } - } - - let (_, el) = init::("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(ctx: &Context, state: &State) -> Html - where - C: Component, - { - if state.stop_listening { - html! { - - } - } else { - let link = ctx.link().clone(); - let cb = Callback::from(move |_| { - link.send_message(Message::Action); - scheduler::start_now(); - }); - html! { - - } - } - } - } - - let (link, el) = init::("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(ctx: &Context, state: &State) -> Html - where - C: Component, - { - 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! { - - } - } - } - - let (_, el) = init::("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(ctx: &Context, state: &State) -> Html - where - C: Component, - { - 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! { - - } - } - } - - let (_, el) = init::("a"); - - assert_count(&el, 0); - - el.click(); - assert_count(&el, 1); - - el.click(); - assert_count(&el, 2); - } - - fn test_input_listener(make_event: impl Fn() -> E) - where - E: JsCast + std::fmt::Debug, - { - struct Input; - - impl Mixin for Input { - fn view(ctx: &Context, state: &State) -> Html - where - C: Component, - { - if state.stop_listening { - html! { -
    - -

    {state.text.clone()}

    -
    - } - } 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! { -
    - -

    {state.text.clone()}

    -
    - } - } - } - } - - let (link, input_el) = init::("input"); - let input_el = input_el.dyn_into::().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::() - .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() - }) - } -} diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index 898d2517928..80e0f82028f 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -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, - new: &IndexMap<&'static str, A>, - old: &IndexMap<&'static str, B>, - ) where - A: AsRef, - B: AsRef, - { - 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(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> 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, - ) -> 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::*; diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 0587a0ab9b6..76b098523a2 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -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>> - = 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 { - 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>, - mountable: Option>, + pub(crate) type_id: TypeId, + pub(crate) mountable: Box, pub(crate) node_ref: NodeRef, pub(crate) key: Option, +} - #[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::(), node_ref, - mountable: Some(Box::new(PropsWrapper::::new(props))), - scope: None, + mountable: Box::new(PropsWrapper::::new(props)), key, - - #[cfg(debug_assertions)] - id: Self::next_id(), } } - - pub(crate) fn root_vnode(&self) -> Option + '_> { - self.scope.as_ref().and_then(|scope| scope.root_vnode()) - } - - /// Take ownership of [Box] or panic with error message, if component is not mounted - #[inline] - fn take_scope(&mut self) -> Box { - 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; - fn mount( - self: Box, - node_ref: NodeRef, - parent_scope: &AnyScope, - parent: Element, - next_sibling: NodeRef, - ) -> Box; - fn reuse(self: Box, 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 { - props: Rc, -} - -impl PropsWrapper { - fn new(props: Rc) -> Self { - Self { props } - } -} - -impl Mountable for PropsWrapper { - fn copy(&self) -> Box { - let wrapper: PropsWrapper = PropsWrapper { - props: Rc::clone(&self.props), - }; - Box::new(wrapper) - } - - fn mount( - self: Box, - node_ref: NodeRef, - parent_scope: &AnyScope, - parent: Element, - next_sibling: NodeRef, - ) -> Box { - let scope: Scope = Scope::new(Some(parent_scope.clone())); - scope.mount_in_place(parent, next_sibling, node_ref, self.props); - - Box::new(scope) - } - - fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { - let scope: Scope = 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 = 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, - ) -> 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 fmt::Debug for VChild { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("VChild<_>") @@ -308,608 +121,18 @@ impl fmt::Debug for VChild { #[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 { - Comp - } - - fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { - unimplemented!(); - } - - fn view(&self, _ctx: &Context) -> Html { - html! {
    } - } - } - - #[test] - fn update_loop() { - let document = gloo_utils::document(); - let parent_scope: AnyScope = crate::html::Scope::::new(None).into(); - let parent_element = document.create_element("div").unwrap(); - - let mut ancestor = html! { }; - ancestor.apply(&parent_scope, &parent_element, NodeRef::default(), None); - scheduler::start_now(); - - for _ in 0..10000 { - let mut node = html! { }; - node.apply( - &parent_scope, - &parent_element, - NodeRef::default(), - Some(ancestor), - ); - scheduler::start_now(); - ancestor = node; - } - } - - #[test] - fn set_properties_to_component() { - html! { - - }; - - html! { - - }; - - html! { - - }; - - html! { - - }; - - let props = Props { - field_1: 1, - field_2: 1, - }; - - html! { - - }; - } - - #[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! { }); - check_key(html! { }); - check_key(html! { }); - check_key(html! { }); - check_key(html! { }); - } - - #[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! { }); - check_node_ref(html! { }); - check_node_ref(html! { }); - check_node_ref(html! { }); - check_node_ref(html! { }); - } - - #[test] - fn vchild_partialeq() { - let vchild1: VChild = VChild::new( - Props { - field_1: 1, - field_2: 1, - }, - NodeRef::default(), - None, - ); - - let vchild2: VChild = VChild::new( - Props { - field_1: 1, - field_2: 1, - }, - NodeRef::default(), - None, - ); - - let vchild3: VChild = 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 - } - fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { - unimplemented!(); - } - fn changed(&mut self, _ctx: &Context) -> bool { - unimplemented!(); - } - fn view(&self, ctx: &Context) -> Html { - let item_iter = ctx - .props() - .children - .iter() - .map(|item| html! {
  • { item }
  • }); - html! { -
      { for item_iter }
    - } - } - } - - 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! {{ text }}) - .collect(); - let children_renderer = Children::new(children.clone()); - let expected_html = "\ -
      \ -
    • a
    • \ -
    • b
    • \ -
    • c
    • \ -
    "; - - let prop_method = html! { - - }; - assert_eq!(get_html(prop_method, &scope, &parent), expected_html); - - let children_renderer_method = html! { - - { children_renderer } - - }; - assert_eq!( - get_html(children_renderer_method, &scope, &parent), - expected_html - ); - - let direct_method = html! { - - { children.clone() } - - }; - assert_eq!(get_html(direct_method, &scope, &parent), expected_html); - - let for_method = html! { - - { for children } - - }; - 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! { }; - 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 { - _marker: PhantomData, - } - - #[derive(Properties, Clone, PartialEq)] - struct CompProps { - #[prop_or_default] - children: Children, - } - - impl Component for Comp { - type Message = (); - type Properties = CompProps; - - fn create(_: &Context) -> Self { - Comp { - _marker: PhantomData::default(), - } - } - - fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { - unimplemented!(); - } - - fn view(&self, ctx: &Context) -> Html { - html! { - <>{ ctx.props().children.clone() } - } - } - } - - struct A; - struct B; - - #[test] - fn diff() { - let layout1 = TestLayout { - name: "1", - node: html! { - > - >> - {"C"} - > - }, - expected: "C", - }; - - let layout2 = TestLayout { - name: "2", - node: html! { - > - {"A"} - > - }, - expected: "A", - }; - - let layout3 = TestLayout { - name: "3", - node: html! { - > - >> - {"B"} - > - }, - expected: "B", - }; - - let layout4 = TestLayout { - name: "4", - node: html! { - > - >{"A"}> - {"B"} - > - }, - expected: "AB", - }; - - let layout5 = TestLayout { - name: "5", - node: html! { - > - <> - > - {"A"} - > - - {"B"} - > - }, - expected: "AB", - }; - - let layout6 = TestLayout { - name: "6", - node: html! { - > - <> - > - {"A"} - > - {"B"} - - {"C"} - > - }, - expected: "ABC", - }; - - let layout7 = TestLayout { - name: "7", - node: html! { - > - <> - > - {"A"} - > - > - {"B"} - > - - {"C"} - > - }, - expected: "ABC", - }; - - let layout8 = TestLayout { - name: "8", - node: html! { - > - <> - > - {"A"} - > - > - > - {"B"} - > - > - - {"C"} - > - }, - expected: "ABC", - }; - - let layout9 = TestLayout { - name: "9", - node: html! { - > - <> - <> - {"A"} - - > - > - {"B"} - > - > - - {"C"} - > - }, - expected: "ABC", - }; - - let layout10 = TestLayout { - name: "10", - node: html! { - > - <> - > - > - {"A"} - > - > - <> - {"B"} - - - {"C"} - > - }, - expected: "ABC", - }; - - let layout11 = TestLayout { - name: "11", - node: html! { - > - <> - <> - > - > - {"A"} - > - {"B"} - > - - - {"C"} - > - }, - expected: "ABC", - }; - - let layout12 = TestLayout { - name: "12", - node: html! { - > - <> - >> - <> - > - <> - > - {"A"} - > - <> - > - >> - <> - {"B"} - <> - >> - > - - > - <> - - >> - - {"C"} - >> - <> - > - }, - 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 - } - - fn view(&self, ctx: &Context) -> Html { - html! { -
      - { for ctx.props().children.iter().map(|child| html! {
    • { child }
    • }) } -
    - } - } - } - - let layout = TestLayout { - name: "13", - node: html! { - - if true { - { "hello" } - { "world" } - } else { - { "goodbye" } - { "world" } - } - - }, - expected: "
    • helloworld
    ", - }; - - diff_layouts(vec![layout]); - } -} - #[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))] mod ssr_tests { use tokio::test; diff --git a/packages/yew/src/virtual_dom/vlist.rs b/packages/yew/src/virtual_dom/vlist.rs index e655cf873ac..9304f00d2f6 100644 --- a/packages/yew/src/virtual_dom/vlist.rs +++ b/packages/yew/src/virtual_dom/vlist.rs @@ -1,18 +1,15 @@ //! This module contains fragments implementation. -use super::{Key, VDiff, VNode, VText}; -use crate::html::{AnyScope, NodeRef}; -use std::collections::HashMap; +use super::{Key, VNode}; use std::ops::{Deref, DerefMut}; -use web_sys::Element; /// This struct represents a fragment of the Virtual DOM tree. #[derive(Clone, Debug, PartialEq)] pub struct VList { /// The list of child [VNode]s - children: Vec, + pub(crate) children: Vec, /// All [VNode]s in the VList have keys - fully_keyed: bool, + pub(crate) fully_keyed: bool, pub key: Option, } @@ -41,47 +38,6 @@ impl DerefMut for VList { } } -/// Log an operation during tests for debugging purposes -/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. -macro_rules! test_log { - ($fmt:literal, $($arg:expr),* $(,)?) => { - #[cfg(all(test, feature = "wasm_test", verbose_tests))] - ::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*); - }; -} - -struct ElementWriter<'s> { - parent_scope: &'s AnyScope, - parent: &'s Element, - next_sibling: NodeRef, -} - -impl<'s> ElementWriter<'s> { - fn add(self, node: &mut VNode) -> Self { - test_log!("adding: {:?}", node); - self.write(node, None) - } - - fn patch(self, node: &mut VNode, ancestor: VNode) -> Self { - test_log!("patching: {:?} -> {:?}", ancestor, node); - self.write(node, Some(ancestor)) - } - - fn write(self, node: &mut VNode, ancestor: Option) -> Self { - test_log!("parent={:?}", self.parent.outer_html()); - // Advance the next sibling reference (from right to left) and log it for testing purposes - // Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate. - #[cfg(all(test, feature = "wasm_test", verbose_tests))] - let current = format!("{:?}", self.next_sibling); - let next = node.apply(self.parent_scope, self.parent, self.next_sibling, ancestor); - test_log!("advance next_sibling: {:?} -> {:?}", current, next); - Self { - next_sibling: next, - ..self - } - } -} - impl VList { /// Creates a new empty [VList] instance. pub const fn new() -> Self { @@ -126,167 +82,12 @@ impl VList { pub fn recheck_fully_keyed(&mut self) { self.fully_keyed = self.children.iter().all(|ch| ch.has_key()); } - - /// Diff and patch unkeyed child lists - fn apply_unkeyed( - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - lefts: &mut [VNode], - rights: Vec, - ) -> NodeRef { - let mut diff = lefts.len() as isize - rights.len() as isize; - let mut lefts_it = lefts.iter_mut().rev(); - let mut rights_it = rights.into_iter().rev(); - let mut writer = ElementWriter { - parent_scope, - parent, - next_sibling, - }; - - // Add missing nodes - while diff > 0 { - let l = lefts_it.next().unwrap(); - writer = writer.add(l); - diff -= 1; - } - // Remove extra nodes - while diff < 0 { - let mut r = rights_it.next().unwrap(); - test_log!("removing: {:?}", r); - r.detach(parent, false); - diff += 1; - } - - for (l, r) in lefts_it.zip(rights_it) { - writer = writer.patch(l, r); - } - - writer.next_sibling - } - - /// Diff and patch fully keyed child lists. - /// - /// Optimized for node addition or removal from either end of the list and small changes in the - /// middle. - fn apply_keyed( - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - lefts: &mut [VNode], - rights: Vec, - ) -> NodeRef { - macro_rules! map_keys { - ($src:expr) => { - $src.iter() - .map(|v| v.key().expect("unkeyed child in fully keyed list")) - .collect::>() - }; - } - let lefts_keys = map_keys!(lefts); - let rights_keys = map_keys!(rights); - - /// Find the first differing key in 2 iterators - fn matching_len<'a, 'b>( - a: impl Iterator, - b: impl Iterator, - ) -> usize { - a.zip(b).take_while(|(a, b)| a == b).count() - } - - // Find first key mismatch from the front - let from_start = matching_len(lefts_keys.iter(), rights_keys.iter()); - - if from_start == std::cmp::min(lefts.len(), rights.len()) { - // No key changes - return Self::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights); - } - - let mut writer = ElementWriter { - parent_scope, - parent, - next_sibling, - }; - // Find first key mismatch from the back - let from_end = matching_len( - lefts_keys[from_start..].iter().rev(), - rights_keys[from_start..].iter().rev(), - ); - // We partially deconstruct the rights vector in several steps. - let mut rights = rights; - - // Diff matching children at the end - let lefts_to = lefts_keys.len() - from_end; - let rights_to = rights_keys.len() - from_end; - for (l, r) in lefts[lefts_to..] - .iter_mut() - .zip(rights.drain(rights_to..)) - .rev() - { - writer = writer.patch(l, r); - } - - // Diff mismatched children in the middle - let mut next_right_key: Option<&Key> = None; - let mut rights_diff: HashMap<&Key, (VNode, Option<&Key>)> = - HashMap::with_capacity(rights_to - from_start); - for (k, v) in rights_keys[from_start..rights_to] - .iter() - .zip(rights.drain(from_start..)) // rights_to.. has been drained already - .rev() - { - let next_r_key = std::mem::replace(&mut next_right_key, Some(k)); - rights_diff.insert(k, (v, next_r_key)); - } - let mut next_left_key: Option<&Key> = None; - for (l_key, l) in lefts_keys[from_start..lefts_to] - .iter() - .zip(lefts[from_start..lefts_to].iter_mut()) - .rev() - { - match rights_diff.remove(l_key) { - // Reorder and diff any existing children - Some((r, next_r_key)) => { - match (next_r_key, next_left_key) { - // If the next sibling was already the same, we don't need to move the node - (Some(r_next), Some(l_next)) if r_next == l_next => (), - _ => { - test_log!("moving as next: {:?}", r); - r.move_before(parent, &writer.next_sibling.get()); - } - } - writer = writer.patch(l, r); - } - // Add new children - None => { - writer = writer.add(l); - } - } - next_left_key = Some(l_key); - } - - // Remove any extra rights - for (_, (mut r, _)) in rights_diff.drain() { - test_log!("removing: {:?}", r); - r.detach(parent, false); - } - - // Diff matching children at the start - for (l, r) in lefts[..from_start] - .iter_mut() - .zip(rights.into_iter()) // from_start.. has been drained already - .rev() - { - writer = writer.patch(l, r); - } - - writer.next_sibling - } } #[cfg(feature = "ssr")] mod feat_ssr { use super::*; + use crate::html::AnyScope; impl VList { pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) { @@ -306,990 +107,6 @@ mod feat_ssr { } } -impl VDiff for VList { - fn detach(&mut self, parent: &Element, parent_to_detach: bool) { - for mut child in self.children.drain(..) { - child.detach(parent, parent_to_detach); - } - } - - fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) { - let mut last_node_ref = next_sibling; - - for node in self.children.iter().rev() { - node.shift(previous_parent, next_parent, last_node_ref); - last_node_ref = NodeRef::default(); - last_node_ref.set(node.first_node()); - } - } - - fn apply( - &mut self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ancestor: Option, - ) -> NodeRef { - // Here, we will try to diff the previous list elements with the new - // ones we want to insert. For that, we will use two lists: - // - lefts: new elements to render in the DOM - // - rights: previously rendered elements. - // - // The left items are known since we want to insert them - // (self.children). For the right ones, we will look at the ancestor, - // i.e. the current DOM list element that we want to replace with self. - - if self.children.is_empty() { - // Without a placeholder the next element becomes first - // and corrupts the order of rendering - // We use empty text element to stake out a place - self.add_child(VText::new("").into()); - } - - let lefts = &mut self.children; - let (rights, rights_fully_keyed) = match ancestor { - // If the ancestor is also a VList, then the "right" list is the previously - // rendered items. - Some(VNode::VList(v)) => (v.children, v.fully_keyed), - - // If the ancestor was not a VList, then the "right" list is a single node - Some(v) => { - let has_key = v.has_key(); - (vec![v], has_key) - } - - // No unkeyed nodes in an empty VList - _ => (vec![], true), - }; - test_log!("lefts: {:?}", lefts); - test_log!("rights: {:?}", rights); - - #[allow(clippy::let_and_return)] - let first = if self.fully_keyed && rights_fully_keyed { - Self::apply_keyed(parent_scope, parent, next_sibling, lefts, rights) - } else { - Self::apply_unkeyed(parent_scope, parent, next_sibling, lefts, rights) - }; - test_log!("result: {:?}", lefts); - first - } -} - -#[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"} - {"b"} - <> - {"c"} - {"d"} - - {"e"} - - }, - expected: "abcde", - }; - - let layout2 = TestLayout { - name: "2", - node: html! { - <> - {"a"} - {"b"} - <> - {"e"} - {"f"} - - }, - expected: "abef", - }; - - let layout3 = TestLayout { - name: "3", - node: html! { - <> - {"a"} - <> - {"b"} - {"e"} - - }, - expected: "abe", - }; - - let layout4 = TestLayout { - name: "4", - node: html! { - <> - {"a"} - <> - {"c"} - {"d"} - - {"b"} - {"e"} - - }, - expected: "acdbe", - }; - - diff_layouts(vec![layout1, layout2, layout3, layout4]); - } -} - -#[cfg(test)] -mod layout_tests_keys { - extern crate self as yew; - - use crate::html; - use crate::tests::layout_tests::{diff_layouts, TestLayout}; - use crate::virtual_dom::VNode; - use crate::{Children, Component, Context, Html, Properties}; - 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(Properties, Clone, PartialEq)] - struct CountingCompProps { - id: usize, - #[prop_or(false)] - can_change: bool, - } - - impl Component for Comp { - type Message = (); - type Properties = CountingCompProps; - - fn create(_: &Context) -> Self { - Comp {} - } - - fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { - unimplemented!(); - } - - fn view(&self, ctx: &Context) -> Html { - html! {

    { ctx.props().id }

    } - } - } - - #[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() - } - - fn update(&mut self, _ctx: &Context, _: Self::Message) -> bool { - unimplemented!(); - } - - fn view(&self, ctx: &Context) -> Html { - html! { <>{ for ctx.props().children.iter() } } - } - } - - #[test] - fn diff() { - let mut layouts = vec![]; - - let vref_node: Node = gloo_utils::document().create_element("i").unwrap().into(); - layouts.push(TestLayout { - name: "All VNode types as children", - node: html! { - <> - {"a"} - - {"c"} - {"d"} - - - {"foo"} - {"bar"} - - {VNode::VRef(vref_node)} - - }, - expected: "acd

    0

    foobar", - }); - - layouts.extend(vec![ - TestLayout { - name: "Inserting into VList first child - before", - node: html! { - <> - - - -

    - - }, - expected: "

    ", - }, - TestLayout { - name: "Inserting into VList first child - after", - node: html! { - <> - - - - -

    - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "No matches - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "No matches - after", - node: html! { - <> - -

    - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Append - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Append - after", - node: html! { - <> - - -

    - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Prepend - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Prepend - after", - node: html! { - <> -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete first - before", - node: html! { - <> - - -

    - - }, - expected: "

    ", - }, - TestLayout { - name: "Delete first - after", - node: html! { - <> - -

    - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete last - before", - node: html! { - <> - - -

    - - }, - expected: "

    ", - }, - TestLayout { - name: "Delete last - after", - node: html! { - <> - - - - }, - expected: "", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete last and change node type - before", - node: html! { - <> - - -

    - - }, - expected: "

    ", - }, - TestLayout { - name: "Delete last - after", - node: html! { - <> - - - - - }, - expected: "", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete middle - before", - node: html! { - <> - - -

    - - - }, - expected: "

    ", - }, - TestLayout { - name: "Delete middle - after", - node: html! { - <> - - -

    - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Delete middle and change node type - before", - node: html! { - <> - - -

    - - - }, - expected: "

    ", - }, - TestLayout { - name: "Delete middle and change node type- after", - node: html! { - <> - - -

    - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse - before", - node: html! { - <> - - -

    - - - }, - expected: "

    ", - }, - TestLayout { - name: "Reverse - after", - node: html! { - <> - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse and change node type - before", - node: html! { - <> - - - - - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Reverse and change node type - after", - node: html! { - <> - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 1&2 - before", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap 1&2 - after", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 1&2 and change node type - before", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap 1&2 and change node type - after", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "test - before", - node: html! { - <> - - -

    - - - - - -

    - - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap 4&5 - after", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 4&5 - before", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap 4&5 - after", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap 1&5 - before", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap 1&5 - after", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Move 2 after 4 - before", - node: html! { - <> - - -

    - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Move 2 after 4 - after", - node: html! { - <> - -

    - - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap lists - before", - node: html! { - <> - - - - - - - - - - }, - expected: "", - }, - TestLayout { - name: "Swap lists - after", - node: html! { - <> - - - - - - - - - - }, - expected: "", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Swap lists with in-between - before", - node: html! { - <> - - - - -

    - - - - - - }, - expected: "

    ", - }, - TestLayout { - name: "Swap lists with in-between - after", - node: html! { - <> - - - - -

    - - - - - - }, - expected: "

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Insert VComp front - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Insert VComp front - after", - node: html! { - <> - - - - - }, - expected: "

    0

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Insert VComp middle - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Insert VComp middle - after", - node: html! { - <> - - - - - }, - expected: "

    0

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Insert VComp back - before", - node: html! { - <> - - - - }, - expected: "", - }, - TestLayout { - name: "Insert VComp back - after", - node: html! { - <> - - - - - }, - expected: "

    0

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse VComp children - before", - node: html! { - <> - - - - - }, - expected: "

    1

    2

    3

    ", - }, - TestLayout { - name: "Reverse VComp children - after", - node: html! { - <> - - - - - }, - expected: "

    3

    2

    1

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reverse VComp children with children - before", - node: html! { - <> -

    {"11"}

    {"12"}

    -

    {"21"}

    {"22"}

    -

    {"31"}

    {"32"}

    - - }, - expected: "

    11

    12

    21

    22

    31

    32

    ", - }, - TestLayout { - name: "Reverse VComp children with children - after", - node: html! { - <> -

    {"31"}

    {"32"}

    -

    {"21"}

    {"22"}

    -

    {"11"}

    {"12"}

    - - }, - expected: "

    31

    32

    21

    22

    11

    12

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Complex component update - before", - node: html! { - - - - - }, - expected: "

    1

    2

    ", - }, - TestLayout { - name: "Complex component update - after", - node: html! { - - - - - -

    {"2"}

    -
    -
    - }, - expected: "

    1

    2

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Reorder VComp children with children - before", - node: html! { - <> -

    {"1"}

    -

    {"3"}

    -

    {"5"}

    -

    {"2"}

    -

    {"4"}

    -

    {"6"}

    - - }, - expected: "

    1

    3

    5

    2

    4

    6

    ", - }, - TestLayout { - name: "Reorder VComp children with children - after", - node: html! { - <> - - - - - - - - }, - expected: "

    6

    5

    4

    3

    2

    1

    ", - }, - ]); - - layouts.extend(vec![ - TestLayout { - name: "Replace and reorder components - before", - node: html! { - -

    {"1"}

    -

    {"2"}

    -

    {"3"}

    -
    - }, - expected: "

    1

    2

    3

    ", - }, - TestLayout { - name: "Replace and reorder components - after", - node: html! { - - - - - - }, - expected: "

    3

    2

    1

    ", - }, - ]); - - diff_layouts(layouts); - } -} - #[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))] mod ssr_tests { use tokio::test; diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index dde3d045223..3b98a0e7ae4 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -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 { + 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 { - 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) { - 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, - ) -> 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 for VNode { } } +impl From for VNode { + #[inline] + fn from(vportal: VPortal) -> Self { + VNode::VPortal(vportal) + } +} + impl From> 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: "", - }; - - let layout2 = TestLayout { - name: "2", - node: vref_node_2, - expected: "", - }; - - diff_layouts(vec![layout1, layout2]); - } -} diff --git a/packages/yew/src/virtual_dom/vportal.rs b/packages/yew/src/virtual_dom/vportal.rs index b1ef039d598..abcb4f1a19b 100644 --- a/packages/yew/src/virtual_dom/vportal.rs +++ b/packages/yew/src/virtual_dom/vportal.rs @@ -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, - /// 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, - ) -> 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) -> Self { + pub fn new_before(content: VNode, host: Element, inner_sibling: Option) -> 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 { - 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! { -
    - {VNode::VRef(first_target.clone().into())} - {VNode::VRef(second_target.clone().into())} - {VNode::VPortal(VPortal::new( - html! { {"PORTAL"} }, - first_target.clone(), - ))} - {"AFTER"} -
    - }, - expected: "
    PORTALAFTER
    ", - }); - layouts.push(TestLayout { - name: "Portal - second target", - node: html! { -
    - {VNode::VRef(first_target.clone().into())} - {VNode::VRef(second_target.clone().into())} - {VNode::VPortal(VPortal::new( - html! { {"PORTAL"} }, - second_target.clone(), - ))} - {"AFTER"} -
    - }, - expected: "
    PORTALAFTER
    ", - }); - layouts.push(TestLayout { - name: "Portal - replaced by text", - node: html! { -
    - {VNode::VRef(first_target.clone().into())} - {VNode::VRef(second_target.clone().into())} - {"FOO"} - {"AFTER"} -
    - }, - expected: "
    FOOAFTER
    ", - }); - layouts.push(TestLayout { - name: "Portal - next sibling", - node: html! { -
    - {VNode::VRef(target_with_child.clone().into())} - {VNode::VPortal(VPortal::new_before( - html! { {"PORTAL"} }, - target_with_child.clone(), - Some(target_child.clone().into()), - ))} -
    - }, - expected: "
    PORTAL
    ", - }); - - diff_layouts(layouts) - } } diff --git a/packages/yew/src/virtual_dom/vsuspense.rs b/packages/yew/src/virtual_dom/vsuspense.rs index 8d47e7a3053..690b6b94bee 100644 --- a/packages/yew/src/virtual_dom/vsuspense.rs +++ b/packages/yew/src/virtual_dom/vsuspense.rs @@ -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, - + pub(crate) children: Box, /// Fallback nodes when suspended. - fallback: Box, - + pub(crate) fallback: Box, /// The element to attach to when children is not attached to DOM - detached_parent: Option, - + pub(crate) detached_parent: Option, /// Whether the current status is suspended. - suspended: bool, - + pub(crate) suspended: bool, /// The Key. pub(crate) key: Option, } @@ -37,122 +32,12 @@ impl VSuspense { key, } } - - pub(crate) fn first_node(&self) -> Option { - 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, - ) -> 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) { diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index 0df3eafd966..ed9debd0dba 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1,18 +1,14 @@ //! This module contains the implementation of a virtual element node [VTag]. -use super::{Apply, AttrValue, Attributes, Key, Listener, Listeners, VDiff, VList, VNode}; -use crate::html::{AnyScope, IntoPropValue, NodeRef}; -use gloo::console; -use gloo_utils::document; -use std::borrow::Cow; +use super::{AttrValue, Attributes, Key, Listener, Listeners, VList, VNode}; +use crate::html::{IntoPropValue, NodeRef}; use std::cmp::PartialEq; -use std::hint::unreachable_unchecked; use std::marker::PhantomData; use std::mem; use std::ops::Deref; use std::rc::Rc; -use wasm_bindgen::JsCast; -use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; +use std::{borrow::Cow, ops::DerefMut}; +use web_sys::{HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; /// SVG namespace string used for creating svg elements pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; @@ -20,110 +16,81 @@ pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; /// Default namespace for html elements pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; -// Value field corresponding to an [Element]'s `value` property +/// Value field corresponding to an [Element]'s `value` property #[derive(Clone, Debug, Eq, PartialEq)] -struct Value(Option, PhantomData); +pub(crate) struct Value(Option, PhantomData); -impl Default for Value { +impl Default for Value { fn default() -> Self { - Value(None, PhantomData) + Self::new(None) } } -impl Apply for Value { - type Element = T; - - fn apply(&mut self, el: &Self::Element) { - if let Some(v) = &self.0 { - el.set_value(v); - } +impl Value { + /// Create a new value. The caller should take care that the value is valid for the element's `value` property + fn new(value: Option) -> Self { + Value(value, PhantomData) } - - fn apply_diff(&mut self, el: &Self::Element, ancestor: Self) { - match (&self.0, &ancestor.0) { - (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) => (), - } + /// Set a new value. The caller should take care that the value is valid for the element's `value` property + fn set(&mut self, value: Option) { + self.0 = value; } } -/// Able to have its value read or set -trait AccessValue { - fn value(&self) -> String; - fn set_value(&self, v: &str); -} - -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 Deref for Value { + type Target = Option; + fn deref(&self) -> &Self::Target { + &self.0 + } } -impl_access_value! {InputElement TextAreaElement} /// Fields specific to -/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag]s +/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag](crate::virtual_dom::VTag)s #[derive(Debug, Clone, Default, Eq, PartialEq)] -struct InputFields { +pub(crate) struct InputFields { /// Contains a value of an /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). - value: Value, - + pub(crate) value: Value, /// Represents `checked` attribute of /// [input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-checked). /// It exists to override standard behavior of `checked` attribute, because /// in original HTML it sets `defaultChecked` value of `InputElement`, but for reactive /// frameworks it's more useful to control `checked` value of an `InputElement`. - checked: bool, + pub(crate) checked: bool, } -impl Apply for InputFields { - type Element = InputElement; - - fn apply(&mut self, el: &Self::Element) { - // 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); +impl Deref for InputFields { + type Target = Value; - self.value.apply(el); + fn deref(&self) -> &Self::Target { + &self.value } +} - fn apply_diff(&mut self, el: &Self::Element, ancestor: 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); +impl DerefMut for InputFields { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.value + } +} - self.value.apply_diff(el, ancestor.value); +impl InputFields { + /// Crate new attributes for an [InputElement] element + fn new(value: Option, checked: bool) -> Self { + Self { + value: Value::new(value), + checked, + } } } /// [VTag] fields that are specific to different [VTag] kinds. /// Decreases the memory footprint of [VTag] by avoiding impossible field and value combinations. #[derive(Debug, Clone)] -enum VTagInner { +pub(crate) enum VTagInner { /// Fields specific to /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) /// [VTag]s Input(InputFields), - /// Fields specific to /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) /// [VTag]s @@ -132,12 +99,10 @@ enum VTagInner { /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) value: Value, }, - /// Fields for all other kinds of [VTag]s Other { /// A tag of the element. tag: Cow<'static, str>, - /// List of child nodes children: VList, }, @@ -146,39 +111,19 @@ enum VTagInner { /// A type for a virtual /// [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) /// representation. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct VTag { /// [VTag] fields that are specific to different [VTag] kinds. - inner: VTagInner, - + pub(crate) inner: VTagInner, /// List of attached listeners. - listeners: Listeners, - - /// A reference to the DOM [`Element`]. - reference: Option, - + pub(crate) listeners: Listeners, /// A node reference used for DOM access in Component lifecycle methods pub node_ref: NodeRef, - /// List of attributes. pub attributes: Attributes, - pub key: Option, } -impl Clone for VTag { - fn clone(&self) -> Self { - VTag { - inner: self.inner.clone(), - reference: None, - listeners: self.listeners.clone(), - attributes: self.attributes.clone(), - node_ref: self.node_ref.clone(), - key: self.key.clone(), - } - } -} - impl VTag { /// Creates a new [VTag] instance with `tag` name (cannot be changed later in DOM). pub fn new(tag: impl Into>) -> Self { @@ -221,12 +166,12 @@ impl VTag { listeners: Listeners, ) -> Self { VTag::new_base( - VTagInner::Input(InputFields { - value: Value(value, PhantomData), + VTagInner::Input(InputFields::new( + value, // In HTML node `checked` attribute sets `defaultChecked` parameter, // but we use own field to control real `checked` parameter checked, - }), + )), node_ref, key, attributes, @@ -254,7 +199,7 @@ impl VTag { ) -> Self { VTag::new_base( VTagInner::Textarea { - value: Value(value, PhantomData), + value: Value::new(value), }, node_ref, key, @@ -301,7 +246,6 @@ impl VTag { ) -> Self { VTag { inner, - reference: None, attributes, listeners, node_ref, @@ -309,7 +253,7 @@ impl VTag { } } - /// Returns tag of an [Element]. In HTML tags are always uppercase. + /// Returns tag of an [Element](web_sys::Element). In HTML tags are always uppercase. pub fn tag(&self) -> &str { match &self.inner { VTagInner::Input { .. } => "input", @@ -321,7 +265,7 @@ impl VTag { /// Add [VNode] child. pub fn add_child(&mut self, child: VNode) { if let VTagInner::Other { children, .. } = &mut self.inner { - children.add_child(child); + children.add_child(child) } } @@ -360,8 +304,8 @@ impl VTag { /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) pub fn value(&self) -> Option<&AttrValue> { match &self.inner { - VTagInner::Input(f) => f.value.0.as_ref(), - VTagInner::Textarea { value } => value.0.as_ref(), + VTagInner::Input(f) => f.as_ref(), + VTagInner::Textarea { value } => value.as_ref(), VTagInner::Other { .. } => None, } } @@ -372,10 +316,10 @@ impl VTag { pub fn set_value(&mut self, value: impl IntoPropValue>) { match &mut self.inner { VTagInner::Input(f) => { - f.value.0 = value.into_prop_value(); + f.set(value.into_prop_value()); } VTagInner::Textarea { value: dst } => { - dst.0 = value.into_prop_value(); + dst.set(value.into_prop_value()); } VTagInner::Other { .. } => (), } @@ -400,12 +344,6 @@ impl VTag { } } - /// Returns reference to the [Element] associated with this [VTag], if this [VTag] has already - /// been mounted in the DOM - pub fn reference(&self) -> Option<&Element> { - self.reference.as_ref() - } - /// Adds a key-value pair to attributes /// /// Not every attribute works when it set as an attribute. We use workarounds for: @@ -431,7 +369,7 @@ impl VTag { .insert(key, value.into_prop_value()); } - /// Add event listener on the [VTag]'s [Element]. + /// Add event listener on the [VTag]'s [Element](web_sys::Element). /// Returns `true` if the listener has been added, `false` otherwise. pub fn add_listener(&mut self, listener: Rc) -> bool { match &mut self.listeners { @@ -446,183 +384,13 @@ impl VTag { self.set_listeners(listeners.into()); true } - Listeners::Registered(_) => false, } } - /// Set event listeners on the [VTag]'s [Element] + /// Set event listeners on the [VTag]'s [Element](web_sys::Element) pub fn set_listeners(&mut self, listeners: Box<[Option>]>) { self.listeners = Listeners::Pending(listeners); } - - fn create_element(&self, parent: &Element) -> Element { - let tag = self.tag(); - if tag == "svg" - || parent - .namespace_uri() - .map_or(false, |ns| ns == SVG_NAMESPACE) - { - let namespace = Some(SVG_NAMESPACE); - document() - .create_element_ns(namespace, tag) - .expect("can't create namespaced element for vtag") - } else { - document() - .create_element(tag) - .expect("can't create element for vtag") - } - } -} - -impl VDiff for VTag { - /// Remove VTag from parent. - fn detach(&mut self, parent: &Element, parent_to_detach: bool) { - let node = self - .reference - .take() - .expect("tried to remove not rendered VTag from DOM"); - - self.listeners.unregister(); - - // recursively remove its children - if let VTagInner::Other { children, .. } = &mut self.inner { - // This tag will be removed, so there's no point to remove any child. - children.detach(&node, true); - } - if !parent_to_detach { - let result = parent.remove_child(&node); - - if result.is_err() { - console::warn!("Node not found to remove VTag"); - } - } - // It could be that the ref was already reused when rendering another element. - // Only unset the ref it still belongs to our node - if self.node_ref.get().as_ref() == Some(&node) { - self.node_ref.set(None); - } - } - - 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 tag over DOM [Element], but it also compares this with an ancestor [VTag] - /// to compute what to patch in the actual DOM nodes. - fn apply( - &mut self, - parent_scope: &AnyScope, - parent: &Element, - next_sibling: NodeRef, - ancestor: Option, - ) -> NodeRef { - // This kind of branching patching routine reduces branch predictor misses and the need to - // unpack the enums (including `Option`s) all the time, resulting in a more streamlined - // patching flow - let (ancestor_tag, el) = match ancestor { - Some(mut ancestor) => { - // If the ancestor is a tag of the same type, don't recreate, keep the - // old tag and update its attributes and children. - if match &ancestor { - VNode::VTag(a) => { - self.key == a.key - && match (&self.inner, &a.inner) { - (VTagInner::Input(_), VTagInner::Input(_)) - | (VTagInner::Textarea { .. }, VTagInner::Textarea { .. }) => true, - ( - VTagInner::Other { tag: l, .. }, - VTagInner::Other { tag: r, .. }, - ) => l == r, - _ => false, - } - } - _ => false, - } { - match ancestor { - VNode::VTag(mut a) => { - // Preserve the reference that already exists - let el = a.reference.take().unwrap(); - if self.node_ref.get().as_ref() == self.reference.as_deref() { - a.node_ref.set(None); - } - (Some(a), el) - } - _ => unsafe { unreachable_unchecked() }, - } - } else { - let el = self.create_element(parent); - super::insert_node(&el, parent, ancestor.first_node().as_ref()); - ancestor.detach(parent, false); - (None, el) - } - } - None => (None, { - let el = self.create_element(parent); - super::insert_node(&el, parent, next_sibling.get().as_ref()); - el - }), - }; - - match ancestor_tag { - None => { - self.attributes.apply(&el); - self.listeners.apply(&el); - - match &mut self.inner { - VTagInner::Input(f) => { - f.apply(el.unchecked_ref()); - } - VTagInner::Textarea { value } => { - value.apply(el.unchecked_ref()); - } - VTagInner::Other { children, .. } => { - if !children.is_empty() { - children.apply(parent_scope, &el, NodeRef::default(), None); - } - } - } - } - Some(ancestor) => { - self.attributes.apply_diff(&el, ancestor.attributes); - self.listeners.apply_diff(&el, ancestor.listeners); - - match (&mut self.inner, ancestor.inner) { - (VTagInner::Input(new), VTagInner::Input(old)) => { - new.apply_diff(el.unchecked_ref(), old); - } - (VTagInner::Textarea { value: new }, VTagInner::Textarea { value: old }) => { - new.apply_diff(el.unchecked_ref(), old); - } - ( - VTagInner::Other { children: new, .. }, - VTagInner::Other { - children: mut old, .. - }, - ) => { - if !new.is_empty() { - new.apply(parent_scope, &el, NodeRef::default(), Some(old.into())); - } else if !old.is_empty() { - old.detach(&el, false); - } - } - // Can not happen, because we checked for tag equability above - _ => unsafe { unreachable_unchecked() }, - } - } - }; - - self.node_ref.set(Some(el.deref().clone())); - self.reference = el.into(); - self.node_ref.clone() - } } impl PartialEq for VTag { @@ -630,10 +398,7 @@ impl PartialEq for VTag { use VTagInner::*; (match (&self.inner, &other.inner) { - ( - Input(l), - Input (r), - ) => l == r, + (Input(l), Input(r)) => l == r, (Textarea { value: value_l }, Textarea { value: value_r }) => value_l == value_r, (Other { tag: tag_l, .. }, Other { tag: tag_r, .. }) => tag_l == tag_r, _ => false, @@ -650,7 +415,7 @@ impl PartialEq for VTag { #[cfg(feature = "ssr")] mod feat_ssr { use super::*; - use crate::virtual_dom::VText; + use crate::{html::AnyScope, virtual_dom::VText}; use std::fmt::Write; impl VTag { @@ -704,809 +469,6 @@ mod feat_ssr { } } -#[cfg(test)] -mod tests { - use super::*; - use crate::{html, 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); - - fn test_scope() -> AnyScope { - AnyScope::test() - } - - #[test] - fn it_compares_tags() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -

    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_text() { - let a = html! { -
    { "correct" }
    - }; - - let b = html! { -
    { "correct" }
    - }; - - let c = html! { -
    { "incorrect" }
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_attributes_static() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_attributes_dynamic() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_children() { - let a = html! { -
    -

    -
    - }; - - let b = html! { -
    -

    -
    - }; - - let c = html! { -
    - -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_classes_static() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -
    - }; - - let d = html! { -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - assert_ne!(a, d); - } - - #[test] - fn it_compares_classes_dynamic() { - let a = html! { -
    - }; - - let b = html! { -
    - }; - - let c = html! { -
    - }; - - let d = html! { -
    - }; - - assert_eq!(a, b); - assert_ne!(a, c); - assert_ne!(a, d); - } - - fn assert_vtag(node: &VNode) -> &VTag { - if let VNode::VTag(vtag) = node { - return vtag; - } - panic!("should be vtag"); - } - - fn assert_vtag_mut(node: &mut VNode) -> &mut VTag { - if let VNode::VTag(vtag) = node { - return vtag; - } - panic!("should be vtag"); - } - - fn assert_namespace(vtag: &VTag, namespace: &'static str) { - assert_eq!( - vtag.reference.as_ref().unwrap().namespace_uri().unwrap(), - namespace - ); - } - - #[test] - fn supports_svg() { - let document = web_sys::window().unwrap().document().unwrap(); - - let scope = test_scope(); - let div_el = document.create_element("div").unwrap(); - let namespace = SVG_NAMESPACE; - let namespace = Some(namespace); - let svg_el = document.create_element_ns(namespace, "svg").unwrap(); - - let mut g_node = html! { }; - let path_node = html! { }; - let mut svg_node = html! { {path_node} }; - - let svg_tag = assert_vtag_mut(&mut svg_node); - svg_tag.apply(&scope, &div_el, NodeRef::default(), None); - assert_namespace(svg_tag, SVG_NAMESPACE); - let path_tag = assert_vtag(svg_tag.children().get(0).unwrap()); - assert_namespace(path_tag, SVG_NAMESPACE); - - let g_tag = assert_vtag_mut(&mut g_node); - g_tag.apply(&scope, &div_el, NodeRef::default(), None); - assert_namespace(g_tag, HTML_NAMESPACE); - g_tag.reference = None; - - g_tag.apply(&scope, &svg_el, NodeRef::default(), None); - assert_namespace(g_tag, SVG_NAMESPACE); - } - - #[test] - fn it_compares_values() { - let a = html! { - - }; - - let b = html! { - - }; - - let c = html! { - - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_kinds() { - let a = html! { - - }; - - let b = html! { - - }; - - let c = html! { - - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_compares_checked() { - let a = html! { - - }; - - let b = html! { - - }; - - let c = html! { - - }; - - assert_eq!(a, b); - assert_ne!(a, c); - } - - #[test] - fn it_allows_aria_attributes() { - let a = html! { -

    - - -

    -

    - }; - if let VNode::VTag(vtag) = a { - assert_eq!( - vtag.attributes - .iter() - .find(|(k, _)| k == &"aria-controls") - .map(|(_, v)| v), - Some("it-works") - ); - } else { - panic!("vtag expected"); - } - } - - #[test] - fn it_does_not_set_missing_class_name() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - let mut elem = html! {
    }; - VDiff::apply(&mut elem, &scope, &parent, NodeRef::default(), None); - let vtag = assert_vtag_mut(&mut elem); - // test if the className has not been set - assert!(!vtag.reference.as_ref().unwrap().has_attribute("class")); - } - - fn test_set_class_name(gen_html: impl FnOnce() -> Html) { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - let mut elem = gen_html(); - VDiff::apply(&mut elem, &scope, &parent, NodeRef::default(), None); - let vtag = assert_vtag_mut(&mut elem); - // test if the className has been set - assert!(vtag.reference.as_ref().unwrap().has_attribute("class")); - } - - #[test] - fn it_sets_class_name_static() { - test_set_class_name(|| html! {
    }); - } - - #[test] - fn it_sets_class_name_dynamic() { - test_set_class_name(|| html! {
    }); - } - - #[test] - fn controlled_input_synced() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - let expected = "not_changed_value"; - - // Initial state - let mut elem = html! { }; - VDiff::apply(&mut elem, &scope, &parent, NodeRef::default(), None); - let vtag = if let VNode::VTag(vtag) = elem { - vtag - } else { - panic!("should be vtag") - }; - - // User input - let input_ref = vtag.reference.as_ref().unwrap(); - let input = input_ref.dyn_ref::(); - input.unwrap().set_value("User input"); - - let ancestor = vtag; - let mut elem = html! { }; - let vtag = assert_vtag_mut(&mut elem); - - // Sync happens here - vtag.apply( - &scope, - &parent, - NodeRef::default(), - Some(VNode::VTag(ancestor)), - ); - - // Get new current value of the input element - let input_ref = vtag.reference.as_ref().unwrap(); - let input = input_ref.dyn_ref::().unwrap(); - - let current_value = input.value(); - - // check whether not changed virtual dom value has been set to the input element - assert_eq!(current_value, expected); - } - - #[test] - fn uncontrolled_input_unsynced() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - // Initial state - let mut elem = html! { }; - VDiff::apply(&mut elem, &scope, &parent, NodeRef::default(), None); - let vtag = if let VNode::VTag(vtag) = elem { - vtag - } else { - panic!("should be vtag") - }; - - // User input - let input_ref = vtag.reference.as_ref().unwrap(); - let input = input_ref.dyn_ref::(); - input.unwrap().set_value("User input"); - - let ancestor = vtag; - let mut elem = html! { }; - let vtag = assert_vtag_mut(&mut elem); - - // Value should not be refreshed - vtag.apply( - &scope, - &parent, - NodeRef::default(), - Some(VNode::VTag(ancestor)), - ); - - // Get user value of the input element - let input_ref = vtag.reference.as_ref().unwrap(); - let input = input_ref.dyn_ref::().unwrap(); - - let current_value = input.value(); - - // check whether not changed virtual dom value has been set to the input element - assert_eq!(current_value, "User input"); - - // Need to remove the element to clean up the dirty state of the DOM. Failing this causes - // event listener tests to fail. - parent.remove(); - } - - #[test] - fn dynamic_tags_work() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - let mut elem = html! { <@{ - let mut builder = String::new(); - builder.push('a'); - builder - }/> }; - - VDiff::apply(&mut elem, &scope, &parent, NodeRef::default(), None); - let vtag = assert_vtag_mut(&mut elem); - // make sure the new tag name is used internally - assert_eq!(vtag.tag(), "a"); - - // Element.tagName is always in the canonical upper-case form. - assert_eq!(vtag.reference.as_ref().unwrap().tag_name(), "A"); - } - - #[test] - fn dynamic_tags_handle_value_attribute() { - let mut div_el = html! { - <@{"div"} value="Hello"/> - }; - let div_vtag = assert_vtag_mut(&mut div_el); - assert!(div_vtag.value().is_none()); - let v: Option<&str> = div_vtag - .attributes - .iter() - .find(|(k, _)| k == &"value") - .map(|(_, v)| AsRef::as_ref(v)); - assert_eq!(v, Some("Hello")); - - let mut input_el = html! { - <@{"input"} value="World"/> - }; - let input_vtag = assert_vtag_mut(&mut input_el); - assert_eq!(input_vtag.value(), Some(&AttrValue::Static("World"))); - assert!(!input_vtag.attributes.iter().any(|(k, _)| k == "value")); - } - - #[test] - fn dynamic_tags_handle_weird_capitalization() { - let mut el = html! { - <@{"tExTAREa"}/> - }; - let vtag = assert_vtag_mut(&mut el); - assert_eq!(vtag.tag(), "textarea"); - } - - #[test] - fn reset_node_ref() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - - document().body().unwrap().append_child(&parent).unwrap(); - - let node_ref = NodeRef::default(); - let mut elem: VNode = html! {
    }; - assert_vtag_mut(&mut elem); - elem.apply(&scope, &parent, NodeRef::default(), None); - let parent_node = parent.deref(); - assert_eq!(node_ref.get(), parent_node.first_child()); - elem.detach(&parent, false); - assert!(node_ref.get().is_none()); - } - - #[test] - fn vtag_reuse_should_reset_ancestors_node_ref() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - document().body().unwrap().append_child(&parent).unwrap(); - - let node_ref_a = NodeRef::default(); - let mut elem_a = html! {
    }; - elem_a.apply(&scope, &parent, NodeRef::default(), None); - - // save the Node to check later that it has been reused. - let node_a = node_ref_a.get().unwrap(); - - let node_ref_b = NodeRef::default(); - let mut elem_b = html! {
    }; - elem_b.apply(&scope, &parent, NodeRef::default(), Some(elem_a)); - - let node_b = node_ref_b.get().unwrap(); - - assert_eq!(node_a, node_b, "VTag should have reused the element"); - assert!( - node_ref_a.get().is_none(), - "node_ref_a should have been reset when the element was reused." - ); - } - - #[test] - fn vtag_should_not_touch_newly_bound_refs() { - let scope = test_scope(); - let parent = document().create_element("div").unwrap(); - document().body().unwrap().append_child(&parent).unwrap(); - - let test_ref = NodeRef::default(); - let mut before = html! { - <> -
    - - }; - let mut after = html! { - <> -
    -
    - - }; - // The point of this diff is to first render the "after" div and then detach the "before" div, - // while both should be bound to the same node ref - - before.apply(&scope, &parent, NodeRef::default(), None); - after.apply(&scope, &parent, NodeRef::default(), Some(before)); - - assert_eq!( - test_ref - .get() - .unwrap() - .dyn_ref::() - .unwrap() - .outer_html(), - "
    " - ); - } -} - -#[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"} -
    • -
    • - {"b"} -
    • -
    - }, - expected: "
    • a
    • b
    ", - }; - - let layout2 = TestLayout { - name: "2", - node: html! { -
      -
    • - {"a"} -
    • -
    • - {"b"} -
    • -
    • - {"d"} -
    • -
    - }, - expected: "
    • a
    • b
    • d
    ", - }; - - let layout3 = TestLayout { - name: "3", - node: html! { -
      -
    • - {"a"} -
    • -
    • - {"b"} -
    • -
    • - {"c"} -
    • -
    • - {"d"} -
    • -
    - }, - expected: "
    • a
    • b
    • c
    • d
    ", - }; - - let layout4 = TestLayout { - name: "4", - node: html! { -
      -
    • - <> - {"a"} - -
    • -
    • - {"b"} -
    • - {"c"} -
    • -
    • - {"d"} -
    • - -
    - }, - expected: "
    • a
    • b
    • c
    • d
    ", - }; - - diff_layouts(vec![layout1, layout2, layout3, layout4]); - } -} - -#[cfg(test)] -mod tests_without_browser { - use crate::html; - - #[test] - fn html_if_bool() { - assert_eq!( - html! { - if true { -
    - } - }, - html! {
    }, - ); - assert_eq!( - html! { - if false { -
    - } else { -
    - } - }, - html! { -
    - }, - ); - assert_eq!( - html! { - if false { -
    - } - }, - html! {}, - ); - - // non-root tests - assert_eq!( - html! { -
    - if true { -
    - } -
    - }, - html! { -
    -
    -
    - }, - ); - assert_eq!( - html! { -
    - if false { -
    - } else { -
    - } -
    - }, - html! { -
    -
    -
    - }, - ); - assert_eq!( - html! { -
    - if false { -
    - } -
    - }, - html! { -
    - <> -
    - }, - ); - } - - #[test] - fn html_if_option() { - let option_foo = Some("foo"); - let none: Option<&'static str> = None; - assert_eq!( - html! { - if let Some(class) = option_foo { -
    - } - }, - html! {
    }, - ); - assert_eq!( - html! { - if let Some(class) = none { -
    - } else { -
    - } - }, - html! {
    }, - ); - assert_eq!( - html! { - if let Some(class) = none { -
    - } - }, - html! {}, - ); - - // non-root tests - assert_eq!( - html! { -
    - if let Some(class) = option_foo { -
    - } -
    - }, - html! {
    }, - ); - assert_eq!( - html! { -
    - if let Some(class) = none { -
    - } else { -
    - } -
    - }, - html! {
    }, - ); - assert_eq!( - html! { -
    - if let Some(class) = none { -
    - } -
    - }, - html! {
    <>
    }, - ); - } -} - #[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))] mod ssr_tests { use tokio::test; diff --git a/packages/yew/src/virtual_dom/vtext.rs b/packages/yew/src/virtual_dom/vtext.rs index 8345a5cfda9..1756e0bbd7b 100644 --- a/packages/yew/src/virtual_dom/vtext.rs +++ b/packages/yew/src/virtual_dom/vtext.rs @@ -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,102 +10,18 @@ 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, } impl VText { /// Creates new virtual text node with a content. pub fn new(text: impl Into) -> Self { - VText { - text: text.into(), - reference: None, - } - } -} - -#[cfg(feature = "ssr")] -mod feat_ssr { - use super::*; - - impl VText { - pub(crate) async fn render_to_string(&self, w: &mut String) { - html_escape::encode_text_to_string(&self.text, w); - } + 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: \"{}\", 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, - ) -> 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()) + write!(f, "VText {{ text: \"{}\" }}", self.text) } } @@ -119,80 +31,14 @@ impl PartialEq for VText { } } -#[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", - }; +#[cfg(feature = "ssr")] +mod feat_ssr { + use super::*; - diff_layouts(vec![layout1, layout2, layout3, layout4]); + impl VText { + pub(crate) async fn render_to_string(&self, w: &mut String) { + html_escape::encode_text_to_string(&self.text, w); + } } } diff --git a/tools/benchmark-hooks/Makefile.toml b/tools/benchmark-hooks/Makefile.toml new file mode 100644 index 00000000000..3073e005b08 --- /dev/null +++ b/tools/benchmark-hooks/Makefile.toml @@ -0,0 +1,5 @@ +# Extends the root Makefile.toml + +[tasks.doc-test] +command = "echo" +args = ["No doctests run for benchmark-hooks"] \ No newline at end of file diff --git a/tools/benchmark-struct/Makefile.toml b/tools/benchmark-struct/Makefile.toml new file mode 100644 index 00000000000..0969abb0973 --- /dev/null +++ b/tools/benchmark-struct/Makefile.toml @@ -0,0 +1,5 @@ +# Extends the root Makefile.toml + +[tasks.doc-test] +command = "echo" +args = ["No doctests run for benchmark-struct"] \ No newline at end of file diff --git a/tools/process-benchmark-results/Makefile.toml b/tools/process-benchmark-results/Makefile.toml new file mode 100644 index 00000000000..5d55b82c898 --- /dev/null +++ b/tools/process-benchmark-results/Makefile.toml @@ -0,0 +1,5 @@ +# Extends the root Makefile.toml + +[tasks.doc-test] +command = "echo" +args = ["No doctests run for process-benchmark-results"] \ No newline at end of file