diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index a8bd40489af..59b0aa75cf2 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -113,7 +113,7 @@ jobs: strategy: matrix: toolchain: - - 1.45.0 # MSRV + - 1.49.0 # MSRV - stable steps: diff --git a/Makefile.toml b/Makefile.toml index 64f301a617d..898ef912a76 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -36,7 +36,7 @@ run_task = { name = ["lint-flow"], fork = true } category = "Testing" description = "Run all tests" dependencies = ["tests-setup"] -env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*"] } +env = { CARGO_MAKE_WORKSPACE_SKIP_MEMBERS = ["**/examples/*", "**/packages/changelog"] } run_task = { name = ["test-flow", "doc-test-flow"], fork = true } [tasks.benchmarks] diff --git a/README.md b/README.md index 1066cd67d4e..2badcc9184c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ API Docs Discord Chat - Rustc Version 1.45+ + Rustc Version 1.49.0+

diff --git a/examples/futures/src/markdown.rs b/examples/futures/src/markdown.rs index 1cdfa60f289..307c8702069 100644 --- a/examples/futures/src/markdown.rs +++ b/examples/futures/src/markdown.rs @@ -53,9 +53,20 @@ pub fn render_markdown(src: &str) -> Html { pre.add_child(top.into()); top = pre; } else if let Tag::Table(aligns) = tag { - for r in top.children.iter_mut() { + for r in top + .children_mut() + .iter_mut() + .map(|ch| ch.iter_mut()) + .flatten() + { if let VNode::VTag(ref mut vtag) = r { - for (i, c) in vtag.children.iter_mut().enumerate() { + for (i, c) in vtag + .children_mut() + .iter_mut() + .map(|ch| ch.iter_mut()) + .flatten() + .enumerate() + { if let VNode::VTag(ref mut vtag) = c { match aligns[i] { Alignment::None => {} @@ -68,7 +79,12 @@ pub fn render_markdown(src: &str) -> Html { } } } else if let Tag::TableHead = tag { - for c in top.children.iter_mut() { + for c in top + .children_mut() + .iter_mut() + .map(|ch| ch.iter_mut()) + .flatten() + { if let VNode::VTag(ref mut vtag) = c { // TODO // vtag.tag = "th".into(); diff --git a/packages/yew-macro/src/html_tree/html_element.rs b/packages/yew-macro/src/html_tree/html_element.rs index 70a05c8835a..3770de65111 100644 --- a/packages/yew-macro/src/html_tree/html_element.rs +++ b/packages/yew-macro/src/html_tree/html_element.rs @@ -97,30 +97,10 @@ impl ToTokens for HtmlElement { children, } = self; - let name_sr = match &name { - TagName::Lit(name) => name.stringify(), - TagName::Expr(name) => { - let expr = &name.expr; - let vtag_name = Ident::new("__yew_vtag_name", expr.span()); - // this way we get a nice error message (with the correct span) when the expression doesn't return a valid value - quote_spanned! {expr.span()=> { - #[allow(unused_braces)] - let mut #vtag_name = ::std::convert::Into::<::std::borrow::Cow::<'static, str>>::into(#expr); - if !#vtag_name.is_ascii() { - ::std::panic!("a dynamic tag returned a tag name containing non ASCII characters: `{}`", #vtag_name); - }; - // convert to lowercase because the runtime checks rely on it. - #vtag_name.to_mut().make_ascii_lowercase(); - #vtag_name - }} - } - }; - let ElementProps { classes, attributes, booleans, - kind, value, checked, node_ref, @@ -128,44 +108,44 @@ impl ToTokens for HtmlElement { listeners, } = &props; - let vtag = Ident::new("__yew_vtag", name.span()); - // attributes with special treatment - let set_node_ref = node_ref.as_ref().map(|attr| { - let value = &attr.value; - quote! { - #vtag.node_ref = #value; - } - }); - let set_key = key.as_ref().map(|attr| { - let value = attr.value.optimize_literals(); - quote! { - #vtag.__macro_set_key(#value); - } - }); - let set_value = value.as_ref().map(|attr| { - let value = attr.value.optimize_literals(); - quote! { - #vtag.set_value(#value); - } - }); - let set_kind = kind.as_ref().map(|attr| { - let value = attr.value.optimize_literals(); - quote! { - #vtag.set_kind(#value); - } - }); - let set_checked = checked.as_ref().map(|attr| { - let value = &attr.value; - quote! { - #vtag.set_checked(#value); - } - }); + let node_ref = node_ref + .as_ref() + .map(|attr| { + let value = &attr.value; + quote_spanned! {value.span()=> + ::yew::html::IntoPropValue::<::yew::html::NodeRef> + ::into_prop_value(#value) + } + }) + .unwrap_or(quote! { ::std::default::Default::default() }); + let key = key + .as_ref() + .map(|attr| { + let value = attr.value.optimize_literals(); + quote_spanned! {value.span()=> + ::std::option::Option::Some( + ::std::convert::Into::<::yew::virtual_dom::Key>::into(#value) + ) + } + }) + .unwrap_or(quote! { ::std::option::Option::None }); + let value = value + .as_ref() + .map(wrap_attr_prop) + .unwrap_or(quote! { ::std::option::Option::None }); + let checked = checked + .as_ref() + .map(|attr| { + let value = &attr.value; + quote_spanned! {value.span()=> #value} + }) + .unwrap_or(quote! { false }); // other attributes - let set_attributes = { + let attributes = { let normal_attrs = attributes.iter().map(|Prop { label, value, .. }| { let key = label.to_lit_str(); let value = value.optimize_literals(); @@ -224,12 +204,12 @@ impl ToTokens for HtmlElement { let attrs = normal_attrs.chain(boolean_attrs).chain(class_attr); quote! { - #vtag.attributes = ::yew::virtual_dom::Attributes::Vec(::std::vec![#(#attrs),*]); + ::yew::virtual_dom::Attributes::Vec(::std::vec![#(#attrs),*]) } }; - let set_listeners = if listeners.is_empty() { - None + let listeners = if listeners.is_empty() { + quote! { ::std::vec![] } } else { let listeners_it = listeners.iter().map(|Prop { label, value, .. }| { let name = &label.name; @@ -238,75 +218,175 @@ impl ToTokens for HtmlElement { } }); - Some(quote! { - #vtag.__macro_set_listeners(::std::vec![#(#listeners_it),*]); - }) + quote! { ::std::vec![#(#listeners_it),*].into_iter().flatten().collect() } }; - let add_children = if children.is_empty() { - None - } else { - Some(quote! { - #[allow(clippy::redundant_clone, unused_braces)] - #vtag.add_children(#children); - }) + let child_list = quote! { + ::yew::virtual_dom::VList{ + key: ::std::option::Option::None, + children: #children, + } }; - // These are the runtime-checks exclusive to dynamic tags. - // For literal tags this is already done at compile-time. - let dyn_tag_runtime_checks = if matches!(&name, TagName::Expr(_)) { - // when Span::source_file Span::start get stabilised or yew-macro introduces a nightly feature flag - // we should expand the panic message to contain the exact location of the dynamic tag. - Some(quote! { - // check void element - if !#vtag.children.is_empty() { - match #vtag.tag() { - "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" - | "meta" | "param" | "source" | "track" | "wbr" => { - ::std::panic!("a dynamic tag tried to create a `<{0}>` tag with children. `<{0}>` is a void element which can't have any children.", #vtag.tag()); + tokens.extend(match &name { + TagName::Lit(name) => { + let name_span = name.span(); + let name = name.to_ascii_lowercase_string(); + match &*name { + "input" => { + quote_spanned! {name_span=> + #[allow(clippy::redundant_clone, unused_braces)] + ::std::convert::Into::<::yew::virtual_dom::VNode>::into( + ::yew::virtual_dom::VTag::__new_input( + #value, + #checked, + #node_ref, + #key, + #attributes, + #listeners, + ), + ) + } + } + "textarea" => { + quote_spanned! {name_span=> + #[allow(clippy::redundant_clone, unused_braces)] + ::std::convert::Into::<::yew::virtual_dom::VNode>::into( + ::yew::virtual_dom::VTag::__new_textarea( + #value, + #node_ref, + #key, + #attributes, + #listeners, + ), + ) } - _ => {} } - }; - - // handle special attribute value - match #vtag.tag() { - "input" | "textarea" => {} _ => { - let __yew_v = #vtag.value.take(); - #vtag.__macro_push_attr(::yew::virtual_dom::PositionalAttr::new("value", __yew_v)); + quote_spanned! {name_span=> + #[allow(clippy::redundant_clone, unused_braces)] + ::std::convert::Into::<::yew::virtual_dom::VNode>::into( + ::yew::virtual_dom::VTag::__new_other( + ::std::borrow::Cow::<'static, str>::Borrowed(#name), + #node_ref, + #key, + #attributes, + #listeners, + #child_list, + ), + ) + } } } - }) - } else { - None - }; - - tokens.extend(quote_spanned! {name.span()=> - { + } + TagName::Expr(name) => { #[allow(unused_braces)] - let mut #vtag = ::yew::virtual_dom::VTag::new(#name_sr); - - #set_node_ref - #set_key - #set_value - #set_kind - #set_checked - #set_attributes - #set_listeners - - #add_children - - #dyn_tag_runtime_checks - { - use ::std::convert::From; - ::yew::virtual_dom::VNode::from(#vtag) - } + let vtag = Ident::new("__yew_vtag", name.span()); + let expr = &name.expr; + let vtag_name = Ident::new("__yew_vtag_name", expr.span()); + + // handle special attribute value + let handle_value_attr = props.value.as_ref().map(|prop| { + let v = prop.value.optimize_literals(); + quote_spanned! {v.span()=> { + __yew_vtag.__macro_push_attr( + ::yew::virtual_dom::PositionalAttr::new("value", #v), + ); + }} + }); + + // this way we get a nice error message (with the correct span) when the expression + // doesn't return a valid value + quote_spanned! {expr.span()=> { + let mut #vtag_name = ::std::convert::Into::< + ::std::borrow::Cow::<'static, str> + >::into(#expr); + if !#vtag_name.is_ascii() { + ::std::panic!( + "a dynamic tag returned a tag name containing non ASCII characters: `{}`", + #vtag_name, + ); + } + // convert to lowercase because the runtime checks rely on it. + #vtag_name.to_mut().make_ascii_lowercase(); + + #[allow(clippy::redundant_clone, unused_braces, clippy::let_and_return)] + let mut #vtag = match ::std::convert::AsRef::::as_ref(&#vtag_name) { + "input" => { + ::yew::virtual_dom::VTag::__new_textarea( + #value, + #node_ref, + #key, + #attributes, + #listeners, + ) + } + "textarea" => { + ::yew::virtual_dom::VTag::__new_textarea( + #value, + #node_ref, + #key, + #attributes, + #listeners, + ) + } + _ => { + let mut __yew_vtag = ::yew::virtual_dom::VTag::__new_other( + #vtag_name, + #node_ref, + #key, + #attributes, + #listeners, + #child_list, + ); + + #handle_value_attr + + __yew_vtag + } + }; + + // These are the runtime-checks exclusive to dynamic tags. + // For literal tags this is already done at compile-time. + // + // When Span::source_file Span::start get stabilised or yew-macro introduces a + // nightly feature flag we should expand the panic message to contain the exact + // location of the dynamic tag. + // + // check void element + if !#vtag.children().is_empty() { + match #vtag.tag() { + "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" + | "link" | "meta" | "param" | "source" | "track" | "wbr" + => { + ::std::panic!( + "a dynamic tag tried to create a `<{0}>` tag with children. `<{0}>` is a void element which can't have any children.", + #vtag.tag(), + ); + } + _ => {} + } + } + + ::std::convert::Into::<::yew::virtual_dom::VNode>::into(#vtag) + }} } }); } } +fn wrap_attr_prop(prop: &Prop) -> TokenStream { + let value = prop.value.optimize_literals(); + quote_spanned! {value.span()=> + ::yew::html::IntoPropValue::< + ::std::option::Option< + ::yew::virtual_dom::AttrValue + > + > + ::into_prop_value(#value) + } +} + struct DynamicName { at: Token![@], expr: Option, diff --git a/packages/yew-macro/src/props/element.rs b/packages/yew-macro/src/props/element.rs index 8716ced3655..71a4b23c183 100644 --- a/packages/yew-macro/src/props/element.rs +++ b/packages/yew-macro/src/props/element.rs @@ -23,7 +23,6 @@ pub struct ElementProps { pub classes: Option, pub booleans: Vec, pub value: Option, - pub kind: Option, pub checked: Option, pub node_ref: Option, pub key: Option, @@ -46,7 +45,6 @@ impl Parse for ElementProps { .pop("class") .map(|prop| ClassesForm::from_expr(prop.value)); let value = props.pop("value"); - let kind = props.pop("type"); let checked = props.pop("checked"); let SpecialProps { node_ref, key } = props.special; @@ -58,7 +56,6 @@ impl Parse for ElementProps { checked, booleans: booleans.into_vec(), value, - kind, node_ref, key, }) diff --git a/packages/yew-macro/tests/html_macro/element-fail.stderr b/packages/yew-macro/tests/html_macro/element-fail.stderr index 114656df0c3..101395b88b0 100644 --- a/packages/yew-macro/tests/html_macro/element-fail.stderr +++ b/packages/yew-macro/tests/html_macro/element-fail.stderr @@ -185,12 +185,19 @@ error[E0277]: the trait bound `(): IntoPropValue>>` is | 43 | html! { }; | ^^ the trait `IntoPropValue>>` is not implemented for `()` + | + ::: $WORKSPACE/packages/yew/src/virtual_dom/mod.rs + | + | pub fn new(key: &'static str, value: impl IntoPropValue>) -> Self { + | -------------------------------- required by this bound in `PositionalAttr::new` error[E0277]: the trait bound `(): IntoPropValue>>` is not satisfied --> $DIR/element-fail.rs:44:26 | 44 | html! { }; | ^^ the trait `IntoPropValue>>` is not implemented for `()` + | + = note: required by `into_prop_value` error[E0277]: the trait bound `(): IntoPropValue>>` is not satisfied --> $DIR/element-fail.rs:45:21 @@ -309,20 +316,25 @@ error[E0277]: the trait bound `Option<{integer}>: IntoPropValue as IntoPropValue>> as IntoPropValue>>> -error[E0308]: mismatched types +error[E0277]: the trait bound `(): IntoPropValue` is not satisfied --> $DIR/element-fail.rs:56:24 | 56 | html! { }; - | ^^ expected struct `yew::NodeRef`, found `()` + | ^^ the trait `IntoPropValue` is not implemented for `()` + | + = note: required by `into_prop_value` -error[E0308]: mismatched types +error[E0277]: the trait bound `Option: IntoPropValue` is not satisfied --> $DIR/element-fail.rs:57:24 | 57 | html! { }; - | ^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `yew::NodeRef`, found enum `Option` + | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoPropValue` is not implemented for `Option` | - = note: expected struct `yew::NodeRef` - found enum `Option` + = help: the following implementations were found: + as IntoPropValue>>> + as IntoPropValue>> + as IntoPropValue>>> + = note: required by `into_prop_value` error[E0277]: the trait bound `Cow<'static, str>: From<{integer}>` is not satisfied --> $DIR/element-fail.rs:71:15 diff --git a/packages/yew/src/html/conversion.rs b/packages/yew/src/html/conversion.rs index d0b66ca6a6f..bf373757498 100644 --- a/packages/yew/src/html/conversion.rs +++ b/packages/yew/src/html/conversion.rs @@ -21,6 +21,7 @@ pub trait IntoPropValue { } impl IntoPropValue for T { + #[inline] fn into_prop_value(self) -> T { self } @@ -29,12 +30,14 @@ impl IntoPropValue for &T where T: ImplicitClone, { + #[inline] fn into_prop_value(self) -> T { self.clone() } } impl IntoPropValue> for T { + #[inline] fn into_prop_value(self) -> Option { Some(self) } @@ -43,6 +46,7 @@ impl IntoPropValue> for &T where T: ImplicitClone, { + #[inline] fn into_prop_value(self) -> Option { Some(self.clone()) } @@ -52,6 +56,7 @@ macro_rules! impl_into_prop { (|$value:ident: $from_ty:ty| -> $to_ty:ty { $conversion:expr }) => { // implement V -> T impl IntoPropValue<$to_ty> for $from_ty { + #[inline] fn into_prop_value(self) -> $to_ty { let $value = self; $conversion @@ -59,6 +64,7 @@ macro_rules! impl_into_prop { } // implement V -> Option impl IntoPropValue> for $from_ty { + #[inline] fn into_prop_value(self) -> Option<$to_ty> { let $value = self; Some({ $conversion }) @@ -66,6 +72,7 @@ macro_rules! impl_into_prop { } // implement Option -> Option impl IntoPropValue> for Option<$from_ty> { + #[inline] fn into_prop_value(self) -> Option<$to_ty> { self.map(IntoPropValue::into_prop_value) } diff --git a/packages/yew/src/html/listener/macros.rs b/packages/yew/src/html/listener/macros.rs index 598f3f1bd6c..bc8d094f0eb 100644 --- a/packages/yew/src/html/listener/macros.rs +++ b/packages/yew/src/html/listener/macros.rs @@ -26,6 +26,7 @@ macro_rules! impl_action { } #[doc(hidden)] + #[inline] pub fn __macro_new(callback: impl IntoPropValue>>) -> Option> { let callback = callback.into_prop_value()?; Some(Rc::new(Self::new(callback))) diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index 5f05bdf9669..d9c134ed52c 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -16,7 +16,7 @@ pub mod vtext; use crate::html::{AnyScope, IntoPropValue, NodeRef}; use gloo::events::EventListener; use indexmap::IndexMap; -use std::{borrow::Cow, collections::HashMap, fmt, hint::unreachable_unchecked, iter, mem, rc::Rc}; +use std::{borrow::Cow, collections::HashMap, fmt, hint::unreachable_unchecked, iter, mem}; use web_sys::{Element, Node}; #[doc(inline)] @@ -47,23 +47,34 @@ impl fmt::Debug for dyn Listener { } } -/// A list of event listeners. -type Listeners = Vec>; - /// Attribute value pub type AttrValue = Cow<'static, str>; +/// 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); +} + /// Key-value tuple which makes up an item of the [`Attributes::Vec`] variant. #[derive(Clone, Debug, Eq, PartialEq)] pub struct PositionalAttr(pub &'static str, pub Option); impl PositionalAttr { /// Create a positional attribute + #[inline] pub fn new(key: &'static str, value: impl IntoPropValue>) -> Self { Self(key, value.into_prop_value()) } /// Create a boolean attribute. /// `present` controls whether the attribute is added + #[inline] pub fn new_boolean(key: &'static str, present: bool) -> Self { let value = if present { Some(Cow::Borrowed(key)) @@ -74,15 +85,18 @@ impl PositionalAttr { } /// Create a placeholder for removed attributes + #[inline] pub fn new_placeholder(key: &'static str) -> Self { Self(key, None) } + #[inline] fn transpose(self) -> Option<(&'static str, AttrValue)> { let Self(key, value) = self; value.map(|v| (key, v)) } + #[inline] fn transposed<'a>(&'a self) -> Option<(&'static str, &'a AttrValue)> { let Self(key, value) = self; value.as_ref().map(|v| (*key, v)) @@ -298,6 +312,46 @@ impl Attributes { } } } + + fn set_attribute(el: &Element, key: &str, value: &str) { + el.set_attribute(&key, &value) + .expect("invalid attribute key") + } +} + +impl Apply for Attributes { + type Element = Element; + + fn apply(&mut self, el: &Element) { + match self { + Self::Vec(v) => { + for attr in v.iter() { + if let Some(v) = &attr.1 { + Self::set_attribute(el, &attr.0, 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) { + for change in Self::diff(self, &ancestor) { + match change { + Patch::Add(key, value) | Patch::Replace(key, value) => { + Self::set_attribute(el, key, value); + } + Patch::Remove(key) => { + el.remove_attribute(&key) + .expect("could not remove attribute"); + } + } + } + } } impl From> for Attributes { diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 058448c007e..0012fee1a38 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -39,8 +39,7 @@ impl VNode { pub(crate) fn first_node(&self) -> Node { match self { VNode::VTag(vtag) => vtag - .reference - .as_ref() + .reference() .expect("VTag is not mounted") .clone() .into(), @@ -133,24 +132,28 @@ impl Default for VNode { } impl From for VNode { + #[inline] fn from(vtext: VText) -> Self { VNode::VText(vtext) } } impl From for VNode { + #[inline] fn from(vlist: VList) -> Self { VNode::VList(vlist) } } impl From for VNode { + #[inline] fn from(vtag: VTag) -> Self { VNode::VTag(Box::new(vtag)) } } impl From for VNode { + #[inline] fn from(vcomp: VComp) -> Self { VNode::VComp(vcomp) } @@ -173,11 +176,10 @@ impl From for VNode { impl> FromIterator for VNode { fn from_iter>(iter: T) -> Self { - let vlist = iter.into_iter().fold(VList::default(), |mut acc, x| { - acc.add_child(x.into()); - acc - }); - VNode::VList(vlist) + VNode::VList(VList { + key: None, + children: iter.into_iter().map(|n| n.into()).collect(), + }) } } diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index e0165c8ea5f..30a99a38857 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -1,21 +1,20 @@ -//! This module contains the implementation of a virtual element node `VTag`. +//! This module contains the implementation of a virtual element node [VTag]. -use super::{ - AttrValue, Attributes, Key, Listener, Listeners, Patch, PositionalAttr, VDiff, VList, VNode, -}; +use super::{Apply, AttrValue, Attributes, Key, Listener, PositionalAttr, VDiff, VList, VNode}; use crate::html::{AnyScope, IntoPropValue, NodeRef}; use crate::utils::document; use gloo::events::EventListener; use log::warn; use std::borrow::Cow; use std::cmp::PartialEq; +use std::hint::unreachable_unchecked; +use std::marker::PhantomData; +use std::mem::MaybeUninit; use std::ops::Deref; use std::rc::Rc; +use std::sync::Once; use wasm_bindgen::JsCast; -use web_sys::{ - Element, HtmlButtonElement, HtmlInputElement as InputElement, - HtmlTextAreaElement as TextAreaElement, -}; +use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; /// SVG namespace string used for creating svg elements pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; @@ -23,60 +22,209 @@ pub const SVG_NAMESPACE: &str = "http://www.w3.org/2000/svg"; /// Default namespace for html elements pub const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; -/// Used to improve performance of runtime element checks -#[derive(Clone, Copy, Debug, PartialEq)] -enum ElementType { - Input, - Textarea, - Button, - Other, +// Value field corresponding to an [Element]'s `value` property +#[derive(Clone, Debug, Eq, PartialEq)] +struct Value(Option, PhantomData); + +impl Default for Value { + fn default() -> Self { + Value(None, PhantomData) + } } -impl ElementType { - fn from_tag(tag: &str) -> Self { - match tag.to_ascii_lowercase().as_str() { - "input" => Self::Input, - "textarea" => Self::Textarea, - "button" => Self::Button, - _ => Self::Other, +impl Apply for Value { + type Element = T; + + fn apply(&mut self, el: &Self::Element) { + if let Some(v) = &self.0 { + el.set_value(v); + } + } + + fn apply_diff(&mut self, el: &Self::Element, ancestor: Self) { + match (&self.0, &ancestor.0) { + (Some(new), Some(_)) => { + // Refresh value from the DOM. It might have changed. + if new != &el.value() { + el.set_value(new); + } + } + (Some(new), None) => el.set_value(new), + (None, Some(_)) => el.set_value(""), + (None, None) => (), } } } -/// A type for a virtual -/// [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) -/// representation. -#[derive(Debug)] -pub struct VTag { - /// A tag of the element. - tag: Cow<'static, str>, - /// Type of element. - element_type: ElementType, - /// A reference to the DOM `Element`. - pub reference: Option, - /// List of attached listeners. - pub listeners: Listeners, - /// List of attributes. - pub attributes: Attributes, - /// List of children nodes - pub children: VList, +/// Able to have its value read or set +trait AccessValue { + fn value(&self) -> String; + fn set_value(&self, v: &str); +} + +macro_rules! impl_access_value { + ($( $type:ty )*) => { + $( + impl AccessValue for $type { + #[inline] + fn value(&self) -> String { + <$type>::value(&self) + } + + #[inline] + fn set_value(&self, v: &str) { + <$type>::set_value(&self, v) + } + } + )* + }; +} +impl_access_value! {InputElement TextAreaElement} + +/// Fields specific to +/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag]s +#[derive(Debug, Clone, Default, Eq, PartialEq)] +struct InputFields { /// Contains a value of an /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). - pub value: Option, - /// Contains - /// [kind](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types) - /// value of an `InputElement`. - pub kind: Option, + value: Value, + /// Represents `checked` attribute of /// [input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-checked). /// It exists to override standard behavior of `checked` attribute, because /// in original HTML it sets `defaultChecked` value of `InputElement`, but for reactive /// frameworks it's more useful to control `checked` value of an `InputElement`. - pub checked: bool, + checked: bool, +} + +impl Apply for InputFields { + type Element = InputElement; + + fn apply(&mut self, el: &Self::Element) { + // IMPORTANT! This parameter has to be set every time + // to prevent strange behaviour in the browser when the DOM changes + el.set_checked(self.checked); + + self.value.apply(el); + } + + fn apply_diff(&mut self, el: &Self::Element, ancestor: Self) { + // IMPORTANT! This parameter has to be set every time + // to prevent strange behaviour in the browser when the DOM changes + el.set_checked(self.checked); + + self.value.apply_diff(el, ancestor.value); + } +} + +/// [VTag] fields that are specific to different [VTag] kinds. +/// Decreases the memory footprint of [VTag] by avoiding impossible field and value combinations. +#[derive(Debug, Clone)] +enum VTagInner { + /// Fields specific to + /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) + /// [VTag]s + Input(InputFields), + + /// Fields specific to + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + /// [VTag]s + Textarea { + /// Contains a value of an + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + value: Value, + }, + + /// Fields for all other kinds of [VTag]s + Other { + /// A tag of the element. + tag: Cow<'static, str>, + + /// List of child nodes + children: VList, + }, +} + +/// A list of event listeners, either registered or pending registration +/// TODO(#943): Compare references of handler to do listeners update better +#[derive(Debug)] +enum Listeners { + /// Listeners pending registration + Pending(Vec>), + + /// Already registered listeners. + /// Keeps handlers for attached listeners to have an opportunity to drop them later + Registered(Vec), +} + +impl Apply for Listeners { + type Element = Element; + + fn apply(&mut self, el: &Self::Element) { + if let Self::Pending(v) = self { + *self = Self::Registered( + std::mem::take(v) + .into_iter() + .map(|l| l.attach(&el)) + .collect(), + ); + } + } + + fn apply_diff(&mut self, el: &Self::Element, _ancestor: Self) { + // All we need to do with `_ancestor` is drop it + + self.apply(el); + } +} + +impl PartialEq for Listeners { + fn eq(&self, other: &Self) -> bool { + use Listeners::*; + + match (self, other) { + (Pending(s), Pending(o)) => { + s.len() == o.len() && s.iter().map(|l| l.kind()).eq(o.iter().map(|l| l.kind())) + } + _ => false, + } + } +} + +impl Clone for Listeners { + fn clone(&self) -> Self { + match self { + Self::Pending(v) => Self::Pending(v.clone()), + Self::Registered(_) => Self::Registered(vec![]), + } + } +} + +impl From>> for Listeners { + fn from(v: Vec>) -> Self { + Self::Pending(v) + } +} + +/// A type for a virtual +/// [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) +/// representation. +#[derive(Debug)] +pub struct VTag { + /// [VTag] fields that are specific to different [VTag] kinds. + inner: VTagInner, + + /// List of attached listeners. + listeners: Listeners, + + /// A reference to the DOM `Element`. + reference: Option, + /// A node reference used for DOM access in Component lifecycle methods pub node_ref: NodeRef, - /// Keeps handler for attached listeners to have an opportunity to drop them later. - captured: Vec, + + /// List of attributes. + pub attributes: Attributes, pub key: Option, } @@ -84,94 +232,250 @@ pub struct VTag { impl Clone for VTag { fn clone(&self) -> Self { VTag { - tag: self.tag.clone(), - element_type: self.element_type, + inner: self.inner.clone(), reference: None, listeners: self.listeners.clone(), attributes: self.attributes.clone(), - children: self.children.clone(), - value: self.value.clone(), - kind: self.kind.clone(), - checked: self.checked, node_ref: self.node_ref.clone(), key: self.key.clone(), - captured: Vec::new(), } } } impl VTag { - /// Creates a new `VTag` instance with `tag` name (cannot be changed later in DOM). + /// Creates a new [VTag] instance with `tag` name (cannot be changed later in DOM). pub fn new(tag: impl Into>) -> Self { - let tag = tag.into(); - let element_type = ElementType::from_tag(&tag); + let tag: Cow<'static, str> = tag.into(); + Self::new_base( + match &*tag.to_ascii_lowercase() { + "input" => VTagInner::Input(Default::default()), + "textarea" => VTagInner::Textarea { + value: Default::default(), + }, + _ => VTagInner::Other { + tag, + children: Default::default(), + }, + }, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ) + } + + /// Creates a new + /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) [VTag] + /// instance. + /// + /// Unlike [VTag::new()], this sets all the public fields of [VTag] in one call. This allows the + /// compiler to inline property and child list construction in the `html!` macro. This enables + /// higher instruction parallelism by reducing data dependency and avoids `memcpy` of Vtag + /// fields. + #[doc(hidden)] + #[allow(clippy::too_many_arguments)] + pub fn __new_input( + value: Option, + checked: bool, + node_ref: NodeRef, + key: Option, + // at bottom for more readable macro-expanded coded + attributes: Attributes, + listeners: Vec>, + ) -> Self { + VTag::new_base( + VTagInner::Input(InputFields { + value: Value(value, PhantomData), + // In HTML node `checked` attribute sets `defaultChecked` parameter, + // but we use own field to control real `checked` parameter + checked, + }), + node_ref, + key, + attributes, + listeners, + ) + } + + /// Creates a new + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) [VTag] + /// instance. + /// + /// Unlike [VTag::new()], this sets all the public fields of [VTag] in one call. This allows the + /// compiler to inline property and child list construction in the `html!` macro. This enables + /// higher instruction parallelism by reducing data dependency and avoids `memcpy` of Vtag + /// fields. + #[doc(hidden)] + #[allow(clippy::too_many_arguments)] + pub fn __new_textarea( + value: Option, + node_ref: NodeRef, + key: Option, + // at bottom for more readable macro-expanded coded + attributes: Attributes, + listeners: Vec>, + ) -> Self { + VTag::new_base( + VTagInner::Textarea { + value: Value(value, PhantomData), + }, + node_ref, + key, + attributes, + listeners, + ) + } + + /// Creates a new [VTag] instance with `tag` name (cannot be changed later in DOM). + /// + /// Unlike [VTag::new()], this sets all the public fields of [VTag] in one call. This allows the + /// compiler to inline property and child list construction in the `html!` macro. This enables + /// higher instruction parallelism by reducing data dependency and avoids `memcpy` of Vtag + /// fields. + #[doc(hidden)] + #[allow(clippy::too_many_arguments)] + pub fn __new_other( + tag: Cow<'static, str>, + node_ref: NodeRef, + key: Option, + // at bottom for more readable macro-expanded coded + attributes: Attributes, + listeners: Vec>, + children: VList, + ) -> Self { + VTag::new_base( + VTagInner::Other { tag, children }, + node_ref, + key, + attributes, + listeners, + ) + } + + /// Constructs a [VTag] from [VTagInner] and fields common to all [VTag] kinds + #[inline] + #[allow(clippy::too_many_arguments)] + fn new_base( + inner: VTagInner, + node_ref: NodeRef, + key: Option, + attributes: Attributes, + listeners: Vec>, + ) -> Self { VTag { - tag, - element_type, + inner, reference: None, - attributes: Attributes::new(), - listeners: Vec::new(), - captured: Vec::new(), - children: VList::new(), - node_ref: NodeRef::default(), - key: None, - value: None, - kind: None, - // In HTML node `checked` attribute sets `defaultChecked` parameter, - // but we use own field to control real `checked` parameter - checked: false, + attributes, + listeners: listeners.into(), + node_ref, + key, } } - /// Returns tag of an `Element`. In HTML tags are always uppercase. + /// Returns tag of an [Element]. In HTML tags are always uppercase. pub fn tag(&self) -> &str { - &self.tag + match &self.inner { + VTagInner::Input { .. } => "input", + VTagInner::Textarea { .. } => "textarea", + VTagInner::Other { tag, .. } => tag.as_ref(), + } } - /// Add `VNode` child. + /// Add [VNode] child. pub fn add_child(&mut self, child: VNode) { - self.children.add_child(child); + if let VTagInner::Other { children, .. } = &mut self.inner { + children.add_child(child); + } } - /// Add multiple `VNode` children. + /// Add multiple [VNode] children. pub fn add_children(&mut self, children: impl IntoIterator) { - self.children.add_children(children); + if let VTagInner::Other { children: dst, .. } = &mut self.inner { + dst.add_children(children) + } + } + + /// Returns a reference to the children of this [VTag] + pub fn children(&self) -> &VList { + match &self.inner { + VTagInner::Other { children, .. } => children, + _ => { + static mut EMPTY: MaybeUninit = MaybeUninit::uninit(); + static ONCE: Once = Once::new(); + unsafe { + ONCE.call_once(|| { + EMPTY = MaybeUninit::new(VList::default()); + }); + &*EMPTY.as_ptr() + } + } + } + } + + /// Returns a mutable reference to the children of this [VTag], if the node can have + // children + pub fn children_mut(&mut self) -> Option<&mut VList> { + match &mut self.inner { + VTagInner::Other { children, .. } => Some(children), + _ => None, + } + } + + /// Returns the `value` of an + /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) or + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) + pub fn value(&self) -> Option<&AttrValue> { + match &self.inner { + VTagInner::Input(f) => f.value.0.as_ref(), + VTagInner::Textarea { value } => value.0.as_ref(), + VTagInner::Other { .. } => None, + } } /// Sets `value` for an - /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). + /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) or + /// [TextArea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) pub fn set_value(&mut self, value: impl IntoPropValue>) { - self.value = value.into_prop_value(); + match &mut self.inner { + VTagInner::Input(f) => { + f.value.0 = value.into_prop_value(); + } + VTagInner::Textarea { value: dst } => { + dst.0 = value.into_prop_value(); + } + VTagInner::Other { .. } => (), + } } - /// Sets `kind` property of an + /// Returns `checked` property of an /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). - /// Same as set `type` attribute. - pub fn set_kind(&mut self, value: impl IntoPropValue>) { - self.kind = value.into_prop_value(); - } - - #[doc(hidden)] - pub fn __macro_set_key(&mut self, value: impl Into) { - self.key = Some(value.into()) + /// (Not a value of node's attribute). + pub fn checked(&mut self) -> bool { + match &mut self.inner { + VTagInner::Input(f) => f.checked, + _ => false, + } } /// Sets `checked` property of an /// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input). /// (Not a value of node's attribute). pub fn set_checked(&mut self, value: bool) { - self.checked = value; + if let VTagInner::Input(f) = &mut self.inner { + f.checked = value; + } } - #[doc(hidden)] - pub fn __macro_set_node_ref(&mut self, value: impl IntoPropValue) { - self.node_ref = value.into_prop_value() + /// Returns reference to the [Element] associated with this [VTag], if this [VTag] has already + /// been mounted in the DOM + pub fn reference(&self) -> Option<&Element> { + self.reference.as_ref() } /// Adds a key-value pair to attributes /// /// Not every attribute works when it set as an attribute. We use workarounds for: - /// `type/kind`, `value` and `checked`. + /// `value` and `checked`. pub fn add_attribute(&mut self, key: &'static str, value: impl Into) { self.attributes .get_mut_index_map() @@ -181,7 +485,7 @@ impl VTag { /// Sets attributes to a virtual node. /// /// Not every attribute works when it set as an attribute. We use workarounds for: - /// `type/kind`, `value` and `checked`. + /// `value` and `checked`. pub fn set_attributes(&mut self, attrs: impl Into) { self.attributes = attrs.into(); } @@ -198,175 +502,17 @@ impl VTag { /// It's boxed because we want to keep it in a single list. /// Later `Listener::attach` will attach an actual listener to a DOM node. pub fn add_listener(&mut self, listener: Rc) { - self.listeners.push(listener); + if let Listeners::Pending(v) = &mut self.listeners { + v.push(listener); + } } /// Adds new listeners to the node. /// They are boxed because we want to keep them in a single list. /// Later `Listener::attach` will attach an actual listener to a DOM node. - pub fn add_listeners(&mut self, listeners: Listeners) { - self.listeners.extend(listeners); - } - - #[doc(hidden)] - pub fn __macro_set_listeners( - &mut self, - listeners: impl IntoIterator>>, - ) { - self.listeners = listeners.into_iter().flatten().collect(); - } - - /// Every render it removes all listeners and attach it back later - /// TODO(#943): Compare references of handler to do listeners update better - fn recreate_listeners(&mut self, ancestor: &mut Option>) { - if let Some(ancestor) = ancestor.as_mut() { - ancestor.captured.clear(); - } - - let element = self.reference.clone().expect("element expected"); - - for listener in self.listeners.drain(..) { - let handle = listener.attach(&element); - self.captured.push(handle); - } - } - - fn refresh_value(&mut self) { - // Don't refresh value if the element is not controlled - if self.value.is_none() { - return; - } - - if let Some(element) = self.reference.as_ref() { - if self.element_type == ElementType::Input { - let input_el = element.dyn_ref::(); - if let Some(input) = input_el { - let current_value = input.value(); - self.set_value(Cow::Owned(current_value)) - } - } else if self.element_type == ElementType::Textarea { - let textarea_el = element.dyn_ref::(); - if let Some(tae) = textarea_el { - let value = tae.value(); - self.set_value(Cow::Owned(value)); - } - } - } - } - - /// Compares new kind with ancestor and produces a patch to apply, if any - fn diff_kind<'a>(&'a self, ancestor: &'a Option>) -> Option> { - match ( - self.kind.as_ref(), - ancestor.as_ref().and_then(|anc| anc.kind.as_ref()), - ) { - (Some(ref left), Some(ref right)) => { - if left != right { - Some(Patch::Replace(&**left, ())) - } else { - None - } - } - (Some(ref left), None) => Some(Patch::Add(&**left, ())), - (None, Some(right)) => Some(Patch::Remove(&**right)), - (None, None) => None, - } - } - - /// Compares new value with ancestor and produces a patch to apply, if any - fn diff_value<'a>(&'a self, ancestor: &'a Option>) -> Option> { - match ( - self.value.as_ref(), - ancestor.as_ref().and_then(|anc| anc.value.as_ref()), - ) { - (Some(ref left), Some(ref right)) => { - if left != right { - Some(Patch::Replace(&**left, ())) - } else { - None - } - } - (Some(ref left), None) => Some(Patch::Add(&**left, ())), - (None, Some(right)) => Some(Patch::Remove(&**right)), - (None, None) => None, - } - } - - fn apply_diffs(&mut self, ancestor: &mut Option>) { - let changes = if let Some(old_attributes) = ancestor.as_mut().map(|a| &mut a.attributes) { - Attributes::diff(&self.attributes, old_attributes) - } else { - self.attributes - .iter() - .map(|(k, v)| Patch::Add(k, v)) - .collect() - }; - - let element = self.reference.as_ref().expect("element expected"); - - for change in changes { - match change { - Patch::Add(key, value) | Patch::Replace(key, value) => { - element - .set_attribute(&key, &value) - .expect("invalid attribute key"); - } - Patch::Remove(key) => { - element - .remove_attribute(&key) - .expect("could not remove attribute"); - } - } - } - - if self.element_type == ElementType::Button { - if let Some(button) = element.dyn_ref::() { - if let Some(change) = self.diff_kind(ancestor) { - let kind = match change { - Patch::Add(kind, _) | Patch::Replace(kind, _) => kind, - Patch::Remove(_) => "", - }; - button.set_type(kind); - } - } - } - - // `input` element has extra parameters to control - // I override behavior of attributes to make it more clear - // and useful in templates. For example I interpret `checked` - // attribute as `checked` parameter, not `defaultChecked` as browsers do - if self.element_type == ElementType::Input { - if let Some(input) = element.dyn_ref::() { - if let Some(change) = self.diff_kind(ancestor) { - let kind = match change { - Patch::Add(kind, _) | Patch::Replace(kind, _) => kind, - Patch::Remove(_) => "", - }; - input.set_type(kind) - } - - if let Some(change) = self.diff_value(ancestor) { - let raw_value = match change { - Patch::Add(kind, _) | Patch::Replace(kind, _) => kind, - Patch::Remove(_) => "", - }; - input.set_value(raw_value) - } - - // IMPORTANT! This parameter has to be set every time - // to prevent strange behaviour in the browser when the DOM changes - set_checked(&input, self.checked); - } - } else if self.element_type == ElementType::Textarea { - if let Some(tae) = { element.dyn_ref::() } { - if let Some(change) = self.diff_value(ancestor) { - let value = match change { - Patch::Add(kind, _) | Patch::Replace(kind, _) => kind, - Patch::Remove(_) => "", - }; - tae.set_value(value); - } - } + pub fn add_listeners(&mut self, listeners: Vec>) { + if let Listeners::Pending(v) = &mut self.listeners { + v.extend(listeners); } } @@ -398,14 +544,16 @@ impl VDiff for VTag { .expect("tried to remove not rendered VTag from DOM"); // recursively remove its children - self.children.detach(&node); + if let VTagInner::Other { children, .. } = &mut self.inner { + children.detach(&node); + } if parent.remove_child(&node).is_err() { warn!("Node not found to remove VTag"); } self.node_ref.set(None); } - /// Renders virtual tag over DOM `Element`, but it also compares this with an ancestor `VTag` + /// Renders virtual tag over DOM [Element], but it also compares this with an ancestor [VTag] /// to compute what to patch in the actual DOM nodes. fn apply( &mut self, @@ -414,74 +562,123 @@ impl VDiff for VTag { next_sibling: NodeRef, ancestor: Option, ) -> NodeRef { - let mut ancestor_tag = ancestor.and_then(|mut ancestor| { - match ancestor { + // This kind of branching patching routine reduces branch predictor misses and the need to + // unpack the enums (including `Option`s) all the time, resulting in a more streamlined + // patching flow + let (ancestor_tag, el) = match ancestor { + Some(mut ancestor) => { // If the ancestor is a tag of the same type, don't recreate, keep the // old tag and update its attributes and children. - VNode::VTag(vtag) if self.tag() == vtag.tag() && self.key == vtag.key => Some(vtag), - _ => { - let element = self.create_element(parent); - super::insert_node(&element, parent, Some(ancestor.first_node())); - self.reference = Some(element); + if match &ancestor { + VNode::VTag(a) => { + self.key == a.key + && match (&self.inner, &a.inner) { + (VTagInner::Input(_), VTagInner::Input(_)) + | (VTagInner::Textarea { .. }, VTagInner::Textarea { .. }) => true, + ( + VTagInner::Other { tag: l, .. }, + VTagInner::Other { tag: r, .. }, + ) => l == r, + _ => false, + } + } + _ => false, + } { + match ancestor { + VNode::VTag(mut a) => { + // Preserve the reference that already exists + let el = a.reference.take().unwrap(); + (Some(a), el) + } + _ => unsafe { unreachable_unchecked() }, + } + } else { + let el = self.create_element(parent); + super::insert_node(&el, parent, Some(ancestor.first_node())); ancestor.detach(parent); - None + (None, el) } } - }); - - if let Some(ref mut ancestor_tag) = &mut ancestor_tag { - // Refresh the current value to later compare it against the desired value - // since it may have been changed since we last set it. - ancestor_tag.refresh_value(); - // Preserve the reference that already exists. - self.reference = ancestor_tag.reference.take(); - } else if self.reference.is_none() { - let element = self.create_element(parent); - super::insert_node(&element, parent, next_sibling.get()); - self.reference = Some(element); - } + None => (None, { + let el = self.create_element(parent); + super::insert_node(&el, parent, next_sibling.get()); + el + }), + }; - self.apply_diffs(&mut ancestor_tag); - self.recreate_listeners(&mut ancestor_tag); - - // Process children - let element = self.reference.as_ref().expect("Reference should be set"); - if !self.children.is_empty() { - self.children.apply( - parent_scope, - element, - NodeRef::default(), - ancestor_tag.map(|a| a.children.into()), - ); - } else if let Some(mut ancestor_tag) = ancestor_tag { - ancestor_tag.children.detach(element); - } + match ancestor_tag { + None => { + self.attributes.apply(&el); + self.listeners.apply(&el); + + match &mut self.inner { + VTagInner::Input(f) => { + f.apply(el.unchecked_ref()); + } + VTagInner::Textarea { value } => { + value.apply(el.unchecked_ref()); + } + VTagInner::Other { children, .. } => { + if !children.is_empty() { + children.apply(parent_scope, &el, NodeRef::default(), None); + } + } + } + } + Some(ancestor) => { + self.attributes.apply_diff(&el, ancestor.attributes); + self.listeners.apply_diff(&el, ancestor.listeners); + + match (&mut self.inner, ancestor.inner) { + (VTagInner::Input(new), VTagInner::Input(old)) => { + new.apply_diff(el.unchecked_ref(), old); + } + (VTagInner::Textarea { value: new }, VTagInner::Textarea { value: old }) => { + new.apply_diff(el.unchecked_ref(), old); + } + ( + VTagInner::Other { children: new, .. }, + VTagInner::Other { + children: mut old, .. + }, + ) => { + if !new.is_empty() { + new.apply(parent_scope, &el, NodeRef::default(), Some(old.into())); + } else if !old.is_empty() { + old.detach(&el); + } + } + // Can not happen, because we checked for tag equability above + _ => unsafe { unreachable_unchecked() }, + } + } + }; - let node = element.deref(); - self.node_ref.set(Some(node.clone())); + self.node_ref.set(Some(el.deref().clone())); + self.reference = el.into(); self.node_ref.clone() } } -/// Set `checked` value for the `InputElement`. -fn set_checked(input: &InputElement, value: bool) { - input.set_checked(value); -} - impl PartialEq for VTag { fn eq(&self, other: &VTag) -> bool { - self.tag == other.tag - && self.value == other.value - && self.kind == other.kind - && self.checked == other.checked - && self.listeners.len() == other.listeners.len() - && self - .listeners - .iter() - .map(|l| l.kind()) - .eq(other.listeners.iter().map(|l| l.kind())) + use VTagInner::*; + + (match (&self.inner, &other.inner) { + ( + Input(l), + Input (r), + ) => l == r, + (Textarea { value: value_l }, Textarea { value: value_r }) => value_l == value_r, + (Other { tag: tag_l, .. }, Other { tag: tag_r, .. }) => tag_l == tag_r, + _ => false, + }) && self.listeners.eq(&other.listeners) && self.attributes == other.attributes - && self.children == other.children + // Diff children last, as recursion is the most expensive + && match (&self.inner, &other.inner) { + (Other { children: ch_l, .. }, Other { children: ch_r, .. }) => ch_l == ch_r, + _ => true, + } } } @@ -601,7 +798,14 @@ mod tests { assert_eq!(c, d); } - fn assert_vtag(node: &mut VNode) -> &mut VTag { + fn assert_vtag(node: &VNode) -> &VTag { + if let VNode::VTag(vtag) = node { + return vtag; + } + panic!("should be vtag"); + } + + fn assert_vtag_mut(node: &mut VNode) -> &mut VTag { if let VNode::VTag(vtag) = node { return vtag; } @@ -629,13 +833,13 @@ mod tests { let path_node = html! { }; let mut svg_node = html! { {path_node} }; - let svg_tag = assert_vtag(&mut svg_node); + let svg_tag = assert_vtag_mut(&mut svg_node); svg_tag.apply(&scope, &div_el, NodeRef::default(), None); assert_namespace(svg_tag, SVG_NAMESPACE); - let path_tag = assert_vtag(svg_tag.children.get_mut(0).unwrap()); + let path_tag = assert_vtag(svg_tag.children().get(0).unwrap()); assert_namespace(path_tag, SVG_NAMESPACE); - let g_tag = assert_vtag(&mut g_node); + let g_tag = assert_vtag_mut(&mut g_node); g_tag.apply(&scope, &div_el, NodeRef::default(), None); assert_namespace(g_tag, HTML_NAMESPACE); g_tag.reference = None; @@ -743,7 +947,7 @@ mod tests { let mut elem = html! {
}; elem.apply(&scope, &parent, NodeRef::default(), None); - let vtag = assert_vtag(&mut elem); + let vtag = assert_vtag_mut(&mut elem); // test if the className has not been set assert!(!vtag.reference.as_ref().unwrap().has_attribute("class")); } @@ -757,7 +961,7 @@ mod tests { let mut elem = html! {
}; elem.apply(&scope, &parent, NodeRef::default(), None); - let vtag = assert_vtag(&mut elem); + let vtag = assert_vtag_mut(&mut elem); // test if the className has been set assert!(vtag.reference.as_ref().unwrap().has_attribute("class")); } @@ -787,7 +991,7 @@ mod tests { let ancestor = vtag; let mut elem = html! { }; - let vtag = assert_vtag(&mut elem); + let vtag = assert_vtag_mut(&mut elem); // Sync happens here vtag.apply( @@ -830,7 +1034,7 @@ mod tests { let ancestor = vtag; let mut elem = html! { }; - let vtag = assert_vtag(&mut elem); + let vtag = assert_vtag_mut(&mut elem); // Value should not be refreshed vtag.apply( @@ -864,7 +1068,7 @@ mod tests { }/> }; elem.apply(&scope, &parent, NodeRef::default(), None); - let vtag = assert_vtag(&mut elem); + let vtag = assert_vtag_mut(&mut elem); // make sure the new tag name is used internally assert_eq!(vtag.tag(), "a"); @@ -877,8 +1081,8 @@ mod tests { let mut div_el = html! { <@{"div"} value="Hello"/> }; - let div_vtag = assert_vtag(&mut div_el); - assert!(div_vtag.value.is_none()); + let div_vtag = assert_vtag_mut(&mut div_el); + assert!(div_vtag.value().is_none()); let v: Option<&str> = div_vtag .attributes .iter() @@ -889,8 +1093,8 @@ mod tests { let mut input_el = html! { <@{"input"} value="World"/> }; - let input_vtag = assert_vtag(&mut input_el); - assert_eq!(input_vtag.value, Some(Cow::Borrowed("World"))); + let input_vtag = assert_vtag_mut(&mut input_el); + assert_eq!(input_vtag.value(), Some(&Cow::Borrowed("World"))); assert!(!input_vtag.attributes.iter().any(|(k, _)| k == "value")); } @@ -899,7 +1103,7 @@ mod tests { let mut el = html! { <@{"tExTAREa"}/> }; - let vtag = assert_vtag(&mut el); + let vtag = assert_vtag_mut(&mut el); assert_eq!(vtag.tag(), "textarea"); } @@ -912,7 +1116,7 @@ mod tests { let node_ref = NodeRef::default(); let mut elem: VNode = html! {
}; - assert_vtag(&mut elem); + assert_vtag_mut(&mut elem); elem.apply(&scope, &parent, NodeRef::default(), None); let parent_node = parent.deref(); assert_eq!(node_ref.get(), parent_node.first_child()); diff --git a/website/docs/getting-started/project-setup.md b/website/docs/getting-started/project-setup.md index dd7236e2e22..abd89271fbf 100644 --- a/website/docs/getting-started/project-setup.md +++ b/website/docs/getting-started/project-setup.md @@ -14,7 +14,7 @@ Your local development environment will need a couple of tools to compile, build To install Rust follow the [official instructions](https://www.rust-lang.org/tools/install). :::important -The minimum supported Rust version (MSRV) for Yew is `1.45.0`. Older versions can cause unexpected issues accompanied by incomprehensible error messages. +The minimum supported Rust version (MSRV) for Yew is `1.49.0`. Older versions can cause unexpected issues accompanied by incomprehensible error messages. You can check your toolchain version using `rustup show` (under "active toolchain") or alternatively `rustc --version`. To update your toolchain, run `rustup update`. ::: diff --git a/website/versioned_docs/version-0.18.0/getting-started/project-setup.md b/website/versioned_docs/version-0.18.0/getting-started/project-setup.md index c061430865e..4390b9837e9 100644 --- a/website/versioned_docs/version-0.18.0/getting-started/project-setup.md +++ b/website/versioned_docs/version-0.18.0/getting-started/project-setup.md @@ -12,7 +12,7 @@ You also need to install the `wasm32-unknown-unknown` target to compile Rust to If you're using rustup, you just need to run `rustup target add wasm32-unknown-unknown`. :::important -The minimum supported Rust version (MSRV) for Yew is `1.45.0`. Older versions can cause unexpected issues accompanied by incomprehensible error messages. +The minimum supported Rust version (MSRV) for Yew is `1.49.0`. Older versions can cause unexpected issues accompanied by incomprehensible error messages. You can check your toolchain version using `rustup show` (under "active toolchain") or alternatively `rustc --version`. To update your toolchain, run `rustup update`. :::