Optimise vtag (#1447)

* yew/vtag: remove needless indirection

* yew/vtag: reduce VTag memory footprint

* Revert "yew/vtag: remove needless indirection"

This reverts commit 7c59c61e0e.

* yew/vtag,yew-macro: optimise attribute setting and memory usage

* yew/vtag,yew-macro: reduce string memory footprint and use static strings more

* yew,yew-macro: opportunistically use static memory for VText

* yew/vtag: use String instead of Box<str>

* yew-macro: remove one extra iteration

* yew/vtag: remove API extension for textarea

* yew/vtag: remove extra calls

* yew-macro: preconstruct StringRef for class literals

* yew-macro: construct non-dynamic VTags in-place

* yew-macro: Insert class and href into attrs in-place

* *: run stable checks

* yew/vtag: use key-pair vector for attributes

* yew/macro,yew: use trait associated methods with paths, where possible

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* Update yew/src/virtual_dom/vtag.rs

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>

* yew/vtag: comment clarification

as suggested by @teymour-aldridge

* yew/vtag: fix added test failing

* yew/vlist: revert removing key

* yew/vtag,yew-macro: stash VTagInner changes for later

* yew/vtag: restore diff_attributes() generating a patch list + add bechmarks

* yew: fix becnhmarks running with std_web

* yew: remove Href

* yew/vtag: fix comment changes

* examples: fix trait impl

* yew: swap Stringref with Cow

* examples: remove redundant clone

* ci: fix stable check

* yew/VText: remove needless constructor

* *: remove needless trait full paths

* yew/benchmarks: add IndexMap attribute diff becnhmark

* yew-macro: fix stderr regressions

* yew: convert Attributes to enum

with variants optimised for both the html! macro and the VTag API

* yew/benchmarks: move feature guard

* Update examples/common/src/markdown.rs

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* Update examples/common/src/markdown.rs

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* Update examples/common/src/markdown.rs

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* Update examples/common/src/markdown.rs

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* Update examples/common/src/markdown.rs

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* examples: remove unused import

* Apply suggestions from code review

Co-authored-by: Justin Starry <justin.m.starry@gmail.com>

* yew-macro: rebuild stderr

* yew-macro: accept Into<Cow> for dynamic tags

* yew-macro: remove unneeded {} wrapping

* yew: revert doc comment

* yew/vtag: clean up attribute type conversion

* yew-macro: remove now supported literal failures

Co-authored-by: Teymour Aldridge <42674621+teymour-aldridge@users.noreply.github.com>
Co-authored-by: Justin Starry <justin.starry@icloud.com>
Co-authored-by: Justin Starry <justin.m.starry@gmail.com>
This commit is contained in:
bakape 2020-08-22 13:45:02 +03:00 committed by GitHub
parent 1e3f4e54d3
commit 2b584ca37b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 750 additions and 257 deletions

18
ci/run_benchmarks.sh Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail # https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
# SETUP
test_flags=("--headless" "--firefox")
test_features="wasm_test"
echo "running benchmarks with flags: ${test_flags[*]} and features: ${test_features}"
# TESTS
set -x
(cd yew &&
wasm-pack test "${test_flags[@]}" --release \
-- --features "${test_features}" bench
)

View File

@ -13,12 +13,13 @@ use yew::{html, Html};
fn add_class(vtag: &mut VTag, class: &str) {
let mut classes: Classes = vtag
.attributes
.get("class")
.map(AsRef::as_ref)
.iter()
.find(|(k, _)| k == &"class")
.map(|(_, v)| AsRef::as_ref(v))
.unwrap_or("")
.into();
classes.push(class);
vtag.add_attribute("class", &classes);
vtag.add_attribute("class", classes.to_string());
}
/// Renders a string of Markdown to HTML with the default options (footnotes
@ -72,7 +73,7 @@ pub fn render_markdown(src: &str) -> Html {
if let VNode::VTag(ref mut vtag) = c {
// TODO
// vtag.tag = "th".into();
vtag.add_attribute("scope", &"col");
vtag.add_attribute("scope", "col");
}
}
}
@ -84,7 +85,7 @@ pub fn render_markdown(src: &str) -> Html {
}
Event::Text(text) => add_child!(VText::new(text.to_string()).into()),
Event::Rule => add_child!(VTag::new("hr").into()),
Event::SoftBreak => add_child!(VText::new("\n".to_string()).into()),
Event::SoftBreak => add_child!(VText::new("\n").into()),
Event::HardBreak => add_child!(VTag::new("br").into()),
_ => println!("Unknown event: {:#?}", ev),
}
@ -109,7 +110,7 @@ fn make_tag(t: Tag) -> VTag {
}
Tag::BlockQuote => {
let mut el = VTag::new("blockquote");
el.add_attribute("class", &"blockquote");
el.add_attribute("class", "blockquote");
el
}
Tag::CodeBlock(code_block_kind) => {
@ -121,10 +122,10 @@ fn make_tag(t: Tag) -> VTag {
// highlighting support by locating the language classes and applying dom transforms
// on their contents.
match lang.as_ref() {
"html" => el.add_attribute("class", &"html-language"),
"rust" => el.add_attribute("class", &"rust-language"),
"java" => el.add_attribute("class", &"java-language"),
"c" => el.add_attribute("class", &"c-language"),
"html" => el.add_attribute("class", "html-language"),
"rust" => el.add_attribute("class", "rust-language"),
"java" => el.add_attribute("class", "java-language"),
"c" => el.add_attribute("class", "c-language"),
_ => {} // Add your own language highlighting support
};
}
@ -135,13 +136,13 @@ fn make_tag(t: Tag) -> VTag {
Tag::List(Some(1)) => VTag::new("ol"),
Tag::List(Some(ref start)) => {
let mut el = VTag::new("ol");
el.add_attribute("start", start);
el.add_attribute("start", start.to_string());
el
}
Tag::Item => VTag::new("li"),
Tag::Table(_) => {
let mut el = VTag::new("table");
el.add_attribute("class", &"table");
el.add_attribute("class", "table");
el
}
Tag::TableHead => VTag::new("th"),
@ -149,36 +150,36 @@ fn make_tag(t: Tag) -> VTag {
Tag::TableCell => VTag::new("td"),
Tag::Emphasis => {
let mut el = VTag::new("span");
el.add_attribute("class", &"font-italic");
el.add_attribute("class", "font-italic");
el
}
Tag::Strong => {
let mut el = VTag::new("span");
el.add_attribute("class", &"font-weight-bold");
el.add_attribute("class", "font-weight-bold");
el
}
Tag::Link(_link_type, ref href, ref title) => {
let mut el = VTag::new("a");
el.add_attribute("href", href);
el.add_attribute("href", href.to_string());
let title = title.clone().into_string();
if title != "" {
el.add_attribute("title", &title);
el.add_attribute("title", title);
}
el
}
Tag::Image(_link_type, ref src, ref title) => {
let mut el = VTag::new("img");
el.add_attribute("src", src);
el.add_attribute("src", src.to_string());
let title = title.clone().into_string();
if title != "" {
el.add_attribute("title", &title);
el.add_attribute("title", title);
}
el
}
Tag::FootnoteDefinition(ref _footnote_id) => VTag::new("span"), // Footnotes are not rendered as anything special
Tag::Strikethrough => {
let mut el = VTag::new("span");
el.add_attribute("class", &"text-decoration-strikethrough");
el.add_attribute("class", "text-decoration-strikethrough");
el
}
}

View File

@ -1,6 +1,7 @@
#![recursion_limit = "512"]
use serde_derive::{Deserialize, Serialize};
use std::borrow::Cow;
use strum::IntoEnumIterator;
use strum_macros::{EnumIter, ToString};
use wasm_bindgen::prelude::*;
@ -8,7 +9,7 @@ use yew::events::KeyboardEvent;
use yew::format::Json;
use yew::services::storage::{Area, StorageService};
use yew::web_sys::HtmlInputElement as InputElement;
use yew::{html, Component, ComponentLink, Href, Html, InputData, NodeRef, ShouldRender};
use yew::{html, Component, ComponentLink, Html, InputData, NodeRef, ShouldRender};
const KEY: &str = "yew.todomvc.self";
@ -190,11 +191,16 @@ impl Component for Model {
impl Model {
fn view_filter(&self, filter: Filter) -> Html {
let cls = if self.state.filter == filter {
"selected"
} else {
"not-selected"
};
let flt = filter.clone();
html! {
<li>
<a class=if self.state.filter == flt { "selected" } else { "not-selected" }
href=&flt
<a class=cls
href=filter
onclick=self.link.callback(move |_| Msg::SetFilter(flt.clone()))>
{ filter }
</a>
@ -272,8 +278,8 @@ pub enum Filter {
Completed,
}
impl<'a> Into<Href> for &'a Filter {
fn into(self) -> Href {
impl<'a> Into<Cow<'static, str>> for &'a Filter {
fn into(self) -> Cow<'static, str> {
match *self {
Filter::All => "#/".into(),
Filter::Active => "#/active".into(),

View File

@ -137,7 +137,7 @@ where
let view_option = |value: &T| {
let flag = selected == Some(value);
html! {
<option value=value selected=flag>{ value.to_string() }</option>
<option value=value.to_string() selected=flag>{ value.to_string() }</option>
}
};

View File

@ -36,9 +36,9 @@ impl<COMP: Component> VTagProducer<COMP> {
self
}
pub fn attribute(mut self, name: String, value: String) -> Self {
pub fn attribute(mut self, name: &'static str, value: String) -> Self {
let effect = Effect::new(move |mut vtag: VTag, _scope: &ScopeHolder<COMP>| {
vtag.add_attribute(&name, &value);
vtag.add_attribute(name, value);
vtag
});
self.effects.push(effect);
@ -56,7 +56,7 @@ impl<COMP: Component> VTagProducer<COMP> {
pub fn classes(mut self, classes: Classes) -> Self {
let effect = Effect::new(move |mut vtag: VTag, _scope: &ScopeHolder<COMP>| {
vtag.add_attribute("class", &classes.to_string());
vtag.add_attribute("class", classes.to_string());
vtag
});
self.effects.push(effect);

View File

@ -44,7 +44,10 @@ impl PeekValue<()> for HtmlNode {
impl ToTokens for HtmlNode {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(match &self {
HtmlNode::Literal(lit) => quote! {#lit},
HtmlNode::Literal(lit) => {
let sr = crate::stringify::Constructor::from(lit.as_ref());
quote! { ::yew::virtual_dom::VText::new(#sr) }
}
HtmlNode::Expression(expr) => quote_spanned! {expr.span()=> #expr},
});
}

View File

@ -4,6 +4,7 @@ use super::HtmlChildrenTree;
use super::HtmlDashedName;
use super::HtmlProp as TagAttribute;
use super::HtmlPropSuffix as TagSuffix;
use crate::stringify;
use crate::{non_capitalized_ascii, Peek, PeekValue};
use boolinator::Boolinator;
use proc_macro2::{Delimiter, Span};
@ -104,7 +105,7 @@ impl ToTokens for HtmlTag {
let name = match &tag_name {
TagName::Lit(name) => {
let name_str = name.to_string();
quote! {#name_str}
quote! { ::std::borrow::Cow::<'static, str>::Borrowed(#name_str) }
}
TagName::Expr(name) => {
let expr = &name.expr;
@ -112,7 +113,7 @@ impl ToTokens for HtmlTag {
// 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::borrow::Cow::<'static, str>::from(#expr);
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);
}
@ -132,50 +133,72 @@ impl ToTokens for HtmlTag {
checked,
node_ref,
key,
href,
listeners,
} = &attributes;
let vtag = Ident::new("__yew_vtag", tag_name.span());
let attr_pairs = attributes.iter().map(|TagAttribute { label, value }| {
let mut attr_pairs: Vec<_> = attributes
.iter()
.map(|TagAttribute { label, value }| {
let label_str = label.to_string();
quote_spanned! {value.span()=> (#label_str.to_owned(), (#value).to_string()) }
});
let sr = stringify::Constructor::from(value);
quote! { (#label_str, #sr) }
})
.collect();
let set_booleans = booleans.iter().map(|TagAttribute { label, value }| {
let label_str = label.to_string();
// We use `set_boolean_attribute` instead of inlining an if statement to avoid
// the `suspicious_else_formatting` clippy warning.
quote_spanned! {value.span()=> #vtag.set_boolean_attribute(&#label_str, #value); }
quote_spanned! {value.span()=> {
#[allow(clippy::suspicious_else_formatting)]
if #value {
#vtag.push_attribute(
#label_str,
::std::borrow::Cow::<'static, str>::Borrowed(#label_str),
);
}
}}
});
let set_kind = kind.iter().map(|kind| {
quote_spanned! {kind.span()=> #vtag.set_kind(&(#kind)); }
let sr = stringify::Constructor::from(kind);
quote_spanned! {kind.span()=> #vtag.set_kind(#sr); }
});
let set_value = value.iter().map(|value| {
quote_spanned! {value.span()=> #vtag.set_value(&(#value)); }
});
let add_href = href.iter().map(|href| {
quote_spanned! {href.span()=>
let __yew_href: ::yew::html::Href = (#href).into();
#vtag.add_attribute("href", &__yew_href);
}
});
let set_checked = checked.iter().map(|checked| {
quote_spanned! {checked.span()=> #vtag.set_checked(#checked); }
});
let set_classes = classes.iter().map(|classes_form| match classes_form {
ClassesForm::Tuple(classes) => quote! {
let __yew_classes = ::yew::virtual_dom::Classes::default()#(.extend(#classes))*;
let set_classes = match classes {
Some(ClassesForm::Tuple(classes)) => Some(quote! {
let __yew_classes
= ::yew::virtual_dom::Classes::default()#(.extend(#classes))*;
if !__yew_classes.is_empty() {
#vtag.add_attribute("class", &__yew_classes);
#vtag.push_attribute("class", __yew_classes.to_string());
}
},
ClassesForm::Single(classes) => quote! {
let __yew_classes = ::std::convert::Into::<::yew::virtual_dom::Classes>::into(#classes);
}),
Some(ClassesForm::Single(classes)) => match stringify::try_stringify_expr(classes) {
Some(s) => {
if !s.is_empty() {
let sr = stringify::Constructor::from(s);
attr_pairs.push(quote! { ("class", #sr) });
}
None
}
None => Some(quote! {
let __yew_classes
= ::std::convert::Into::<::yew::virtual_dom::Classes>::into(#classes);
if !__yew_classes.is_empty() {
#vtag.add_attribute("class", &__yew_classes);
#vtag.push_attribute(
"class",
::std::string::ToString::to_string(&__yew_classes),
);
}
}),
},
});
None => None,
};
let set_classes_it = set_classes.iter();
let set_node_ref = node_ref.iter().map(|node_ref| {
quote! {
#vtag.node_ref = #node_ref;
@ -186,16 +209,18 @@ impl ToTokens for HtmlTag {
#vtag.key = Some(::yew::virtual_dom::Key::from(#key));
}
});
let listeners = listeners.iter().map(|listener| {
let listeners: Vec<_> = listeners
.iter()
.map(|listener| {
let name = &listener.label.name;
let callback = &listener.value;
quote_spanned! {name.span()=> ::yew::html::#name::Wrapper::new(
<::yew::virtual_dom::VTag as ::yew::virtual_dom::Transformer<_, _>>::transform(
#callback
)
<::yew::virtual_dom::VTag as ::yew::virtual_dom::Transformer<_, _>>
::transform(#callback),
)}
});
})
.collect();
// These are the runtime-checks exclusive to dynamic tags.
// For literal tags this is already done at compile-time.
@ -219,7 +244,7 @@ impl ToTokens for HtmlTag {
"input" | "textarea" => {}
_ => {
if let ::std::option::Option::Some(value) = #vtag.value.take() {
#vtag.attributes.insert("value".to_string(), value);
#vtag.push_attribute("value", value);
}
}
}
@ -228,21 +253,37 @@ impl ToTokens for HtmlTag {
None
};
// Constant ifs - easy for the compiler to optimise out
// Attribute setting ordered to reduce reallocation on collection expansion
let has_attrs = !attr_pairs.is_empty();
let has_listeners = !listeners.is_empty();
let has_children = !children.is_empty();
tokens.extend(quote! {{
#[allow(unused_braces)]
let mut #vtag = ::yew::virtual_dom::VTag::new(#name);
#(#set_kind)*
#(#set_value)*
#(#add_href)*
#(#set_checked)*
#(#set_booleans)*
#(#set_classes)*
#(#set_node_ref)*
#(#set_key)*
#[allow(redundant_clone, unused_braces)]
#vtag.add_attributes(vec![#(#attr_pairs),*]);
#(#set_kind)*
#[allow(clippy::suspicious_else_formatting)]
if #has_attrs {
#vtag.attributes = ::yew::virtual_dom::Attributes::Vec(vec![#(#attr_pairs),*]);
}
#(#set_booleans)*
#(#set_classes_it)*
#(#set_checked)*
#(#set_value)*
if #has_listeners {
#vtag.add_listeners(vec![#(::std::rc::Rc::new(#listeners)),*]);
}
if #has_children {
#[allow(redundant_clone, unused_braces)]
#vtag.add_children(#children);
}
#dyn_tag_runtime_checks
#[allow(unused_braces)]
::yew::virtual_dom::VNode::from(#vtag)
}});
}

View File

@ -16,7 +16,6 @@ pub struct TagAttributes {
pub checked: Option<Expr>,
pub node_ref: Option<Expr>,
pub key: Option<Expr>,
pub href: Option<Expr>,
}
pub enum ClassesForm {
@ -316,8 +315,6 @@ impl Parse for TagAttributes {
let node_ref = TagAttributes::remove_attr(&mut attributes, "ref");
let key = TagAttributes::remove_attr(&mut attributes, "key");
let href = TagAttributes::remove_attr(&mut attributes, "href");
Ok(TagAttributes {
attributes,
classes,
@ -327,7 +324,6 @@ impl Parse for TagAttributes {
value,
kind,
node_ref,
href,
key,
})
}

View File

@ -131,9 +131,10 @@ impl Parse for HtmlRootVNode {
impl ToTokens for HtmlRootVNode {
fn to_tokens(&self, tokens: &mut TokenStream) {
let new_tokens = self.0.to_token_stream();
tokens.extend(quote! {
tokens.extend(quote! {{
#[allow(unused_braces)]
::yew::virtual_dom::VNode::from(#new_tokens)
});
}});
}
}

View File

@ -59,6 +59,7 @@
mod derive_props;
mod html_tree;
mod stringify;
use derive_props::DerivePropsInput;
use html_tree::{HtmlRoot, HtmlRootVNode};

View File

@ -0,0 +1,67 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{Expr, Lit};
/// Attempt converting expression to str, if it's a literal
pub fn try_stringify_expr(src: &Expr) -> Option<String> {
match src {
Expr::Lit(l) => try_stringify_lit(&l.lit),
_ => None,
}
}
/// Attempt converting literal to str literal
fn try_stringify_lit(src: &Lit) -> Option<String> {
match src {
Lit::Str(v) => Some(v.value()),
Lit::Char(v) => Some(v.value().to_string()),
Lit::Int(v) => Some(v.base10_digits().to_string()),
Lit::Float(v) => Some(v.base10_digits().to_string()),
Lit::Bool(v) => Some(v.value.to_string()),
_ => None,
}
}
/// Converts literals and expressions to Cow<'static, str> construction calls
pub struct Constructor(TokenStream);
macro_rules! stringify_at_runtime {
($src:expr) => {{
let src = $src;
Self(quote! {
::std::borrow::Cow::<'static, str>::Owned(
::std::string::ToString::to_string(&#src),
)
})
}};
}
impl From<&Expr> for Constructor {
fn from(src: &Expr) -> Self {
match try_stringify_expr(src) {
Some(s) => Self::from(s),
None => stringify_at_runtime!(src),
}
}
}
impl From<&Lit> for Constructor {
fn from(src: &Lit) -> Self {
match try_stringify_lit(src) {
Some(s) => Self::from(s),
None => stringify_at_runtime!(src),
}
}
}
impl From<String> for Constructor {
fn from(src: String) -> Self {
Self(quote! { ::std::borrow::Cow::<'static, str>::Borrowed(#src) })
}
}
impl ToTokens for Constructor {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(std::iter::once(self.0.clone()));
}
}

View File

@ -285,13 +285,13 @@ error[E0599]: no method named `build` found for struct `ChildContainerProperties
candidate #1: `proc_macro::bridge::server::TokenStreamBuilder`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<&str>` is not satisfied
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vtext::VText>` is not satisfied
--> $DIR/html-component-fail.rs:113:5
|
113 | html! { <ChildContainer>{ "Not allowed" }</ChildContainer> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::From<&str>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>`
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::convert::From<yew::virtual_dom::vtext::VText>` is not implemented for `yew::virtual_dom::vcomp::VChild<Child>`
|
= note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `&str`
= note: required because of the requirements on the impl of `std::convert::Into<yew::virtual_dom::vcomp::VChild<Child>>` for `yew::virtual_dom::vtext::VText`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `yew::virtual_dom::vcomp::VChild<Child>: std::convert::From<yew::virtual_dom::vnode::VNode>` is not satisfied

View File

@ -9,10 +9,8 @@ fn compile_fail() {
// unsupported literals
html! { b'a' };
html! { b"str" };
html! { 1111111111111111111111111111111111111111111111111111111111111111111111111111 };
html! { <span>{ b'a' }</span> };
html! { <span>{ b"str" }</span> };
html! { <span>{ 1111111111111111111111111111111111111111111111111111111111111111111111111111 }</span> };
let not_node = || ();
html! {

View File

@ -22,30 +22,18 @@ error: unsupported type
11 | html! { b"str" };
| ^^^^^^
error: integer literal is too large
--> $DIR/html-node-fail.rs:12:14
error: unsupported type
--> $DIR/html-node-fail.rs:12:22
|
12 | html! { 1111111111111111111111111111111111111111111111111111111111111111111111111111 };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12 | html! { <span>{ b'a' }</span> };
| ^^^^
error: unsupported type
--> $DIR/html-node-fail.rs:13:22
|
13 | html! { <span>{ b'a' }</span> };
| ^^^^
error: unsupported type
--> $DIR/html-node-fail.rs:14:22
|
14 | html! { <span>{ b"str" }</span> };
13 | html! { <span>{ b"str" }</span> };
| ^^^^^^
error: integer literal is too large
--> $DIR/html-node-fail.rs:15:22
|
15 | html! { <span>{ 1111111111111111111111111111111111111111111111111111111111111111111111111111 }</span> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0425]: cannot find value `invalid` in this scope
--> $DIR/html-node-fail.rs:7:13
|
@ -65,9 +53,9 @@ error[E0277]: `()` doesn't implement `std::fmt::Display`
= note: required by `std::convert::From::from`
error[E0277]: `()` doesn't implement `std::fmt::Display`
--> $DIR/html-node-fail.rs:19:9
--> $DIR/html-node-fail.rs:17:9
|
19 | not_node()
17 | not_node()
| ^^^^^^^^^^ `()` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `()`

View File

@ -155,14 +155,16 @@ error[E0308]: mismatched types
| ^ expected `bool`, found integer
error[E0277]: `()` doesn't implement `std::fmt::Display`
--> $DIR/html-tag-fail.rs:28:25
--> $DIR/html-tag-fail.rs:28:5
|
28 | html! { <input type=() /> };
| ^^ `()` cannot be formatted with the default formatter
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `()` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `()`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required because of the requirements on the impl of `std::string::ToString` for `()`
= note: required by `std::string::ToString::to_string`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: `()` doesn't implement `std::fmt::Display`
--> $DIR/html-tag-fail.rs:29:26
@ -174,16 +176,17 @@ error[E0277]: `()` doesn't implement `std::fmt::Display`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required because of the requirements on the impl of `std::string::ToString` for `()`
error[E0277]: the trait bound `yew::html::Href: std::convert::From<()>` is not satisfied
--> $DIR/html-tag-fail.rs:30:21
error[E0277]: `()` doesn't implement `std::fmt::Display`
--> $DIR/html-tag-fail.rs:30:5
|
30 | html! { <a href=() /> };
| ^^ the trait `std::convert::From<()>` is not implemented for `yew::html::Href`
| ^^^^^^^^^^^^^^^^^^^^^^^^ `()` cannot be formatted with the default formatter
|
= help: the following implementations were found:
<yew::html::Href as std::convert::From<&'a str>>
<yew::html::Href as std::convert::From<std::string::String>>
= note: required because of the requirements on the impl of `std::convert::Into<yew::html::Href>` for `()`
= help: the trait `std::fmt::Display` is not implemented for `()`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required because of the requirements on the impl of `std::string::ToString` for `()`
= note: required by `std::string::ToString::to_string`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0308]: mismatched types
--> $DIR/html-tag-fail.rs:32:20
@ -203,22 +206,17 @@ error[E0308]: mismatched types
= note: expected enum `yew::callback::Callback<web_sys::features::gen_MouseEvent::MouseEvent>`
found enum `yew::callback::Callback<std::string::String>`
error[E0599]: no method named `to_string` found for struct `NotToString` in the current scope
--> $DIR/html-tag-fail.rs:35:27
error[E0277]: `NotToString` doesn't implement `std::fmt::Display`
--> $DIR/html-tag-fail.rs:35:5
|
3 | struct NotToString;
| -------------------
| |
| method `to_string` not found for this
| doesn't satisfy `NotToString: std::fmt::Display`
| doesn't satisfy `NotToString: std::string::ToString`
...
35 | html! { <input string=NotToString /> };
| ^^^^^^^^^^^ method not found in `NotToString`
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `NotToString` cannot be formatted with the default formatter
|
= note: the method `to_string` exists but the following trait bounds were not satisfied:
`NotToString: std::fmt::Display`
which is required by `NotToString: std::string::ToString`
= help: the trait `std::fmt::Display` is not implemented for `NotToString`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required because of the requirements on the impl of `std::string::ToString` for `NotToString`
= note: required by `std::string::ToString::to_string`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0308]: mismatched types
--> $DIR/html-tag-fail.rs:37:24
@ -238,4 +236,5 @@ error[E0277]: the trait bound `std::borrow::Cow<'static, str>: std::convert::Fro
<std::borrow::Cow<'a, [T]> as std::convert::From<std::vec::Vec<T>>>
<std::borrow::Cow<'a, std::ffi::CStr> as std::convert::From<&'a std::ffi::CStr>>
and 11 others
= note: required by `std::convert::From::from`
= note: required because of the requirements on the impl of `std::convert::Into<std::borrow::Cow<'static, str>>` for `{integer}`
= note: required by `std::convert::Into::into`

View File

@ -1,12 +1,13 @@
#![recursion_limit = "512"]
use serde_derive::{Deserialize, Serialize};
use std::borrow::Cow;
use strum::IntoEnumIterator;
use strum_macros::{EnumIter, ToString};
use yew::events::IKeyboardEvent;
use yew::format::Json;
use yew::services::storage::{Area, StorageService};
use yew::{html, Component, ComponentLink, Href, Html, InputData, KeyPressEvent, ShouldRender};
use yew::{html, Component, ComponentLink, Html, InputData, KeyPressEvent, ShouldRender};
const KEY: &str = "yew.todomvc.self";
@ -168,11 +169,16 @@ impl Component for Model {
impl Model {
fn view_filter(&self, filter: Filter) -> Html {
let cls = if self.state.filter == filter {
"selected"
} else {
"not-selected"
};
let flt = filter.clone();
html! {
<li>
<a class=if self.state.filter == flt { "selected" } else { "not-selected" }
href=&flt
<a class=cls
href=flt
onclick=self.link.callback(move |_| Msg::SetFilter(flt.clone()))>
{ filter }
</a>
@ -248,8 +254,8 @@ pub enum Filter {
Completed,
}
impl<'a> Into<Href> for &'a Filter {
fn into(self) -> Href {
impl<'a> Into<Cow<'static, str>> for &'a Filter {
fn into(self) -> Cow<'static, str> {
match *self {
Filter::All => "#/".into(),
Filter::Active => "#/active".into(),

View File

@ -110,6 +110,7 @@ wasm-bindgen = "0.2.60"
wasm-bindgen-test = "0.3.4"
base64 = "0.12.0"
ssri = "6.0.0"
easybench-wasm = "0.2.1"
[target.'cfg(target_os = "emscripten")'.dependencies]
ryu = "1.0.2" # 1.0.1 breaks emscripten

View File

@ -500,32 +500,6 @@ impl EmptyBuilder {
/// Link to component's scope for creating callbacks.
pub type ComponentLink<COMP> = Scope<COMP>;
/// A bridging type for checking `href` attribute value.
#[derive(Debug)]
pub struct Href {
link: String,
}
impl From<String> for Href {
fn from(link: String) -> Self {
Href { link }
}
}
impl<'a> From<&'a str> for Href {
fn from(link: &'a str) -> Self {
Href {
link: link.to_owned(),
}
}
}
impl ToString for Href {
fn to_string(&self) -> String {
self.link.to_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -206,7 +206,7 @@ pub mod prelude {
pub use crate::callback::Callback;
pub use crate::events::*;
pub use crate::html::{
Children, ChildrenWithProps, Component, ComponentLink, Href, Html, NodeRef, Properties,
Children, ChildrenWithProps, Component, ComponentLink, Html, NodeRef, Properties,
Renderable, ShouldRender,
};
pub use crate::macros::*;

View File

@ -15,8 +15,8 @@ pub mod vtext;
use crate::html::{AnyScope, NodeRef};
use cfg_if::cfg_if;
use indexmap::set::IndexSet;
use std::collections::HashMap;
use indexmap::{IndexMap, IndexSet};
use std::borrow::Cow;
use std::fmt;
use std::rc::Rc;
cfg_if! {
@ -60,8 +60,77 @@ impl fmt::Debug for dyn Listener {
/// A list of event listeners.
type Listeners = Vec<Rc<dyn Listener>>;
/// A map of attributes.
type Attributes = HashMap<String, String>;
/// A collection of attributes for an element
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum Attributes {
/// A vector is ideal because most of the time the list will neither change
/// length nor key order.
Vec(Vec<(&'static str, Cow<'static, str>)>),
/// IndexMap is used to provide runtime attribute deduplication in cases where the html! macro
/// was not used to guarantee it.
IndexMap(IndexMap<&'static str, Cow<'static, str>>),
}
impl Attributes {
/// Construct a default Attributes instance
pub fn new() -> Self {
Default::default()
}
/// Construct new IndexMap variant from Vec variant
pub(crate) fn new_indexmap(v: Vec<(&'static str, Cow<'static, str>)>) -> Self {
Self::IndexMap(v.into_iter().collect())
}
/// Return iterator over attribute key-value pairs
pub fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&'static str, &'a str)> + 'a> {
macro_rules! pack {
($src:expr) => {
Box::new($src.iter().map(|(k, v)| (*k, v.as_ref())))
};
}
match self {
Self::Vec(v) => pack!(v),
Self::IndexMap(m) => pack!(m),
}
}
}
impl AsMut<IndexMap<&'static str, Cow<'static, str>>> for Attributes {
fn as_mut(&mut self) -> &mut IndexMap<&'static str, Cow<'static, str>> {
match self {
Self::IndexMap(m) => m,
Self::Vec(v) => {
*self = Self::new_indexmap(std::mem::take(v));
self.as_mut()
}
}
}
}
macro_rules! impl_attrs_from {
($($from:path => $variant:ident)*) => {
$(
impl From<$from> for Attributes {
fn from(v: $from) -> Self {
Self::$variant(v)
}
}
)*
};
}
impl_attrs_from! {
Vec<(&'static str, Cow<'static, str>)> => Vec
IndexMap<&'static str, Cow<'static, str>> => IndexMap
}
impl Default for Attributes {
fn default() -> Self {
Self::Vec(Default::default())
}
}
/// A set of classes.
#[derive(Debug, Clone, Default)]
@ -343,7 +412,7 @@ mod layout_tests {
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()).into();
let mut empty_node: VNode = VText::new("").into();
// Tests each layout independently
let next_sibling = NodeRef::new(end_node.into());

View File

@ -95,7 +95,7 @@ impl VDiff for VList {
// Without a placeholder the next element becomes first
// and corrupts the order of rendering
// We use empty text element to stake out a place
let placeholder = VText::new("".into());
let placeholder = VText::new("");
self.children.push(placeholder.into());
}

View File

@ -5,6 +5,7 @@ use crate::html::{AnyScope, NodeRef};
use crate::utils::document;
use cfg_if::cfg_if;
use cfg_match::cfg_match;
use indexmap::IndexMap;
use log::warn;
use std::borrow::Cow;
use std::cmp::PartialEq;
@ -76,7 +77,7 @@ pub struct VTag {
/// Contains
/// [kind](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types)
/// value of an `InputElement`.
pub kind: Option<String>,
pub kind: Option<Cow<'static, str>>,
/// 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
@ -112,7 +113,7 @@ impl Clone for VTag {
impl VTag {
/// Creates a new `VTag` instance with `tag` name (cannot be changed later in DOM).
pub fn new<S: Into<Cow<'static, str>>>(tag: S) -> Self {
pub fn new(tag: impl Into<Cow<'static, str>>) -> Self {
let tag: Cow<'static, str> = tag.into();
let element_type = ElementType::from_tag(&tag);
VTag {
@ -157,8 +158,8 @@ impl VTag {
/// Sets `kind` property of an
/// [InputElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
/// Same as set `type` attribute.
pub fn set_kind<T: ToString>(&mut self, value: &T) {
self.kind = Some(value.to_string());
pub fn set_kind(&mut self, value: impl Into<Cow<'static, str>>) {
self.kind = Some(value.into());
}
/// Sets `checked` property of an
@ -168,34 +169,41 @@ impl VTag {
self.checked = value;
}
/// Adds attribute to a virtual node. Not every attribute works when
/// it set as attribute. We use workarounds for:
/// Pushes a key-value pair to the Vec variant of attributes.
///
/// It is the responsibility of the caller to ensure the attribute list does not contain
/// duplicate keys.
///
/// Not every attribute works when it set as an attribute. We use workarounds for:
/// `type/kind`, `value` and `checked`.
///
/// If this virtual node has this attribute present, the value is replaced.
pub fn add_attribute<T: ToString>(&mut self, name: &str, value: &T) {
self.attributes.insert(name.to_owned(), value.to_string());
}
/// Sets a boolean attribute if `value` is true. Removes if `value` is false. The name
/// of the attribute will be used as the value.
///
/// Example: `<button disabled="disabled">`
pub fn set_boolean_attribute(&mut self, name: &str, value: bool) {
if value {
self.attributes.insert(name.to_owned(), name.to_owned());
} else {
self.attributes.remove(name);
pub fn push_attribute(&mut self, key: &'static str, value: impl Into<Cow<'static, str>>) {
if let Attributes::Vec(v) = &mut self.attributes {
v.push((key, value.into()));
}
}
/// Adds attributes to a virtual node. Not every attribute works when
/// it set as attribute. We use workarounds for:
/// 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`.
pub fn add_attributes(&mut self, attrs: Vec<(String, String)>) {
for (name, value) in attrs {
self.attributes.insert(name, value);
pub fn add_attribute(&mut self, key: &'static str, value: impl Into<Cow<'static, str>>) {
self.attributes_mut().insert(key, value.into());
}
/// Returns a mutable reference to the IndexMap variant of attributes.
///
/// Not every attribute works when it set as an attribute. We use workarounds for:
/// `type/kind`, `value` and `checked`.
pub fn attributes_mut(&mut self) -> &mut IndexMap<&'static str, Cow<'static, str>> {
self.attributes.as_mut()
}
/// 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`.
pub fn set_attributes(&mut self, attrs: impl Into<Attributes>) {
self.attributes = attrs.into();
}
/// Adds new listener to the node.
@ -209,9 +217,7 @@ impl VTag {
/// 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: Vec<Rc<dyn Listener>>) {
for listener in listeners {
self.listeners.push(listener);
}
self.listeners.extend(listeners);
}
/// Every render it removes all listeners and attach it back later
@ -261,36 +267,132 @@ impl VTag {
}
}
/// This handles patching of attributes when the keys are equal but
/// the values are different.
fn diff_attributes<'a>(
&'a self,
ancestor: &'a Option<Box<Self>>,
) -> impl Iterator<Item = Patch<&'a str, &'a str>> + 'a {
// Only change what is necessary.
let to_add_or_replace =
self.attributes.iter().filter_map(move |(key, value)| {
match ancestor
.as_ref()
.and_then(|ancestor| ancestor.attributes.get(&**key))
{
None => Some(Patch::Add(&**key, &**value)),
Some(ancestor_value) if value != ancestor_value => {
Some(Patch::Replace(&**key, &**value))
}
_ => None,
}
});
let to_remove = ancestor
.iter()
.flat_map(|ancestor| ancestor.attributes.keys())
.filter(move |key| !self.attributes.contains_key(&**key))
.map(|key| Patch::Remove(&**key));
/// Diffs attributes between the old and new lists.
///
/// This method optimises for the lists being the same length, having the
/// same keys in the same order - most common case.
fn diff_attributes_vectors<'a>(
new: &'a [(&'static str, Cow<'static, str>)],
old: &'a [(&'static str, Cow<'static, str>)],
) -> Vec<Patch<&'static str, &'a str>> {
let mut out = Vec::new();
to_add_or_replace.chain(to_remove)
if new.len() != old.len() {
diff_incompatible(&mut out, new, old);
return out;
}
/// Similar to `diff_attributes` except there is only a single `kind`.
for (i, (n, o)) in new.iter().zip(old).enumerate() {
if n.0 != o.0 {
diff_incompatible(&mut out, &new[i..], &old[i..]);
break;
}
if n.1 != o.1 {
out.push(Patch::Replace(n.0, &n.1));
}
}
return out;
/// Diffs attribute lists that do not contain the same set of keys in
/// the same order
fn diff_incompatible<'a>(
dst: &mut Vec<Patch<&'static str, &'a str>>,
new: &'a [(&'static str, Cow<'static, str>)],
old: &'a [(&'static str, Cow<'static, str>)],
) {
use std::collections::HashMap;
macro_rules! collect {
($src:expr) => {
$src.iter()
.map(|(k, v)| (*k, v))
.collect::<HashMap<&'static str, &Cow<'static, str>>>()
};
}
let new = collect!(new);
let old = collect!(old);
for (k, n_v) in new.iter() {
match old.get(k) {
Some(o_v) => {
if n_v != o_v {
dst.push(Patch::Replace(k, n_v));
}
}
None => dst.push(Patch::Add(k, n_v)),
};
}
for k in old.keys() {
if !new.contains_key(k) {
dst.push(Patch::Remove(k));
}
}
}
}
/// Diffs attributes between the old and new IndexMaps.
///
/// This method must be used, if either the new or old node were created without using the html!
/// macro, that provides compile-time attribute deduplication.
fn diff_attributes_indexmaps<'a>(
new: &'a IndexMap<&'static str, Cow<'static, str>>,
old: &'a IndexMap<&'static str, Cow<'static, str>>,
) -> Vec<Patch<&'static str, &'a str>> {
use indexmap::map::Iter;
use std::iter::Peekable;
let mut out = Vec::new();
let mut new_iter = new.iter().peekable();
let mut old_iter = old.iter().peekable();
loop {
if new_iter.peek().is_none() || old_iter.peek().is_none() {
break;
}
match (new_iter.next(), old_iter.next()) {
(Some(n), Some(o)) => {
if n.0 != o.0 {
break;
}
if n.1 != o.1 {
out.push(Patch::Replace(*n.0, n.1.as_ref()));
}
}
_ => break,
}
}
diff_incompatible(&mut out, new_iter, old_iter, &new, &old);
return out;
fn diff_incompatible<'a>(
dst: &mut Vec<Patch<&'static str, &'a str>>,
new_keys: Peekable<Iter<'a, &'static str, Cow<'static, str>>>,
old_keys: Peekable<Iter<'a, &'static str, Cow<'static, str>>>,
new_map: &'a IndexMap<&'static str, Cow<'static, str>>,
old_map: &'a IndexMap<&'static str, Cow<'static, str>>,
) {
for (k, n_v) in new_keys {
match old_map.get(k) {
Some(o_v) => {
if n_v != o_v {
dst.push(Patch::Replace(k, n_v));
}
}
None => dst.push(Patch::Add(k, n_v)),
};
}
for (k, _) in old_keys {
if !new_map.contains_key(k) {
dst.push(Patch::Remove(k));
}
}
}
}
/// Compares new kind with ancestor and produces a patch to apply, if any
fn diff_kind<'a>(&'a self, ancestor: &'a Option<Box<Self>>) -> Option<Patch<&'a str, ()>> {
match (
self.kind.as_ref(),
@ -309,7 +411,7 @@ impl VTag {
}
}
/// Almost identical in spirit to `diff_kind`
/// Compares new value with ancestor and produces a patch to apply, if any
fn diff_value<'a>(&'a self, ancestor: &'a Option<Box<Self>>) -> Option<Patch<&'a str, ()>> {
match (
self.value.as_ref(),
@ -328,14 +430,45 @@ impl VTag {
}
}
fn apply_diffs(&mut self, ancestor: &Option<Box<Self>>) {
fn apply_diffs(&mut self, ancestor: &mut Option<Box<Self>>) {
let element = self.reference.as_ref().expect("element expected");
// Update parameters
let changes = self.diff_attributes(ancestor);
// apply attribute patches including an optional "class"-attribute patch
for change in changes {
// Apply attribute patches including an optional "class"-attribute patch.
macro_rules! add_all {
($src:expr) => {
$src.iter()
.map(|(k, v)| Patch::Add(*k, v.as_ref()))
.collect()
};
}
for change in match (
&mut self.attributes,
&mut ancestor.as_mut().map(|a| &mut a.attributes),
) {
(Attributes::Vec(new), Some(Attributes::Vec(old))) => {
Self::diff_attributes_vectors(new, old)
}
(Attributes::IndexMap(new), Some(Attributes::IndexMap(old))) => {
Self::diff_attributes_indexmaps(new, old)
}
(Attributes::Vec(new), None) => add_all!(new),
(Attributes::IndexMap(new), None) => add_all!(new),
(Attributes::Vec(new), Some(Attributes::IndexMap(old))) => {
self.attributes = Attributes::new_indexmap(std::mem::take(new));
match &self.attributes {
Attributes::IndexMap(new) => Self::diff_attributes_indexmaps(new, old),
_ => unreachable!(),
}
}
(Attributes::IndexMap(new), Some(Attributes::Vec(old))) => {
ancestor.as_mut().unwrap().attributes =
Attributes::new_indexmap(std::mem::take(old));
match &ancestor.as_ref().map(|a| &a.attributes) {
Some(Attributes::IndexMap(old)) => Self::diff_attributes_indexmaps(new, old),
_ => unreachable!(),
}
}
} {
match change {
Patch::Add(key, value) | Patch::Replace(key, value) => {
element
@ -345,7 +478,8 @@ impl VTag {
Patch::Remove(key) => {
cfg_match! {
feature = "std_web" => element.remove_attribute(&key),
feature = "web_sys" => element.remove_attribute(&key).expect("could not remove attribute"),
feature = "web_sys" => element.remove_attribute(&key)
.expect("could not remove attribute"),
};
}
}
@ -431,7 +565,8 @@ impl VTag {
}
fn create_element(&self, parent: &Element) -> Element {
if self.tag == "svg"
let tag = self.tag();
if tag == "svg"
|| parent
.namespace_uri()
.map_or(false, |ns| ns == SVG_NAMESPACE)
@ -441,11 +576,11 @@ impl VTag {
feature = "web_sys" => Some(SVG_NAMESPACE),
};
document()
.create_element_ns(namespace, &self.tag)
.create_element_ns(namespace, tag)
.expect("can't create namespaced element for vtag")
} else {
document()
.create_element(&self.tag)
.create_element(tag)
.expect("can't create element for vtag")
}
}
@ -480,7 +615,7 @@ impl VDiff for VTag {
match 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),
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()));
@ -503,7 +638,7 @@ impl VDiff for VTag {
self.reference = Some(element);
}
self.apply_diffs(&ancestor_tag);
self.apply_diffs(&mut ancestor_tag);
self.recreate_listeners(&mut ancestor_tag);
// Process children
@ -713,8 +848,9 @@ mod tests {
/// Returns the class attribute as str reference, or "" if the attribute is not set.
fn get_class_str(vtag: &VTag) -> &str {
vtag.attributes
.get("class")
.map(AsRef::as_ref)
.iter()
.find(|(k, _)| k == &"class")
.map(|(_, v)| AsRef::as_ref(v))
.unwrap_or("")
}
@ -836,26 +972,32 @@ mod tests {
let d_arr = [""];
let d = html! { <div class=&d_arr[..]></div> };
macro_rules! has_class {
($vtag:expr) => {
$vtag.attributes.iter().any(|(k, _)| k == "class")
};
}
if let VNode::VTag(vtag) = a {
assert!(!vtag.attributes.contains_key("class"));
assert!(!has_class!(vtag));
} else {
panic!("vtag expected");
}
if let VNode::VTag(vtag) = b {
assert!(!vtag.attributes.contains_key("class"));
assert!(!has_class!(vtag));
} else {
panic!("vtag expected");
}
if let VNode::VTag(vtag) = c {
assert!(!vtag.attributes.contains_key("class"));
assert!(!has_class!(vtag));
} else {
panic!("vtag expected");
}
if let VNode::VTag(vtag) = d {
assert!(!vtag.attributes.contains_key("class"));
assert!(!vtag.attributes.iter().any(|(k, _)| k == "class"));
} else {
panic!("vtag expected");
}
@ -911,7 +1053,7 @@ mod tests {
#[test]
fn keeps_order_of_classes() {
let a = html! {
<div class="class-1 class-2 class-3",></div>
<div class=vec!["class-1", "class-2", "class-3"],></div>
};
if let VNode::VTag(vtag) = a {
@ -997,10 +1139,12 @@ mod tests {
</p>
};
if let VNode::VTag(vtag) = a {
assert!(vtag.attributes.contains_key("aria-controls"));
assert_eq!(
vtag.attributes.get("aria-controls"),
Some(&"it-works".into())
vtag.attributes
.iter()
.find(|(k, _)| k == &"aria-controls")
.map(|(_, v)| v),
Some("it-works")
);
} else {
panic!("vtag expected");
@ -1364,7 +1508,7 @@ mod tests {
elem.apply(&scope, &parent, NodeRef::default(), None);
let vtag = assert_vtag(&mut elem);
// make sure the new tag name is used internally
assert_eq!(vtag.tag, "a");
assert_eq!(vtag.tag(), "a");
#[cfg(feature = "web_sys")]
// Element.tagName is always in the canonical upper-case form.
@ -1378,17 +1522,19 @@ mod tests {
};
let div_vtag = assert_vtag(&mut div_el);
assert!(div_vtag.value.is_none());
assert_eq!(
div_vtag.attributes.get("value").map(String::as_str),
Some("Hello")
);
let v: Option<&str> = div_vtag
.attributes
.iter()
.find(|(k, _)| k == &"value")
.map(|(_, v)| AsRef::as_ref(v));
assert_eq!(v, Some("Hello"));
let mut input_el = html! {
<@{"input"} value="World"/>
};
let input_vtag = assert_vtag(&mut input_el);
assert_eq!(input_vtag.value, Some("World".to_string()));
assert!(!input_vtag.attributes.contains_key("value"));
assert!(!input_vtag.attributes.iter().any(|(k, _)| k == "value"));
}
#[test]
@ -1519,3 +1665,180 @@ mod layout_tests {
diff_layouts(vec![layout1, layout2, layout3, layout4]);
}
}
#[cfg(all(test, feature = "web_sys", feature = "wasm_test"))]
mod benchmarks {
use super::{Patch, VTag};
use easybench_wasm::bench_env_limit;
use std::borrow::Cow;
use std::collections::HashMap;
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
wasm_bindgen_test_configure!(run_in_browser);
// In seconds
const BENCHMARK_DURATION: f64 = 1.0;
fn diff_attributes_hashmap<'a>(
new: &'a HashMap<&'static str, Cow<'static, str>>,
old: &'a HashMap<&'static str, Cow<'static, str>>,
) -> Vec<Patch<&'static str, &'a str>> {
// Only change what is necessary.
let to_add_or_replace = new
.iter()
.filter_map(move |(key, value)| match old.get(key) {
None => Some(Patch::Add(&**key, &**value)),
Some(ancestor_value) if value != ancestor_value => {
Some(Patch::Replace(&**key, &**value))
}
_ => None,
});
let to_remove = old
.keys()
.filter(move |key| !new.contains_key(&**key))
.map(|key| Patch::Remove(&**key));
to_add_or_replace.chain(to_remove).collect()
}
macro_rules! bench_attrs {
($name:ident, $args:expr) => {
#[test]
fn $name() {
let env = $args;
macro_rules! build_map {
($type:ident, $src:expr) => {
&$src
.into_iter()
.collect::<$type<&'static str, Cow<'static, str>>>();
};
}
wasm_bindgen_test::console_log!(
"{}: hashmaps: {}",
stringify!($name),
bench_env_limit(BENCHMARK_DURATION, env.clone(), |(a, b)| {
format!(
"{:?}",
diff_attributes_hashmap(build_map!(HashMap, a), build_map!(HashMap, b))
)
})
);
wasm_bindgen_test::console_log!(
"{}: indexmaps: {}",
stringify!($name),
bench_env_limit(BENCHMARK_DURATION, env.clone(), |(a, b)| {
use indexmap::IndexMap;
format!(
"{:?}",
VTag::diff_attributes_indexmaps(
build_map!(IndexMap, a),
build_map!(IndexMap, b)
)
)
})
);
wasm_bindgen_test::console_log!(
"{}: vectors: {}",
stringify!($name),
bench_env_limit(BENCHMARK_DURATION, env.clone(), |(a, b)| format!(
"{:?}",
VTag::diff_attributes_vectors(&a, &b)
))
);
}
};
}
// Fill vector wit more attributes
fn extend_attrs(dst: &mut Vec<(&'static str, Cow<'static, str>)>) {
dst.extend(vec![
("oh", Cow::Borrowed("danny")),
("boy", Cow::Borrowed("the")),
("pipes", Cow::Borrowed("the")),
("are", Cow::Borrowed("calling")),
("from", Cow::Borrowed("glen")),
("to", Cow::Borrowed("glen")),
("and", Cow::Borrowed("down")),
("the", Cow::Borrowed("mountain")),
("side", Cow::Borrowed("")),
]);
}
bench_attrs! {
bench_diff_attributes_same,
{
let mut old: Vec<(&'static str, Cow<'static, str>)> = vec![
("disable", Cow::Borrowed("disable")),
("style", Cow::Borrowed("display: none;")),
("class", Cow::Borrowed("lass")),
];
extend_attrs(&mut old);
(old.clone(), old)
}
}
bench_attrs! {
bench_diff_attributes_append,
{
let mut old = vec![
("disable", Cow::Borrowed("disable")),
("style", Cow::Borrowed("display: none;")),
("class", Cow::Borrowed("lass")),
];
extend_attrs(&mut old);
let mut new = old.clone();
new.push(("hidden", Cow::Borrowed("hidden")));
(new, old)
}
}
bench_attrs! {
bench_diff_attributes_change_first,
{
let mut old = vec![
("disable", Cow::Borrowed("disable")),
("style", Cow::Borrowed("display: none;")),
("class", Cow::Borrowed("lass")),
];
extend_attrs(&mut old);
let mut new = old.clone();
new[0] = ("disable", Cow::Borrowed("enable"));
(new, old)
}
}
bench_attrs! {
bench_diff_attributes_change_middle,
{
let mut old = vec![
("disable", Cow::Borrowed("disable")),
("style", Cow::Borrowed("display: none;")),
("class", Cow::Borrowed("lass")),
];
extend_attrs(&mut old);
let mut new = old.clone();
let mid = &mut new.get_mut(old.len()/2).unwrap();
mid.1 = Cow::Borrowed("changed");
(new, old)
}
}
bench_attrs! {
bench_diff_attributes_change_last,
{
let mut old = vec![
("disable", Cow::Borrowed("disable")),
("style", Cow::Borrowed("display: none;")),
("class", Cow::Borrowed("lass")),
];
extend_attrs(&mut old);
let mut new = old.clone();
let last = &mut new.get_mut(old.len()-1).unwrap();
last.1 = Cow::Borrowed("changed");
(new, old)
}
}
}

View File

@ -5,6 +5,7 @@ use crate::html::{AnyScope, NodeRef};
use crate::utils::document;
use cfg_if::cfg_if;
use log::warn;
use std::borrow::Cow;
use std::cmp::PartialEq;
cfg_if! {
if #[cfg(feature = "std_web")] {
@ -20,16 +21,16 @@ cfg_if! {
#[derive(Clone, Debug)]
pub struct VText {
/// Contains a text of the node.
pub text: String,
pub text: Cow<'static, str>,
/// A reference to the `TextNode`.
pub reference: Option<TextNode>,
}
impl VText {
/// Creates new virtual text node with a content.
pub fn new(text: String) -> Self {
pub fn new(text: impl Into<Cow<'static, str>>) -> Self {
VText {
text,
text: text.into(),
reference: None,
}
}