mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
Introduce explicit internal datastructures modeling dom state (#2330)
* detach destructures now * add failing keyed-list issue * crude port to the new bundle infrastructure * port over the infrastructure the new bcomp is especially nice and lost a few unwraps owed to not having to reserve space for a scope before rendering. Note also that bsuspense has been slimmed a bit, storing the suspended flag implicitly in the state. some naming is not perfect yet and has to be adjusted still. * mass rename: apply -> reconcile * get rid of move_before in favor of shift * generate id directly when creating a new scope * bundle for text nodes * work on naming: ancestor -> bundle * slightly optimize list reconciler, add doccomments * address review * add internal documentation * address review comments rename fields in bsuspense convert to gloo::events * move even more stuff into dom_bundle to scope exports - app_handle and layout_tests are now in there - items are publically re-exported in crate::dom_bundle - dom_bundle itself is private - btag and bcomp get their own submodules - bcomp now contains the lifecycle and scope impls * move replace into Reconcilable * move lifecycle and scope back into html as per review * move back Value and InputFields into html * actually only type-check format args in production * fix documentation link * move btag_impl up into containing module * shift comps immediately shifting the rendered Nodes does not tie into the lifecycle, as such it can happen immediately * use list-bundle in tag-bundle * fix cargo make tests * improve 05_swap benchmark * fix a blunder where I swapped operands * fix naming of BNode variants
This commit is contained in:
parent
221b4dfa51
commit
78d4204a9a
@ -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
|
||||
|
||||
@ -1,17 +1,16 @@
|
||||
//! This module contains the `App` struct, which is used to bootstrap
|
||||
//! a component in an isolated scope.
|
||||
//! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope.
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::html::{BaseComponent, NodeRef, Scope, Scoped};
|
||||
use std::rc::Rc;
|
||||
use super::{ComponentRenderState, Scoped};
|
||||
use crate::html::{BaseComponent, Scope};
|
||||
use crate::NodeRef;
|
||||
use std::{ops::Deref, rc::Rc};
|
||||
use web_sys::Element;
|
||||
|
||||
/// An instance of an application.
|
||||
#[derive(Debug)]
|
||||
pub struct AppHandle<COMP: BaseComponent> {
|
||||
/// `Scope` holder
|
||||
pub(crate) scope: Scope<COMP>,
|
||||
scope: Scope<COMP>,
|
||||
}
|
||||
|
||||
impl<COMP> AppHandle<COMP>
|
||||
@ -27,14 +26,17 @@ where
|
||||
let app = Self {
|
||||
scope: Scope::new(None),
|
||||
};
|
||||
let node_ref = NodeRef::default();
|
||||
let initial_render_state =
|
||||
ComponentRenderState::new(element, NodeRef::default(), &node_ref);
|
||||
app.scope
|
||||
.mount_in_place(element, NodeRef::default(), NodeRef::default(), props);
|
||||
.mount_in_place(initial_render_state, node_ref, props);
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
/// Schedule the app for destruction
|
||||
pub fn destroy(mut self) {
|
||||
pub fn destroy(self) {
|
||||
self.scope.destroy(false)
|
||||
}
|
||||
}
|
||||
886
packages/yew/src/dom_bundle/bcomp.rs
Normal file
886
packages/yew/src/dom_bundle/bcomp.rs
Normal file
@ -0,0 +1,886 @@
|
||||
//! This module contains the bundle implementation of a virtual component [BComp].
|
||||
|
||||
use super::{insert_node, BNode, DomBundle, Reconcilable};
|
||||
use crate::html::{AnyScope, BaseComponent, Scope};
|
||||
use crate::virtual_dom::{Key, VComp, VNode};
|
||||
use crate::NodeRef;
|
||||
#[cfg(feature = "ssr")]
|
||||
use futures::channel::oneshot;
|
||||
#[cfg(feature = "ssr")]
|
||||
use futures::future::{FutureExt, LocalBoxFuture};
|
||||
use gloo_utils::document;
|
||||
use std::cell::Ref;
|
||||
use std::{any::TypeId, borrow::Borrow};
|
||||
use std::{fmt, rc::Rc};
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
/// A virtual component. Compare with [VComp].
|
||||
pub struct BComp {
|
||||
type_id: TypeId,
|
||||
scope: Box<dyn Scoped>,
|
||||
node_ref: NodeRef,
|
||||
key: Option<Key>,
|
||||
}
|
||||
|
||||
impl BComp {
|
||||
/// Get the key of the underlying component
|
||||
pub(super) fn key(&self) -> Option<&Key> {
|
||||
self.key.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for BComp {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"BComp {{ root: {:?} }}",
|
||||
self.scope.as_ref().render_state(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl DomBundle for BComp {
|
||||
fn detach(self, _parent: &Element, parent_to_detach: bool) {
|
||||
self.scope.destroy_boxed(parent_to_detach);
|
||||
}
|
||||
|
||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
||||
self.scope.shift_node(next_parent.clone(), next_sibling);
|
||||
}
|
||||
}
|
||||
|
||||
impl Reconcilable for VComp {
|
||||
type Bundle = BComp;
|
||||
|
||||
fn attach(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> (NodeRef, Self::Bundle) {
|
||||
let VComp {
|
||||
type_id,
|
||||
mountable,
|
||||
node_ref,
|
||||
key,
|
||||
} = self;
|
||||
|
||||
let scope = mountable.mount(
|
||||
node_ref.clone(),
|
||||
parent_scope,
|
||||
parent.to_owned(),
|
||||
next_sibling,
|
||||
);
|
||||
|
||||
(
|
||||
node_ref.clone(),
|
||||
BComp {
|
||||
type_id,
|
||||
node_ref,
|
||||
key,
|
||||
scope,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn reconcile_node(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef {
|
||||
match bundle {
|
||||
// If the existing bundle is the same type, reuse it and update its properties
|
||||
BNode::Comp(ref mut bcomp)
|
||||
if self.type_id == bcomp.type_id && self.key == bcomp.key =>
|
||||
{
|
||||
self.reconcile(parent_scope, parent, next_sibling, bcomp)
|
||||
}
|
||||
_ => self.replace(parent_scope, parent, next_sibling, bundle),
|
||||
}
|
||||
}
|
||||
|
||||
fn reconcile(
|
||||
self,
|
||||
_parent_scope: &AnyScope,
|
||||
_parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bcomp: &mut Self::Bundle,
|
||||
) -> NodeRef {
|
||||
let VComp {
|
||||
mountable,
|
||||
node_ref,
|
||||
key,
|
||||
type_id: _,
|
||||
} = self;
|
||||
|
||||
bcomp.key = key;
|
||||
let old_ref = std::mem::replace(&mut bcomp.node_ref, node_ref.clone());
|
||||
bcomp.node_ref.reuse(old_ref);
|
||||
mountable.reuse(node_ref.clone(), bcomp.scope.borrow(), next_sibling);
|
||||
node_ref
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Mountable {
|
||||
fn copy(&self) -> Box<dyn Mountable>;
|
||||
fn mount(
|
||||
self: Box<Self>,
|
||||
node_ref: NodeRef,
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> Box<dyn Scoped>;
|
||||
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn render_to_string<'a>(
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
) -> LocalBoxFuture<'a, ()>;
|
||||
}
|
||||
|
||||
pub struct PropsWrapper<COMP: BaseComponent> {
|
||||
props: Rc<COMP::Properties>,
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> PropsWrapper<COMP> {
|
||||
pub fn new(props: Rc<COMP::Properties>) -> Self {
|
||||
Self { props }
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
||||
fn copy(&self) -> Box<dyn Mountable> {
|
||||
let wrapper: PropsWrapper<COMP> = PropsWrapper {
|
||||
props: Rc::clone(&self.props),
|
||||
};
|
||||
Box::new(wrapper)
|
||||
}
|
||||
|
||||
fn mount(
|
||||
self: Box<Self>,
|
||||
node_ref: NodeRef,
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> Box<dyn Scoped> {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
let initial_render_state = ComponentRenderState::new(parent, next_sibling, &node_ref);
|
||||
scope.mount_in_place(initial_render_state, node_ref, self.props);
|
||||
|
||||
Box::new(scope)
|
||||
}
|
||||
|
||||
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) {
|
||||
let scope: Scope<COMP> = scope.to_any().downcast();
|
||||
scope.reuse(self.props, node_ref, next_sibling);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn render_to_string<'a>(
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
) -> LocalBoxFuture<'a, ()> {
|
||||
async move {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
scope.render_to_string(w, self.props.clone()).await;
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ComponentRenderState {
|
||||
root_node: BNode,
|
||||
/// When a component has no parent, it means that it should not be rendered.
|
||||
parent: Option<Element>,
|
||||
next_sibling: NodeRef,
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
html_sender: Option<oneshot::Sender<VNode>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ComponentRenderState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.root_node.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentRenderState {
|
||||
/// Prepare a place in the DOM to hold the eventual [VNode] from rendering a component
|
||||
pub(crate) fn new(parent: Element, next_sibling: NodeRef, node_ref: &NodeRef) -> Self {
|
||||
let placeholder = {
|
||||
let placeholder: Node = document().create_text_node("").into();
|
||||
insert_node(&placeholder, &parent, next_sibling.get().as_ref());
|
||||
node_ref.set(Some(placeholder.clone()));
|
||||
BNode::Ref(placeholder)
|
||||
};
|
||||
Self {
|
||||
root_node: placeholder,
|
||||
parent: Some(parent),
|
||||
next_sibling,
|
||||
#[cfg(feature = "ssr")]
|
||||
html_sender: None,
|
||||
}
|
||||
}
|
||||
/// Set up server-side rendering of a component
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(crate) fn new_ssr(tx: oneshot::Sender<VNode>) -> Self {
|
||||
use super::blist::BList;
|
||||
|
||||
Self {
|
||||
root_node: BNode::List(BList::new()),
|
||||
parent: None,
|
||||
next_sibling: NodeRef::default(),
|
||||
html_sender: Some(tx),
|
||||
}
|
||||
}
|
||||
/// Reuse the render state, asserting a new next_sibling
|
||||
pub(crate) fn reuse(&mut self, next_sibling: NodeRef) {
|
||||
self.next_sibling = next_sibling;
|
||||
}
|
||||
/// Shift the rendered content to a new DOM position
|
||||
pub(crate) fn shift(&mut self, new_parent: Element, next_sibling: NodeRef) {
|
||||
self.root_node.shift(&new_parent, next_sibling.clone());
|
||||
|
||||
self.parent = Some(new_parent);
|
||||
self.next_sibling = next_sibling;
|
||||
}
|
||||
/// Reconcile the rendered content with a new [VNode]
|
||||
pub(crate) fn reconcile(&mut self, root: VNode, scope: &AnyScope) -> NodeRef {
|
||||
if let Some(ref parent) = self.parent {
|
||||
let next_sibling = self.next_sibling.clone();
|
||||
|
||||
root.reconcile_node(scope, parent, next_sibling, &mut self.root_node)
|
||||
} else {
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(tx) = self.html_sender.take() {
|
||||
tx.send(root).unwrap();
|
||||
}
|
||||
NodeRef::default()
|
||||
}
|
||||
}
|
||||
/// Detach the rendered content from the DOM
|
||||
pub(crate) fn detach(self, parent_to_detach: bool) {
|
||||
if let Some(ref m) = self.parent {
|
||||
self.root_node.detach(m, parent_to_detach);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_trigger_rendered(&self) -> bool {
|
||||
self.parent.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Scoped {
|
||||
fn to_any(&self) -> AnyScope;
|
||||
/// Get the render state if it hasn't already been destroyed
|
||||
fn render_state(&self) -> Option<Ref<'_, ComponentRenderState>>;
|
||||
/// Shift the node associated with this scope to a new place
|
||||
fn shift_node(&self, parent: Element, next_sibling: NodeRef);
|
||||
/// Process an event to destroy a component
|
||||
fn destroy(self, parent_to_detach: bool);
|
||||
fn destroy_boxed(self: Box<Self>, parent_to_detach: bool);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dom_bundle::{DomBundle, Reconcilable};
|
||||
use crate::scheduler;
|
||||
use crate::{
|
||||
html,
|
||||
virtual_dom::{Key, VChild, VNode},
|
||||
Children, Component, Context, Html, NodeRef, Properties,
|
||||
};
|
||||
use gloo_utils::document;
|
||||
use std::ops::Deref;
|
||||
use web_sys::Element;
|
||||
use web_sys::Node;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
struct Comp;
|
||||
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
struct Props {
|
||||
#[prop_or_default]
|
||||
field_1: u32,
|
||||
#[prop_or_default]
|
||||
field_2: u32,
|
||||
}
|
||||
|
||||
impl Component for Comp {
|
||||
type Message = ();
|
||||
type Properties = Props;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Comp
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
html! { <div/> }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_loop() {
|
||||
let document = gloo_utils::document();
|
||||
let parent_scope: AnyScope = AnyScope::test();
|
||||
let parent_element = document.create_element("div").unwrap();
|
||||
|
||||
let comp = html! { <Comp></Comp> };
|
||||
let (_, mut bundle) = comp.attach(&parent_scope, &parent_element, NodeRef::default());
|
||||
scheduler::start_now();
|
||||
|
||||
for _ in 0..10000 {
|
||||
let node = html! { <Comp></Comp> };
|
||||
node.reconcile_node(
|
||||
&parent_scope,
|
||||
&parent_element,
|
||||
NodeRef::default(),
|
||||
&mut bundle,
|
||||
);
|
||||
scheduler::start_now();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_properties_to_component() {
|
||||
html! {
|
||||
<Comp />
|
||||
};
|
||||
|
||||
html! {
|
||||
<Comp field_1=1 />
|
||||
};
|
||||
|
||||
html! {
|
||||
<Comp field_2=2 />
|
||||
};
|
||||
|
||||
html! {
|
||||
<Comp field_1=1 field_2=2 />
|
||||
};
|
||||
|
||||
let props = Props {
|
||||
field_1: 1,
|
||||
field_2: 1,
|
||||
};
|
||||
|
||||
html! {
|
||||
<Comp ..props />
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_component_key() {
|
||||
let test_key: Key = "test".to_string().into();
|
||||
let check_key = |vnode: VNode| {
|
||||
assert_eq!(vnode.key(), Some(&test_key));
|
||||
};
|
||||
|
||||
let props = Props {
|
||||
field_1: 1,
|
||||
field_2: 1,
|
||||
};
|
||||
let props_2 = props.clone();
|
||||
|
||||
check_key(html! { <Comp key={test_key.clone()} /> });
|
||||
check_key(html! { <Comp key={test_key.clone()} field_1=1 /> });
|
||||
check_key(html! { <Comp field_1=1 key={test_key.clone()} /> });
|
||||
check_key(html! { <Comp key={test_key.clone()} ..props /> });
|
||||
check_key(html! { <Comp key={test_key.clone()} ..props_2 /> });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_component_node_ref() {
|
||||
let test_node: Node = document().create_text_node("test").into();
|
||||
let test_node_ref = NodeRef::new(test_node);
|
||||
let check_node_ref = |vnode: VNode| {
|
||||
let vcomp = match vnode {
|
||||
VNode::VComp(vcomp) => vcomp,
|
||||
_ => unreachable!("should be a vcomp"),
|
||||
};
|
||||
assert_eq!(vcomp.node_ref, test_node_ref);
|
||||
};
|
||||
|
||||
let props = Props {
|
||||
field_1: 1,
|
||||
field_2: 1,
|
||||
};
|
||||
let props_2 = props.clone();
|
||||
|
||||
check_node_ref(html! { <Comp ref={test_node_ref.clone()} /> });
|
||||
check_node_ref(html! { <Comp ref={test_node_ref.clone()} field_1=1 /> });
|
||||
check_node_ref(html! { <Comp field_1=1 ref={test_node_ref.clone()} /> });
|
||||
check_node_ref(html! { <Comp ref={test_node_ref.clone()} ..props /> });
|
||||
check_node_ref(html! { <Comp ref={test_node_ref.clone()} ..props_2 /> });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vchild_partialeq() {
|
||||
let vchild1: VChild<Comp> = VChild::new(
|
||||
Props {
|
||||
field_1: 1,
|
||||
field_2: 1,
|
||||
},
|
||||
NodeRef::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
let vchild2: VChild<Comp> = VChild::new(
|
||||
Props {
|
||||
field_1: 1,
|
||||
field_2: 1,
|
||||
},
|
||||
NodeRef::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
let vchild3: VChild<Comp> = VChild::new(
|
||||
Props {
|
||||
field_1: 2,
|
||||
field_2: 2,
|
||||
},
|
||||
NodeRef::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(vchild1, vchild2);
|
||||
assert_ne!(vchild1, vchild3);
|
||||
assert_ne!(vchild2, vchild3);
|
||||
}
|
||||
|
||||
#[derive(Clone, Properties, PartialEq)]
|
||||
pub struct ListProps {
|
||||
pub children: Children,
|
||||
}
|
||||
pub struct List;
|
||||
impl Component for List {
|
||||
type Message = ();
|
||||
type Properties = ListProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
|
||||
unimplemented!();
|
||||
}
|
||||
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
||||
unimplemented!();
|
||||
}
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let item_iter = ctx
|
||||
.props()
|
||||
.children
|
||||
.iter()
|
||||
.map(|item| html! {<li>{ item }</li>});
|
||||
html! {
|
||||
<ul>{ for item_iter }</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_parent() -> (AnyScope, Element) {
|
||||
let scope = AnyScope::test();
|
||||
let parent = document().create_element("div").unwrap();
|
||||
|
||||
document().body().unwrap().append_child(&parent).unwrap();
|
||||
|
||||
(scope, parent)
|
||||
}
|
||||
|
||||
fn get_html(node: Html, scope: &AnyScope, parent: &Element) -> String {
|
||||
// clear parent
|
||||
parent.set_inner_html("");
|
||||
|
||||
node.attach(scope, parent, NodeRef::default());
|
||||
scheduler::start_now();
|
||||
parent.inner_html()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_ways_of_passing_children_work() {
|
||||
let (scope, parent) = setup_parent();
|
||||
|
||||
let children: Vec<_> = vec!["a", "b", "c"]
|
||||
.drain(..)
|
||||
.map(|text| html! {<span>{ text }</span>})
|
||||
.collect();
|
||||
let children_renderer = Children::new(children.clone());
|
||||
let expected_html = "\
|
||||
<ul>\
|
||||
<li><span>a</span></li>\
|
||||
<li><span>b</span></li>\
|
||||
<li><span>c</span></li>\
|
||||
</ul>";
|
||||
|
||||
let prop_method = html! {
|
||||
<List children={children_renderer.clone()} />
|
||||
};
|
||||
assert_eq!(get_html(prop_method, &scope, &parent), expected_html);
|
||||
|
||||
let children_renderer_method = html! {
|
||||
<List>
|
||||
{ children_renderer }
|
||||
</List>
|
||||
};
|
||||
assert_eq!(
|
||||
get_html(children_renderer_method, &scope, &parent),
|
||||
expected_html
|
||||
);
|
||||
|
||||
let direct_method = html! {
|
||||
<List>
|
||||
{ children.clone() }
|
||||
</List>
|
||||
};
|
||||
assert_eq!(get_html(direct_method, &scope, &parent), expected_html);
|
||||
|
||||
let for_method = html! {
|
||||
<List>
|
||||
{ for children }
|
||||
</List>
|
||||
};
|
||||
assert_eq!(get_html(for_method, &scope, &parent), expected_html);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_node_ref() {
|
||||
let scope = AnyScope::test();
|
||||
let parent = document().create_element("div").unwrap();
|
||||
|
||||
document().body().unwrap().append_child(&parent).unwrap();
|
||||
|
||||
let node_ref = NodeRef::default();
|
||||
let elem = html! { <Comp ref={node_ref.clone()}></Comp> };
|
||||
let (_, elem) = elem.attach(&scope, &parent, NodeRef::default());
|
||||
scheduler::start_now();
|
||||
let parent_node = parent.deref();
|
||||
assert_eq!(node_ref.get(), parent_node.first_child());
|
||||
elem.detach(&parent, false);
|
||||
scheduler::start_now();
|
||||
assert!(node_ref.get().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
extern crate self as yew;
|
||||
|
||||
use crate::html;
|
||||
use crate::tests::layout_tests::{diff_layouts, TestLayout};
|
||||
use crate::{Children, Component, Context, Html, Properties};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
struct Comp<T> {
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
struct CompProps {
|
||||
#[prop_or_default]
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl<T: 'static> Component for Comp<T> {
|
||||
type Message = ();
|
||||
type Properties = CompProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Comp {
|
||||
_marker: PhantomData::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>{ ctx.props().children.clone() }</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct A;
|
||||
struct B;
|
||||
|
||||
#[test]
|
||||
fn diff() {
|
||||
let layout1 = TestLayout {
|
||||
name: "1",
|
||||
node: html! {
|
||||
<Comp<A>>
|
||||
<Comp<B>></Comp<B>>
|
||||
{"C"}
|
||||
</Comp<A>>
|
||||
},
|
||||
expected: "C",
|
||||
};
|
||||
|
||||
let layout2 = TestLayout {
|
||||
name: "2",
|
||||
node: html! {
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
},
|
||||
expected: "A",
|
||||
};
|
||||
|
||||
let layout3 = TestLayout {
|
||||
name: "3",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<Comp<A>></Comp<A>>
|
||||
{"B"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "B",
|
||||
};
|
||||
|
||||
let layout4 = TestLayout {
|
||||
name: "4",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<Comp<A>>{"A"}</Comp<A>>
|
||||
{"B"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "AB",
|
||||
};
|
||||
|
||||
let layout5 = TestLayout {
|
||||
name: "5",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
</>
|
||||
{"B"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "AB",
|
||||
};
|
||||
|
||||
let layout6 = TestLayout {
|
||||
name: "6",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
{"B"}
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout7 = TestLayout {
|
||||
name: "7",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
<Comp<A>>
|
||||
{"B"}
|
||||
</Comp<A>>
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout8 = TestLayout {
|
||||
name: "8",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
<Comp<A>>
|
||||
<Comp<A>>
|
||||
{"B"}
|
||||
</Comp<A>>
|
||||
</Comp<A>>
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout9 = TestLayout {
|
||||
name: "9",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<>
|
||||
{"A"}
|
||||
</>
|
||||
<Comp<A>>
|
||||
<Comp<A>>
|
||||
{"B"}
|
||||
</Comp<A>>
|
||||
</Comp<A>>
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout10 = TestLayout {
|
||||
name: "10",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
</Comp<A>>
|
||||
<>
|
||||
{"B"}
|
||||
</>
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout11 = TestLayout {
|
||||
name: "11",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<>
|
||||
<Comp<A>>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
{"B"}
|
||||
</Comp<A>>
|
||||
</>
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout12 = TestLayout {
|
||||
name: "12",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>></Comp<A>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
<></>
|
||||
<Comp<A>>
|
||||
<Comp<A>></Comp<A>>
|
||||
<></>
|
||||
{"B"}
|
||||
<></>
|
||||
<Comp<A>></Comp<A>>
|
||||
</Comp<A>>
|
||||
</>
|
||||
</Comp<A>>
|
||||
<></>
|
||||
</>
|
||||
<Comp<A>></Comp<A>>
|
||||
</>
|
||||
{"C"}
|
||||
<Comp<A>></Comp<A>>
|
||||
<></>
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
diff_layouts(vec![
|
||||
layout1, layout2, layout3, layout4, layout5, layout6, layout7, layout8, layout9,
|
||||
layout10, layout11, layout12,
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_with_children() {
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct Props {
|
||||
children: Children,
|
||||
}
|
||||
|
||||
struct ComponentWithChildren;
|
||||
|
||||
impl Component for ComponentWithChildren {
|
||||
type Message = ();
|
||||
type Properties = Props;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<ul>
|
||||
{ for ctx.props().children.iter().map(|child| html! { <li>{ child }</li> }) }
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let layout = TestLayout {
|
||||
name: "13",
|
||||
node: html! {
|
||||
<ComponentWithChildren>
|
||||
if true {
|
||||
<span>{ "hello" }</span>
|
||||
<span>{ "world" }</span>
|
||||
} else {
|
||||
<span>{ "goodbye" }</span>
|
||||
<span>{ "world" }</span>
|
||||
}
|
||||
</ComponentWithChildren>
|
||||
},
|
||||
expected: "<ul><li><span>hello</span><span>world</span></li></ul>",
|
||||
};
|
||||
|
||||
diff_layouts(vec![layout]);
|
||||
}
|
||||
}
|
||||
1382
packages/yew/src/dom_bundle/blist.rs
Normal file
1382
packages/yew/src/dom_bundle/blist.rs
Normal file
File diff suppressed because it is too large
Load Diff
254
packages/yew/src/dom_bundle/bnode.rs
Normal file
254
packages/yew/src/dom_bundle/bnode.rs
Normal file
@ -0,0 +1,254 @@
|
||||
//! This module contains the bundle version of an abstract node [BNode]
|
||||
|
||||
use super::{BComp, BList, BPortal, BSuspense, BTag, BText};
|
||||
use crate::dom_bundle::{DomBundle, Reconcilable};
|
||||
use crate::html::{AnyScope, NodeRef};
|
||||
use crate::virtual_dom::{Key, VNode};
|
||||
use gloo::console;
|
||||
use std::fmt;
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
/// The bundle implementation to [VNode].
|
||||
pub enum BNode {
|
||||
/// A bind between `VTag` and `Element`.
|
||||
Tag(Box<BTag>),
|
||||
/// A bind between `VText` and `TextNode`.
|
||||
Text(BText),
|
||||
/// A bind between `VComp` and `Element`.
|
||||
Comp(BComp),
|
||||
/// A holder for a list of other nodes.
|
||||
List(BList),
|
||||
/// A portal to another part of the document
|
||||
Portal(BPortal),
|
||||
/// A holder for any `Node` (necessary for replacing node).
|
||||
Ref(Node),
|
||||
/// A suspendible document fragment.
|
||||
Suspense(Box<BSuspense>),
|
||||
}
|
||||
|
||||
impl BNode {
|
||||
/// Get the key of the underlying node
|
||||
pub(super) fn key(&self) -> Option<&Key> {
|
||||
match self {
|
||||
Self::Comp(bsusp) => bsusp.key(),
|
||||
Self::List(blist) => blist.key(),
|
||||
Self::Ref(_) => None,
|
||||
Self::Tag(btag) => btag.key(),
|
||||
Self::Text(_) => None,
|
||||
Self::Portal(bportal) => bportal.key(),
|
||||
Self::Suspense(bsusp) => bsusp.key(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DomBundle for BNode {
|
||||
/// Remove VNode from parent.
|
||||
fn detach(self, parent: &Element, parent_to_detach: bool) {
|
||||
match self {
|
||||
Self::Tag(vtag) => vtag.detach(parent, parent_to_detach),
|
||||
Self::Text(btext) => btext.detach(parent, parent_to_detach),
|
||||
Self::Comp(bsusp) => bsusp.detach(parent, parent_to_detach),
|
||||
Self::List(blist) => blist.detach(parent, parent_to_detach),
|
||||
Self::Ref(ref node) => {
|
||||
// Always remove user-defined nodes to clear possible parent references of them
|
||||
if parent.remove_child(node).is_err() {
|
||||
console::warn!("Node not found to remove VRef");
|
||||
}
|
||||
}
|
||||
Self::Portal(bportal) => bportal.detach(parent, parent_to_detach),
|
||||
Self::Suspense(bsusp) => bsusp.detach(parent, parent_to_detach),
|
||||
}
|
||||
}
|
||||
|
||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
||||
match self {
|
||||
Self::Tag(ref vtag) => vtag.shift(next_parent, next_sibling),
|
||||
Self::Text(ref btext) => btext.shift(next_parent, next_sibling),
|
||||
Self::Comp(ref bsusp) => bsusp.shift(next_parent, next_sibling),
|
||||
Self::List(ref vlist) => vlist.shift(next_parent, next_sibling),
|
||||
Self::Ref(ref node) => {
|
||||
next_parent
|
||||
.insert_before(node, next_sibling.get().as_ref())
|
||||
.unwrap();
|
||||
}
|
||||
Self::Portal(ref vportal) => vportal.shift(next_parent, next_sibling),
|
||||
Self::Suspense(ref vsuspense) => vsuspense.shift(next_parent, next_sibling),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Reconcilable for VNode {
|
||||
type Bundle = BNode;
|
||||
|
||||
fn attach(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> (NodeRef, Self::Bundle) {
|
||||
match self {
|
||||
VNode::VTag(vtag) => {
|
||||
let (node_ref, tag) = vtag.attach(parent_scope, parent, next_sibling);
|
||||
(node_ref, tag.into())
|
||||
}
|
||||
VNode::VText(vtext) => {
|
||||
let (node_ref, text) = vtext.attach(parent_scope, parent, next_sibling);
|
||||
(node_ref, text.into())
|
||||
}
|
||||
VNode::VComp(vcomp) => {
|
||||
let (node_ref, comp) = vcomp.attach(parent_scope, parent, next_sibling);
|
||||
(node_ref, comp.into())
|
||||
}
|
||||
VNode::VList(vlist) => {
|
||||
let (node_ref, list) = vlist.attach(parent_scope, parent, next_sibling);
|
||||
(node_ref, list.into())
|
||||
}
|
||||
VNode::VRef(node) => {
|
||||
super::insert_node(&node, parent, next_sibling.get().as_ref());
|
||||
(NodeRef::new(node.clone()), BNode::Ref(node))
|
||||
}
|
||||
VNode::VPortal(vportal) => {
|
||||
let (node_ref, portal) = vportal.attach(parent_scope, parent, next_sibling);
|
||||
(node_ref, portal.into())
|
||||
}
|
||||
VNode::VSuspense(vsuspsense) => {
|
||||
let (node_ref, suspsense) = vsuspsense.attach(parent_scope, parent, next_sibling);
|
||||
(node_ref, suspsense.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reconcile_node(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef {
|
||||
self.reconcile(parent_scope, parent, next_sibling, bundle)
|
||||
}
|
||||
|
||||
fn reconcile(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef {
|
||||
match self {
|
||||
VNode::VTag(vtag) => vtag.reconcile_node(parent_scope, parent, next_sibling, bundle),
|
||||
VNode::VText(vtext) => vtext.reconcile_node(parent_scope, parent, next_sibling, bundle),
|
||||
VNode::VComp(vcomp) => vcomp.reconcile_node(parent_scope, parent, next_sibling, bundle),
|
||||
VNode::VList(vlist) => vlist.reconcile_node(parent_scope, parent, next_sibling, bundle),
|
||||
VNode::VRef(node) => {
|
||||
let _existing = match bundle {
|
||||
BNode::Ref(ref n) if &node == n => n,
|
||||
_ => {
|
||||
return VNode::VRef(node).replace(
|
||||
parent_scope,
|
||||
parent,
|
||||
next_sibling,
|
||||
bundle,
|
||||
);
|
||||
}
|
||||
};
|
||||
NodeRef::new(node)
|
||||
}
|
||||
VNode::VPortal(vportal) => {
|
||||
vportal.reconcile_node(parent_scope, parent, next_sibling, bundle)
|
||||
}
|
||||
VNode::VSuspense(vsuspsense) => {
|
||||
vsuspsense.reconcile_node(parent_scope, parent, next_sibling, bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BText> for BNode {
|
||||
#[inline]
|
||||
fn from(btext: BText) -> Self {
|
||||
Self::Text(btext)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BList> for BNode {
|
||||
#[inline]
|
||||
fn from(blist: BList) -> Self {
|
||||
Self::List(blist)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BTag> for BNode {
|
||||
#[inline]
|
||||
fn from(btag: BTag) -> Self {
|
||||
Self::Tag(Box::new(btag))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BComp> for BNode {
|
||||
#[inline]
|
||||
fn from(bcomp: BComp) -> Self {
|
||||
Self::Comp(bcomp)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BPortal> for BNode {
|
||||
#[inline]
|
||||
fn from(bportal: BPortal) -> Self {
|
||||
Self::Portal(bportal)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BSuspense> for BNode {
|
||||
#[inline]
|
||||
fn from(bsusp: BSuspense) -> Self {
|
||||
Self::Suspense(Box::new(bsusp))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for BNode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
Self::Tag(ref vtag) => vtag.fmt(f),
|
||||
Self::Text(ref btext) => btext.fmt(f),
|
||||
Self::Comp(ref bsusp) => bsusp.fmt(f),
|
||||
Self::List(ref vlist) => vlist.fmt(f),
|
||||
Self::Ref(ref vref) => write!(f, "VRef ( \"{}\" )", crate::utils::print_node(vref)),
|
||||
Self::Portal(ref vportal) => vportal.fmt(f),
|
||||
Self::Suspense(ref bsusp) => bsusp.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
use super::*;
|
||||
use crate::tests::layout_tests::{diff_layouts, TestLayout};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
fn diff() {
|
||||
let document = gloo_utils::document();
|
||||
let vref_node_1 = VNode::VRef(document.create_element("i").unwrap().into());
|
||||
let vref_node_2 = VNode::VRef(document.create_element("b").unwrap().into());
|
||||
|
||||
let layout1 = TestLayout {
|
||||
name: "1",
|
||||
node: vref_node_1,
|
||||
expected: "<i></i>",
|
||||
};
|
||||
|
||||
let layout2 = TestLayout {
|
||||
name: "2",
|
||||
node: vref_node_2,
|
||||
expected: "<b></b>",
|
||||
};
|
||||
|
||||
diff_layouts(vec![layout1, layout2]);
|
||||
}
|
||||
}
|
||||
190
packages/yew/src/dom_bundle/bportal.rs
Normal file
190
packages/yew/src/dom_bundle/bportal.rs
Normal file
@ -0,0 +1,190 @@
|
||||
//! This module contains the bundle implementation of a portal [BPortal].
|
||||
|
||||
use super::test_log;
|
||||
use super::BNode;
|
||||
use crate::dom_bundle::{DomBundle, Reconcilable};
|
||||
use crate::html::{AnyScope, NodeRef};
|
||||
use crate::virtual_dom::Key;
|
||||
use crate::virtual_dom::VPortal;
|
||||
use web_sys::Element;
|
||||
|
||||
/// The bundle implementation to [VPortal].
|
||||
#[derive(Debug)]
|
||||
pub struct BPortal {
|
||||
/// The element under which the content is inserted.
|
||||
host: Element,
|
||||
/// The next sibling after the inserted content
|
||||
inner_sibling: NodeRef,
|
||||
/// The inserted node
|
||||
node: Box<BNode>,
|
||||
}
|
||||
|
||||
impl DomBundle for BPortal {
|
||||
fn detach(self, _: &Element, _parent_to_detach: bool) {
|
||||
test_log!("Detaching portal from host{:?}", self.host.outer_html());
|
||||
self.node.detach(&self.host, false);
|
||||
test_log!("Detached portal from host{:?}", self.host.outer_html());
|
||||
}
|
||||
|
||||
fn shift(&self, _next_parent: &Element, _next_sibling: NodeRef) {
|
||||
// portals have nothing in it's original place of DOM, we also do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
impl Reconcilable for VPortal {
|
||||
type Bundle = BPortal;
|
||||
|
||||
fn attach(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
_parent: &Element,
|
||||
host_next_sibling: NodeRef,
|
||||
) -> (NodeRef, Self::Bundle) {
|
||||
let Self {
|
||||
host,
|
||||
inner_sibling,
|
||||
node,
|
||||
} = self;
|
||||
let (_, inner) = node.attach(parent_scope, &host, inner_sibling.clone());
|
||||
(
|
||||
host_next_sibling,
|
||||
BPortal {
|
||||
host,
|
||||
node: Box::new(inner),
|
||||
inner_sibling,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn reconcile_node(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef {
|
||||
match bundle {
|
||||
BNode::Portal(portal) => self.reconcile(parent_scope, parent, next_sibling, portal),
|
||||
_ => self.replace(parent_scope, parent, next_sibling, bundle),
|
||||
}
|
||||
}
|
||||
|
||||
fn reconcile(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
portal: &mut Self::Bundle,
|
||||
) -> NodeRef {
|
||||
let Self {
|
||||
host,
|
||||
inner_sibling,
|
||||
node,
|
||||
} = self;
|
||||
|
||||
let old_host = std::mem::replace(&mut portal.host, host);
|
||||
let old_inner_sibling = std::mem::replace(&mut portal.inner_sibling, inner_sibling);
|
||||
|
||||
if old_host != portal.host || old_inner_sibling != portal.inner_sibling {
|
||||
// Remount the inner node somewhere else instead of diffing
|
||||
// Move the node, but keep the state
|
||||
portal
|
||||
.node
|
||||
.shift(&portal.host, portal.inner_sibling.clone());
|
||||
}
|
||||
node.reconcile_node(parent_scope, parent, next_sibling.clone(), &mut portal.node);
|
||||
next_sibling
|
||||
}
|
||||
}
|
||||
|
||||
impl BPortal {
|
||||
/// Get the key of the underlying portal
|
||||
pub(super) fn key(&self) -> Option<&Key> {
|
||||
self.node.key()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
extern crate self as yew;
|
||||
|
||||
use crate::html;
|
||||
use crate::tests::layout_tests::{diff_layouts, TestLayout};
|
||||
use crate::virtual_dom::VNode;
|
||||
use yew::virtual_dom::VPortal;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
fn diff() {
|
||||
let mut layouts = vec![];
|
||||
let first_target = gloo_utils::document().create_element("i").unwrap();
|
||||
let second_target = gloo_utils::document().create_element("o").unwrap();
|
||||
let target_with_child = gloo_utils::document().create_element("i").unwrap();
|
||||
let target_child = gloo_utils::document().create_element("s").unwrap();
|
||||
target_with_child.append_child(&target_child).unwrap();
|
||||
|
||||
layouts.push(TestLayout {
|
||||
name: "Portal - first target",
|
||||
node: html! {
|
||||
<div>
|
||||
{VNode::VRef(first_target.clone().into())}
|
||||
{VNode::VRef(second_target.clone().into())}
|
||||
{VNode::VPortal(VPortal::new(
|
||||
html! { {"PORTAL"} },
|
||||
first_target.clone(),
|
||||
))}
|
||||
{"AFTER"}
|
||||
</div>
|
||||
},
|
||||
expected: "<div><i>PORTAL</i><o></o>AFTER</div>",
|
||||
});
|
||||
layouts.push(TestLayout {
|
||||
name: "Portal - second target",
|
||||
node: html! {
|
||||
<div>
|
||||
{VNode::VRef(first_target.clone().into())}
|
||||
{VNode::VRef(second_target.clone().into())}
|
||||
{VNode::VPortal(VPortal::new(
|
||||
html! { {"PORTAL"} },
|
||||
second_target.clone(),
|
||||
))}
|
||||
{"AFTER"}
|
||||
</div>
|
||||
},
|
||||
expected: "<div><i></i><o>PORTAL</o>AFTER</div>",
|
||||
});
|
||||
layouts.push(TestLayout {
|
||||
name: "Portal - replaced by text",
|
||||
node: html! {
|
||||
<div>
|
||||
{VNode::VRef(first_target.clone().into())}
|
||||
{VNode::VRef(second_target.clone().into())}
|
||||
{"FOO"}
|
||||
{"AFTER"}
|
||||
</div>
|
||||
},
|
||||
expected: "<div><i></i><o></o>FOOAFTER</div>",
|
||||
});
|
||||
layouts.push(TestLayout {
|
||||
name: "Portal - next sibling",
|
||||
node: html! {
|
||||
<div>
|
||||
{VNode::VRef(target_with_child.clone().into())}
|
||||
{VNode::VPortal(VPortal::new_before(
|
||||
html! { {"PORTAL"} },
|
||||
target_with_child.clone(),
|
||||
Some(target_child.clone().into()),
|
||||
))}
|
||||
</div>
|
||||
},
|
||||
expected: "<div><i>PORTAL<s></s></i></div>",
|
||||
});
|
||||
|
||||
diff_layouts(layouts)
|
||||
}
|
||||
}
|
||||
178
packages/yew/src/dom_bundle/bsuspense.rs
Normal file
178
packages/yew/src/dom_bundle/bsuspense.rs
Normal file
@ -0,0 +1,178 @@
|
||||
//! This module contains the bundle version of a supsense [BSuspense]
|
||||
|
||||
use super::{BNode, DomBundle, Reconcilable};
|
||||
use crate::html::AnyScope;
|
||||
use crate::virtual_dom::{Key, VSuspense};
|
||||
use crate::NodeRef;
|
||||
use web_sys::Element;
|
||||
|
||||
/// The bundle implementation to [VSuspense]
|
||||
#[derive(Debug)]
|
||||
pub struct BSuspense {
|
||||
children_bundle: BNode,
|
||||
/// The supsense is suspended if fallback contains [Some] bundle
|
||||
fallback_bundle: Option<BNode>,
|
||||
detached_parent: Element,
|
||||
key: Option<Key>,
|
||||
}
|
||||
|
||||
impl BSuspense {
|
||||
/// Get the key of the underlying suspense
|
||||
pub(super) fn key(&self) -> Option<&Key> {
|
||||
self.key.as_ref()
|
||||
}
|
||||
/// Get the bundle node that actually shows up in the dom
|
||||
fn active_node(&self) -> &BNode {
|
||||
self.fallback_bundle
|
||||
.as_ref()
|
||||
.unwrap_or(&self.children_bundle)
|
||||
}
|
||||
}
|
||||
|
||||
impl DomBundle for BSuspense {
|
||||
fn detach(self, parent: &Element, parent_to_detach: bool) {
|
||||
if let Some(fallback) = self.fallback_bundle {
|
||||
fallback.detach(parent, parent_to_detach);
|
||||
self.children_bundle.detach(&self.detached_parent, false);
|
||||
} else {
|
||||
self.children_bundle.detach(parent, parent_to_detach);
|
||||
}
|
||||
}
|
||||
|
||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
||||
self.active_node().shift(next_parent, next_sibling)
|
||||
}
|
||||
}
|
||||
|
||||
impl Reconcilable for VSuspense {
|
||||
type Bundle = BSuspense;
|
||||
|
||||
fn attach(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> (NodeRef, Self::Bundle) {
|
||||
let VSuspense {
|
||||
children,
|
||||
fallback,
|
||||
detached_parent,
|
||||
suspended,
|
||||
key,
|
||||
} = self;
|
||||
let detached_parent = detached_parent.expect("no detached parent?");
|
||||
|
||||
// When it's suspended, we render children into an element that is detached from the dom
|
||||
// tree while rendering fallback UI into the original place where children resides in.
|
||||
if suspended {
|
||||
let (_child_ref, children_bundle) =
|
||||
children.attach(parent_scope, &detached_parent, NodeRef::default());
|
||||
let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling);
|
||||
(
|
||||
fallback_ref,
|
||||
BSuspense {
|
||||
children_bundle,
|
||||
fallback_bundle: Some(fallback),
|
||||
detached_parent,
|
||||
key,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let (child_ref, children_bundle) = children.attach(parent_scope, parent, next_sibling);
|
||||
(
|
||||
child_ref,
|
||||
BSuspense {
|
||||
children_bundle,
|
||||
fallback_bundle: None,
|
||||
detached_parent,
|
||||
key,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn reconcile_node(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef {
|
||||
match bundle {
|
||||
// We only preserve the child state if they are the same suspense.
|
||||
BNode::Suspense(m)
|
||||
if m.key == self.key
|
||||
&& self.detached_parent.as_ref() == Some(&m.detached_parent) =>
|
||||
{
|
||||
self.reconcile(parent_scope, parent, next_sibling, m)
|
||||
}
|
||||
_ => self.replace(parent_scope, parent, next_sibling, bundle),
|
||||
}
|
||||
}
|
||||
|
||||
fn reconcile(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
suspense: &mut Self::Bundle,
|
||||
) -> NodeRef {
|
||||
let VSuspense {
|
||||
children,
|
||||
fallback,
|
||||
detached_parent,
|
||||
suspended,
|
||||
key: _,
|
||||
} = self;
|
||||
let detached_parent = detached_parent.expect("no detached parent?");
|
||||
|
||||
let children_bundle = &mut suspense.children_bundle;
|
||||
// no need to update key & detached_parent
|
||||
|
||||
// When it's suspended, we render children into an element that is detached from the dom
|
||||
// tree while rendering fallback UI into the original place where children resides in.
|
||||
match (suspended, &mut suspense.fallback_bundle) {
|
||||
// Both suspended, reconcile children into detached_parent, fallback into the DOM
|
||||
(true, Some(fallback_bundle)) => {
|
||||
children.reconcile_node(
|
||||
parent_scope,
|
||||
&detached_parent,
|
||||
NodeRef::default(),
|
||||
children_bundle,
|
||||
);
|
||||
|
||||
fallback.reconcile_node(parent_scope, parent, next_sibling, fallback_bundle)
|
||||
}
|
||||
// Not suspended, just reconcile the children into the DOM
|
||||
(false, None) => {
|
||||
children.reconcile_node(parent_scope, parent, next_sibling, children_bundle)
|
||||
}
|
||||
// Freshly suspended. Shift children into the detached parent, then add fallback to the DOM
|
||||
(true, None) => {
|
||||
children_bundle.shift(&detached_parent, NodeRef::default());
|
||||
|
||||
children.reconcile_node(
|
||||
parent_scope,
|
||||
&detached_parent,
|
||||
NodeRef::default(),
|
||||
children_bundle,
|
||||
);
|
||||
// first render of fallback
|
||||
let (fallback_ref, fallback) = fallback.attach(parent_scope, parent, next_sibling);
|
||||
suspense.fallback_bundle = Some(fallback);
|
||||
fallback_ref
|
||||
}
|
||||
// Freshly unsuspended. Detach fallback from the DOM, then shift children into it.
|
||||
(false, Some(_)) => {
|
||||
suspense
|
||||
.fallback_bundle
|
||||
.take()
|
||||
.unwrap() // We just matched Some(_)
|
||||
.detach(parent, false);
|
||||
|
||||
children_bundle.shift(parent, next_sibling.clone());
|
||||
children.reconcile_node(parent_scope, parent, next_sibling, children_bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
275
packages/yew/src/dom_bundle/btag/attributes.rs
Normal file
275
packages/yew/src/dom_bundle/btag/attributes.rs
Normal file
@ -0,0 +1,275 @@
|
||||
use super::Apply;
|
||||
use crate::virtual_dom::vtag::{InputFields, Value};
|
||||
use crate::virtual_dom::Attributes;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
use std::iter;
|
||||
use std::ops::Deref;
|
||||
use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
|
||||
|
||||
impl<T: AccessValue> Apply for Value<T> {
|
||||
type Element = T;
|
||||
type Bundle = Self;
|
||||
|
||||
fn apply(self, el: &Self::Element) -> Self {
|
||||
if let Some(v) = self.deref() {
|
||||
el.set_value(v);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn apply_diff(self, el: &Self::Element, bundle: &mut Self) {
|
||||
match (self.deref(), (*bundle).deref()) {
|
||||
(Some(new), Some(_)) => {
|
||||
// Refresh value from the DOM. It might have changed.
|
||||
if new.as_ref() != el.value() {
|
||||
el.set_value(new);
|
||||
}
|
||||
}
|
||||
(Some(new), None) => el.set_value(new),
|
||||
(None, Some(_)) => el.set_value(""),
|
||||
(None, None) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_access_value {
|
||||
($( $type:ty )*) => {
|
||||
$(
|
||||
impl AccessValue for $type {
|
||||
#[inline]
|
||||
fn value(&self) -> String {
|
||||
<$type>::value(&self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_value(&self, v: &str) {
|
||||
<$type>::set_value(&self, v)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
impl_access_value! {InputElement TextAreaElement}
|
||||
|
||||
/// Able to have its value read or set
|
||||
pub trait AccessValue {
|
||||
fn value(&self) -> String;
|
||||
fn set_value(&self, v: &str);
|
||||
}
|
||||
|
||||
impl Apply for InputFields {
|
||||
type Element = InputElement;
|
||||
type Bundle = Self;
|
||||
|
||||
fn apply(mut self, el: &Self::Element) -> Self {
|
||||
// IMPORTANT! This parameter has to be set every time
|
||||
// to prevent strange behaviour in the browser when the DOM changes
|
||||
el.set_checked(self.checked);
|
||||
|
||||
self.value = self.value.apply(el);
|
||||
self
|
||||
}
|
||||
|
||||
fn apply_diff(self, el: &Self::Element, bundle: &mut Self) {
|
||||
// IMPORTANT! This parameter has to be set every time
|
||||
// to prevent strange behaviour in the browser when the DOM changes
|
||||
el.set_checked(self.checked);
|
||||
|
||||
self.value.apply_diff(el, &mut bundle.value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Attributes {
|
||||
#[cold]
|
||||
fn apply_diff_index_maps<'a, A, B>(
|
||||
el: &Element,
|
||||
// this makes it possible to diff `&'a IndexMap<_, A>` and `IndexMap<_, &'a A>`.
|
||||
mut new_iter: impl Iterator<Item = (&'static str, &'a str)>,
|
||||
new: &IndexMap<&'static str, A>,
|
||||
old: &IndexMap<&'static str, B>,
|
||||
) where
|
||||
A: AsRef<str>,
|
||||
B: AsRef<str>,
|
||||
{
|
||||
let mut old_iter = old.iter();
|
||||
loop {
|
||||
match (new_iter.next(), old_iter.next()) {
|
||||
(Some((new_key, new_value)), Some((old_key, old_value))) => {
|
||||
if new_key != *old_key {
|
||||
break;
|
||||
}
|
||||
if new_value != old_value.as_ref() {
|
||||
Self::set_attribute(el, new_key, new_value);
|
||||
}
|
||||
}
|
||||
// new attributes
|
||||
(Some(attr), None) => {
|
||||
for (key, value) in iter::once(attr).chain(new_iter) {
|
||||
match old.get(key) {
|
||||
Some(old_value) => {
|
||||
if value != old_value.as_ref() {
|
||||
Self::set_attribute(el, key, value);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
Self::set_attribute(el, key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// removed attributes
|
||||
(None, Some(attr)) => {
|
||||
for (key, _) in iter::once(attr).chain(old_iter) {
|
||||
if !new.contains_key(key) {
|
||||
Self::remove_attribute(el, key);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert [Attributes] pair to [HashMap]s and patch changes to `el`.
|
||||
/// Works with any [Attributes] variants.
|
||||
#[cold]
|
||||
fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) {
|
||||
fn collect<'a>(src: &'a Attributes) -> HashMap<&'static str, &'a str> {
|
||||
use Attributes::*;
|
||||
|
||||
match src {
|
||||
Static(arr) => (*arr).iter().map(|[k, v]| (*k, *v)).collect(),
|
||||
Dynamic { keys, values } => keys
|
||||
.iter()
|
||||
.zip(values.iter())
|
||||
.filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref())))
|
||||
.collect(),
|
||||
IndexMap(m) => m.iter().map(|(k, v)| (*k, v.as_ref())).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
let new = collect(new);
|
||||
let old = collect(old);
|
||||
|
||||
// Update existing or set new
|
||||
for (k, new) in new.iter() {
|
||||
if match old.get(k) {
|
||||
Some(old) => old != new,
|
||||
None => true,
|
||||
} {
|
||||
el.set_attribute(k, new).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove missing
|
||||
for k in old.keys() {
|
||||
if !new.contains_key(k) {
|
||||
Self::remove_attribute(el, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_attribute(el: &Element, key: &str, value: &str) {
|
||||
el.set_attribute(key, value).expect("invalid attribute key")
|
||||
}
|
||||
|
||||
fn remove_attribute(el: &Element, key: &str) {
|
||||
el.remove_attribute(key)
|
||||
.expect("could not remove attribute")
|
||||
}
|
||||
}
|
||||
|
||||
impl Apply for Attributes {
|
||||
type Element = Element;
|
||||
type Bundle = Self;
|
||||
|
||||
fn apply(self, el: &Element) -> Self {
|
||||
match &self {
|
||||
Self::Static(arr) => {
|
||||
for kv in arr.iter() {
|
||||
Self::set_attribute(el, kv[0], kv[1]);
|
||||
}
|
||||
}
|
||||
Self::Dynamic { keys, values } => {
|
||||
for (k, v) in keys.iter().zip(values.iter()) {
|
||||
if let Some(v) = v {
|
||||
Self::set_attribute(el, k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::IndexMap(m) => {
|
||||
for (k, v) in m.iter() {
|
||||
Self::set_attribute(el, k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn apply_diff(self, el: &Element, bundle: &mut Self) {
|
||||
#[inline]
|
||||
fn ptr_eq<T>(a: &[T], b: &[T]) -> bool {
|
||||
std::ptr::eq(a, b)
|
||||
}
|
||||
|
||||
let ancestor = std::mem::replace(bundle, self);
|
||||
let bundle = &*bundle; // reborrow it immutably from here
|
||||
match (bundle, ancestor) {
|
||||
// Hot path
|
||||
(Self::Static(new), Self::Static(old)) if ptr_eq(new, old) => (),
|
||||
// Hot path
|
||||
(
|
||||
Self::Dynamic {
|
||||
keys: new_k,
|
||||
values: new_v,
|
||||
},
|
||||
Self::Dynamic {
|
||||
keys: old_k,
|
||||
values: old_v,
|
||||
},
|
||||
) if ptr_eq(new_k, old_k) => {
|
||||
// Double zipping does not optimize well, so use asserts and unsafe instead
|
||||
assert!(new_k.len() == new_v.len());
|
||||
assert!(new_k.len() == old_v.len());
|
||||
for i in 0..new_k.len() {
|
||||
macro_rules! key {
|
||||
() => {
|
||||
unsafe { new_k.get_unchecked(i) }
|
||||
};
|
||||
}
|
||||
macro_rules! set {
|
||||
($new:expr) => {
|
||||
Self::set_attribute(el, key!(), $new)
|
||||
};
|
||||
}
|
||||
|
||||
match unsafe { (new_v.get_unchecked(i), old_v.get_unchecked(i)) } {
|
||||
(Some(new), Some(old)) => {
|
||||
if new != old {
|
||||
set!(new);
|
||||
}
|
||||
}
|
||||
(Some(new), None) => set!(new),
|
||||
(None, Some(_)) => {
|
||||
Self::remove_attribute(el, key!());
|
||||
}
|
||||
(None, None) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
// For VTag's constructed outside the html! macro
|
||||
(Self::IndexMap(new), Self::IndexMap(ref old)) => {
|
||||
let new_iter = new.iter().map(|(k, v)| (*k, v.as_ref()));
|
||||
Self::apply_diff_index_maps(el, new_iter, new, old);
|
||||
}
|
||||
// Cold path. Happens only with conditional swapping and reordering of `VTag`s with the
|
||||
// same tag and no keys.
|
||||
(new, ref ancestor) => {
|
||||
Self::apply_diff_as_maps(el, new, ancestor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
709
packages/yew/src/dom_bundle/btag/listeners.rs
Normal file
709
packages/yew/src/dom_bundle/btag/listeners.rs
Normal file
@ -0,0 +1,709 @@
|
||||
use super::Apply;
|
||||
use crate::dom_bundle::test_log;
|
||||
use crate::virtual_dom::{Listener, ListenerKind, Listeners};
|
||||
use gloo::events::{EventListener, EventListenerOptions, EventListenerPhase};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Element, Event};
|
||||
|
||||
thread_local! {
|
||||
/// Global event listener registry
|
||||
static REGISTRY: RefCell<Registry> = Default::default();
|
||||
|
||||
/// Key used to store listener id on element
|
||||
static LISTENER_ID_PROP: wasm_bindgen::JsValue = "__yew_listener_id".into();
|
||||
|
||||
/// Cached reference to the document body
|
||||
static BODY: web_sys::HtmlElement = gloo_utils::document().body().unwrap();
|
||||
}
|
||||
|
||||
/// Bubble events during delegation
|
||||
static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
/// Set, if events should bubble up the DOM tree, calling any matching callbacks.
|
||||
///
|
||||
/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event
|
||||
/// handling performance.
|
||||
///
|
||||
/// Note that yew uses event delegation and implements internal even bubbling for performance
|
||||
/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event
|
||||
/// handler has no effect.
|
||||
///
|
||||
/// This function should be called before any component is mounted.
|
||||
pub fn set_event_bubbling(bubble: bool) {
|
||||
BUBBLE_EVENTS.store(bubble, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// An active set of listeners on an element
|
||||
#[derive(Debug)]
|
||||
pub(super) enum ListenerRegistration {
|
||||
/// No listeners registered.
|
||||
NoReg,
|
||||
/// Added to global registry by ID
|
||||
Registered(u32),
|
||||
}
|
||||
|
||||
impl Apply for Listeners {
|
||||
type Element = Element;
|
||||
type Bundle = ListenerRegistration;
|
||||
|
||||
fn apply(self, el: &Self::Element) -> ListenerRegistration {
|
||||
match self {
|
||||
Self::Pending(pending) => ListenerRegistration::register(el, &pending),
|
||||
Self::None => ListenerRegistration::NoReg,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_diff(self, el: &Self::Element, bundle: &mut ListenerRegistration) {
|
||||
use ListenerRegistration::*;
|
||||
use Listeners::*;
|
||||
|
||||
match (self, bundle) {
|
||||
(Pending(pending), Registered(ref id)) => {
|
||||
// Reuse the ID
|
||||
test_log!("reusing listeners for {}", id);
|
||||
Registry::with(|reg| reg.patch(id, &*pending));
|
||||
}
|
||||
(Pending(pending), bundle @ NoReg) => {
|
||||
*bundle = ListenerRegistration::register(el, &pending);
|
||||
test_log!(
|
||||
"registering listeners for {}",
|
||||
match bundle {
|
||||
ListenerRegistration::Registered(id) => id,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
);
|
||||
}
|
||||
(None, bundle @ Registered(_)) => {
|
||||
let id = match bundle {
|
||||
ListenerRegistration::Registered(ref id) => id,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
test_log!("unregistering listeners for {}", id);
|
||||
Registry::with(|reg| reg.unregister(id));
|
||||
*bundle = NoReg;
|
||||
}
|
||||
(None, NoReg) => {
|
||||
test_log!("{}", &"unchanged empty listeners");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl ListenerRegistration {
|
||||
/// Register listeners and return their handle ID
|
||||
fn register(el: &Element, pending: &[Option<Rc<dyn Listener>>]) -> Self {
|
||||
Self::Registered(Registry::with(|reg| {
|
||||
let id = reg.set_listener_id(el);
|
||||
reg.register(id, pending);
|
||||
id
|
||||
}))
|
||||
}
|
||||
|
||||
/// Remove any registered event listeners from the global registry
|
||||
pub(super) fn unregister(&self) {
|
||||
if let Self::Registered(id) = self {
|
||||
Registry::with(|r| r.unregister(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, Eq, PartialEq, Debug)]
|
||||
struct EventDescriptor {
|
||||
kind: ListenerKind,
|
||||
passive: bool,
|
||||
}
|
||||
|
||||
impl From<&dyn Listener> for EventDescriptor {
|
||||
fn from(l: &dyn Listener) -> Self {
|
||||
Self {
|
||||
kind: l.kind(),
|
||||
passive: l.passive(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures global event handler registration.
|
||||
//
|
||||
// Separate struct to DRY, while avoiding partial struct mutability.
|
||||
#[derive(Default, Debug)]
|
||||
struct GlobalHandlers {
|
||||
/// Events with registered handlers that are possibly passive
|
||||
handling: HashSet<EventDescriptor>,
|
||||
|
||||
/// Keep track of all listeners to drop them on registry drop.
|
||||
/// The registry is never dropped in production.
|
||||
#[cfg(test)]
|
||||
registered: Vec<(ListenerKind, EventListener)>,
|
||||
}
|
||||
|
||||
impl GlobalHandlers {
|
||||
/// Ensure a descriptor has a global event handler assigned
|
||||
fn ensure_handled(&mut self, desc: EventDescriptor) {
|
||||
if !self.handling.contains(&desc) {
|
||||
let cl = {
|
||||
let desc = desc.clone();
|
||||
BODY.with(move |body| {
|
||||
let options = EventListenerOptions {
|
||||
phase: EventListenerPhase::Capture,
|
||||
passive: desc.passive,
|
||||
};
|
||||
EventListener::new_with_options(
|
||||
body,
|
||||
desc.kind.type_name(),
|
||||
options,
|
||||
move |e: &Event| Registry::handle(desc.clone(), e.clone()),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
// Never drop the closure as this event handler is static
|
||||
#[cfg(not(test))]
|
||||
cl.forget();
|
||||
#[cfg(test)]
|
||||
self.registered.push((desc.kind.clone(), cl));
|
||||
|
||||
self.handling.insert(desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Global multiplexing event handler registry
|
||||
#[derive(Default, Debug)]
|
||||
struct Registry {
|
||||
/// Counter for assigning new IDs
|
||||
id_counter: u32,
|
||||
|
||||
/// Registered global event handlers
|
||||
global: GlobalHandlers,
|
||||
|
||||
/// Contains all registered event listeners by listener ID
|
||||
by_id: HashMap<u32, HashMap<EventDescriptor, Vec<Rc<dyn Listener>>>>,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
/// Run f with access to global Registry
|
||||
#[inline]
|
||||
fn with<R>(f: impl FnOnce(&mut Registry) -> R) -> R {
|
||||
REGISTRY.with(|r| f(&mut *r.borrow_mut()))
|
||||
}
|
||||
|
||||
/// Register all passed listeners under ID
|
||||
fn register(&mut self, id: u32, listeners: &[Option<Rc<dyn Listener>>]) {
|
||||
let mut by_desc =
|
||||
HashMap::<EventDescriptor, Vec<Rc<dyn Listener>>>::with_capacity(listeners.len());
|
||||
for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
|
||||
let desc = EventDescriptor::from(l.deref());
|
||||
self.global.ensure_handled(desc.clone());
|
||||
by_desc.entry(desc).or_default().push(l);
|
||||
}
|
||||
self.by_id.insert(id, by_desc);
|
||||
}
|
||||
|
||||
/// Patch an already registered set of handlers
|
||||
fn patch(&mut self, id: &u32, listeners: &[Option<Rc<dyn Listener>>]) {
|
||||
if let Some(by_desc) = self.by_id.get_mut(id) {
|
||||
// Keeping empty vectors is fine. Those don't do much and should happen rarely.
|
||||
for v in by_desc.values_mut() {
|
||||
v.clear()
|
||||
}
|
||||
|
||||
for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
|
||||
let desc = EventDescriptor::from(l.deref());
|
||||
self.global.ensure_handled(desc.clone());
|
||||
by_desc.entry(desc).or_default().push(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unregister any existing listeners for ID
|
||||
fn unregister(&mut self, id: &u32) {
|
||||
self.by_id.remove(id);
|
||||
}
|
||||
|
||||
/// Set unique listener ID onto element and return it
|
||||
fn set_listener_id(&mut self, el: &Element) -> u32 {
|
||||
let id = self.id_counter;
|
||||
self.id_counter += 1;
|
||||
|
||||
LISTENER_ID_PROP.with(|prop| {
|
||||
if !js_sys::Reflect::set(el, prop, &js_sys::Number::from(id)).unwrap() {
|
||||
panic!("failed to set listener ID property");
|
||||
}
|
||||
});
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Handle a global event firing
|
||||
fn handle(desc: EventDescriptor, event: Event) {
|
||||
let target = match event
|
||||
.target()
|
||||
.and_then(|el| el.dyn_into::<web_sys::Element>().ok())
|
||||
{
|
||||
Some(el) => el,
|
||||
None => return,
|
||||
};
|
||||
|
||||
Self::run_handlers(desc, event, target);
|
||||
}
|
||||
|
||||
fn run_handlers(desc: EventDescriptor, event: Event, target: web_sys::Element) {
|
||||
let run_handler = |el: &web_sys::Element| {
|
||||
if let Some(l) = LISTENER_ID_PROP
|
||||
.with(|prop| js_sys::Reflect::get(el, prop).ok())
|
||||
.and_then(|v| v.dyn_into().ok())
|
||||
.and_then(|num: js_sys::Number| {
|
||||
Registry::with(|r| {
|
||||
r.by_id
|
||||
.get(&(num.value_of() as u32))
|
||||
.and_then(|s| s.get(&desc))
|
||||
.cloned()
|
||||
})
|
||||
})
|
||||
{
|
||||
for l in l {
|
||||
l.handle(event.clone());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
run_handler(&target);
|
||||
|
||||
if BUBBLE_EVENTS.load(Ordering::Relaxed) {
|
||||
let mut el = target;
|
||||
while !event.cancel_bubble() {
|
||||
el = match el.parent_element() {
|
||||
Some(el) => el,
|
||||
None => break,
|
||||
};
|
||||
run_handler(&el);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "wasm_test"))]
|
||||
mod tests {
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
use web_sys::{Event, EventInit, MouseEvent};
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
use crate::{html, html::TargetCast, scheduler, AppHandle, Component, Context, Html};
|
||||
use gloo_utils::document;
|
||||
use wasm_bindgen::JsCast;
|
||||
use yew::Callback;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Message {
|
||||
Action,
|
||||
StopListening,
|
||||
SetText(String),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
stop_listening: bool,
|
||||
action: u32,
|
||||
text: String,
|
||||
}
|
||||
|
||||
trait Mixin {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
let link = ctx.link().clone();
|
||||
let onclick = Callback::from(move |_| {
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
if state.stop_listening {
|
||||
html! {
|
||||
<a>{state.action}</a>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<a {onclick}>
|
||||
{state.action}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Comp<M>
|
||||
where
|
||||
M: Mixin + 'static,
|
||||
{
|
||||
state: State,
|
||||
pd: PhantomData<M>,
|
||||
}
|
||||
|
||||
impl<M> Component for Comp<M>
|
||||
where
|
||||
M: Mixin + 'static,
|
||||
{
|
||||
type Message = Message;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Comp {
|
||||
state: Default::default(),
|
||||
pd: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Message::Action => {
|
||||
self.state.action += 1;
|
||||
}
|
||||
Message::StopListening => {
|
||||
self.state.stop_listening = true;
|
||||
}
|
||||
Message::SetText(s) => {
|
||||
self.state.text = s;
|
||||
}
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> crate::Html {
|
||||
M::view(ctx, &self.state)
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_count(el: &web_sys::HtmlElement, count: isize) {
|
||||
assert_eq!(el.text_content(), Some(count.to_string()))
|
||||
}
|
||||
|
||||
fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement {
|
||||
document()
|
||||
.query_selector(tag)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<web_sys::HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn init<M>(tag: &str) -> (AppHandle<Comp<M>>, web_sys::HtmlElement)
|
||||
where
|
||||
M: Mixin,
|
||||
{
|
||||
// Remove any existing listeners and elements
|
||||
super::Registry::with(|r| *r = Default::default());
|
||||
if let Some(el) = document().query_selector(tag).unwrap() {
|
||||
el.parent_element().unwrap().remove();
|
||||
}
|
||||
|
||||
let root = document().create_element("div").unwrap();
|
||||
document().body().unwrap().append_child(&root).unwrap();
|
||||
let app = crate::start_app_in_element::<Comp<M>>(root);
|
||||
scheduler::start_now();
|
||||
|
||||
(app, get_el_by_tag(tag))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synchronous() {
|
||||
struct Synchronous;
|
||||
|
||||
impl Mixin for Synchronous {}
|
||||
|
||||
let (link, el) = init::<Synchronous>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 1);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
|
||||
link.send_message(Message::StopListening);
|
||||
scheduler::start_now();
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn non_bubbling_event() {
|
||||
struct NonBubbling;
|
||||
|
||||
impl Mixin for NonBubbling {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
let link = ctx.link().clone();
|
||||
let onblur = Callback::from(move |_| {
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
html! {
|
||||
<div>
|
||||
<a>
|
||||
<input id="input" {onblur} type="text" />
|
||||
{state.action}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (_, el) = init::<NonBubbling>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
let input = document().get_element_by_id("input").unwrap();
|
||||
|
||||
input
|
||||
.dispatch_event(
|
||||
&Event::new_with_event_init_dict("blur", &{
|
||||
let mut dict = EventInit::new();
|
||||
dict.bubbles(false);
|
||||
dict
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_count(&el, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bubbling() {
|
||||
struct Bubbling;
|
||||
|
||||
impl Mixin for Bubbling {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
if state.stop_listening {
|
||||
html! {
|
||||
<div>
|
||||
<a>
|
||||
{state.action}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
let link = ctx.link().clone();
|
||||
let cb = Callback::from(move |_| {
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
html! {
|
||||
<div onclick={cb.clone()}>
|
||||
<a onclick={cb}>
|
||||
{state.action}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (link, el) = init::<Bubbling>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 4);
|
||||
|
||||
link.send_message(Message::StopListening);
|
||||
scheduler::start_now();
|
||||
el.click();
|
||||
assert_count(&el, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_bubbling() {
|
||||
struct CancelBubbling;
|
||||
|
||||
impl Mixin for CancelBubbling {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
let link = ctx.link().clone();
|
||||
let onclick = Callback::from(move |_| {
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
let link = ctx.link().clone();
|
||||
let onclick2 = Callback::from(move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
html! {
|
||||
<div onclick={onclick}>
|
||||
<a onclick={onclick2}>
|
||||
{state.action}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (_, el) = init::<CancelBubbling>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 1);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_bubbling_nested() {
|
||||
// Here an event is being delivered to a DOM node which does
|
||||
// _not_ have a listener but which is contained within an
|
||||
// element that does and which cancels the bubble.
|
||||
struct CancelBubbling;
|
||||
|
||||
impl Mixin for CancelBubbling {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
let link = ctx.link().clone();
|
||||
let onclick = Callback::from(move |_| {
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
let link = ctx.link().clone();
|
||||
let onclick2 = Callback::from(move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
html! {
|
||||
<div onclick={onclick}>
|
||||
<div onclick={onclick2}>
|
||||
<a>
|
||||
{state.action}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (_, el) = init::<CancelBubbling>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 1);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
}
|
||||
|
||||
fn test_input_listener<E>(make_event: impl Fn() -> E)
|
||||
where
|
||||
E: JsCast + std::fmt::Debug,
|
||||
{
|
||||
struct Input;
|
||||
|
||||
impl Mixin for Input {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
if state.stop_listening {
|
||||
html! {
|
||||
<div>
|
||||
<input type="text" />
|
||||
<p>{state.text.clone()}</p>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
let link = ctx.link().clone();
|
||||
let onchange = Callback::from(move |e: web_sys::Event| {
|
||||
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||
link.send_message(Message::SetText(el.value()));
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
let link = ctx.link().clone();
|
||||
let oninput = Callback::from(move |e: web_sys::InputEvent| {
|
||||
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||
link.send_message(Message::SetText(el.value()));
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<input type="text" {onchange} {oninput} />
|
||||
<p>{state.text.clone()}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (link, input_el) = init::<Input>("input");
|
||||
let input_el = input_el.dyn_into::<web_sys::HtmlInputElement>().unwrap();
|
||||
let p_el = get_el_by_tag("p");
|
||||
|
||||
assert_eq!(&p_el.text_content().unwrap(), "");
|
||||
for mut s in ["foo", "bar", "baz"].iter() {
|
||||
input_el.set_value(s);
|
||||
if s == &"baz" {
|
||||
link.send_message(Message::StopListening);
|
||||
scheduler::start_now();
|
||||
|
||||
s = &"bar";
|
||||
}
|
||||
input_el
|
||||
.dyn_ref::<web_sys::EventTarget>()
|
||||
.unwrap()
|
||||
.dispatch_event(&make_event().dyn_into().unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(&p_el.text_content().unwrap(), s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oninput() {
|
||||
test_input_listener(|| {
|
||||
web_sys::InputEvent::new_with_event_init_dict(
|
||||
"input",
|
||||
web_sys::InputEventInit::new().bubbles(true),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn onchange() {
|
||||
test_input_listener(|| {
|
||||
web_sys::Event::new_with_event_init_dict(
|
||||
"change",
|
||||
web_sys::EventInit::new().bubbles(true),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
}
|
||||
1078
packages/yew/src/dom_bundle/btag/mod.rs
Normal file
1078
packages/yew/src/dom_bundle/btag/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
163
packages/yew/src/dom_bundle/btext.rs
Normal file
163
packages/yew/src/dom_bundle/btext.rs
Normal file
@ -0,0 +1,163 @@
|
||||
//! This module contains the bundle implementation of text [BText].
|
||||
|
||||
use super::{insert_node, BNode, DomBundle, Reconcilable};
|
||||
use crate::html::AnyScope;
|
||||
use crate::virtual_dom::{AttrValue, VText};
|
||||
use crate::NodeRef;
|
||||
use gloo::console;
|
||||
use gloo_utils::document;
|
||||
use web_sys::{Element, Text as TextNode};
|
||||
|
||||
/// The bundle implementation to [VText]
|
||||
pub struct BText {
|
||||
text: AttrValue,
|
||||
text_node: TextNode,
|
||||
}
|
||||
|
||||
impl DomBundle for BText {
|
||||
fn detach(self, parent: &Element, parent_to_detach: bool) {
|
||||
if !parent_to_detach {
|
||||
let result = parent.remove_child(&self.text_node);
|
||||
|
||||
if result.is_err() {
|
||||
console::warn!("Node not found to remove VText");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef) {
|
||||
let node = &self.text_node;
|
||||
|
||||
next_parent
|
||||
.insert_before(node, next_sibling.get().as_ref())
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Reconcilable for VText {
|
||||
type Bundle = BText;
|
||||
|
||||
fn attach(
|
||||
self,
|
||||
_parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> (NodeRef, Self::Bundle) {
|
||||
let Self { text } = self;
|
||||
let text_node = document().create_text_node(&text);
|
||||
insert_node(&text_node, parent, next_sibling.get().as_ref());
|
||||
let node_ref = NodeRef::new(text_node.clone().into());
|
||||
(node_ref, BText { text, text_node })
|
||||
}
|
||||
|
||||
/// Renders virtual node over existing `TextNode`, but only if value of text has changed.
|
||||
fn reconcile_node(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef {
|
||||
match bundle {
|
||||
BNode::Text(btext) => self.reconcile(parent_scope, parent, next_sibling, btext),
|
||||
_ => self.replace(parent_scope, parent, next_sibling, bundle),
|
||||
}
|
||||
}
|
||||
fn reconcile(
|
||||
self,
|
||||
_parent_scope: &AnyScope,
|
||||
_parent: &Element,
|
||||
_next_sibling: NodeRef,
|
||||
btext: &mut Self::Bundle,
|
||||
) -> NodeRef {
|
||||
let Self { text } = self;
|
||||
let ancestor_text = std::mem::replace(&mut btext.text, text);
|
||||
if btext.text != ancestor_text {
|
||||
btext.text_node.set_node_value(Some(&btext.text));
|
||||
}
|
||||
NodeRef::new(btext.text_node.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BText {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "BText {{ text: \"{}\" }}", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
extern crate self as yew;
|
||||
|
||||
use crate::html;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
fn text_as_root() {
|
||||
html! {
|
||||
"Text Node As Root"
|
||||
};
|
||||
|
||||
html! {
|
||||
{ "Text Node As Root" }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
extern crate self as yew;
|
||||
|
||||
use crate::html;
|
||||
use crate::tests::layout_tests::{diff_layouts, TestLayout};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
fn diff() {
|
||||
let layout1 = TestLayout {
|
||||
name: "1",
|
||||
node: html! { "a" },
|
||||
expected: "a",
|
||||
};
|
||||
|
||||
let layout2 = TestLayout {
|
||||
name: "2",
|
||||
node: html! { "b" },
|
||||
expected: "b",
|
||||
};
|
||||
|
||||
let layout3 = TestLayout {
|
||||
name: "3",
|
||||
node: html! {
|
||||
<>
|
||||
{"a"}
|
||||
{"b"}
|
||||
</>
|
||||
},
|
||||
expected: "ab",
|
||||
};
|
||||
|
||||
let layout4 = TestLayout {
|
||||
name: "4",
|
||||
node: html! {
|
||||
<>
|
||||
{"b"}
|
||||
{"a"}
|
||||
</>
|
||||
},
|
||||
expected: "ba",
|
||||
};
|
||||
|
||||
diff_layouts(vec![layout1, layout2, layout3, layout4]);
|
||||
}
|
||||
}
|
||||
151
packages/yew/src/dom_bundle/mod.rs
Normal file
151
packages/yew/src/dom_bundle/mod.rs
Normal file
@ -0,0 +1,151 @@
|
||||
//! Realizing a virtual dom on the actual DOM
|
||||
//!
|
||||
//! A bundle, borrowed from the mathematical meaning, is any structure over some base space.
|
||||
//! In our case, the base space is the virtual dom we're trying to render.
|
||||
//! In order to efficiently implement updates, and diffing, additional information has to be
|
||||
//! kept around. This information is carried in the bundle.
|
||||
|
||||
mod app_handle;
|
||||
mod bcomp;
|
||||
mod blist;
|
||||
mod bnode;
|
||||
mod bportal;
|
||||
mod bsuspense;
|
||||
mod btag;
|
||||
mod btext;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use self::bcomp::BComp;
|
||||
use self::blist::BList;
|
||||
use self::bnode::BNode;
|
||||
use self::bportal::BPortal;
|
||||
use self::bsuspense::BSuspense;
|
||||
use self::btag::BTag;
|
||||
use self::btext::BText;
|
||||
|
||||
pub(crate) use self::bcomp::{ComponentRenderState, Mountable, PropsWrapper, Scoped};
|
||||
|
||||
#[doc(hidden)] // Publically exported from crate::app_handle
|
||||
pub use self::app_handle::AppHandle;
|
||||
#[doc(hidden)] // Publically exported from crate::events
|
||||
pub use self::btag::set_event_bubbling;
|
||||
#[cfg(test)]
|
||||
#[doc(hidden)] // Publically exported from crate::tests
|
||||
pub use self::tests::layout_tests;
|
||||
|
||||
use crate::html::AnyScope;
|
||||
use crate::NodeRef;
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
trait DomBundle {
|
||||
/// Remove self from parent.
|
||||
///
|
||||
/// Parent to detach is `true` if the parent element will also be detached.
|
||||
fn detach(self, parent: &Element, parent_to_detach: bool);
|
||||
|
||||
/// Move elements from one parent to another parent.
|
||||
/// This is for example used by `VSuspense` to preserve component state without detaching
|
||||
/// (which destroys component state).
|
||||
fn shift(&self, next_parent: &Element, next_sibling: NodeRef);
|
||||
}
|
||||
|
||||
/// This trait provides features to update a tree by calculating a difference against another tree.
|
||||
trait Reconcilable {
|
||||
type Bundle: DomBundle;
|
||||
|
||||
/// Attach a virtual node to the DOM tree.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `parent_scope`: the parent `Scope` used for passing messages to the
|
||||
/// parent `Component`.
|
||||
/// - `parent`: the parent node in the DOM.
|
||||
/// - `next_sibling`: to find where to put the node.
|
||||
///
|
||||
/// Returns a reference to the newly inserted element.
|
||||
fn attach(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> (NodeRef, Self::Bundle);
|
||||
|
||||
/// Scoped diff apply to other tree.
|
||||
///
|
||||
/// Virtual rendering for the node. It uses parent node and existing
|
||||
/// children (virtual and DOM) to check the difference and apply patches to
|
||||
/// the actual DOM representation.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `parent_scope`: the parent `Scope` used for passing messages to the
|
||||
/// parent `Component`.
|
||||
/// - `parent`: the parent node in the DOM.
|
||||
/// - `next_sibling`: the next sibling, used to efficiently find where to
|
||||
/// put the node.
|
||||
/// - `bundle`: the node that this node will be replacing in the DOM. This
|
||||
/// method will remove the `bundle` from the `parent` if it is of the wrong
|
||||
/// kind, and otherwise reuse it.
|
||||
///
|
||||
/// Returns a reference to the newly inserted element.
|
||||
fn reconcile_node(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef;
|
||||
|
||||
fn reconcile(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut Self::Bundle,
|
||||
) -> NodeRef;
|
||||
|
||||
/// Replace an existing bundle by attaching self and detaching the existing one
|
||||
fn replace(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut BNode,
|
||||
) -> NodeRef
|
||||
where
|
||||
Self: Sized,
|
||||
Self::Bundle: Into<BNode>,
|
||||
{
|
||||
let (self_ref, self_) = self.attach(parent_scope, parent, next_sibling);
|
||||
let ancestor = std::mem::replace(bundle, self_.into());
|
||||
ancestor.detach(parent, false);
|
||||
self_ref
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a concrete [Node] into the DOM
|
||||
fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) {
|
||||
match next_sibling {
|
||||
Some(next_sibling) => parent
|
||||
.insert_before(node, Some(next_sibling))
|
||||
.expect("failed to insert tag before next sibling"),
|
||||
None => parent.append_child(node).expect("failed to append child"),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "wasm_test", verbose_tests))]
|
||||
macro_rules! test_log {
|
||||
($fmt:literal, $($arg:expr),* $(,)?) => {
|
||||
::wasm_bindgen_test::console_log!(concat!("\t ", $fmt), $($arg),*);
|
||||
};
|
||||
}
|
||||
#[cfg(not(all(test, feature = "wasm_test", verbose_tests)))]
|
||||
macro_rules! test_log {
|
||||
($fmt:literal, $($arg:expr),* $(,)?) => {
|
||||
// Only type-check the format expression, do not run any side effects
|
||||
let _ = || { std::format_args!(concat!("\t ", $fmt), $($arg),*); };
|
||||
};
|
||||
}
|
||||
/// Log an operation during tests for debugging purposes
|
||||
/// Set RUSTFLAGS="--cfg verbose_tests" environment variable to activate.
|
||||
pub(self) use test_log;
|
||||
@ -1,6 +1,7 @@
|
||||
use crate::html::{AnyScope, Scope};
|
||||
use crate::dom_bundle::{BNode, DomBundle, Reconcilable};
|
||||
use crate::html::AnyScope;
|
||||
use crate::scheduler;
|
||||
use crate::virtual_dom::{VDiff, VNode, VText};
|
||||
use crate::virtual_dom::VNode;
|
||||
use crate::{Component, Context, Html};
|
||||
use gloo::console::log;
|
||||
use web_sys::Node;
|
||||
@ -37,21 +38,20 @@ pub struct TestLayout<'a> {
|
||||
|
||||
pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
|
||||
let document = gloo_utils::document();
|
||||
let parent_scope: AnyScope = Scope::<Comp>::new(None).into();
|
||||
let parent_scope: AnyScope = AnyScope::test();
|
||||
let parent_element = document.create_element("div").unwrap();
|
||||
let parent_node: Node = parent_element.clone().into();
|
||||
let end_node = document.create_text_node("END");
|
||||
parent_node.append_child(&end_node).unwrap();
|
||||
let mut empty_node: VNode = VText::new("").into();
|
||||
|
||||
// Tests each layout independently
|
||||
let next_sibling = NodeRef::new(end_node.into());
|
||||
for layout in layouts.iter() {
|
||||
// Apply the layout
|
||||
let mut node = layout.node.clone();
|
||||
let vnode = layout.node.clone();
|
||||
log!("Independently apply layout '{}'", layout.name);
|
||||
|
||||
node.apply(&parent_scope, &parent_element, next_sibling.clone(), None);
|
||||
let (_, mut bundle) = vnode.attach(&parent_scope, &parent_element, next_sibling.clone());
|
||||
scheduler::start_now();
|
||||
assert_eq!(
|
||||
parent_element.inner_html(),
|
||||
@ -61,15 +61,15 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
|
||||
);
|
||||
|
||||
// Diff with no changes
|
||||
let mut node_clone = layout.node.clone();
|
||||
let vnode = layout.node.clone();
|
||||
|
||||
log!("Independently reapply layout '{}'", layout.name);
|
||||
|
||||
node_clone.apply(
|
||||
vnode.reconcile_node(
|
||||
&parent_scope,
|
||||
&parent_element,
|
||||
next_sibling.clone(),
|
||||
Some(node),
|
||||
&mut bundle,
|
||||
);
|
||||
scheduler::start_now();
|
||||
assert_eq!(
|
||||
@ -80,12 +80,7 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
|
||||
);
|
||||
|
||||
// Detach
|
||||
empty_node.clone().apply(
|
||||
&parent_scope,
|
||||
&parent_element,
|
||||
next_sibling.clone(),
|
||||
Some(node_clone),
|
||||
);
|
||||
bundle.detach(&parent_element, false);
|
||||
scheduler::start_now();
|
||||
assert_eq!(
|
||||
parent_element.inner_html(),
|
||||
@ -96,16 +91,16 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
|
||||
}
|
||||
|
||||
// Sequentially apply each layout
|
||||
let mut ancestor: Option<VNode> = None;
|
||||
let mut bundle: Option<BNode> = None;
|
||||
for layout in layouts.iter() {
|
||||
let mut next_node = layout.node.clone();
|
||||
let next_vnode = layout.node.clone();
|
||||
|
||||
log!("Sequentially apply layout '{}'", layout.name);
|
||||
next_node.apply(
|
||||
next_vnode.reconcile_sequentially(
|
||||
&parent_scope,
|
||||
&parent_element,
|
||||
next_sibling.clone(),
|
||||
ancestor,
|
||||
&mut bundle,
|
||||
);
|
||||
scheduler::start_now();
|
||||
assert_eq!(
|
||||
@ -114,19 +109,18 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
|
||||
"Sequential apply failed for layout '{}'",
|
||||
layout.name,
|
||||
);
|
||||
ancestor = Some(next_node);
|
||||
}
|
||||
|
||||
// Sequentially detach each layout
|
||||
for layout in layouts.into_iter().rev() {
|
||||
let mut next_node = layout.node.clone();
|
||||
let next_vnode = layout.node.clone();
|
||||
|
||||
log!("Sequentially detach layout '{}'", layout.name);
|
||||
next_node.apply(
|
||||
next_vnode.reconcile_sequentially(
|
||||
&parent_scope,
|
||||
&parent_element,
|
||||
next_sibling.clone(),
|
||||
ancestor,
|
||||
&mut bundle,
|
||||
);
|
||||
scheduler::start_now();
|
||||
assert_eq!(
|
||||
@ -135,11 +129,12 @@ pub fn diff_layouts(layouts: Vec<TestLayout<'_>>) {
|
||||
"Sequential detach failed for layout '{}'",
|
||||
layout.name,
|
||||
);
|
||||
ancestor = Some(next_node);
|
||||
}
|
||||
|
||||
// Detach last layout
|
||||
empty_node.apply(&parent_scope, &parent_element, next_sibling, ancestor);
|
||||
if let Some(bundle) = bundle {
|
||||
bundle.detach(&parent_element, false);
|
||||
}
|
||||
scheduler::start_now();
|
||||
assert_eq!(
|
||||
parent_element.inner_html(),
|
||||
26
packages/yew/src/dom_bundle/tests/mod.rs
Normal file
26
packages/yew/src/dom_bundle/tests/mod.rs
Normal file
@ -0,0 +1,26 @@
|
||||
pub mod layout_tests;
|
||||
|
||||
use super::Reconcilable;
|
||||
|
||||
use crate::virtual_dom::VNode;
|
||||
use crate::{dom_bundle::BNode, html::AnyScope, NodeRef};
|
||||
use web_sys::Element;
|
||||
|
||||
impl VNode {
|
||||
fn reconcile_sequentially(
|
||||
self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
bundle: &mut Option<BNode>,
|
||||
) -> NodeRef {
|
||||
match bundle {
|
||||
None => {
|
||||
let (self_ref, node) = self.attach(parent_scope, parent, next_sibling);
|
||||
*bundle = Some(node);
|
||||
self_ref
|
||||
}
|
||||
Some(bundle) => self.reconcile_node(parent_scope, parent, next_sibling, bundle),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,16 @@
|
||||
//! Component lifecycle module
|
||||
|
||||
use super::{AnyScope, BaseComponent, Scope};
|
||||
use crate::html::{RenderError, RenderResult};
|
||||
use super::scope::{AnyScope, Scope};
|
||||
use super::BaseComponent;
|
||||
use crate::dom_bundle::ComponentRenderState;
|
||||
use crate::html::RenderError;
|
||||
use crate::scheduler::{self, Runnable, Shared};
|
||||
use crate::suspense::{Suspense, Suspension};
|
||||
use crate::virtual_dom::{VDiff, VNode};
|
||||
use crate::Callback;
|
||||
use crate::{Context, NodeRef};
|
||||
#[cfg(feature = "ssr")]
|
||||
use futures::channel::oneshot;
|
||||
use crate::{Callback, Context, HtmlResult, NodeRef};
|
||||
use std::any::Any;
|
||||
use std::rc::Rc;
|
||||
use web_sys::Element;
|
||||
|
||||
pub(crate) struct CompStateInner<COMP>
|
||||
pub struct CompStateInner<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
@ -26,8 +23,8 @@ where
|
||||
///
|
||||
/// Mostly a thin wrapper that passes the context to a component's lifecycle
|
||||
/// methods.
|
||||
pub(crate) trait Stateful {
|
||||
fn view(&self) -> RenderResult<VNode>;
|
||||
pub trait Stateful {
|
||||
fn view(&self) -> HtmlResult;
|
||||
fn rendered(&mut self, first_render: bool);
|
||||
fn destroy(&mut self);
|
||||
|
||||
@ -44,7 +41,7 @@ impl<COMP> Stateful for CompStateInner<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
fn view(&self) -> RenderResult<VNode> {
|
||||
fn view(&self) -> HtmlResult {
|
||||
self.component.view(&self.context)
|
||||
}
|
||||
|
||||
@ -93,23 +90,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ComponentState {
|
||||
pub(crate) inner: Box<dyn Stateful>,
|
||||
pub struct ComponentState {
|
||||
pub(super) inner: Box<dyn Stateful>,
|
||||
|
||||
pub(crate) root_node: VNode,
|
||||
|
||||
/// When a component has no parent, it means that it should not be rendered.
|
||||
parent: Option<Element>,
|
||||
|
||||
next_sibling: NodeRef,
|
||||
pub(super) render_state: ComponentRenderState,
|
||||
node_ref: NodeRef,
|
||||
has_rendered: bool,
|
||||
|
||||
suspension: Option<Suspension>,
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
html_sender: Option<oneshot::Sender<VNode>>,
|
||||
|
||||
// Used for debug logging
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) vcomp_id: usize,
|
||||
@ -117,13 +106,10 @@ pub(crate) struct ComponentState {
|
||||
|
||||
impl ComponentState {
|
||||
pub(crate) fn new<COMP: BaseComponent>(
|
||||
parent: Option<Element>,
|
||||
next_sibling: NodeRef,
|
||||
root_node: VNode,
|
||||
initial_render_state: ComponentRenderState,
|
||||
node_ref: NodeRef,
|
||||
scope: Scope<COMP>,
|
||||
props: Rc<COMP::Properties>,
|
||||
#[cfg(feature = "ssr")] html_sender: Option<oneshot::Sender<VNode>>,
|
||||
) -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
let vcomp_id = scope.vcomp_id;
|
||||
@ -136,31 +122,22 @@ impl ComponentState {
|
||||
|
||||
Self {
|
||||
inner,
|
||||
root_node,
|
||||
parent,
|
||||
next_sibling,
|
||||
render_state: initial_render_state,
|
||||
node_ref,
|
||||
suspension: None,
|
||||
has_rendered: false,
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
html_sender,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CreateRunner<COMP: BaseComponent> {
|
||||
pub(crate) parent: Option<Element>,
|
||||
pub(crate) next_sibling: NodeRef,
|
||||
pub(crate) placeholder: VNode,
|
||||
pub(crate) node_ref: NodeRef,
|
||||
pub(crate) props: Rc<COMP::Properties>,
|
||||
pub(crate) scope: Scope<COMP>,
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(crate) html_sender: Option<oneshot::Sender<VNode>>,
|
||||
pub struct CreateRunner<COMP: BaseComponent> {
|
||||
pub initial_render_state: ComponentRenderState,
|
||||
pub node_ref: NodeRef,
|
||||
pub props: Rc<COMP::Properties>,
|
||||
pub scope: Scope<COMP>,
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
|
||||
@ -168,34 +145,28 @@ impl<COMP: BaseComponent> Runnable for CreateRunner<COMP> {
|
||||
let mut current_state = self.scope.state.borrow_mut();
|
||||
if current_state.is_none() {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(self.scope.vcomp_id, "create");
|
||||
super::log_event(self.scope.vcomp_id, "create");
|
||||
|
||||
*current_state = Some(ComponentState::new(
|
||||
self.parent,
|
||||
self.next_sibling,
|
||||
self.placeholder,
|
||||
self.initial_render_state,
|
||||
self.node_ref,
|
||||
self.scope.clone(),
|
||||
self.props,
|
||||
#[cfg(feature = "ssr")]
|
||||
self.html_sender,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum UpdateEvent {
|
||||
pub enum UpdateEvent {
|
||||
/// Drain messages for a component.
|
||||
Message,
|
||||
/// Wraps properties, node ref, and next sibling for a component.
|
||||
Properties(Rc<dyn Any>, NodeRef, NodeRef),
|
||||
/// Shift Scope.
|
||||
Shift(Element, NodeRef),
|
||||
}
|
||||
|
||||
pub(crate) struct UpdateRunner {
|
||||
pub(crate) state: Shared<Option<ComponentState>>,
|
||||
pub(crate) event: UpdateEvent,
|
||||
pub struct UpdateRunner {
|
||||
pub state: Shared<Option<ComponentState>>,
|
||||
pub event: UpdateEvent,
|
||||
}
|
||||
|
||||
impl Runnable for UpdateRunner {
|
||||
@ -207,27 +178,15 @@ impl Runnable for UpdateRunner {
|
||||
// When components are updated, a new node ref could have been passed in
|
||||
state.node_ref = node_ref;
|
||||
// When components are updated, their siblings were likely also updated
|
||||
state.next_sibling = next_sibling;
|
||||
state.render_state.reuse(next_sibling);
|
||||
// Only trigger changed if props were changed
|
||||
|
||||
state.inner.props_changed(props)
|
||||
}
|
||||
UpdateEvent::Shift(parent, next_sibling) => {
|
||||
state.root_node.shift(
|
||||
state.parent.as_ref().unwrap(),
|
||||
&parent,
|
||||
next_sibling.clone(),
|
||||
);
|
||||
|
||||
state.parent = Some(parent);
|
||||
state.next_sibling = next_sibling;
|
||||
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(
|
||||
super::log_event(
|
||||
state.vcomp_id,
|
||||
format!("update(schedule_render={})", schedule_render),
|
||||
);
|
||||
@ -245,46 +204,38 @@ impl Runnable for UpdateRunner {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DestroyRunner {
|
||||
pub(crate) state: Shared<Option<ComponentState>>,
|
||||
pub(crate) parent_to_detach: bool,
|
||||
pub struct DestroyRunner {
|
||||
pub state: Shared<Option<ComponentState>>,
|
||||
pub parent_to_detach: bool,
|
||||
}
|
||||
|
||||
impl Runnable for DestroyRunner {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(mut state) = self.state.borrow_mut().take() {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "destroy");
|
||||
super::log_event(state.vcomp_id, "destroy");
|
||||
|
||||
state.inner.destroy();
|
||||
|
||||
if let Some(ref m) = state.parent {
|
||||
state.root_node.detach(m, self.parent_to_detach);
|
||||
state.node_ref.set(None);
|
||||
}
|
||||
state.render_state.detach(self.parent_to_detach);
|
||||
state.node_ref.set(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RenderRunner {
|
||||
pub(crate) state: Shared<Option<ComponentState>>,
|
||||
pub struct RenderRunner {
|
||||
pub state: Shared<Option<ComponentState>>,
|
||||
}
|
||||
|
||||
impl Runnable for RenderRunner {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(state) = self.state.borrow_mut().as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "render");
|
||||
super::log_event(state.vcomp_id, "render");
|
||||
|
||||
match state.inner.view() {
|
||||
Ok(m) => {
|
||||
Ok(root) => {
|
||||
// Currently not suspended, we remove any previous suspension and update
|
||||
// normally.
|
||||
let mut root = m;
|
||||
if state.parent.is_some() {
|
||||
std::mem::swap(&mut root, &mut state.root_node);
|
||||
}
|
||||
|
||||
if let Some(m) = state.suspension.take() {
|
||||
let comp_scope = state.inner.any_scope();
|
||||
|
||||
@ -294,15 +245,11 @@ impl Runnable for RenderRunner {
|
||||
suspense.resume(m);
|
||||
}
|
||||
|
||||
if let Some(ref m) = state.parent {
|
||||
let ancestor = Some(root);
|
||||
let new_root = &mut state.root_node;
|
||||
let scope = state.inner.any_scope();
|
||||
let next_sibling = state.next_sibling.clone();
|
||||
|
||||
let node = new_root.apply(&scope, m, next_sibling, ancestor);
|
||||
state.node_ref.link(node);
|
||||
let scope = state.inner.any_scope();
|
||||
let node = state.render_state.reconcile(root, &scope);
|
||||
state.node_ref.link(node);
|
||||
|
||||
if state.render_state.should_trigger_rendered() {
|
||||
let first_render = !state.has_rendered;
|
||||
state.has_rendered = true;
|
||||
|
||||
@ -314,11 +261,6 @@ impl Runnable for RenderRunner {
|
||||
},
|
||||
first_render,
|
||||
);
|
||||
} else {
|
||||
#[cfg(feature = "ssr")]
|
||||
if let Some(tx) = state.html_sender.take() {
|
||||
tx.send(root).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -372,8 +314,8 @@ impl Runnable for RenderRunner {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RenderedRunner {
|
||||
pub(crate) state: Shared<Option<ComponentState>>,
|
||||
struct RenderedRunner {
|
||||
state: Shared<Option<ComponentState>>,
|
||||
first_render: bool,
|
||||
}
|
||||
|
||||
@ -381,9 +323,9 @@ impl Runnable for RenderedRunner {
|
||||
fn run(self: Box<Self>) {
|
||||
if let Some(state) = self.state.borrow_mut().as_mut() {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(state.vcomp_id, "rendered");
|
||||
super::log_event(state.vcomp_id, "rendered");
|
||||
|
||||
if state.suspension.is_none() && state.parent.is_some() {
|
||||
if state.suspension.is_none() {
|
||||
state.inner.rendered(self.first_render);
|
||||
}
|
||||
}
|
||||
@ -394,10 +336,13 @@ impl Runnable for RenderedRunner {
|
||||
mod tests {
|
||||
extern crate self as yew;
|
||||
|
||||
use crate::dom_bundle::ComponentRenderState;
|
||||
use crate::html;
|
||||
use crate::html::*;
|
||||
use crate::Properties;
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
@ -515,10 +460,12 @@ mod tests {
|
||||
let document = gloo_utils::document();
|
||||
let scope = Scope::<Comp>::new(None);
|
||||
let el = document.create_element("div").unwrap();
|
||||
let node_ref = NodeRef::default();
|
||||
let render_state = ComponentRenderState::new(el, NodeRef::default(), &node_ref);
|
||||
let lifecycle = props.lifecycle.clone();
|
||||
|
||||
lifecycle.borrow_mut().clear();
|
||||
scope.mount_in_place(el, NodeRef::default(), NodeRef::default(), Rc::new(props));
|
||||
scope.mount_in_place(render_state, node_ref, Rc::new(props));
|
||||
crate::scheduler::start_now();
|
||||
|
||||
assert_eq!(&lifecycle.borrow_mut().deref()[..], expected);
|
||||
|
||||
@ -8,16 +8,52 @@ mod scope;
|
||||
use super::{Html, HtmlResult, IntoHtmlResult};
|
||||
pub use children::*;
|
||||
pub use properties::*;
|
||||
pub(crate) use scope::Scoped;
|
||||
pub use scope::{AnyScope, Scope, SendAsMessage};
|
||||
use std::rc::Rc;
|
||||
#[cfg(debug_assertions)]
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
thread_local! {
|
||||
static EVENT_HISTORY: std::cell::RefCell<std::collections::HashMap<usize, Vec<String>>>
|
||||
= Default::default();
|
||||
static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
}
|
||||
|
||||
/// Push [Component] event to lifecycle debugging registry
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn log_event(vcomp_id: usize, event: impl ToString) {
|
||||
EVENT_HISTORY.with(|h| {
|
||||
h.borrow_mut()
|
||||
.entry(vcomp_id)
|
||||
.or_default()
|
||||
.push(event.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
/// Get [Component] event log from lifecycle debugging registry
|
||||
#[cfg(debug_assertions)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn get_event_log(vcomp_id: usize) -> Vec<String> {
|
||||
EVENT_HISTORY.with(|h| {
|
||||
h.borrow()
|
||||
.get(&vcomp_id)
|
||||
.map(|l| (*l).clone())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn next_id() -> usize {
|
||||
COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// The [`Component`]'s context. This contains component's [`Scope`] and and props and
|
||||
/// is passed to every lifecycle method.
|
||||
#[derive(Debug)]
|
||||
pub struct Context<COMP: BaseComponent> {
|
||||
pub(crate) scope: Scope<COMP>,
|
||||
pub(crate) props: Rc<COMP::Properties>,
|
||||
scope: Scope<COMP>,
|
||||
props: Rc<COMP::Properties>,
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Context<COMP> {
|
||||
|
||||
@ -9,17 +9,16 @@ use super::{
|
||||
};
|
||||
use crate::callback::Callback;
|
||||
use crate::context::{ContextHandle, ContextProvider};
|
||||
use crate::dom_bundle::{ComponentRenderState, Scoped};
|
||||
use crate::html::NodeRef;
|
||||
use crate::scheduler::{self, Shared};
|
||||
use crate::virtual_dom::{insert_node, VNode};
|
||||
use gloo_utils::document;
|
||||
use std::any::TypeId;
|
||||
use std::cell::{Ref, RefCell};
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::{fmt, iter};
|
||||
use web_sys::{Element, Node};
|
||||
use web_sys::Element;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MsgQueue<Msg>(Shared<Vec<Msg>>);
|
||||
@ -65,9 +64,6 @@ pub struct AnyScope {
|
||||
type_id: TypeId,
|
||||
parent: Option<Rc<AnyScope>>,
|
||||
state: Shared<Option<ComponentState>>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) vcomp_id: usize,
|
||||
}
|
||||
|
||||
impl fmt::Debug for AnyScope {
|
||||
@ -82,9 +78,6 @@ impl<COMP: BaseComponent> From<Scope<COMP>> for AnyScope {
|
||||
type_id: TypeId::of::<COMP>(),
|
||||
parent: scope.parent,
|
||||
state: scope.state,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id: scope.vcomp_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,9 +89,6 @@ impl AnyScope {
|
||||
type_id: TypeId::of::<()>(),
|
||||
parent: None,
|
||||
state: Rc::new(RefCell::new(None)),
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,44 +153,41 @@ impl AnyScope {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait Scoped {
|
||||
fn to_any(&self) -> AnyScope;
|
||||
fn root_vnode(&self) -> Option<Ref<'_, VNode>>;
|
||||
fn destroy(&mut self, parent_to_detach: bool);
|
||||
fn shift_node(&self, parent: Element, next_sibling: NodeRef);
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Scoped for Scope<COMP> {
|
||||
fn to_any(&self) -> AnyScope {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn root_vnode(&self) -> Option<Ref<'_, VNode>> {
|
||||
fn render_state(&self) -> Option<Ref<'_, ComponentRenderState>> {
|
||||
let state_ref = self.state.borrow();
|
||||
|
||||
// check that component hasn't been destroyed
|
||||
state_ref.as_ref()?;
|
||||
|
||||
Some(Ref::map(state_ref, |state_ref| {
|
||||
&state_ref.as_ref().unwrap().root_node
|
||||
&state_ref.as_ref().unwrap().render_state
|
||||
}))
|
||||
}
|
||||
|
||||
/// Process an event to destroy a component
|
||||
fn destroy(&mut self, parent_to_detach: bool) {
|
||||
fn destroy(self, parent_to_detach: bool) {
|
||||
scheduler::push_component_destroy(DestroyRunner {
|
||||
state: self.state.clone(),
|
||||
state: self.state,
|
||||
parent_to_detach,
|
||||
});
|
||||
// Not guaranteed to already have the scheduler started
|
||||
scheduler::start();
|
||||
}
|
||||
|
||||
fn destroy_boxed(self: Box<Self>, parent_to_detach: bool) {
|
||||
self.destroy(parent_to_detach)
|
||||
}
|
||||
|
||||
fn shift_node(&self, parent: Element, next_sibling: NodeRef) {
|
||||
scheduler::push_component_update(UpdateRunner {
|
||||
state: self.state.clone(),
|
||||
event: UpdateEvent::Shift(parent, next_sibling),
|
||||
});
|
||||
let mut state_ref = self.state.borrow_mut();
|
||||
if let Some(render_state) = state_ref.as_mut() {
|
||||
render_state.render_state.shift(parent, next_sibling)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -258,14 +245,12 @@ impl<COMP: BaseComponent> Scope<COMP> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Crate a scope with an optional parent scope
|
||||
pub(crate) fn new(parent: Option<AnyScope>) -> Self {
|
||||
let parent = parent.map(Rc::new);
|
||||
let state = Rc::new(RefCell::new(None));
|
||||
let pending_messages = MsgQueue::new();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let vcomp_id = parent.as_ref().map(|p| p.vcomp_id).unwrap_or_default();
|
||||
|
||||
Scope {
|
||||
_marker: PhantomData,
|
||||
pending_messages,
|
||||
@ -273,37 +258,23 @@ impl<COMP: BaseComponent> Scope<COMP> {
|
||||
parent,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
vcomp_id,
|
||||
vcomp_id: super::next_id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mounts a component with `props` to the specified `element` in the DOM.
|
||||
pub(crate) fn mount_in_place(
|
||||
&self,
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
initial_render_state: ComponentRenderState,
|
||||
node_ref: NodeRef,
|
||||
props: Rc<COMP::Properties>,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(self.vcomp_id, "create placeholder");
|
||||
let placeholder = {
|
||||
let placeholder: Node = document().create_text_node("").into();
|
||||
insert_node(&placeholder, &parent, next_sibling.get().as_ref());
|
||||
node_ref.set(Some(placeholder.clone()));
|
||||
VNode::VRef(placeholder)
|
||||
};
|
||||
|
||||
scheduler::push_component_create(
|
||||
CreateRunner {
|
||||
parent: Some(parent),
|
||||
next_sibling,
|
||||
placeholder,
|
||||
initial_render_state,
|
||||
node_ref,
|
||||
props,
|
||||
scope: self.clone(),
|
||||
#[cfg(feature = "ssr")]
|
||||
html_sender: None,
|
||||
},
|
||||
RenderRunner {
|
||||
state: self.state.clone(),
|
||||
@ -320,7 +291,7 @@ impl<COMP: BaseComponent> Scope<COMP> {
|
||||
next_sibling: NodeRef,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
crate::virtual_dom::vcomp::log_event(self.vcomp_id, "reuse");
|
||||
super::log_event(self.vcomp_id, "reuse");
|
||||
|
||||
self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling));
|
||||
}
|
||||
@ -346,6 +317,9 @@ impl<COMP: BaseComponent> Scope<COMP> {
|
||||
}
|
||||
|
||||
/// Send a batch of messages to the component.
|
||||
///
|
||||
/// This is slightly more efficient than calling [`send_message`](Self::send_message)
|
||||
/// in a loop.
|
||||
pub fn send_message_batch(&self, mut messages: Vec<COMP::Message>) {
|
||||
let msg_len = messages.len();
|
||||
|
||||
@ -410,24 +384,11 @@ mod feat_ssr {
|
||||
use futures::channel::oneshot;
|
||||
|
||||
impl<COMP: BaseComponent> Scope<COMP> {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, props: Rc<COMP::Properties>) {
|
||||
pub(crate) async fn render_to_string(self, w: &mut String, props: Rc<COMP::Properties>) {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let initial_render_state = ComponentRenderState::new_ssr(tx);
|
||||
|
||||
scheduler::push_component_create(
|
||||
CreateRunner {
|
||||
parent: None,
|
||||
next_sibling: NodeRef::default(),
|
||||
placeholder: VNode::default(),
|
||||
node_ref: NodeRef::default(),
|
||||
props,
|
||||
scope: self.clone(),
|
||||
html_sender: Some(tx),
|
||||
},
|
||||
RenderRunner {
|
||||
state: self.state.clone(),
|
||||
},
|
||||
);
|
||||
scheduler::start();
|
||||
self.mount_in_place(initial_render_state, NodeRef::default(), props);
|
||||
|
||||
let html = rx.await.unwrap();
|
||||
|
||||
@ -442,6 +403,7 @@ mod feat_ssr {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))]
|
||||
#[cfg(any(target_arch = "wasm32", feature = "tokio"))]
|
||||
mod feat_io {
|
||||
|
||||
@ -263,9 +263,9 @@ pub mod macros {
|
||||
pub use crate::props;
|
||||
}
|
||||
|
||||
mod app_handle;
|
||||
pub mod callback;
|
||||
pub mod context;
|
||||
mod dom_bundle;
|
||||
pub mod functional;
|
||||
pub mod html;
|
||||
mod io_coop;
|
||||
@ -274,18 +274,21 @@ mod sealed;
|
||||
#[cfg(feature = "ssr")]
|
||||
mod server_renderer;
|
||||
pub mod suspense;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
pub mod utils;
|
||||
pub mod virtual_dom;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use server_renderer::*;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
pub use crate::dom_bundle::layout_tests;
|
||||
}
|
||||
|
||||
/// The module that contains all events available in the framework.
|
||||
pub mod events {
|
||||
pub use crate::html::TargetCast;
|
||||
|
||||
pub use crate::virtual_dom::listeners::set_event_bubbling;
|
||||
pub use crate::dom_bundle::set_event_bubbling;
|
||||
|
||||
#[doc(no_inline)]
|
||||
pub use web_sys::{
|
||||
@ -294,7 +297,7 @@ pub mod events {
|
||||
};
|
||||
}
|
||||
|
||||
pub use crate::app_handle::AppHandle;
|
||||
pub use crate::dom_bundle::AppHandle;
|
||||
use web_sys::Element;
|
||||
|
||||
use crate::html::BaseComponent;
|
||||
@ -324,7 +327,7 @@ where
|
||||
COMP: BaseComponent,
|
||||
COMP::Properties: Default,
|
||||
{
|
||||
start_app_with_props_in_element(element, COMP::Properties::default())
|
||||
start_app_with_props_in_element::<COMP>(element, COMP::Properties::default())
|
||||
}
|
||||
|
||||
/// Starts an yew app mounted to the body of the document.
|
||||
@ -334,7 +337,7 @@ where
|
||||
COMP: BaseComponent,
|
||||
COMP::Properties: Default,
|
||||
{
|
||||
start_app_with_props(COMP::Properties::default())
|
||||
start_app_with_props::<COMP>(COMP::Properties::default())
|
||||
}
|
||||
|
||||
/// The main entry point of a Yew application. This function does the
|
||||
@ -356,7 +359,7 @@ pub fn start_app_with_props<COMP>(props: COMP::Properties) -> AppHandle<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
{
|
||||
start_app_with_props_in_element(
|
||||
start_app_with_props_in_element::<COMP>(
|
||||
gloo_utils::document()
|
||||
.body()
|
||||
.expect("no body node found")
|
||||
@ -374,9 +377,9 @@ where
|
||||
/// use yew::prelude::*;
|
||||
/// ```
|
||||
pub mod prelude {
|
||||
pub use crate::app_handle::AppHandle;
|
||||
pub use crate::callback::Callback;
|
||||
pub use crate::context::{ContextHandle, ContextProvider};
|
||||
pub use crate::dom_bundle::AppHandle;
|
||||
pub use crate::events::*;
|
||||
pub use crate::html::{
|
||||
create_portal, BaseComponent, Children, ChildrenWithProps, Classes, Component, Context,
|
||||
|
||||
@ -3,8 +3,8 @@ use super::*;
|
||||
use crate::html::Scope;
|
||||
|
||||
/// A Yew Server-side Renderer.
|
||||
#[cfg_attr(documenting, doc(cfg(feature = "ssr")))]
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(documenting, doc(cfg(feature = "ssr")))]
|
||||
pub struct ServerRenderer<COMP>
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
|
||||
@ -1 +0,0 @@
|
||||
pub mod layout_tests;
|
||||
@ -1,40 +1,4 @@
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
ops::Deref,
|
||||
rc::Rc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use wasm_bindgen::{prelude::*, JsCast};
|
||||
use web_sys::{Element, Event};
|
||||
|
||||
thread_local! {
|
||||
/// Global event listener registry
|
||||
static REGISTRY: RefCell<Registry> = Default::default();
|
||||
|
||||
/// Key used to store listener id on element
|
||||
static LISTENER_ID_PROP: wasm_bindgen::JsValue = "__yew_listener_id".into();
|
||||
|
||||
/// Cached reference to the document body
|
||||
static BODY: web_sys::HtmlElement = gloo_utils::document().body().unwrap();
|
||||
}
|
||||
|
||||
/// Bubble events during delegation
|
||||
static BUBBLE_EVENTS: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
/// Set, if events should bubble up the DOM tree, calling any matching callbacks.
|
||||
///
|
||||
/// Bubbling is enabled by default. Disabling bubbling can lead to substantial improvements in event
|
||||
/// handling performance.
|
||||
///
|
||||
/// Note that yew uses event delegation and implements internal even bubbling for performance
|
||||
/// reasons. Calling `Event.stopPropagation()` or `Event.stopImmediatePropagation()` in the event
|
||||
/// handler has no effect.
|
||||
///
|
||||
/// This function should be called before any component is mounted.
|
||||
pub fn set_event_bubbling(bubble: bool) {
|
||||
BUBBLE_EVENTS.store(bubble, Ordering::Relaxed);
|
||||
}
|
||||
use std::rc::Rc;
|
||||
|
||||
/// The [Listener] trait is an universal implementation of an event listener
|
||||
/// which is used to bind Rust-listener to JS-listener (DOM).
|
||||
@ -75,10 +39,10 @@ macro_rules! gen_listener_kinds {
|
||||
}
|
||||
|
||||
impl ListenerKind {
|
||||
pub fn type_name(&self) -> &str {
|
||||
pub fn type_name(&self) -> std::borrow::Cow<'static, str> {
|
||||
match self {
|
||||
Self::other(type_name) => type_name.as_ref(),
|
||||
kind => &kind.as_ref()[2..],
|
||||
Self::other(type_name) => type_name.clone(),
|
||||
$( Self::$kind => stringify!($kind)[2..].into(), )*
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -200,90 +164,16 @@ pub enum Listeners {
|
||||
/// Distinct from `Pending` with an empty slice to avoid an allocation.
|
||||
None,
|
||||
|
||||
/// Added to global registry by ID
|
||||
Registered(u32),
|
||||
|
||||
/// Not yet added to the element or registry
|
||||
Pending(Box<[Option<Rc<dyn Listener>>]>),
|
||||
}
|
||||
|
||||
impl Listeners {
|
||||
/// Register listeners and return their handle ID
|
||||
fn register(el: &Element, pending: &[Option<Rc<dyn Listener>>]) -> Self {
|
||||
Self::Registered(Registry::with(|reg| {
|
||||
let id = reg.set_listener_id(el);
|
||||
reg.register(id, pending);
|
||||
id
|
||||
}))
|
||||
}
|
||||
|
||||
/// Remove any registered event listeners from the global registry
|
||||
pub(super) fn unregister(&self) {
|
||||
if let Self::Registered(id) = self {
|
||||
Registry::with(|r| r.unregister(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Apply for Listeners {
|
||||
type Element = Element;
|
||||
|
||||
fn apply(&mut self, el: &Self::Element) {
|
||||
if let Self::Pending(pending) = self {
|
||||
*self = Self::register(el, pending);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_diff(&mut self, el: &Self::Element, ancestor: Self) {
|
||||
use Listeners::*;
|
||||
|
||||
match (std::mem::take(self), ancestor) {
|
||||
(Pending(pending), Registered(id)) => {
|
||||
// Reuse the ID
|
||||
Registry::with(|reg| reg.patch(&id, &*pending));
|
||||
*self = Registered(id);
|
||||
}
|
||||
(Pending(pending), None) => {
|
||||
*self = Self::register(el, &pending);
|
||||
}
|
||||
(None, Registered(id)) => {
|
||||
Registry::with(|reg| reg.unregister(&id));
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Listeners {
|
||||
fn eq(&self, rhs: &Self) -> bool {
|
||||
use Listeners::*;
|
||||
|
||||
match (self, rhs) {
|
||||
(None, None) => true,
|
||||
(Registered(lhs), Registered(rhs)) => lhs == rhs,
|
||||
(Registered(registered_id), Pending(pending))
|
||||
| (Pending(pending), Registered(registered_id)) => {
|
||||
use std::option::Option::None;
|
||||
|
||||
Registry::with(|reg| match reg.by_id.get(registered_id) {
|
||||
Some(reg) => {
|
||||
if reg.len() != pending.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
pending.iter().filter_map(|l| l.as_ref()).all(|l| {
|
||||
match reg.get(&EventDescriptor::from(l.deref())) {
|
||||
Some(reg) => reg.iter().any(|reg| {
|
||||
#[allow(clippy::vtable_address_comparisons)]
|
||||
Rc::ptr_eq(reg, l)
|
||||
}),
|
||||
None => false,
|
||||
}
|
||||
})
|
||||
}
|
||||
None => false,
|
||||
})
|
||||
}
|
||||
(Pending(lhs), Pending(rhs)) => {
|
||||
if lhs.len() != rhs.len() {
|
||||
false
|
||||
@ -303,7 +193,7 @@ impl PartialEq for Listeners {
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
(None, Pending(pending)) | (Pending(pending), None) => pending.len() == 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -311,7 +201,7 @@ impl PartialEq for Listeners {
|
||||
impl Clone for Listeners {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::None | Self::Registered(_) => Self::None,
|
||||
Self::None => Self::None,
|
||||
Self::Pending(v) => Self::Pending(v.clone()),
|
||||
}
|
||||
}
|
||||
@ -322,623 +212,3 @@ impl Default for Listeners {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, Eq, PartialEq, Debug)]
|
||||
struct EventDescriptor {
|
||||
kind: ListenerKind,
|
||||
passive: bool,
|
||||
}
|
||||
|
||||
impl From<&dyn Listener> for EventDescriptor {
|
||||
fn from(l: &dyn Listener) -> Self {
|
||||
Self {
|
||||
kind: l.kind(),
|
||||
passive: l.passive(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures global event handler registration.
|
||||
//
|
||||
// Separate struct to DRY, while avoiding partial struct mutability.
|
||||
#[derive(Default, Debug)]
|
||||
struct GlobalHandlers {
|
||||
/// Events with registered handlers that are possibly passive
|
||||
handling: HashSet<EventDescriptor>,
|
||||
|
||||
/// Keep track of all listeners to drop them on registry drop.
|
||||
/// The registry is never dropped in production.
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
registered: Vec<(ListenerKind, Closure<dyn Fn(web_sys::Event)>)>,
|
||||
}
|
||||
|
||||
impl GlobalHandlers {
|
||||
/// Ensure a descriptor has a global event handler assigned
|
||||
fn ensure_handled(&mut self, desc: EventDescriptor) {
|
||||
if !self.handling.contains(&desc) {
|
||||
let cl = BODY.with(|body| {
|
||||
let cl = Closure::wrap(Box::new({
|
||||
let desc = desc.clone();
|
||||
move |e: Event| Registry::handle(desc.clone(), e)
|
||||
}) as Box<dyn Fn(Event)>);
|
||||
AsRef::<web_sys::EventTarget>::as_ref(body)
|
||||
.add_event_listener_with_callback_and_add_event_listener_options(
|
||||
desc.kind.type_name(),
|
||||
cl.as_ref().unchecked_ref(),
|
||||
&{
|
||||
let mut opts = web_sys::AddEventListenerOptions::new();
|
||||
opts.capture(true);
|
||||
// We need to explicitly set passive to override any browser defaults
|
||||
opts.passive(desc.passive);
|
||||
opts
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("could not register global listener: {:?}", e))
|
||||
.unwrap();
|
||||
cl
|
||||
});
|
||||
|
||||
// Never drop the closure as this event handler is static
|
||||
#[cfg(not(test))]
|
||||
cl.forget();
|
||||
#[cfg(test)]
|
||||
self.registered.push((desc.kind.clone(), cl));
|
||||
|
||||
self.handling.insert(desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enable resetting between tests
|
||||
#[cfg(test)]
|
||||
impl Drop for GlobalHandlers {
|
||||
fn drop(&mut self) {
|
||||
BODY.with(|body| {
|
||||
for (kind, cl) in std::mem::take(&mut self.registered) {
|
||||
AsRef::<web_sys::EventTarget>::as_ref(body)
|
||||
.remove_event_listener_with_callback(
|
||||
kind.type_name(),
|
||||
cl.as_ref().unchecked_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Global multiplexing event handler registry
|
||||
#[derive(Default, Debug)]
|
||||
struct Registry {
|
||||
/// Counter for assigning new IDs
|
||||
id_counter: u32,
|
||||
|
||||
/// Registered global event handlers
|
||||
global: GlobalHandlers,
|
||||
|
||||
/// Contains all registered event listeners by listener ID
|
||||
by_id: HashMap<u32, HashMap<EventDescriptor, Vec<Rc<dyn Listener>>>>,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
/// Run f with access to global Registry
|
||||
#[inline]
|
||||
fn with<R>(f: impl FnOnce(&mut Registry) -> R) -> R {
|
||||
REGISTRY.with(|r| f(&mut *r.borrow_mut()))
|
||||
}
|
||||
|
||||
/// Register all passed listeners under ID
|
||||
fn register(&mut self, id: u32, listeners: &[Option<Rc<dyn Listener>>]) {
|
||||
let mut by_desc =
|
||||
HashMap::<EventDescriptor, Vec<Rc<dyn Listener>>>::with_capacity(listeners.len());
|
||||
for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
|
||||
let desc = EventDescriptor::from(l.deref());
|
||||
self.global.ensure_handled(desc.clone());
|
||||
by_desc.entry(desc).or_default().push(l);
|
||||
}
|
||||
self.by_id.insert(id, by_desc);
|
||||
}
|
||||
|
||||
/// Patch an already registered set of handlers
|
||||
fn patch(&mut self, id: &u32, listeners: &[Option<Rc<dyn Listener>>]) {
|
||||
if let Some(by_desc) = self.by_id.get_mut(id) {
|
||||
// Keeping empty vectors is fine. Those don't do much and should happen rarely.
|
||||
for v in by_desc.values_mut() {
|
||||
v.clear()
|
||||
}
|
||||
|
||||
for l in listeners.iter().filter_map(|l| l.as_ref()).cloned() {
|
||||
let desc = EventDescriptor::from(l.deref());
|
||||
self.global.ensure_handled(desc.clone());
|
||||
by_desc.entry(desc).or_default().push(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unregister any existing listeners for ID
|
||||
fn unregister(&mut self, id: &u32) {
|
||||
self.by_id.remove(id);
|
||||
}
|
||||
|
||||
/// Set unique listener ID onto element and return it
|
||||
fn set_listener_id(&mut self, el: &Element) -> u32 {
|
||||
let id = self.id_counter;
|
||||
self.id_counter += 1;
|
||||
|
||||
LISTENER_ID_PROP.with(|prop| {
|
||||
if !js_sys::Reflect::set(el, prop, &js_sys::Number::from(id)).unwrap() {
|
||||
panic!("failed to set listener ID property");
|
||||
}
|
||||
});
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Handle a global event firing
|
||||
fn handle(desc: EventDescriptor, event: Event) {
|
||||
let target = match event
|
||||
.target()
|
||||
.and_then(|el| el.dyn_into::<web_sys::Element>().ok())
|
||||
{
|
||||
Some(el) => el,
|
||||
None => return,
|
||||
};
|
||||
|
||||
Self::run_handlers(desc, event, target);
|
||||
}
|
||||
|
||||
fn run_handlers(desc: EventDescriptor, event: Event, target: web_sys::Element) {
|
||||
let run_handler = |el: &web_sys::Element| {
|
||||
if let Some(l) = LISTENER_ID_PROP
|
||||
.with(|prop| js_sys::Reflect::get(el, prop).ok())
|
||||
.and_then(|v| v.dyn_into().ok())
|
||||
.and_then(|num: js_sys::Number| {
|
||||
Registry::with(|r| {
|
||||
r.by_id
|
||||
.get(&(num.value_of() as u32))
|
||||
.and_then(|s| s.get(&desc))
|
||||
.cloned()
|
||||
})
|
||||
})
|
||||
{
|
||||
for l in l {
|
||||
l.handle(event.clone());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
run_handler(&target);
|
||||
|
||||
if BUBBLE_EVENTS.load(Ordering::Relaxed) {
|
||||
let mut el = target;
|
||||
while !event.cancel_bubble() {
|
||||
el = match el.parent_element() {
|
||||
Some(el) => el,
|
||||
None => break,
|
||||
};
|
||||
run_handler(&el);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "wasm_test"))]
|
||||
mod tests {
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
use web_sys::{Event, EventInit, MouseEvent};
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
use crate::{html, html::TargetCast, scheduler, AppHandle, Component, Context, Html};
|
||||
use gloo_utils::document;
|
||||
use wasm_bindgen::JsCast;
|
||||
use yew::Callback;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Message {
|
||||
Action,
|
||||
StopListening,
|
||||
SetText(String),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
stop_listening: bool,
|
||||
action: u32,
|
||||
text: String,
|
||||
}
|
||||
|
||||
trait Mixin {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
let link = ctx.link().clone();
|
||||
let onclick = Callback::from(move |_| {
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
if state.stop_listening {
|
||||
html! {
|
||||
<a>{state.action}</a>
|
||||
}
|
||||
} else {
|
||||
html! {
|
||||
<a {onclick}>
|
||||
{state.action}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Comp<M>
|
||||
where
|
||||
M: Mixin + 'static,
|
||||
{
|
||||
state: State,
|
||||
pd: PhantomData<M>,
|
||||
}
|
||||
|
||||
impl<M> Component for Comp<M>
|
||||
where
|
||||
M: Mixin + 'static,
|
||||
{
|
||||
type Message = Message;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Comp {
|
||||
state: Default::default(),
|
||||
pd: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Message::Action => {
|
||||
self.state.action += 1;
|
||||
}
|
||||
Message::StopListening => {
|
||||
self.state.stop_listening = true;
|
||||
}
|
||||
Message::SetText(s) => {
|
||||
self.state.text = s;
|
||||
}
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> crate::Html {
|
||||
M::view(ctx, &self.state)
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_count(el: &web_sys::HtmlElement, count: isize) {
|
||||
assert_eq!(el.text_content(), Some(count.to_string()))
|
||||
}
|
||||
|
||||
fn get_el_by_tag(tag: &str) -> web_sys::HtmlElement {
|
||||
document()
|
||||
.query_selector(tag)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<web_sys::HtmlElement>()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn init<M>(tag: &str) -> (AppHandle<Comp<M>>, web_sys::HtmlElement)
|
||||
where
|
||||
M: Mixin,
|
||||
{
|
||||
// Remove any existing listeners and elements
|
||||
super::Registry::with(|r| *r = Default::default());
|
||||
if let Some(el) = document().query_selector(tag).unwrap() {
|
||||
el.parent_element().unwrap().remove();
|
||||
}
|
||||
|
||||
let root = document().create_element("div").unwrap();
|
||||
document().body().unwrap().append_child(&root).unwrap();
|
||||
let app = crate::start_app_in_element::<Comp<M>>(root);
|
||||
scheduler::start_now();
|
||||
|
||||
(app, get_el_by_tag(tag))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synchronous() {
|
||||
struct Synchronous;
|
||||
|
||||
impl Mixin for Synchronous {}
|
||||
|
||||
let (link, el) = init::<Synchronous>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 1);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
|
||||
link.send_message(Message::StopListening);
|
||||
scheduler::start_now();
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn non_bubbling_event() {
|
||||
struct NonBubbling;
|
||||
|
||||
impl Mixin for NonBubbling {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
let link = ctx.link().clone();
|
||||
let onblur = Callback::from(move |_| {
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
html! {
|
||||
<div>
|
||||
<a>
|
||||
<input id="input" {onblur} type="text" />
|
||||
{state.action}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (_, el) = init::<NonBubbling>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
let input = document().get_element_by_id("input").unwrap();
|
||||
|
||||
input
|
||||
.dispatch_event(
|
||||
&Event::new_with_event_init_dict("blur", &{
|
||||
let mut dict = EventInit::new();
|
||||
dict.bubbles(false);
|
||||
dict
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_count(&el, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bubbling() {
|
||||
struct Bubbling;
|
||||
|
||||
impl Mixin for Bubbling {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
if state.stop_listening {
|
||||
html! {
|
||||
<div>
|
||||
<a>
|
||||
{state.action}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
let link = ctx.link().clone();
|
||||
let cb = Callback::from(move |_| {
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
html! {
|
||||
<div onclick={cb.clone()}>
|
||||
<a onclick={cb}>
|
||||
{state.action}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (link, el) = init::<Bubbling>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 4);
|
||||
|
||||
link.send_message(Message::StopListening);
|
||||
scheduler::start_now();
|
||||
el.click();
|
||||
assert_count(&el, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_bubbling() {
|
||||
struct CancelBubbling;
|
||||
|
||||
impl Mixin for CancelBubbling {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
let link = ctx.link().clone();
|
||||
let onclick = Callback::from(move |_| {
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
let link = ctx.link().clone();
|
||||
let onclick2 = Callback::from(move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
html! {
|
||||
<div onclick={onclick}>
|
||||
<a onclick={onclick2}>
|
||||
{state.action}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (_, el) = init::<CancelBubbling>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 1);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_bubbling_nested() {
|
||||
// Here an event is being delivered to a DOM node which does
|
||||
// _not_ have a listener but which is contained within an
|
||||
// element that does and which cancels the bubble.
|
||||
struct CancelBubbling;
|
||||
|
||||
impl Mixin for CancelBubbling {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
let link = ctx.link().clone();
|
||||
let onclick = Callback::from(move |_| {
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
let link = ctx.link().clone();
|
||||
let onclick2 = Callback::from(move |e: MouseEvent| {
|
||||
e.stop_propagation();
|
||||
link.send_message(Message::Action);
|
||||
scheduler::start_now();
|
||||
});
|
||||
html! {
|
||||
<div onclick={onclick}>
|
||||
<div onclick={onclick2}>
|
||||
<a>
|
||||
{state.action}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (_, el) = init::<CancelBubbling>("a");
|
||||
|
||||
assert_count(&el, 0);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 1);
|
||||
|
||||
el.click();
|
||||
assert_count(&el, 2);
|
||||
}
|
||||
|
||||
fn test_input_listener<E>(make_event: impl Fn() -> E)
|
||||
where
|
||||
E: JsCast + std::fmt::Debug,
|
||||
{
|
||||
struct Input;
|
||||
|
||||
impl Mixin for Input {
|
||||
fn view<C>(ctx: &Context<C>, state: &State) -> Html
|
||||
where
|
||||
C: Component<Message = Message>,
|
||||
{
|
||||
if state.stop_listening {
|
||||
html! {
|
||||
<div>
|
||||
<input type="text" />
|
||||
<p>{state.text.clone()}</p>
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
let link = ctx.link().clone();
|
||||
let onchange = Callback::from(move |e: web_sys::Event| {
|
||||
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||
link.send_message(Message::SetText(el.value()));
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
let link = ctx.link().clone();
|
||||
let oninput = Callback::from(move |e: web_sys::InputEvent| {
|
||||
let el: web_sys::HtmlInputElement = e.target_unchecked_into();
|
||||
link.send_message(Message::SetText(el.value()));
|
||||
scheduler::start_now();
|
||||
});
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<input type="text" {onchange} {oninput} />
|
||||
<p>{state.text.clone()}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (link, input_el) = init::<Input>("input");
|
||||
let input_el = input_el.dyn_into::<web_sys::HtmlInputElement>().unwrap();
|
||||
let p_el = get_el_by_tag("p");
|
||||
|
||||
assert_eq!(&p_el.text_content().unwrap(), "");
|
||||
for mut s in ["foo", "bar", "baz"].iter() {
|
||||
input_el.set_value(s);
|
||||
if s == &"baz" {
|
||||
link.send_message(Message::StopListening);
|
||||
scheduler::start_now();
|
||||
|
||||
s = &"bar";
|
||||
}
|
||||
input_el
|
||||
.dyn_ref::<web_sys::EventTarget>()
|
||||
.unwrap()
|
||||
.dispatch_event(&make_event().dyn_into().unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(&p_el.text_content().unwrap(), s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oninput() {
|
||||
test_input_listener(|| {
|
||||
web_sys::InputEvent::new_with_event_init_dict(
|
||||
"input",
|
||||
web_sys::InputEventInit::new().bubbles(true),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn onchange() {
|
||||
test_input_listener(|| {
|
||||
web_sys::Event::new_with_event_init_dict(
|
||||
"change",
|
||||
web_sys::EventInit::new().bubbles(true),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,12 +19,6 @@ pub mod vtag;
|
||||
#[doc(hidden)]
|
||||
pub mod vtext;
|
||||
|
||||
use crate::html::{AnyScope, NodeRef};
|
||||
use indexmap::IndexMap;
|
||||
use std::borrow::Cow;
|
||||
use std::{collections::HashMap, fmt, hint::unreachable_unchecked, iter};
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use self::key::Key;
|
||||
#[doc(inline)]
|
||||
@ -43,9 +37,13 @@ pub use self::vsuspense::VSuspense;
|
||||
pub use self::vtag::VTag;
|
||||
#[doc(inline)]
|
||||
pub use self::vtext::VText;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::{fmt, hint::unreachable_unchecked};
|
||||
|
||||
/// Attribute value
|
||||
#[derive(Debug)]
|
||||
@ -181,18 +179,6 @@ mod tests_attr_value {
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies contained changes to DOM [Element]
|
||||
trait Apply {
|
||||
/// [Element] type to apply the changes to
|
||||
type Element;
|
||||
|
||||
/// Apply contained values to [Element] with no ancestor
|
||||
fn apply(&mut self, el: &Self::Element);
|
||||
|
||||
/// Apply diff between [self] and `ancestor` to [Element].
|
||||
fn apply_diff(&mut self, el: &Self::Element, ancestor: Self);
|
||||
}
|
||||
|
||||
/// A collection of attributes for an element
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub enum Attributes {
|
||||
@ -271,194 +257,6 @@ impl Attributes {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn apply_diff_index_maps<'a, A, B>(
|
||||
el: &Element,
|
||||
// this makes it possible to diff `&'a IndexMap<_, A>` and `IndexMap<_, &'a A>`.
|
||||
mut new_iter: impl Iterator<Item = (&'static str, &'a str)>,
|
||||
new: &IndexMap<&'static str, A>,
|
||||
old: &IndexMap<&'static str, B>,
|
||||
) where
|
||||
A: AsRef<str>,
|
||||
B: AsRef<str>,
|
||||
{
|
||||
let mut old_iter = old.iter();
|
||||
loop {
|
||||
match (new_iter.next(), old_iter.next()) {
|
||||
(Some((new_key, new_value)), Some((old_key, old_value))) => {
|
||||
if new_key != *old_key {
|
||||
break;
|
||||
}
|
||||
if new_value != old_value.as_ref() {
|
||||
Self::set_attribute(el, new_key, new_value);
|
||||
}
|
||||
}
|
||||
// new attributes
|
||||
(Some(attr), None) => {
|
||||
for (key, value) in iter::once(attr).chain(new_iter) {
|
||||
match old.get(key) {
|
||||
Some(old_value) => {
|
||||
if value != old_value.as_ref() {
|
||||
Self::set_attribute(el, key, value);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
Self::set_attribute(el, key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
// removed attributes
|
||||
(None, Some(attr)) => {
|
||||
for (key, _) in iter::once(attr).chain(old_iter) {
|
||||
if !new.contains_key(key) {
|
||||
Self::remove_attribute(el, key);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert [Attributes] pair to [HashMap]s and patch changes to `el`.
|
||||
/// Works with any [Attributes] variants.
|
||||
#[cold]
|
||||
fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) {
|
||||
fn collect<'a>(src: &'a Attributes) -> HashMap<&'static str, &'a str> {
|
||||
use Attributes::*;
|
||||
|
||||
match src {
|
||||
Static(arr) => (*arr).iter().map(|[k, v]| (*k, *v)).collect(),
|
||||
Dynamic { keys, values } => keys
|
||||
.iter()
|
||||
.zip(values.iter())
|
||||
.filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref())))
|
||||
.collect(),
|
||||
IndexMap(m) => m.iter().map(|(k, v)| (*k, v.as_ref())).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
let new = collect(new);
|
||||
let old = collect(old);
|
||||
|
||||
// Update existing or set new
|
||||
for (k, new) in new.iter() {
|
||||
if match old.get(k) {
|
||||
Some(old) => old != new,
|
||||
None => true,
|
||||
} {
|
||||
el.set_attribute(k, new).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove missing
|
||||
for k in old.keys() {
|
||||
if !new.contains_key(k) {
|
||||
Self::remove_attribute(el, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_attribute(el: &Element, key: &str, value: &str) {
|
||||
el.set_attribute(key, value).expect("invalid attribute key")
|
||||
}
|
||||
|
||||
fn remove_attribute(el: &Element, key: &str) {
|
||||
el.remove_attribute(key)
|
||||
.expect("could not remove attribute")
|
||||
}
|
||||
}
|
||||
|
||||
impl Apply for Attributes {
|
||||
type Element = Element;
|
||||
|
||||
fn apply(&mut self, el: &Element) {
|
||||
match self {
|
||||
Self::Static(arr) => {
|
||||
for kv in arr.iter() {
|
||||
Self::set_attribute(el, kv[0], kv[1]);
|
||||
}
|
||||
}
|
||||
Self::Dynamic { keys, values } => {
|
||||
for (k, v) in keys.iter().zip(values.iter()) {
|
||||
if let Some(v) = v {
|
||||
Self::set_attribute(el, k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::IndexMap(m) => {
|
||||
for (k, v) in m.iter() {
|
||||
Self::set_attribute(el, k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_diff(&mut self, el: &Element, ancestor: Self) {
|
||||
#[inline]
|
||||
fn ptr_eq<T>(a: &[T], b: &[T]) -> bool {
|
||||
a.as_ptr() == b.as_ptr()
|
||||
}
|
||||
|
||||
match (self, ancestor) {
|
||||
// Hot path
|
||||
(Self::Static(new), Self::Static(old)) if ptr_eq(new, old) => (),
|
||||
// Hot path
|
||||
(
|
||||
Self::Dynamic {
|
||||
keys: new_k,
|
||||
values: new_v,
|
||||
},
|
||||
Self::Dynamic {
|
||||
keys: old_k,
|
||||
values: old_v,
|
||||
},
|
||||
) if ptr_eq(new_k, old_k) => {
|
||||
// Double zipping does not optimize well, so use asserts and unsafe instead
|
||||
assert!(new_k.len() == new_v.len());
|
||||
assert!(new_k.len() == old_v.len());
|
||||
for i in 0..new_k.len() {
|
||||
macro_rules! key {
|
||||
() => {
|
||||
unsafe { new_k.get_unchecked(i) }
|
||||
};
|
||||
}
|
||||
macro_rules! set {
|
||||
($new:expr) => {
|
||||
Self::set_attribute(el, key!(), $new)
|
||||
};
|
||||
}
|
||||
|
||||
match unsafe { (new_v.get_unchecked(i), old_v.get_unchecked(i)) } {
|
||||
(Some(new), Some(old)) => {
|
||||
if new != old {
|
||||
set!(new);
|
||||
}
|
||||
}
|
||||
(Some(new), None) => set!(new),
|
||||
(None, Some(_)) => {
|
||||
Self::remove_attribute(el, key!());
|
||||
}
|
||||
(None, None) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
// For VTag's constructed outside the html! macro
|
||||
(Self::IndexMap(new), Self::IndexMap(old)) => {
|
||||
let new_iter = new.iter().map(|(k, v)| (*k, v.as_ref()));
|
||||
Self::apply_diff_index_maps(el, new_iter, new, &old);
|
||||
}
|
||||
// Cold path. Happens only with conditional swapping and reordering of `VTag`s with the
|
||||
// same tag and no keys.
|
||||
(new, ancestor) => {
|
||||
Self::apply_diff_as_maps(el, new, &ancestor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IndexMap<&'static str, AttrValue>> for Attributes {
|
||||
@ -473,66 +271,6 @@ impl Default for Attributes {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(#938): What about implementing `VDiff` for `Element`?
|
||||
// It would make it possible to include ANY element into the tree.
|
||||
// `Ace` editor embedding for example?
|
||||
|
||||
/// This trait provides features to update a tree by calculating a difference against another tree.
|
||||
pub(crate) trait VDiff {
|
||||
/// Remove self from parent.
|
||||
///
|
||||
/// Parent to detach is `true` if the parent element will also be detached.
|
||||
fn detach(&mut self, parent: &Element, parent_to_detach: bool);
|
||||
|
||||
/// Move elements from one parent to another parent.
|
||||
/// This is currently only used by `VSuspense` to preserve component state without detaching
|
||||
/// (which destroys component state).
|
||||
/// Prefer `detach` then apply if possible.
|
||||
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef);
|
||||
|
||||
/// Scoped diff apply to other tree.
|
||||
///
|
||||
/// Virtual rendering for the node. It uses parent node and existing
|
||||
/// children (virtual and DOM) to check the difference and apply patches to
|
||||
/// the actual DOM representation.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `parent_scope`: the parent `Scope` used for passing messages to the
|
||||
/// parent `Component`.
|
||||
/// - `parent`: the parent node in the DOM.
|
||||
/// - `next_sibling`: the next sibling, used to efficiently find where to
|
||||
/// put the node.
|
||||
/// - `ancestor`: the node that this node will be replacing in the DOM. This
|
||||
/// method will _always_ remove the `ancestor` from the `parent`.
|
||||
///
|
||||
/// Returns a reference to the newly inserted element.
|
||||
///
|
||||
/// ### Internal Behavior Notice:
|
||||
///
|
||||
/// Note that these modify the DOM by modifying the reference that _already_
|
||||
/// exists on the `ancestor`. If `self.reference` exists (which it
|
||||
/// _shouldn't_) this method will panic.
|
||||
///
|
||||
/// The exception to this is obviously `VRef` which simply uses the inner
|
||||
/// `Node` directly (always removes the `Node` that exists).
|
||||
fn apply(
|
||||
&mut self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
ancestor: Option<VNode>,
|
||||
) -> NodeRef;
|
||||
}
|
||||
|
||||
pub(crate) fn insert_node(node: &Node, parent: &Element, next_sibling: Option<&Node>) {
|
||||
match next_sibling {
|
||||
Some(next_sibling) => parent
|
||||
.insert_before(node, Some(next_sibling))
|
||||
.expect("failed to insert tag before next sibling"),
|
||||
None => parent.append_child(node).expect("failed to append child"),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "wasm_bench"))]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
@ -1,77 +1,41 @@
|
||||
//! This module contains the implementation of a virtual component (`VComp`).
|
||||
|
||||
use super::{Key, VDiff, VNode};
|
||||
use crate::html::{AnyScope, BaseComponent, NodeRef, Scope, Scoped};
|
||||
#[cfg(feature = "ssr")]
|
||||
use futures::future::{FutureExt, LocalBoxFuture};
|
||||
use super::Key;
|
||||
use crate::dom_bundle::{Mountable, PropsWrapper};
|
||||
use crate::html::{BaseComponent, NodeRef};
|
||||
use std::any::TypeId;
|
||||
use std::borrow::Borrow;
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
#[cfg(debug_assertions)]
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use web_sys::Element;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
thread_local! {
|
||||
static EVENT_HISTORY: std::cell::RefCell<std::collections::HashMap<usize, Vec<String>>>
|
||||
= Default::default();
|
||||
static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
}
|
||||
|
||||
/// Push [VComp] event to lifecycle debugging registry
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn log_event(vcomp_id: usize, event: impl ToString) {
|
||||
EVENT_HISTORY.with(|h| {
|
||||
h.borrow_mut()
|
||||
.entry(vcomp_id)
|
||||
.or_default()
|
||||
.push(event.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
/// Get [VComp] event log from lifecycle debugging registry
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn get_event_log(vcomp_id: usize) -> Vec<String> {
|
||||
EVENT_HISTORY.with(|h| {
|
||||
h.borrow()
|
||||
.get(&vcomp_id)
|
||||
.map(|l| (*l).clone())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
thread_local! {}
|
||||
|
||||
/// A virtual component.
|
||||
pub struct VComp {
|
||||
type_id: TypeId,
|
||||
scope: Option<Box<dyn Scoped>>,
|
||||
mountable: Option<Box<dyn Mountable>>,
|
||||
pub(crate) type_id: TypeId,
|
||||
pub(crate) mountable: Box<dyn Mountable>,
|
||||
pub(crate) node_ref: NodeRef,
|
||||
pub(crate) key: Option<Key>,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) id: usize,
|
||||
impl fmt::Debug for VComp {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("VComp")
|
||||
.field("type_id", &self.type_id)
|
||||
.field("node_ref", &self.node_ref)
|
||||
.field("mountable", &"..")
|
||||
.field("key", &self.key)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for VComp {
|
||||
fn clone(&self) -> Self {
|
||||
if self.scope.is_some() {
|
||||
panic!("Mounted components are not allowed to be cloned!");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
log_event(self.id, "clone");
|
||||
|
||||
Self {
|
||||
type_id: self.type_id,
|
||||
scope: None,
|
||||
mountable: self.mountable.as_ref().map(|m| m.copy()),
|
||||
mountable: self.mountable.copy(),
|
||||
node_ref: self.node_ref.clone(),
|
||||
key: self.key.clone(),
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,155 +100,10 @@ impl VComp {
|
||||
VComp {
|
||||
type_id: TypeId::of::<COMP>(),
|
||||
node_ref,
|
||||
mountable: Some(Box::new(PropsWrapper::<COMP>::new(props))),
|
||||
scope: None,
|
||||
mountable: Box::new(PropsWrapper::<COMP>::new(props)),
|
||||
key,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
id: Self::next_id(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn root_vnode(&self) -> Option<impl Deref<Target = VNode> + '_> {
|
||||
self.scope.as_ref().and_then(|scope| scope.root_vnode())
|
||||
}
|
||||
|
||||
/// Take ownership of [Box<dyn Scoped>] or panic with error message, if component is not mounted
|
||||
#[inline]
|
||||
fn take_scope(&mut self) -> Box<dyn Scoped> {
|
||||
self.scope.take().unwrap_or_else(|| {
|
||||
#[cfg(not(debug_assertions))]
|
||||
panic!("no scope; VComp should be mounted");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
panic!(
|
||||
"no scope; VComp should be mounted after: {:?}",
|
||||
get_event_log(self.id)
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn next_id() -> usize {
|
||||
COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
}
|
||||
|
||||
trait Mountable {
|
||||
fn copy(&self) -> Box<dyn Mountable>;
|
||||
fn mount(
|
||||
self: Box<Self>,
|
||||
node_ref: NodeRef,
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> Box<dyn Scoped>;
|
||||
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef);
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn render_to_string<'a>(
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
) -> LocalBoxFuture<'a, ()>;
|
||||
}
|
||||
|
||||
struct PropsWrapper<COMP: BaseComponent> {
|
||||
props: Rc<COMP::Properties>,
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> PropsWrapper<COMP> {
|
||||
fn new(props: Rc<COMP::Properties>) -> Self {
|
||||
Self { props }
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> Mountable for PropsWrapper<COMP> {
|
||||
fn copy(&self) -> Box<dyn Mountable> {
|
||||
let wrapper: PropsWrapper<COMP> = PropsWrapper {
|
||||
props: Rc::clone(&self.props),
|
||||
};
|
||||
Box::new(wrapper)
|
||||
}
|
||||
|
||||
fn mount(
|
||||
self: Box<Self>,
|
||||
node_ref: NodeRef,
|
||||
parent_scope: &AnyScope,
|
||||
parent: Element,
|
||||
next_sibling: NodeRef,
|
||||
) -> Box<dyn Scoped> {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
scope.mount_in_place(parent, next_sibling, node_ref, self.props);
|
||||
|
||||
Box::new(scope)
|
||||
}
|
||||
|
||||
fn reuse(self: Box<Self>, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) {
|
||||
let scope: Scope<COMP> = scope.to_any().downcast();
|
||||
scope.reuse(self.props, node_ref, next_sibling);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
fn render_to_string<'a>(
|
||||
&'a self,
|
||||
w: &'a mut String,
|
||||
parent_scope: &'a AnyScope,
|
||||
) -> LocalBoxFuture<'a, ()> {
|
||||
async move {
|
||||
let scope: Scope<COMP> = Scope::new(Some(parent_scope.clone()));
|
||||
scope.render_to_string(w, self.props.clone()).await;
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
impl VDiff for VComp {
|
||||
fn detach(&mut self, _parent: &Element, parent_to_detach: bool) {
|
||||
self.take_scope().destroy(parent_to_detach);
|
||||
}
|
||||
|
||||
fn shift(&self, _previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
|
||||
let scope = self.scope.as_ref().unwrap();
|
||||
scope.shift_node(next_parent.clone(), next_sibling);
|
||||
}
|
||||
|
||||
fn apply(
|
||||
&mut self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
ancestor: Option<VNode>,
|
||||
) -> NodeRef {
|
||||
let mountable = self
|
||||
.mountable
|
||||
.take()
|
||||
.expect("VComp has already been mounted");
|
||||
|
||||
if let Some(mut ancestor) = ancestor {
|
||||
if let VNode::VComp(ref mut vcomp) = &mut ancestor {
|
||||
// If the ancestor is the same type, reuse it and update its properties
|
||||
if self.type_id == vcomp.type_id && self.key == vcomp.key {
|
||||
self.node_ref.reuse(vcomp.node_ref.clone());
|
||||
let scope = vcomp.take_scope();
|
||||
mountable.reuse(self.node_ref.clone(), scope.borrow(), next_sibling);
|
||||
self.scope = Some(scope);
|
||||
return vcomp.node_ref.clone();
|
||||
}
|
||||
}
|
||||
|
||||
ancestor.detach(parent, false);
|
||||
}
|
||||
|
||||
self.scope = Some(mountable.mount(
|
||||
self.node_ref.clone(),
|
||||
parent_scope,
|
||||
parent.to_owned(),
|
||||
next_sibling,
|
||||
));
|
||||
|
||||
self.node_ref.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for VComp {
|
||||
@ -293,12 +112,6 @@ impl PartialEq for VComp {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for VComp {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "VComp {{ root: {:?} }}", self.root_vnode().as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP: BaseComponent> fmt::Debug for VChild<COMP> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("VChild<_>")
|
||||
@ -308,608 +121,18 @@ impl<COMP: BaseComponent> fmt::Debug for VChild<COMP> {
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
use crate::html::AnyScope;
|
||||
|
||||
impl VComp {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
|
||||
self.mountable
|
||||
.as_ref()
|
||||
.map(|m| m.copy())
|
||||
.unwrap()
|
||||
.render_to_string(w, parent_scope)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scheduler;
|
||||
use crate::{html, Children, Component, Context, Html, NodeRef, Properties};
|
||||
use gloo_utils::document;
|
||||
use web_sys::Node;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
struct Comp;
|
||||
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
struct Props {
|
||||
#[prop_or_default]
|
||||
field_1: u32,
|
||||
#[prop_or_default]
|
||||
field_2: u32,
|
||||
}
|
||||
|
||||
impl Component for Comp {
|
||||
type Message = ();
|
||||
type Properties = Props;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Comp
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
html! { <div/> }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_loop() {
|
||||
let document = gloo_utils::document();
|
||||
let parent_scope: AnyScope = crate::html::Scope::<Comp>::new(None).into();
|
||||
let parent_element = document.create_element("div").unwrap();
|
||||
|
||||
let mut ancestor = html! { <Comp></Comp> };
|
||||
ancestor.apply(&parent_scope, &parent_element, NodeRef::default(), None);
|
||||
scheduler::start_now();
|
||||
|
||||
for _ in 0..10000 {
|
||||
let mut node = html! { <Comp></Comp> };
|
||||
node.apply(
|
||||
&parent_scope,
|
||||
&parent_element,
|
||||
NodeRef::default(),
|
||||
Some(ancestor),
|
||||
);
|
||||
scheduler::start_now();
|
||||
ancestor = node;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_properties_to_component() {
|
||||
html! {
|
||||
<Comp />
|
||||
};
|
||||
|
||||
html! {
|
||||
<Comp field_1=1 />
|
||||
};
|
||||
|
||||
html! {
|
||||
<Comp field_2=2 />
|
||||
};
|
||||
|
||||
html! {
|
||||
<Comp field_1=1 field_2=2 />
|
||||
};
|
||||
|
||||
let props = Props {
|
||||
field_1: 1,
|
||||
field_2: 1,
|
||||
};
|
||||
|
||||
html! {
|
||||
<Comp ..props />
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_component_key() {
|
||||
let test_key: Key = "test".to_string().into();
|
||||
let check_key = |vnode: VNode| {
|
||||
assert_eq!(vnode.key().as_ref(), Some(&test_key));
|
||||
};
|
||||
|
||||
let props = Props {
|
||||
field_1: 1,
|
||||
field_2: 1,
|
||||
};
|
||||
let props_2 = props.clone();
|
||||
|
||||
check_key(html! { <Comp key={test_key.clone()} /> });
|
||||
check_key(html! { <Comp key={test_key.clone()} field_1=1 /> });
|
||||
check_key(html! { <Comp field_1=1 key={test_key.clone()} /> });
|
||||
check_key(html! { <Comp key={test_key.clone()} ..props /> });
|
||||
check_key(html! { <Comp key={test_key.clone()} ..props_2 /> });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_component_node_ref() {
|
||||
let test_node: Node = document().create_text_node("test").into();
|
||||
let test_node_ref = NodeRef::new(test_node);
|
||||
let check_node_ref = |vnode: VNode| {
|
||||
assert_eq!(vnode.unchecked_first_node(), test_node_ref.get().unwrap());
|
||||
};
|
||||
|
||||
let props = Props {
|
||||
field_1: 1,
|
||||
field_2: 1,
|
||||
};
|
||||
let props_2 = props.clone();
|
||||
|
||||
check_node_ref(html! { <Comp ref={test_node_ref.clone()} /> });
|
||||
check_node_ref(html! { <Comp ref={test_node_ref.clone()} field_1=1 /> });
|
||||
check_node_ref(html! { <Comp field_1=1 ref={test_node_ref.clone()} /> });
|
||||
check_node_ref(html! { <Comp ref={test_node_ref.clone()} ..props /> });
|
||||
check_node_ref(html! { <Comp ref={test_node_ref.clone()} ..props_2 /> });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vchild_partialeq() {
|
||||
let vchild1: VChild<Comp> = VChild::new(
|
||||
Props {
|
||||
field_1: 1,
|
||||
field_2: 1,
|
||||
},
|
||||
NodeRef::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
let vchild2: VChild<Comp> = VChild::new(
|
||||
Props {
|
||||
field_1: 1,
|
||||
field_2: 1,
|
||||
},
|
||||
NodeRef::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
let vchild3: VChild<Comp> = VChild::new(
|
||||
Props {
|
||||
field_1: 2,
|
||||
field_2: 2,
|
||||
},
|
||||
NodeRef::default(),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(vchild1, vchild2);
|
||||
assert_ne!(vchild1, vchild3);
|
||||
assert_ne!(vchild2, vchild3);
|
||||
}
|
||||
|
||||
#[derive(Clone, Properties, PartialEq)]
|
||||
pub struct ListProps {
|
||||
pub children: Children,
|
||||
}
|
||||
pub struct List;
|
||||
impl Component for List {
|
||||
type Message = ();
|
||||
type Properties = ListProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
|
||||
unimplemented!();
|
||||
}
|
||||
fn changed(&mut self, _ctx: &Context<Self>) -> bool {
|
||||
unimplemented!();
|
||||
}
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let item_iter = ctx
|
||||
.props()
|
||||
.children
|
||||
.iter()
|
||||
.map(|item| html! {<li>{ item }</li>});
|
||||
html! {
|
||||
<ul>{ for item_iter }</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use super::{AnyScope, Element};
|
||||
|
||||
fn setup_parent() -> (AnyScope, Element) {
|
||||
let scope = AnyScope::test();
|
||||
let parent = document().create_element("div").unwrap();
|
||||
|
||||
document().body().unwrap().append_child(&parent).unwrap();
|
||||
|
||||
(scope, parent)
|
||||
}
|
||||
|
||||
fn get_html(mut node: Html, scope: &AnyScope, parent: &Element) -> String {
|
||||
// clear parent
|
||||
parent.set_inner_html("");
|
||||
|
||||
node.apply(scope, parent, NodeRef::default(), None);
|
||||
scheduler::start_now();
|
||||
parent.inner_html()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_ways_of_passing_children_work() {
|
||||
let (scope, parent) = setup_parent();
|
||||
|
||||
let children: Vec<_> = vec!["a", "b", "c"]
|
||||
.drain(..)
|
||||
.map(|text| html! {<span>{ text }</span>})
|
||||
.collect();
|
||||
let children_renderer = Children::new(children.clone());
|
||||
let expected_html = "\
|
||||
<ul>\
|
||||
<li><span>a</span></li>\
|
||||
<li><span>b</span></li>\
|
||||
<li><span>c</span></li>\
|
||||
</ul>";
|
||||
|
||||
let prop_method = html! {
|
||||
<List children={children_renderer.clone()} />
|
||||
};
|
||||
assert_eq!(get_html(prop_method, &scope, &parent), expected_html);
|
||||
|
||||
let children_renderer_method = html! {
|
||||
<List>
|
||||
{ children_renderer }
|
||||
</List>
|
||||
};
|
||||
assert_eq!(
|
||||
get_html(children_renderer_method, &scope, &parent),
|
||||
expected_html
|
||||
);
|
||||
|
||||
let direct_method = html! {
|
||||
<List>
|
||||
{ children.clone() }
|
||||
</List>
|
||||
};
|
||||
assert_eq!(get_html(direct_method, &scope, &parent), expected_html);
|
||||
|
||||
let for_method = html! {
|
||||
<List>
|
||||
{ for children }
|
||||
</List>
|
||||
};
|
||||
assert_eq!(get_html(for_method, &scope, &parent), expected_html);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_node_ref() {
|
||||
let scope = AnyScope::test();
|
||||
let parent = document().create_element("div").unwrap();
|
||||
|
||||
document().body().unwrap().append_child(&parent).unwrap();
|
||||
|
||||
let node_ref = NodeRef::default();
|
||||
let mut elem: VNode = html! { <Comp ref={node_ref.clone()}></Comp> };
|
||||
elem.apply(&scope, &parent, NodeRef::default(), None);
|
||||
scheduler::start_now();
|
||||
let parent_node = parent.deref();
|
||||
assert_eq!(node_ref.get(), parent_node.first_child());
|
||||
elem.detach(&parent, false);
|
||||
scheduler::start_now();
|
||||
assert!(node_ref.get().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
extern crate self as yew;
|
||||
|
||||
use crate::html;
|
||||
use crate::tests::layout_tests::{diff_layouts, TestLayout};
|
||||
use crate::{Children, Component, Context, Html, Properties};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
struct Comp<T> {
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
struct CompProps {
|
||||
#[prop_or_default]
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl<T: 'static> Component for Comp<T> {
|
||||
type Message = ();
|
||||
type Properties = CompProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Comp {
|
||||
_marker: PhantomData::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, _: Self::Message) -> bool {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>{ ctx.props().children.clone() }</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct A;
|
||||
struct B;
|
||||
|
||||
#[test]
|
||||
fn diff() {
|
||||
let layout1 = TestLayout {
|
||||
name: "1",
|
||||
node: html! {
|
||||
<Comp<A>>
|
||||
<Comp<B>></Comp<B>>
|
||||
{"C"}
|
||||
</Comp<A>>
|
||||
},
|
||||
expected: "C",
|
||||
};
|
||||
|
||||
let layout2 = TestLayout {
|
||||
name: "2",
|
||||
node: html! {
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
},
|
||||
expected: "A",
|
||||
};
|
||||
|
||||
let layout3 = TestLayout {
|
||||
name: "3",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<Comp<A>></Comp<A>>
|
||||
{"B"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "B",
|
||||
};
|
||||
|
||||
let layout4 = TestLayout {
|
||||
name: "4",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<Comp<A>>{"A"}</Comp<A>>
|
||||
{"B"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "AB",
|
||||
};
|
||||
|
||||
let layout5 = TestLayout {
|
||||
name: "5",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
</>
|
||||
{"B"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "AB",
|
||||
};
|
||||
|
||||
let layout6 = TestLayout {
|
||||
name: "6",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
{"B"}
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout7 = TestLayout {
|
||||
name: "7",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
<Comp<A>>
|
||||
{"B"}
|
||||
</Comp<A>>
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout8 = TestLayout {
|
||||
name: "8",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
<Comp<A>>
|
||||
<Comp<A>>
|
||||
{"B"}
|
||||
</Comp<A>>
|
||||
</Comp<A>>
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout9 = TestLayout {
|
||||
name: "9",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<>
|
||||
{"A"}
|
||||
</>
|
||||
<Comp<A>>
|
||||
<Comp<A>>
|
||||
{"B"}
|
||||
</Comp<A>>
|
||||
</Comp<A>>
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout10 = TestLayout {
|
||||
name: "10",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
</Comp<A>>
|
||||
<>
|
||||
{"B"}
|
||||
</>
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout11 = TestLayout {
|
||||
name: "11",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<>
|
||||
<Comp<A>>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
{"B"}
|
||||
</Comp<A>>
|
||||
</>
|
||||
</>
|
||||
{"C"}
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
let layout12 = TestLayout {
|
||||
name: "12",
|
||||
node: html! {
|
||||
<Comp<B>>
|
||||
<>
|
||||
<Comp<A>></Comp<A>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
<>
|
||||
<Comp<A>>
|
||||
{"A"}
|
||||
</Comp<A>>
|
||||
<></>
|
||||
<Comp<A>>
|
||||
<Comp<A>></Comp<A>>
|
||||
<></>
|
||||
{"B"}
|
||||
<></>
|
||||
<Comp<A>></Comp<A>>
|
||||
</Comp<A>>
|
||||
</>
|
||||
</Comp<A>>
|
||||
<></>
|
||||
</>
|
||||
<Comp<A>></Comp<A>>
|
||||
</>
|
||||
{"C"}
|
||||
<Comp<A>></Comp<A>>
|
||||
<></>
|
||||
</Comp<B>>
|
||||
},
|
||||
expected: "ABC",
|
||||
};
|
||||
|
||||
diff_layouts(vec![
|
||||
layout1, layout2, layout3, layout4, layout5, layout6, layout7, layout8, layout9,
|
||||
layout10, layout11, layout12,
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_with_children() {
|
||||
#[derive(Properties, PartialEq)]
|
||||
struct Props {
|
||||
children: Children,
|
||||
}
|
||||
|
||||
struct ComponentWithChildren;
|
||||
|
||||
impl Component for ComponentWithChildren {
|
||||
type Message = ();
|
||||
type Properties = Props;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<ul>
|
||||
{ for ctx.props().children.iter().map(|child| html! { <li>{ child }</li> }) }
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let layout = TestLayout {
|
||||
name: "13",
|
||||
node: html! {
|
||||
<ComponentWithChildren>
|
||||
if true {
|
||||
<span>{ "hello" }</span>
|
||||
<span>{ "world" }</span>
|
||||
} else {
|
||||
<span>{ "goodbye" }</span>
|
||||
<span>{ "world" }</span>
|
||||
}
|
||||
</ComponentWithChildren>
|
||||
},
|
||||
expected: "<ul><li><span>hello</span><span>world</span></li></ul>",
|
||||
};
|
||||
|
||||
diff_layouts(vec![layout]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))]
|
||||
mod ssr_tests {
|
||||
use tokio::test;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,11 @@
|
||||
//! This module contains the implementation of abstract virtual node.
|
||||
|
||||
use super::{Key, VChild, VComp, VDiff, VList, VPortal, VSuspense, VTag, VText};
|
||||
use crate::html::{AnyScope, BaseComponent, NodeRef};
|
||||
use gloo::console;
|
||||
use super::{Key, VChild, VComp, VList, VPortal, VSuspense, VTag, VText};
|
||||
use crate::html::BaseComponent;
|
||||
use std::cmp::PartialEq;
|
||||
use std::fmt;
|
||||
use std::iter::FromIterator;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
use web_sys::{Element, Node};
|
||||
use web_sys::Node;
|
||||
|
||||
/// Bind virtual element to a DOM reference.
|
||||
#[derive(Clone)]
|
||||
@ -30,177 +27,21 @@ pub enum VNode {
|
||||
}
|
||||
|
||||
impl VNode {
|
||||
pub fn key(&self) -> Option<Key> {
|
||||
pub fn key(&self) -> Option<&Key> {
|
||||
match self {
|
||||
VNode::VComp(vcomp) => vcomp.key.clone(),
|
||||
VNode::VList(vlist) => vlist.key.clone(),
|
||||
VNode::VComp(vcomp) => vcomp.key.as_ref(),
|
||||
VNode::VList(vlist) => vlist.key.as_ref(),
|
||||
VNode::VRef(_) => None,
|
||||
VNode::VTag(vtag) => vtag.key.clone(),
|
||||
VNode::VTag(vtag) => vtag.key.as_ref(),
|
||||
VNode::VText(_) => None,
|
||||
VNode::VPortal(vportal) => vportal.node.key(),
|
||||
VNode::VSuspense(vsuspense) => vsuspense.key.clone(),
|
||||
VNode::VSuspense(vsuspense) => vsuspense.key.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the [VNode] has a key without needlessly cloning the key.
|
||||
/// Returns true if the [VNode] has a key.
|
||||
pub fn has_key(&self) -> bool {
|
||||
match self {
|
||||
VNode::VComp(vcomp) => vcomp.key.is_some(),
|
||||
VNode::VList(vlist) => vlist.key.is_some(),
|
||||
VNode::VRef(_) | VNode::VText(_) => false,
|
||||
VNode::VTag(vtag) => vtag.key.is_some(),
|
||||
VNode::VPortal(vportal) => vportal.node.has_key(),
|
||||
VNode::VSuspense(vsuspense) => vsuspense.key.is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the first DOM node if available
|
||||
pub(crate) fn first_node(&self) -> Option<Node> {
|
||||
match self {
|
||||
VNode::VTag(vtag) => vtag.reference().cloned().map(JsCast::unchecked_into),
|
||||
VNode::VText(vtext) => vtext
|
||||
.reference
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.map(JsCast::unchecked_into),
|
||||
VNode::VComp(vcomp) => vcomp.node_ref.get(),
|
||||
VNode::VList(vlist) => vlist.get(0).and_then(VNode::first_node),
|
||||
VNode::VRef(node) => Some(node.clone()),
|
||||
VNode::VPortal(vportal) => vportal.next_sibling(),
|
||||
VNode::VSuspense(vsuspense) => vsuspense.first_node(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the first DOM node that is used to designate the position of the virtual DOM node.
|
||||
pub(crate) fn unchecked_first_node(&self) -> Node {
|
||||
match self {
|
||||
VNode::VTag(vtag) => vtag
|
||||
.reference()
|
||||
.expect("VTag is not mounted")
|
||||
.clone()
|
||||
.into(),
|
||||
VNode::VText(vtext) => {
|
||||
let text_node = vtext.reference.as_ref().expect("VText is not mounted");
|
||||
text_node.clone().into()
|
||||
}
|
||||
VNode::VComp(vcomp) => vcomp.node_ref.get().unwrap_or_else(|| {
|
||||
#[cfg(not(debug_assertions))]
|
||||
panic!("no node_ref; VComp should be mounted");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
panic!(
|
||||
"no node_ref; VComp should be mounted after: {:?}",
|
||||
crate::virtual_dom::vcomp::get_event_log(vcomp.id),
|
||||
);
|
||||
}),
|
||||
VNode::VList(vlist) => vlist
|
||||
.get(0)
|
||||
.expect("VList is not mounted")
|
||||
.unchecked_first_node(),
|
||||
VNode::VRef(node) => node.clone(),
|
||||
VNode::VPortal(_) => panic!("portals have no first node, they are empty inside"),
|
||||
VNode::VSuspense(vsuspense) => {
|
||||
vsuspense.first_node().expect("VSuspense is not mounted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn move_before(&self, parent: &Element, next_sibling: &Option<Node>) {
|
||||
match self {
|
||||
VNode::VList(vlist) => {
|
||||
for node in vlist.iter() {
|
||||
node.move_before(parent, next_sibling);
|
||||
}
|
||||
}
|
||||
VNode::VComp(vcomp) => {
|
||||
vcomp
|
||||
.root_vnode()
|
||||
.expect("VComp has no root vnode")
|
||||
.move_before(parent, next_sibling);
|
||||
}
|
||||
VNode::VPortal(_) => {} // no need to move portals
|
||||
_ => super::insert_node(&self.unchecked_first_node(), parent, next_sibling.as_ref()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl VDiff for VNode {
|
||||
/// Remove VNode from parent.
|
||||
fn detach(&mut self, parent: &Element, parent_to_detach: bool) {
|
||||
match *self {
|
||||
VNode::VTag(ref mut vtag) => vtag.detach(parent, parent_to_detach),
|
||||
VNode::VText(ref mut vtext) => vtext.detach(parent, parent_to_detach),
|
||||
VNode::VComp(ref mut vcomp) => vcomp.detach(parent, parent_to_detach),
|
||||
VNode::VList(ref mut vlist) => vlist.detach(parent, parent_to_detach),
|
||||
VNode::VRef(ref node) => {
|
||||
if parent.remove_child(node).is_err() {
|
||||
console::warn!("Node not found to remove VRef");
|
||||
}
|
||||
}
|
||||
VNode::VPortal(ref mut vportal) => vportal.detach(parent, parent_to_detach),
|
||||
VNode::VSuspense(ref mut vsuspense) => vsuspense.detach(parent, parent_to_detach),
|
||||
}
|
||||
}
|
||||
|
||||
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
|
||||
match *self {
|
||||
VNode::VTag(ref vtag) => vtag.shift(previous_parent, next_parent, next_sibling),
|
||||
VNode::VText(ref vtext) => vtext.shift(previous_parent, next_parent, next_sibling),
|
||||
VNode::VComp(ref vcomp) => vcomp.shift(previous_parent, next_parent, next_sibling),
|
||||
VNode::VList(ref vlist) => vlist.shift(previous_parent, next_parent, next_sibling),
|
||||
VNode::VRef(ref node) => {
|
||||
previous_parent.remove_child(node).unwrap();
|
||||
next_parent
|
||||
.insert_before(node, next_sibling.get().as_ref())
|
||||
.unwrap();
|
||||
}
|
||||
VNode::VPortal(ref vportal) => {
|
||||
vportal.shift(previous_parent, next_parent, next_sibling)
|
||||
}
|
||||
VNode::VSuspense(ref vsuspense) => {
|
||||
vsuspense.shift(previous_parent, next_parent, next_sibling)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(
|
||||
&mut self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
ancestor: Option<VNode>,
|
||||
) -> NodeRef {
|
||||
match *self {
|
||||
VNode::VTag(ref mut vtag) => vtag.apply(parent_scope, parent, next_sibling, ancestor),
|
||||
VNode::VText(ref mut vtext) => {
|
||||
vtext.apply(parent_scope, parent, next_sibling, ancestor)
|
||||
}
|
||||
VNode::VComp(ref mut vcomp) => {
|
||||
vcomp.apply(parent_scope, parent, next_sibling, ancestor)
|
||||
}
|
||||
VNode::VList(ref mut vlist) => {
|
||||
vlist.apply(parent_scope, parent, next_sibling, ancestor)
|
||||
}
|
||||
VNode::VRef(ref mut node) => {
|
||||
if let Some(mut ancestor) = ancestor {
|
||||
// We always remove VRef in case it's meant to be used somewhere else.
|
||||
if let VNode::VRef(n) = &ancestor {
|
||||
if node == n {
|
||||
return NodeRef::new(node.clone());
|
||||
}
|
||||
}
|
||||
ancestor.detach(parent, false);
|
||||
}
|
||||
super::insert_node(node, parent, next_sibling.get().as_ref());
|
||||
NodeRef::new(node.clone())
|
||||
}
|
||||
VNode::VPortal(ref mut vportal) => {
|
||||
vportal.apply(parent_scope, parent, next_sibling, ancestor)
|
||||
}
|
||||
VNode::VSuspense(ref mut vsuspense) => {
|
||||
vsuspense.apply(parent_scope, parent, next_sibling, ancestor)
|
||||
}
|
||||
}
|
||||
self.key().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,6 +86,13 @@ impl From<VSuspense> for VNode {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VPortal> for VNode {
|
||||
#[inline]
|
||||
fn from(vportal: VPortal) -> Self {
|
||||
VNode::VPortal(vportal)
|
||||
}
|
||||
}
|
||||
|
||||
impl<COMP> From<VChild<COMP>> for VNode
|
||||
where
|
||||
COMP: BaseComponent,
|
||||
@ -299,9 +147,9 @@ impl PartialEq for VNode {
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use futures::future::{FutureExt, LocalBoxFuture};
|
||||
|
||||
use super::*;
|
||||
use crate::html::AnyScope;
|
||||
use futures::future::{FutureExt, LocalBoxFuture};
|
||||
|
||||
impl VNode {
|
||||
// Boxing is needed here, due to: https://rust-lang.github.io/async-book/07_workarounds/04_recursion.html
|
||||
@ -335,36 +183,3 @@ mod feat_ssr {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
use super::*;
|
||||
use crate::tests::layout_tests::{diff_layouts, TestLayout};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
fn diff() {
|
||||
let document = gloo_utils::document();
|
||||
let vref_node_1 = VNode::VRef(document.create_element("i").unwrap().into());
|
||||
let vref_node_2 = VNode::VRef(document.create_element("b").unwrap().into());
|
||||
|
||||
let layout1 = TestLayout {
|
||||
name: "1",
|
||||
node: vref_node_1,
|
||||
expected: "<i></i>",
|
||||
};
|
||||
|
||||
let layout2 = TestLayout {
|
||||
name: "2",
|
||||
node: vref_node_2,
|
||||
expected: "<b></b>",
|
||||
};
|
||||
|
||||
diff_layouts(vec![layout1, layout2]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,74 +1,17 @@
|
||||
//! This module contains the implementation of a portal `VPortal`.
|
||||
|
||||
use super::{VDiff, VNode};
|
||||
use crate::html::{AnyScope, NodeRef};
|
||||
use super::VNode;
|
||||
use crate::html::NodeRef;
|
||||
use web_sys::{Element, Node};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VPortal {
|
||||
/// The element under which the content is inserted.
|
||||
pub host: Element,
|
||||
/// The next sibling after the inserted content
|
||||
pub next_sibling: NodeRef,
|
||||
/// The next sibling after the inserted content. Most be a child of `host`.
|
||||
pub inner_sibling: NodeRef,
|
||||
/// The inserted node
|
||||
pub node: Box<VNode>,
|
||||
/// The next sibling after the portal. Set when rendered
|
||||
sibling_ref: NodeRef,
|
||||
}
|
||||
|
||||
impl VDiff for VPortal {
|
||||
fn detach(&mut self, _: &Element, _parent_to_detach: bool) {
|
||||
self.node.detach(&self.host, false);
|
||||
self.sibling_ref.set(None);
|
||||
}
|
||||
|
||||
fn shift(&self, _previous_parent: &Element, _next_parent: &Element, _next_sibling: NodeRef) {
|
||||
// portals have nothing in it's original place of DOM, we also do nothing.
|
||||
}
|
||||
|
||||
fn apply(
|
||||
&mut self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
ancestor: Option<VNode>,
|
||||
) -> NodeRef {
|
||||
let inner_ancestor = match ancestor {
|
||||
Some(VNode::VPortal(old_portal)) => {
|
||||
let VPortal {
|
||||
host: old_host,
|
||||
next_sibling: old_sibling,
|
||||
mut node,
|
||||
..
|
||||
} = old_portal;
|
||||
if old_host != self.host {
|
||||
// Remount the inner node somewhere else instead of diffing
|
||||
node.detach(&old_host, false);
|
||||
None
|
||||
} else if old_sibling != self.next_sibling {
|
||||
// Move the node, but keep the state
|
||||
node.move_before(&self.host, &self.next_sibling.get());
|
||||
Some(*node)
|
||||
} else {
|
||||
Some(*node)
|
||||
}
|
||||
}
|
||||
Some(mut node) => {
|
||||
node.detach(parent, false);
|
||||
None
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
self.node.apply(
|
||||
parent_scope,
|
||||
&self.host,
|
||||
self.next_sibling.clone(),
|
||||
inner_ancestor,
|
||||
);
|
||||
self.sibling_ref = next_sibling.clone();
|
||||
next_sibling
|
||||
}
|
||||
}
|
||||
|
||||
impl VPortal {
|
||||
@ -76,114 +19,22 @@ impl VPortal {
|
||||
pub fn new(content: VNode, host: Element) -> Self {
|
||||
Self {
|
||||
host,
|
||||
next_sibling: NodeRef::default(),
|
||||
inner_sibling: NodeRef::default(),
|
||||
node: Box::new(content),
|
||||
sibling_ref: NodeRef::default(),
|
||||
}
|
||||
}
|
||||
/// Creates a [VPortal] rendering `content` in the DOM hierarchy under `host`.
|
||||
/// If `next_sibling` is given, the content is inserted before that [Node].
|
||||
/// The parent of `next_sibling`, if given, must be `host`.
|
||||
pub fn new_before(content: VNode, host: Element, next_sibling: Option<Node>) -> Self {
|
||||
pub fn new_before(content: VNode, host: Element, inner_sibling: Option<Node>) -> Self {
|
||||
Self {
|
||||
host,
|
||||
next_sibling: {
|
||||
inner_sibling: {
|
||||
let sib_ref = NodeRef::default();
|
||||
sib_ref.set(next_sibling);
|
||||
sib_ref.set(inner_sibling);
|
||||
sib_ref
|
||||
},
|
||||
node: Box::new(content),
|
||||
sibling_ref: NodeRef::default(),
|
||||
}
|
||||
}
|
||||
/// Returns the [Node] following this [VPortal], if this [VPortal]
|
||||
/// has already been mounted in the DOM.
|
||||
pub fn next_sibling(&self) -> Option<Node> {
|
||||
self.sibling_ref.get()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
extern crate self as yew;
|
||||
|
||||
use crate::html;
|
||||
use crate::tests::layout_tests::{diff_layouts, TestLayout};
|
||||
use crate::virtual_dom::VNode;
|
||||
use yew::virtual_dom::VPortal;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
fn diff() {
|
||||
let mut layouts = vec![];
|
||||
let first_target = gloo_utils::document().create_element("i").unwrap();
|
||||
let second_target = gloo_utils::document().create_element("o").unwrap();
|
||||
let target_with_child = gloo_utils::document().create_element("i").unwrap();
|
||||
let target_child = gloo_utils::document().create_element("s").unwrap();
|
||||
target_with_child.append_child(&target_child).unwrap();
|
||||
|
||||
layouts.push(TestLayout {
|
||||
name: "Portal - first target",
|
||||
node: html! {
|
||||
<div>
|
||||
{VNode::VRef(first_target.clone().into())}
|
||||
{VNode::VRef(second_target.clone().into())}
|
||||
{VNode::VPortal(VPortal::new(
|
||||
html! { {"PORTAL"} },
|
||||
first_target.clone(),
|
||||
))}
|
||||
{"AFTER"}
|
||||
</div>
|
||||
},
|
||||
expected: "<div><i>PORTAL</i><o></o>AFTER</div>",
|
||||
});
|
||||
layouts.push(TestLayout {
|
||||
name: "Portal - second target",
|
||||
node: html! {
|
||||
<div>
|
||||
{VNode::VRef(first_target.clone().into())}
|
||||
{VNode::VRef(second_target.clone().into())}
|
||||
{VNode::VPortal(VPortal::new(
|
||||
html! { {"PORTAL"} },
|
||||
second_target.clone(),
|
||||
))}
|
||||
{"AFTER"}
|
||||
</div>
|
||||
},
|
||||
expected: "<div><i></i><o>PORTAL</o>AFTER</div>",
|
||||
});
|
||||
layouts.push(TestLayout {
|
||||
name: "Portal - replaced by text",
|
||||
node: html! {
|
||||
<div>
|
||||
{VNode::VRef(first_target.clone().into())}
|
||||
{VNode::VRef(second_target.clone().into())}
|
||||
{"FOO"}
|
||||
{"AFTER"}
|
||||
</div>
|
||||
},
|
||||
expected: "<div><i></i><o></o>FOOAFTER</div>",
|
||||
});
|
||||
layouts.push(TestLayout {
|
||||
name: "Portal - next sibling",
|
||||
node: html! {
|
||||
<div>
|
||||
{VNode::VRef(target_with_child.clone().into())}
|
||||
{VNode::VPortal(VPortal::new_before(
|
||||
html! { {"PORTAL"} },
|
||||
target_with_child.clone(),
|
||||
Some(target_child.clone().into()),
|
||||
))}
|
||||
</div>
|
||||
},
|
||||
expected: "<div><i>PORTAL<s></s></i></div>",
|
||||
});
|
||||
|
||||
diff_layouts(layouts)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +1,17 @@
|
||||
use super::{Key, VDiff, VNode};
|
||||
use crate::html::{AnyScope, NodeRef};
|
||||
use web_sys::{Element, Node};
|
||||
use super::{Key, VNode};
|
||||
use web_sys::Element;
|
||||
|
||||
/// This struct represents a suspendable DOM fragment.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VSuspense {
|
||||
/// Child nodes.
|
||||
children: Box<VNode>,
|
||||
|
||||
pub(crate) children: Box<VNode>,
|
||||
/// Fallback nodes when suspended.
|
||||
fallback: Box<VNode>,
|
||||
|
||||
pub(crate) fallback: Box<VNode>,
|
||||
/// The element to attach to when children is not attached to DOM
|
||||
detached_parent: Option<Element>,
|
||||
|
||||
pub(crate) detached_parent: Option<Element>,
|
||||
/// Whether the current status is suspended.
|
||||
suspended: bool,
|
||||
|
||||
pub(crate) suspended: bool,
|
||||
/// The Key.
|
||||
pub(crate) key: Option<Key>,
|
||||
}
|
||||
@ -37,122 +32,12 @@ impl VSuspense {
|
||||
key,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn first_node(&self) -> Option<Node> {
|
||||
if self.suspended {
|
||||
self.fallback.first_node()
|
||||
} else {
|
||||
self.children.first_node()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VDiff for VSuspense {
|
||||
fn detach(&mut self, parent: &Element, parent_to_detach: bool) {
|
||||
if self.suspended {
|
||||
self.fallback.detach(parent, parent_to_detach);
|
||||
if let Some(ref m) = self.detached_parent {
|
||||
self.children.detach(m, false);
|
||||
}
|
||||
} else {
|
||||
self.children.detach(parent, parent_to_detach);
|
||||
}
|
||||
}
|
||||
|
||||
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
|
||||
if self.suspended {
|
||||
self.fallback
|
||||
.shift(previous_parent, next_parent, next_sibling);
|
||||
} else {
|
||||
self.children
|
||||
.shift(previous_parent, next_parent, next_sibling);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(
|
||||
&mut self,
|
||||
parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
ancestor: Option<VNode>,
|
||||
) -> NodeRef {
|
||||
let detached_parent = self.detached_parent.as_ref().expect("no detached parent?");
|
||||
|
||||
let (already_suspended, children_ancestor, fallback_ancestor) = match ancestor {
|
||||
Some(VNode::VSuspense(mut m)) => {
|
||||
// We only preserve the child state if they are the same suspense.
|
||||
if m.key != self.key || self.detached_parent != m.detached_parent {
|
||||
m.detach(parent, false);
|
||||
|
||||
(false, None, None)
|
||||
} else {
|
||||
(m.suspended, Some(*m.children), Some(*m.fallback))
|
||||
}
|
||||
}
|
||||
Some(mut m) => {
|
||||
m.detach(parent, false);
|
||||
(false, None, None)
|
||||
}
|
||||
None => (false, None, None),
|
||||
};
|
||||
|
||||
// When it's suspended, we render children into an element that is detached from the dom
|
||||
// tree while rendering fallback UI into the original place where children resides in.
|
||||
match (self.suspended, already_suspended) {
|
||||
(true, true) => {
|
||||
self.children.apply(
|
||||
parent_scope,
|
||||
detached_parent,
|
||||
NodeRef::default(),
|
||||
children_ancestor,
|
||||
);
|
||||
|
||||
self.fallback
|
||||
.apply(parent_scope, parent, next_sibling, fallback_ancestor)
|
||||
}
|
||||
|
||||
(false, false) => {
|
||||
self.children
|
||||
.apply(parent_scope, parent, next_sibling, children_ancestor)
|
||||
}
|
||||
|
||||
(true, false) => {
|
||||
children_ancestor.as_ref().unwrap().shift(
|
||||
parent,
|
||||
detached_parent,
|
||||
NodeRef::default(),
|
||||
);
|
||||
|
||||
self.children.apply(
|
||||
parent_scope,
|
||||
detached_parent,
|
||||
NodeRef::default(),
|
||||
children_ancestor,
|
||||
);
|
||||
|
||||
// first render of fallback, ancestor needs to be None.
|
||||
self.fallback
|
||||
.apply(parent_scope, parent, next_sibling, None)
|
||||
}
|
||||
|
||||
(false, true) => {
|
||||
fallback_ancestor.unwrap().detach(parent, false);
|
||||
|
||||
children_ancestor.as_ref().unwrap().shift(
|
||||
detached_parent,
|
||||
parent,
|
||||
next_sibling.clone(),
|
||||
);
|
||||
self.children
|
||||
.apply(parent_scope, parent, next_sibling, children_ancestor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod feat_ssr {
|
||||
use super::*;
|
||||
use crate::html::AnyScope;
|
||||
|
||||
impl VSuspense {
|
||||
pub(crate) async fn render_to_string(&self, w: &mut String, parent_scope: &AnyScope) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,7 @@
|
||||
//! This module contains the implementation of a virtual text node `VText`.
|
||||
|
||||
use super::{AttrValue, VDiff, VNode};
|
||||
use crate::html::{AnyScope, NodeRef};
|
||||
use gloo::console;
|
||||
use gloo_utils::document;
|
||||
use super::AttrValue;
|
||||
use std::cmp::PartialEq;
|
||||
use web_sys::{Element, Text as TextNode};
|
||||
|
||||
/// A type for a virtual
|
||||
/// [`TextNode`](https://developer.mozilla.org/en-US/docs/Web/API/Document/createTextNode)
|
||||
@ -14,17 +10,24 @@ use web_sys::{Element, Text as TextNode};
|
||||
pub struct VText {
|
||||
/// Contains a text of the node.
|
||||
pub text: AttrValue,
|
||||
/// A reference to the `TextNode`.
|
||||
pub reference: Option<TextNode>,
|
||||
}
|
||||
|
||||
impl VText {
|
||||
/// Creates new virtual text node with a content.
|
||||
pub fn new(text: impl Into<AttrValue>) -> Self {
|
||||
VText {
|
||||
text: text.into(),
|
||||
reference: None,
|
||||
}
|
||||
VText { text: text.into() }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for VText {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "VText {{ text: \"{}\" }}", self.text)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for VText {
|
||||
fn eq(&self, other: &VText) -> bool {
|
||||
self.text == other.text
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,163 +42,6 @@ mod feat_ssr {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for VText {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"VText {{ text: \"{}\", reference: {} }}",
|
||||
self.text,
|
||||
match &self.reference {
|
||||
Some(_) => "Some(...)",
|
||||
None => "None",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl VDiff for VText {
|
||||
/// Remove VText from parent.
|
||||
fn detach(&mut self, parent: &Element, parent_to_detach: bool) {
|
||||
let node = self
|
||||
.reference
|
||||
.take()
|
||||
.expect("tried to remove not rendered VText from DOM");
|
||||
if !parent_to_detach {
|
||||
let result = parent.remove_child(&node);
|
||||
|
||||
if result.is_err() {
|
||||
console::warn!("Node not found to remove VText");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shift(&self, previous_parent: &Element, next_parent: &Element, next_sibling: NodeRef) {
|
||||
let node = self
|
||||
.reference
|
||||
.as_ref()
|
||||
.expect("tried to shift not rendered VTag from DOM");
|
||||
|
||||
previous_parent.remove_child(node).unwrap();
|
||||
next_parent
|
||||
.insert_before(node, next_sibling.get().as_ref())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Renders virtual node over existing `TextNode`, but only if value of text has changed.
|
||||
fn apply(
|
||||
&mut self,
|
||||
_parent_scope: &AnyScope,
|
||||
parent: &Element,
|
||||
next_sibling: NodeRef,
|
||||
ancestor: Option<VNode>,
|
||||
) -> NodeRef {
|
||||
if let Some(mut ancestor) = ancestor {
|
||||
if let VNode::VText(mut vtext) = ancestor {
|
||||
self.reference = vtext.reference.take();
|
||||
let text_node = self
|
||||
.reference
|
||||
.clone()
|
||||
.expect("Rendered VText nodes should have a ref");
|
||||
if self.text != vtext.text {
|
||||
text_node.set_node_value(Some(&self.text));
|
||||
}
|
||||
|
||||
return NodeRef::new(text_node.into());
|
||||
}
|
||||
|
||||
ancestor.detach(parent, false);
|
||||
}
|
||||
|
||||
let text_node = document().create_text_node(&self.text);
|
||||
super::insert_node(&text_node, parent, next_sibling.get().as_ref());
|
||||
self.reference = Some(text_node.clone());
|
||||
NodeRef::new(text_node.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for VText {
|
||||
fn eq(&self, other: &VText) -> bool {
|
||||
self.text == other.text
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
extern crate self as yew;
|
||||
|
||||
use crate::html;
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
fn text_as_root() {
|
||||
html! {
|
||||
"Text Node As Root"
|
||||
};
|
||||
|
||||
html! {
|
||||
{ "Text Node As Root" }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod layout_tests {
|
||||
extern crate self as yew;
|
||||
|
||||
use crate::html;
|
||||
use crate::tests::layout_tests::{diff_layouts, TestLayout};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
|
||||
#[cfg(feature = "wasm_test")]
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[test]
|
||||
fn diff() {
|
||||
let layout1 = TestLayout {
|
||||
name: "1",
|
||||
node: html! { "a" },
|
||||
expected: "a",
|
||||
};
|
||||
|
||||
let layout2 = TestLayout {
|
||||
name: "2",
|
||||
node: html! { "b" },
|
||||
expected: "b",
|
||||
};
|
||||
|
||||
let layout3 = TestLayout {
|
||||
name: "3",
|
||||
node: html! {
|
||||
<>
|
||||
{"a"}
|
||||
{"b"}
|
||||
</>
|
||||
},
|
||||
expected: "ab",
|
||||
};
|
||||
|
||||
let layout4 = TestLayout {
|
||||
name: "4",
|
||||
node: html! {
|
||||
<>
|
||||
{"b"}
|
||||
{"a"}
|
||||
</>
|
||||
},
|
||||
expected: "ba",
|
||||
};
|
||||
|
||||
diff_layouts(vec![layout1, layout2, layout3, layout4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32"), feature = "ssr"))]
|
||||
mod ssr_tests {
|
||||
use tokio::test;
|
||||
|
||||
5
tools/benchmark-hooks/Makefile.toml
Normal file
5
tools/benchmark-hooks/Makefile.toml
Normal file
@ -0,0 +1,5 @@
|
||||
# Extends the root Makefile.toml
|
||||
|
||||
[tasks.doc-test]
|
||||
command = "echo"
|
||||
args = ["No doctests run for benchmark-hooks"]
|
||||
5
tools/benchmark-struct/Makefile.toml
Normal file
5
tools/benchmark-struct/Makefile.toml
Normal file
@ -0,0 +1,5 @@
|
||||
# Extends the root Makefile.toml
|
||||
|
||||
[tasks.doc-test]
|
||||
command = "echo"
|
||||
args = ["No doctests run for benchmark-struct"]
|
||||
5
tools/process-benchmark-results/Makefile.toml
Normal file
5
tools/process-benchmark-results/Makefile.toml
Normal file
@ -0,0 +1,5 @@
|
||||
# Extends the root Makefile.toml
|
||||
|
||||
[tasks.doc-test]
|
||||
command = "echo"
|
||||
args = ["No doctests run for process-benchmark-results"]
|
||||
Loading…
x
Reference in New Issue
Block a user