Bind to properties instead of attributes by default (#2819)

* Set to properties, not attributes

* fix tests

* Add tests

* enable disabled test, fmt

* Introduce @key syntax to forcefully set as attribute

* Everything compiles

* More tests

* id as property

* This was not meant to be committed

* Make test pass, fmt + clippy

* fucking rustfmt

* is this enough formatting

* that was not supposed to be commited

* apply review

* fmt

* fix CI

* will you be happy now, clippy?
This commit is contained in:
Muhammad Hamza 2022-08-15 00:41:04 +05:00 committed by GitHub
parent a4e70914ac
commit 4c35f95350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 601 additions and 125 deletions

View File

@ -14,13 +14,13 @@ fn App() -> Html {
Callback::from(move |_| state.set(*state - 1)) Callback::from(move |_| state.set(*state - 1))
}; };
html!( html! {
<> <>
<p> {"current count: "} {*state} </p> <p> {"current count: "} {*state} </p>
<button onclick={incr_counter}> {"+"} </button> <button onclick={incr_counter}> {"+"} </button>
<button onclick={decr_counter}> {"-"} </button> <button onclick={decr_counter}> {"-"} </button>
</> </>
) }
} }
fn main() { fn main() {

View File

@ -8,7 +8,7 @@ use syn::spanned::Spanned;
use syn::{Block, Expr, Ident, Lit, LitStr, Token}; use syn::{Block, Expr, Ident, Lit, LitStr, Token};
use super::{HtmlChildrenTree, HtmlDashedName, TagTokens}; use super::{HtmlChildrenTree, HtmlDashedName, TagTokens};
use crate::props::{ClassesForm, ElementProps, Prop}; use crate::props::{ClassesForm, ElementProps, Prop, PropDirective};
use crate::stringify::{Stringify, Value}; use crate::stringify::{Stringify, Value};
use crate::{non_capitalized_ascii, Peek, PeekValue}; use crate::{non_capitalized_ascii, Peek, PeekValue};
@ -135,39 +135,58 @@ impl ToTokens for HtmlElement {
// other attributes // other attributes
let attributes = { let attributes = {
let normal_attrs = attributes.iter().map(|Prop { label, value, .. }| { let normal_attrs = attributes.iter().map(
(label.to_lit_str(), value.optimize_literals_tagged()) |Prop {
}); label,
let boolean_attrs = booleans.iter().filter_map(|Prop { label, value, .. }| { value,
let key = label.to_lit_str(); directive,
Some(( ..
key.clone(), }| {
match value { (
Expr::Lit(e) => match &e.lit { label.to_lit_str(),
Lit::Bool(b) => Value::Static(if b.value { value.optimize_literals_tagged(),
quote! { #key } *directive,
} else { )
return None; },
}), );
_ => Value::Dynamic(quote_spanned! {value.span()=> { let boolean_attrs = booleans.iter().filter_map(
::yew::utils::__ensure_type::<::std::primitive::bool>(#value); |Prop {
#key label,
}}), value,
}, directive,
expr => Value::Dynamic( ..
quote_spanned! {expr.span().resolved_at(Span::call_site())=> }| {
if #expr { let key = label.to_lit_str();
::std::option::Option::Some( Some((
::yew::virtual_dom::AttrValue::Static(#key) key.clone(),
) match value {
Expr::Lit(e) => match &e.lit {
Lit::Bool(b) => Value::Static(if b.value {
quote! { #key }
} else { } else {
::std::option::Option::None return None;
} }),
_ => Value::Dynamic(quote_spanned! {value.span()=> {
::yew::utils::__ensure_type::<::std::primitive::bool>(#value);
#key
}}),
}, },
), expr => Value::Dynamic(
}, quote_spanned! {expr.span().resolved_at(Span::call_site())=>
)) if #expr {
}); ::std::option::Option::Some(
::yew::virtual_dom::AttrValue::Static(#key)
)
} else {
::std::option::Option::None
}
},
),
},
*directive,
))
},
);
let class_attr = classes.as_ref().and_then(|classes| match classes { let class_attr = classes.as_ref().and_then(|classes| match classes {
ClassesForm::Tuple(classes) => { ClassesForm::Tuple(classes) => {
let span = classes.span(); let span = classes.span();
@ -196,6 +215,7 @@ impl ToTokens for HtmlElement {
__yew_classes __yew_classes
} }
}), }),
None,
)) ))
} }
ClassesForm::Single(classes) => { ClassesForm::Single(classes) => {
@ -207,6 +227,7 @@ impl ToTokens for HtmlElement {
Some(( Some((
LitStr::new("class", lit.span()), LitStr::new("class", lit.span()),
Value::Static(quote! { #lit }), Value::Static(quote! { #lit }),
None,
)) ))
} }
} }
@ -216,21 +237,34 @@ impl ToTokens for HtmlElement {
Value::Dynamic(quote! { Value::Dynamic(quote! {
::std::convert::Into::<::yew::html::Classes>::into(#classes) ::std::convert::Into::<::yew::html::Classes>::into(#classes)
}), }),
None,
)) ))
} }
} }
} }
}); });
fn apply_as(directive: Option<&PropDirective>) -> TokenStream {
match directive {
Some(PropDirective::ApplyAsProperty(token)) => {
quote_spanned!(token.span()=> ::yew::virtual_dom::ApplyAttributeAs::Property)
}
None => quote!(::yew::virtual_dom::ApplyAttributeAs::Attribute),
}
}
/// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Static` /// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Static`
fn try_into_static(src: &[(LitStr, Value)]) -> Option<TokenStream> { fn try_into_static(
src: &[(LitStr, Value, Option<PropDirective>)],
) -> Option<TokenStream> {
let mut kv = Vec::with_capacity(src.len()); let mut kv = Vec::with_capacity(src.len());
for (k, v) in src.iter() { for (k, v, directive) in src.iter() {
let v = match v { let v = match v {
Value::Static(v) => quote! { #v }, Value::Static(v) => quote! { #v },
Value::Dynamic(_) => return None, Value::Dynamic(_) => return None,
}; };
kv.push(quote! { [ #k, #v ] }); let apply_as = apply_as(directive.as_ref());
kv.push(quote! { ( #k, #v, #apply_as ) });
} }
Some(quote! { ::yew::virtual_dom::Attributes::Static(&[#(#kv),*]) }) Some(quote! { ::yew::virtual_dom::Attributes::Static(&[#(#kv),*]) })
@ -239,10 +273,14 @@ impl ToTokens for HtmlElement {
let attrs = normal_attrs let attrs = normal_attrs
.chain(boolean_attrs) .chain(boolean_attrs)
.chain(class_attr) .chain(class_attr)
.collect::<Vec<(LitStr, Value)>>(); .collect::<Vec<(LitStr, Value, Option<PropDirective>)>>();
try_into_static(&attrs).unwrap_or_else(|| { try_into_static(&attrs).unwrap_or_else(|| {
let keys = attrs.iter().map(|(k, _)| quote! { #k }); let keys = attrs.iter().map(|(k, ..)| quote! { #k });
let values = attrs.iter().map(|(_, v)| wrap_attr_value(v)); let values = attrs.iter().map(|(_, v, directive)| {
let apply_as = apply_as(directive.as_ref());
let value = wrap_attr_value(v);
quote! { ::std::option::Option::map(#value, |it| (it, #apply_as)) }
});
quote! { quote! {
::yew::virtual_dom::Attributes::Dynamic{ ::yew::virtual_dom::Attributes::Dynamic{
keys: &[#(#keys),*], keys: &[#(#keys),*],

View File

@ -13,17 +13,27 @@ use super::CHILDREN_LABEL;
use crate::html_tree::HtmlDashedName; use crate::html_tree::HtmlDashedName;
use crate::stringify::Stringify; use crate::stringify::Stringify;
#[derive(Copy, Clone)]
pub enum PropDirective {
ApplyAsProperty(Token![~]),
}
pub struct Prop { pub struct Prop {
pub directive: Option<PropDirective>,
pub label: HtmlDashedName, pub label: HtmlDashedName,
/// Punctuation between `label` and `value`. /// Punctuation between `label` and `value`.
pub value: Expr, pub value: Expr,
} }
impl Parse for Prop { impl Parse for Prop {
fn parse(input: ParseStream) -> syn::Result<Self> { fn parse(input: ParseStream) -> syn::Result<Self> {
let directive = input
.parse::<Token![~]>()
.map(PropDirective::ApplyAsProperty)
.ok();
if input.peek(Brace) { if input.peek(Brace) {
Self::parse_shorthand_prop_assignment(input) Self::parse_shorthand_prop_assignment(input, directive)
} else { } else {
Self::parse_prop_assignment(input) Self::parse_prop_assignment(input, directive)
} }
} }
} }
@ -33,7 +43,10 @@ impl Prop {
/// Parse a prop using the shorthand syntax `{value}`, short for `value={value}` /// Parse a prop using the shorthand syntax `{value}`, short for `value={value}`
/// This only allows for labels with no hyphens, as it would otherwise create /// This only allows for labels with no hyphens, as it would otherwise create
/// an ambiguity in the syntax /// an ambiguity in the syntax
fn parse_shorthand_prop_assignment(input: ParseStream) -> syn::Result<Self> { fn parse_shorthand_prop_assignment(
input: ParseStream,
directive: Option<PropDirective>,
) -> syn::Result<Self> {
let value; let value;
let _brace = braced!(value in input); let _brace = braced!(value in input);
let expr = value.parse::<Expr>()?; let expr = value.parse::<Expr>()?;
@ -44,7 +57,7 @@ impl Prop {
}) = expr }) = expr
{ {
if let (Some(ident), true) = (path.get_ident(), attrs.is_empty()) { if let (Some(ident), true) = (path.get_ident(), attrs.is_empty()) {
syn::Result::Ok(HtmlDashedName::from(ident.clone())) Ok(HtmlDashedName::from(ident.clone()))
} else { } else {
Err(syn::Error::new_spanned( Err(syn::Error::new_spanned(
path, path,
@ -59,11 +72,18 @@ impl Prop {
)); ));
}?; }?;
Ok(Self { label, value: expr }) Ok(Self {
label,
value: expr,
directive,
})
} }
/// Parse a prop of the form `label={value}` /// Parse a prop of the form `label={value}`
fn parse_prop_assignment(input: ParseStream) -> syn::Result<Self> { fn parse_prop_assignment(
input: ParseStream,
directive: Option<PropDirective>,
) -> syn::Result<Self> {
let label = input.parse::<HtmlDashedName>()?; let label = input.parse::<HtmlDashedName>()?;
let equals = input.parse::<Token![=]>().map_err(|_| { let equals = input.parse::<Token![=]>().map_err(|_| {
syn::Error::new_spanned( syn::Error::new_spanned(
@ -83,7 +103,11 @@ impl Prop {
} }
let value = parse_prop_value(input)?; let value = parse_prop_value(input)?;
Ok(Self { label, value }) Ok(Self {
label,
value,
directive,
})
} }
} }
@ -105,10 +129,13 @@ fn parse_prop_value(input: &ParseBuffer) -> syn::Result<Expr> {
match &expr { match &expr {
Expr::Lit(_) => Ok(expr), Expr::Lit(_) => Ok(expr),
_ => Err(syn::Error::new_spanned( ref exp => Err(syn::Error::new_spanned(
&expr, &expr,
"the property value must be either a literal or enclosed in braces. Consider \ format!(
adding braces around your expression.", "the property value must be either a literal or enclosed in braces. Consider \
adding braces around your expression.: {:#?}",
exp
),
)), )),
} }
} }

View File

@ -61,7 +61,11 @@ impl Parse for PropValue {
impl From<PropValue> for Prop { impl From<PropValue> for Prop {
fn from(prop_value: PropValue) -> Prop { fn from(prop_value: PropValue) -> Prop {
let PropValue { label, value } = prop_value; let PropValue { label, value } = prop_value;
Prop { label, value } Prop {
label,
value,
directive: None,
}
} }
} }

View File

@ -249,7 +249,13 @@ help: escape `type` to use it as an identifier
85 | html! { <Child r#type=0 /> }; 85 | html! { <Child r#type=0 /> };
| ++ | ++
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
ExprTuple {
attrs: [],
paren_token: Paren,
elems: [],
},
)
--> tests/html_macro/component-fail.rs:86:24 --> tests/html_macro/component-fail.rs:86:24
| |
86 | html! { <Child ref=() /> }; 86 | html! { <Child ref=() /> };
@ -309,7 +315,24 @@ error: only one root html element is allowed (hint: you can wrap multiple html e
102 | html! { <Child></Child><Child></Child> }; 102 | html! { <Child></Child><Child></Child> };
| ^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Path(
ExprPath {
attrs: [],
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "num",
span: #0 bytes(3894..3897),
},
arguments: None,
},
],
},
},
)
--> tests/html_macro/component-fail.rs:106:24 --> tests/html_macro/component-fail.rs:106:24
| |
106 | html! { <Child int=num ..props /> }; 106 | html! { <Child int=num ..props /> };

View File

@ -142,61 +142,260 @@ error: dynamic closing tags must not have a body (hint: replace it with just `</
75 | html! { <@{"test"}></@{"test"}> }; 75 | html! { <@{"test"}></@{"test"}> };
| ^^^^^^^^ | ^^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
ExprTuple {
attrs: [],
paren_token: Paren,
elems: [
Lit(
ExprLit {
attrs: [],
lit: Str(
LitStr {
token: "deprecated",
},
),
},
),
Comma,
Lit(
ExprLit {
attrs: [],
lit: Str(
LitStr {
token: "warning",
},
),
},
),
],
},
)
--> tests/html_macro/element-fail.rs:83:24 --> tests/html_macro/element-fail.rs:83:24
| |
83 | html! { <div class=("deprecated", "warning") /> }; 83 | html! { <div class=("deprecated", "warning") /> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
ExprTuple {
attrs: [],
paren_token: Paren,
elems: [],
},
)
--> tests/html_macro/element-fail.rs:84:24 --> tests/html_macro/element-fail.rs:84:24
| |
84 | html! { <input ref=() /> }; 84 | html! { <input ref=() /> };
| ^^ | ^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
ExprTuple {
attrs: [],
paren_token: Paren,
elems: [],
},
)
--> tests/html_macro/element-fail.rs:85:24 --> tests/html_macro/element-fail.rs:85:24
| |
85 | html! { <input ref=() ref=() /> }; 85 | html! { <input ref=() ref=() /> };
| ^^ | ^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Call(
ExprCall {
attrs: [],
func: Path(
ExprPath {
attrs: [],
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "Some",
span: #0 bytes(2632..2636),
},
arguments: None,
},
],
},
},
),
paren_token: Paren,
args: [
Lit(
ExprLit {
attrs: [],
lit: Int(
LitInt {
token: 5,
},
),
},
),
],
},
)
--> tests/html_macro/element-fail.rs:86:28 --> tests/html_macro/element-fail.rs:86:28
| |
86 | html! { <input onfocus=Some(5) /> }; 86 | html! { <input onfocus=Some(5) /> };
| ^^^^^^^ | ^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Path(
ExprPath {
attrs: [],
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "NotToString",
span: #0 bytes(2672..2683),
},
arguments: None,
},
],
},
},
)
--> tests/html_macro/element-fail.rs:87:27 --> tests/html_macro/element-fail.rs:87:27
| |
87 | html! { <input string=NotToString /> }; 87 | html! { <input string=NotToString /> };
| ^^^^^^^^^^^ | ^^^^^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Call(
ExprCall {
attrs: [],
func: Path(
ExprPath {
attrs: [],
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "Some",
span: #0 bytes(2711..2715),
},
arguments: None,
},
],
},
},
),
paren_token: Paren,
args: [
Path(
ExprPath {
attrs: [],
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "NotToString",
span: #0 bytes(2716..2727),
},
arguments: None,
},
],
},
},
),
],
},
)
--> tests/html_macro/element-fail.rs:88:22 --> tests/html_macro/element-fail.rs:88:22
| |
88 | html! { <a media=Some(NotToString) /> }; 88 | html! { <a media=Some(NotToString) /> };
| ^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Call(
ExprCall {
attrs: [],
func: Path(
ExprPath {
attrs: [],
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "Some",
span: #0 bytes(2755..2759),
},
arguments: None,
},
],
},
},
),
paren_token: Paren,
args: [
Lit(
ExprLit {
attrs: [],
lit: Int(
LitInt {
token: 5,
},
),
},
),
],
},
)
--> tests/html_macro/element-fail.rs:89:21 --> tests/html_macro/element-fail.rs:89:21
| |
89 | html! { <a href=Some(5) /> }; 89 | html! { <a href=Some(5) /> };
| ^^^^^^^ | ^^^^^^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
ExprTuple {
attrs: [],
paren_token: Paren,
elems: [],
},
)
--> tests/html_macro/element-fail.rs:90:25 --> tests/html_macro/element-fail.rs:90:25
| |
90 | html! { <input type=() /> }; 90 | html! { <input type=() /> };
| ^^ | ^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
ExprTuple {
attrs: [],
paren_token: Paren,
elems: [],
},
)
--> tests/html_macro/element-fail.rs:91:26 --> tests/html_macro/element-fail.rs:91:26
| |
91 | html! { <input value=() /> }; 91 | html! { <input value=() /> };
| ^^ | ^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Path(
ExprPath {
attrs: [],
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "NotToString",
span: #0 bytes(2862..2873),
},
arguments: None,
},
],
},
},
)
--> tests/html_macro/element-fail.rs:92:27 --> tests/html_macro/element-fail.rs:92:27
| |
92 | html! { <input string=NotToString /> }; 92 | html! { <input string=NotToString /> };

View File

@ -1,65 +1,87 @@
error: this opening fragment has no corresponding closing fragment error: this opening fragment has no corresponding closing fragment
--> $DIR/list-fail.rs:5:13 --> tests/html_macro/list-fail.rs:5:13
| |
5 | html! { <> }; 5 | html! { <> };
| ^^ | ^^
error: this opening fragment has no corresponding closing fragment error: this opening fragment has no corresponding closing fragment
--> $DIR/list-fail.rs:6:15 --> tests/html_macro/list-fail.rs:6:15
| |
6 | html! { <><> }; 6 | html! { <><> };
| ^^ | ^^
error: this opening fragment has no corresponding closing fragment error: this opening fragment has no corresponding closing fragment
--> $DIR/list-fail.rs:7:13 --> tests/html_macro/list-fail.rs:7:13
| |
7 | html! { <><></> }; 7 | html! { <><></> };
| ^^ | ^^
error: this closing fragment has no corresponding opening fragment error: this closing fragment has no corresponding opening fragment
--> $DIR/list-fail.rs:10:13 --> tests/html_macro/list-fail.rs:10:13
| |
10 | html! { </> }; 10 | html! { </> };
| ^^^ | ^^^
error: this closing fragment has no corresponding opening fragment error: this closing fragment has no corresponding opening fragment
--> $DIR/list-fail.rs:11:13 --> tests/html_macro/list-fail.rs:11:13
| |
11 | html! { </></> }; 11 | html! { </></> };
| ^^^ | ^^^
error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<></>`) error: only one root html element is allowed (hint: you can wrap multiple html elements in a fragment `<></>`)
--> $DIR/list-fail.rs:14:18 --> tests/html_macro/list-fail.rs:14:18
| |
14 | html! { <></><></> }; 14 | html! { <></><></> };
| ^^^^^ | ^^^^^
error: expected a valid html element error: expected a valid html element
--> $DIR/list-fail.rs:16:15 --> tests/html_macro/list-fail.rs:16:15
| |
16 | html! { <>invalid</> }; 16 | html! { <>invalid</> };
| ^^^^^^^ | ^^^^^^^
error: expected an expression following this equals sign error: expected an expression following this equals sign
--> $DIR/list-fail.rs:18:17 --> tests/html_macro/list-fail.rs:18:17
| |
18 | html! { <key=></> }; 18 | html! { <key=></> };
| ^^ | ^^
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: MethodCall(
--> $DIR/list-fail.rs:20:18 ExprMethodCall {
attrs: [],
receiver: Lit(
ExprLit {
attrs: [],
lit: Str(
LitStr {
token: "key",
},
),
},
),
dot_token: Dot,
method: Ident {
ident: "to_string",
span: #0 bytes(404..413),
},
turbofish: None,
paren_token: Paren,
args: [],
},
)
--> tests/html_macro/list-fail.rs:20:18
| |
20 | html! { <key="key".to_string()></key> }; 20 | html! { <key="key".to_string()></key> };
| ^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^
error: only a single `key` prop is allowed on a fragment error: only a single `key` prop is allowed on a fragment
--> $DIR/list-fail.rs:23:30 --> tests/html_macro/list-fail.rs:23:30
| |
23 | html! { <key="first key" key="second key" /> }; 23 | html! { <key="first key" key="second key" /> };
| ^^^ | ^^^
error: fragments only accept the `key` prop error: fragments only accept the `key` prop
--> $DIR/list-fail.rs:25:14 --> tests/html_macro/list-fail.rs:25:14
| |
25 | html! { <some_attr="test"></> }; 25 | html! { <some_attr="test"></> };
| ^^^^^^^^^ | ^^^^^^^^^

View File

@ -2,13 +2,14 @@ use std::collections::HashMap;
use std::ops::Deref; use std::ops::Deref;
use indexmap::IndexMap; use indexmap::IndexMap;
use wasm_bindgen::JsValue;
use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
use yew::AttrValue; use yew::AttrValue;
use super::Apply; use super::Apply;
use crate::dom_bundle::BSubtree; use crate::dom_bundle::BSubtree;
use crate::virtual_dom::vtag::{InputFields, Value}; use crate::virtual_dom::vtag::{InputFields, Value};
use crate::virtual_dom::Attributes; use crate::virtual_dom::{ApplyAttributeAs, Attributes};
impl<T: AccessValue> Apply for Value<T> { impl<T: AccessValue> Apply for Value<T> {
type Bundle = Self; type Bundle = Self;
@ -87,23 +88,23 @@ impl Attributes {
#[cold] #[cold]
fn apply_diff_index_maps( fn apply_diff_index_maps(
el: &Element, el: &Element,
new: &IndexMap<AttrValue, AttrValue>, new: &IndexMap<AttrValue, (AttrValue, ApplyAttributeAs)>,
old: &IndexMap<AttrValue, AttrValue>, old: &IndexMap<AttrValue, (AttrValue, ApplyAttributeAs)>,
) { ) {
for (key, value) in new.iter() { for (key, value) in new.iter() {
match old.get(key) { match old.get(key) {
Some(old_value) => { Some(old_value) => {
if value != old_value { if value != old_value {
Self::set_attribute(el, key, value); Self::set(el, key, value.0.as_ref(), value.1);
} }
} }
None => Self::set_attribute(el, key, value), None => Self::set(el, key, value.0.as_ref(), value.1),
} }
} }
for (key, _value) in old.iter() { for (key, (_, apply_as)) in old.iter() {
if !new.contains_key(key) { if !new.contains_key(key) {
Self::remove_attribute(el, key); Self::remove(el, key, *apply_as);
} }
} }
} }
@ -112,17 +113,26 @@ impl Attributes {
/// Works with any [Attributes] variants. /// Works with any [Attributes] variants.
#[cold] #[cold]
fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) { fn apply_diff_as_maps<'a>(el: &Element, new: &'a Self, old: &'a Self) {
fn collect(src: &Attributes) -> HashMap<&str, &str> { fn collect(src: &Attributes) -> HashMap<&str, (&str, ApplyAttributeAs)> {
use Attributes::*; use Attributes::*;
match src { match src {
Static(arr) => (*arr).iter().map(|[k, v]| (*k, *v)).collect(), Static(arr) => (*arr)
.iter()
.map(|(k, v, apply_as)| (*k, (*v, *apply_as)))
.collect(),
Dynamic { keys, values } => keys Dynamic { keys, values } => keys
.iter() .iter()
.zip(values.iter()) .zip(values.iter())
.filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref()))) .filter_map(|(k, v)| {
v.as_ref()
.map(|(v, apply_as)| (*k, (v.as_ref(), *apply_as)))
})
.collect(),
IndexMap(m) => m
.iter()
.map(|(k, (v, apply_as))| (k.as_ref(), (v.as_ref(), *apply_as)))
.collect(), .collect(),
IndexMap(m) => m.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect(),
} }
} }
@ -135,25 +145,42 @@ impl Attributes {
Some(old) => old != new, Some(old) => old != new,
None => true, None => true,
} { } {
el.set_attribute(k, new).unwrap(); Self::set(el, k, new.0, new.1);
} }
} }
// Remove missing // Remove missing
for k in old.keys() { for (k, (_, apply_as)) in old.iter() {
if !new.contains_key(k) { if !new.contains_key(k) {
Self::remove_attribute(el, k); Self::remove(el, k, *apply_as);
} }
} }
} }
fn set_attribute(el: &Element, key: &str, value: &str) { fn set(el: &Element, key: &str, value: &str, apply_as: ApplyAttributeAs) {
el.set_attribute(key, value).expect("invalid attribute key") match apply_as {
ApplyAttributeAs::Attribute => {
el.set_attribute(key, value).expect("invalid attribute key")
}
ApplyAttributeAs::Property => {
let key = JsValue::from_str(key);
let value = JsValue::from_str(value);
js_sys::Reflect::set(el.as_ref(), &key, &value).expect("could not set property");
}
}
} }
fn remove_attribute(el: &Element, key: &str) { fn remove(el: &Element, key: &str, apply_as: ApplyAttributeAs) {
el.remove_attribute(key) match apply_as {
.expect("could not remove attribute") ApplyAttributeAs::Attribute => el
.remove_attribute(key)
.expect("could not remove attribute"),
ApplyAttributeAs::Property => {
let key = JsValue::from_str(key);
js_sys::Reflect::set(el.as_ref(), &key, &JsValue::UNDEFINED)
.expect("could not remove property");
}
}
} }
} }
@ -164,20 +191,20 @@ impl Apply for Attributes {
fn apply(self, _root: &BSubtree, el: &Element) -> Self { fn apply(self, _root: &BSubtree, el: &Element) -> Self {
match &self { match &self {
Self::Static(arr) => { Self::Static(arr) => {
for kv in arr.iter() { for (k, v, apply_as) in arr.iter() {
Self::set_attribute(el, kv[0], kv[1]); Self::set(el, *k, *v, *apply_as);
} }
} }
Self::Dynamic { keys, values } => { Self::Dynamic { keys, values } => {
for (k, v) in keys.iter().zip(values.iter()) { for (k, v) in keys.iter().zip(values.iter()) {
if let Some(v) = v { if let Some((v, apply_as)) = v {
Self::set_attribute(el, k, v) Self::set(el, k, v, *apply_as)
} }
} }
} }
Self::IndexMap(m) => { Self::IndexMap(m) => {
for (k, v) in m.iter() { for (k, (v, apply_as)) in m.iter() {
Self::set_attribute(el, k, v) Self::set(el, k, v, *apply_as)
} }
} }
} }
@ -217,7 +244,7 @@ impl Apply for Attributes {
} }
macro_rules! set { macro_rules! set {
($new:expr) => { ($new:expr) => {
Self::set_attribute(el, key!(), $new) Self::set(el, key!(), $new.0.as_ref(), $new.1)
}; };
} }
@ -228,8 +255,8 @@ impl Apply for Attributes {
} }
} }
(Some(new), None) => set!(new), (Some(new), None) => set!(new),
(None, Some(_)) => { (None, Some(old)) => {
Self::remove_attribute(el, key!()); Self::remove(el, key!(), old.1);
} }
(None, None) => (), (None, None) => (),
} }
@ -247,3 +274,111 @@ impl Apply for Attributes {
} }
} }
} }
#[cfg(target_arch = "wasm32")]
#[cfg(test)]
mod tests {
use std::time::Duration;
use gloo::utils::document;
use js_sys::Reflect;
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
use super::*;
use crate::{function_component, html, Html};
wasm_bindgen_test_configure!(run_in_browser);
fn create_element() -> (Element, BSubtree) {
let element = document()
.create_element("a")
.expect("failed to create element");
let btree = BSubtree::create_root(&element);
(element, btree)
}
#[test]
fn properties_are_set() {
let attrs = Attributes::Static(&[
("href", "https://example.com/", ApplyAttributeAs::Property),
("alt", "somewhere", ApplyAttributeAs::Property),
]);
let (element, btree) = create_element();
attrs.apply(&btree, &element);
assert_eq!(
Reflect::get(element.as_ref(), &JsValue::from_str("href"))
.expect("no href")
.as_string()
.expect("not a string"),
"https://example.com/",
"property `href` not set properly"
);
assert_eq!(
Reflect::get(element.as_ref(), &JsValue::from_str("alt"))
.expect("no alt")
.as_string()
.expect("not a string"),
"somewhere",
"property `alt` not set properly"
);
}
#[test]
fn respects_apply_as() {
let attrs = Attributes::Static(&[
("href", "https://example.com/", ApplyAttributeAs::Attribute),
("alt", "somewhere", ApplyAttributeAs::Property),
]);
let (element, btree) = create_element();
attrs.apply(&btree, &element);
assert_eq!(
element.outer_html(),
"<a href=\"https://example.com/\"></a>",
"should be set as attribute"
);
assert_eq!(
Reflect::get(element.as_ref(), &JsValue::from_str("alt"))
.expect("no alt")
.as_string()
.expect("not a string"),
"somewhere",
"property `alt` not set properly"
);
}
#[test]
fn class_is_always_attrs() {
let attrs = Attributes::Static(&[("class", "thing", ApplyAttributeAs::Attribute)]);
let (element, btree) = create_element();
attrs.apply(&btree, &element);
assert_eq!(element.get_attribute("class").unwrap(), "thing");
}
#[test]
async fn macro_syntax_works() {
#[function_component]
fn Comp() -> Html {
html! { <a href="https://example.com/" ~alt="abc" /> }
}
let output = gloo::utils::document().get_element_by_id("output").unwrap();
yew::Renderer::<Comp>::with_root(output.clone()).render();
gloo::timers::future::sleep(Duration::from_secs(1)).await;
let element = output.query_selector("a").unwrap().unwrap();
assert_eq!(
element.get_attribute("href").unwrap(),
"https://example.com/"
);
assert_eq!(
Reflect::get(element.as_ref(), &JsValue::from_str("alt"))
.expect("no alt")
.as_string()
.expect("not a string"),
"abc",
"property `alt` not set properly"
);
}
}

View File

@ -970,7 +970,7 @@ mod tests {
.unwrap() .unwrap()
.outer_html(), .outer_html(),
"<div tabindex=\"0\"></div>" "<div tabindex=\"0\"></div>"
) );
} }
} }

View File

@ -145,6 +145,14 @@ mod feat_ssr {
} }
} }
/// Defines if the [`Attributes`] is set as element's attribute or property
#[allow(missing_docs)]
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum ApplyAttributeAs {
Attribute,
Property,
}
/// A collection of attributes for an element /// A collection of attributes for an element
#[derive(PartialEq, Eq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
pub enum Attributes { pub enum Attributes {
@ -152,7 +160,7 @@ pub enum Attributes {
/// ///
/// Allows optimizing comparison to a simple pointer equality check and reducing allocations, /// Allows optimizing comparison to a simple pointer equality check and reducing allocations,
/// if the attributes do not change on a node. /// if the attributes do not change on a node.
Static(&'static [[&'static str; 2]]), Static(&'static [(&'static str, &'static str, ApplyAttributeAs)]),
/// Static list of attribute keys with possibility to exclude attributes and dynamic attribute /// Static list of attribute keys with possibility to exclude attributes and dynamic attribute
/// values. /// values.
@ -165,12 +173,12 @@ pub enum Attributes {
/// Attribute values. Matches [keys](Attributes::Dynamic::keys). Optional attributes are /// Attribute values. Matches [keys](Attributes::Dynamic::keys). Optional attributes are
/// designated by setting [None]. /// designated by setting [None].
values: Box<[Option<AttrValue>]>, values: Box<[Option<(AttrValue, ApplyAttributeAs)>]>,
}, },
/// IndexMap is used to provide runtime attribute deduplication in cases where the html! macro /// IndexMap is used to provide runtime attribute deduplication in cases where the html! macro
/// was not used to guarantee it. /// was not used to guarantee it.
IndexMap(IndexMap<AttrValue, AttrValue>), IndexMap(IndexMap<AttrValue, (AttrValue, ApplyAttributeAs)>),
} }
impl Attributes { impl Attributes {
@ -183,19 +191,19 @@ impl Attributes {
/// This function is suboptimal and does not inline well. Avoid on hot paths. /// This function is suboptimal and does not inline well. Avoid on hot paths.
pub fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&'a str, &'a str)> + 'a> { pub fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (&'a str, &'a str)> + 'a> {
match self { match self {
Self::Static(arr) => Box::new(arr.iter().map(|kv| (kv[0], kv[1] as &'a str))), Self::Static(arr) => Box::new(arr.iter().map(|(k, v, _)| (*k, *v as &'a str))),
Self::Dynamic { keys, values } => Box::new( Self::Dynamic { keys, values } => Box::new(
keys.iter() keys.iter()
.zip(values.iter()) .zip(values.iter())
.filter_map(|(k, v)| v.as_ref().map(|v| (*k, v.as_ref()))), .filter_map(|(k, v)| v.as_ref().map(|(v, _)| (*k, v.as_ref()))),
), ),
Self::IndexMap(m) => Box::new(m.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))), Self::IndexMap(m) => Box::new(m.iter().map(|(k, (v, _))| (k.as_ref(), v.as_ref()))),
} }
} }
/// Get a mutable reference to the underlying `IndexMap`. /// Get a mutable reference to the underlying `IndexMap`.
/// If the attributes are stored in the `Vec` variant, it will be converted. /// If the attributes are stored in the `Vec` variant, it will be converted.
pub fn get_mut_index_map(&mut self) -> &mut IndexMap<AttrValue, AttrValue> { pub fn get_mut_index_map(&mut self) -> &mut IndexMap<AttrValue, (AttrValue, ApplyAttributeAs)> {
macro_rules! unpack { macro_rules! unpack {
() => { () => {
match self { match self {
@ -209,7 +217,11 @@ impl Attributes {
match self { match self {
Self::IndexMap(m) => m, Self::IndexMap(m) => m,
Self::Static(arr) => { Self::Static(arr) => {
*self = Self::IndexMap(arr.iter().map(|kv| (kv[0].into(), kv[1].into())).collect()); *self = Self::IndexMap(
arr.iter()
.map(|(k, v, ty)| ((*k).into(), ((*v).into(), *ty)))
.collect(),
);
unpack!() unpack!()
} }
Self::Dynamic { keys, values } => { Self::Dynamic { keys, values } => {
@ -227,7 +239,11 @@ impl Attributes {
} }
impl From<IndexMap<AttrValue, AttrValue>> for Attributes { impl From<IndexMap<AttrValue, AttrValue>> for Attributes {
fn from(v: IndexMap<AttrValue, AttrValue>) -> Self { fn from(map: IndexMap<AttrValue, AttrValue>) -> Self {
let v = map
.into_iter()
.map(|(k, v)| (k, (v, ApplyAttributeAs::Attribute)))
.collect();
Self::IndexMap(v) Self::IndexMap(v)
} }
} }
@ -236,7 +252,7 @@ impl From<IndexMap<&'static str, AttrValue>> for Attributes {
fn from(v: IndexMap<&'static str, AttrValue>) -> Self { fn from(v: IndexMap<&'static str, AttrValue>) -> Self {
let v = v let v = v
.into_iter() .into_iter()
.map(|(k, v)| (AttrValue::Static(k), v)) .map(|(k, v)| (AttrValue::Static(k), (v, ApplyAttributeAs::Attribute)))
.collect(); .collect();
Self::IndexMap(v) Self::IndexMap(v)
} }

View File

@ -9,7 +9,7 @@ use std::rc::Rc;
use web_sys::{HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; use web_sys::{HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement};
use super::{AttrValue, Attributes, Key, Listener, Listeners, VList, VNode}; use super::{ApplyAttributeAs, AttrValue, Attributes, Key, Listener, Listeners, VList, VNode};
use crate::html::{IntoPropValue, NodeRef}; use crate::html::{IntoPropValue, NodeRef};
/// SVG namespace string used for creating svg elements /// SVG namespace string used for creating svg elements
@ -363,9 +363,20 @@ impl VTag {
/// Not every attribute works when it set as an attribute. We use workarounds for: /// Not every attribute works when it set as an attribute. We use workarounds for:
/// `value` and `checked`. /// `value` and `checked`.
pub fn add_attribute(&mut self, key: &'static str, value: impl Into<AttrValue>) { pub fn add_attribute(&mut self, key: &'static str, value: impl Into<AttrValue>) {
self.attributes self.attributes.get_mut_index_map().insert(
.get_mut_index_map() AttrValue::Static(key),
.insert(AttrValue::Static(key), value.into()); (value.into(), ApplyAttributeAs::Attribute),
);
}
/// Set the given key as property on the element
///
/// [`js_sys::Reflect`] is used for setting properties.
pub fn add_property(&mut self, key: &'static str, value: impl Into<AttrValue>) {
self.attributes.get_mut_index_map().insert(
AttrValue::Static(key),
(value.into(), ApplyAttributeAs::Property),
);
} }
/// Sets attributes to a virtual node. /// Sets attributes to a virtual node.
@ -378,9 +389,10 @@ impl VTag {
#[doc(hidden)] #[doc(hidden)]
pub fn __macro_push_attr(&mut self, key: &'static str, value: impl IntoPropValue<AttrValue>) { pub fn __macro_push_attr(&mut self, key: &'static str, value: impl IntoPropValue<AttrValue>) {
self.attributes self.attributes.get_mut_index_map().insert(
.get_mut_index_map() AttrValue::from(key),
.insert(AttrValue::from(key), value.into_prop_value()); (value.into_prop_value(), ApplyAttributeAs::Property),
);
} }
/// Add event listener on the [VTag]'s [Element](web_sys::Element). /// Add event listener on the [VTag]'s [Element](web_sys::Element).

View File

@ -15,7 +15,7 @@ use yew::platform::time::sleep;
use yew::prelude::*; use yew::prelude::*;
use yew::suspense::{use_future, use_future_with_deps, Suspension, SuspensionResult}; use yew::suspense::{use_future, use_future_with_deps, Suspension, SuspensionResult};
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test] #[wasm_bindgen_test]
async fn suspense_works() { async fn suspense_works() {