diff --git a/examples/counter_functional/src/main.rs b/examples/counter_functional/src/main.rs index c85b7549346..54dcd2806d1 100644 --- a/examples/counter_functional/src/main.rs +++ b/examples/counter_functional/src/main.rs @@ -14,13 +14,13 @@ fn App() -> Html { Callback::from(move |_| state.set(*state - 1)) }; - html!( + html! { <> -

{"current count: "} {*state}

- - +

{"current count: "} {*state}

+ + - ) + } } fn main() { diff --git a/packages/yew-macro/src/html_tree/html_element.rs b/packages/yew-macro/src/html_tree/html_element.rs index db4dfae77fb..e12b8177fa1 100644 --- a/packages/yew-macro/src/html_tree/html_element.rs +++ b/packages/yew-macro/src/html_tree/html_element.rs @@ -8,7 +8,7 @@ use syn::spanned::Spanned; use syn::{Block, Expr, Ident, Lit, LitStr, Token}; 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::{non_capitalized_ascii, Peek, PeekValue}; @@ -135,39 +135,58 @@ impl ToTokens for HtmlElement { // other attributes let attributes = { - let normal_attrs = attributes.iter().map(|Prop { label, value, .. }| { - (label.to_lit_str(), value.optimize_literals_tagged()) - }); - let boolean_attrs = booleans.iter().filter_map(|Prop { label, value, .. }| { - let key = label.to_lit_str(); - Some(( - key.clone(), - match value { - Expr::Lit(e) => match &e.lit { - Lit::Bool(b) => Value::Static(if b.value { - quote! { #key } - } else { - 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) - ) + let normal_attrs = attributes.iter().map( + |Prop { + label, + value, + directive, + .. + }| { + ( + label.to_lit_str(), + value.optimize_literals_tagged(), + *directive, + ) + }, + ); + let boolean_attrs = booleans.iter().filter_map( + |Prop { + label, + value, + directive, + .. + }| { + let key = label.to_lit_str(); + Some(( + key.clone(), + match value { + Expr::Lit(e) => match &e.lit { + Lit::Bool(b) => Value::Static(if b.value { + quote! { #key } } 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 { ClassesForm::Tuple(classes) => { let span = classes.span(); @@ -196,6 +215,7 @@ impl ToTokens for HtmlElement { __yew_classes } }), + None, )) } ClassesForm::Single(classes) => { @@ -207,6 +227,7 @@ impl ToTokens for HtmlElement { Some(( LitStr::new("class", lit.span()), Value::Static(quote! { #lit }), + None, )) } } @@ -216,21 +237,34 @@ impl ToTokens for HtmlElement { Value::Dynamic(quote! { ::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` - fn try_into_static(src: &[(LitStr, Value)]) -> Option { + fn try_into_static( + src: &[(LitStr, Value, Option)], + ) -> Option { 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 { Value::Static(v) => quote! { #v }, 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),*]) }) @@ -239,10 +273,14 @@ impl ToTokens for HtmlElement { let attrs = normal_attrs .chain(boolean_attrs) .chain(class_attr) - .collect::>(); + .collect::)>>(); try_into_static(&attrs).unwrap_or_else(|| { - let keys = attrs.iter().map(|(k, _)| quote! { #k }); - let values = attrs.iter().map(|(_, v)| wrap_attr_value(v)); + let keys = attrs.iter().map(|(k, ..)| quote! { #k }); + 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! { ::yew::virtual_dom::Attributes::Dynamic{ keys: &[#(#keys),*], diff --git a/packages/yew-macro/src/props/prop.rs b/packages/yew-macro/src/props/prop.rs index 8dbd568efc2..82ae6375ff1 100644 --- a/packages/yew-macro/src/props/prop.rs +++ b/packages/yew-macro/src/props/prop.rs @@ -13,17 +13,27 @@ use super::CHILDREN_LABEL; use crate::html_tree::HtmlDashedName; use crate::stringify::Stringify; +#[derive(Copy, Clone)] +pub enum PropDirective { + ApplyAsProperty(Token![~]), +} + pub struct Prop { + pub directive: Option, pub label: HtmlDashedName, /// Punctuation between `label` and `value`. pub value: Expr, } impl Parse for Prop { fn parse(input: ParseStream) -> syn::Result { + let directive = input + .parse::() + .map(PropDirective::ApplyAsProperty) + .ok(); if input.peek(Brace) { - Self::parse_shorthand_prop_assignment(input) + Self::parse_shorthand_prop_assignment(input, directive) } 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}` /// This only allows for labels with no hyphens, as it would otherwise create /// an ambiguity in the syntax - fn parse_shorthand_prop_assignment(input: ParseStream) -> syn::Result { + fn parse_shorthand_prop_assignment( + input: ParseStream, + directive: Option, + ) -> syn::Result { let value; let _brace = braced!(value in input); let expr = value.parse::()?; @@ -44,7 +57,7 @@ impl Prop { }) = expr { if let (Some(ident), true) = (path.get_ident(), attrs.is_empty()) { - syn::Result::Ok(HtmlDashedName::from(ident.clone())) + Ok(HtmlDashedName::from(ident.clone())) } else { Err(syn::Error::new_spanned( 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}` - fn parse_prop_assignment(input: ParseStream) -> syn::Result { + fn parse_prop_assignment( + input: ParseStream, + directive: Option, + ) -> syn::Result { let label = input.parse::()?; let equals = input.parse::().map_err(|_| { syn::Error::new_spanned( @@ -83,7 +103,11 @@ impl Prop { } 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 { match &expr { Expr::Lit(_) => Ok(expr), - _ => Err(syn::Error::new_spanned( + ref exp => Err(syn::Error::new_spanned( &expr, - "the property value must be either a literal or enclosed in braces. Consider \ - adding braces around your expression.", + format!( + "the property value must be either a literal or enclosed in braces. Consider \ + adding braces around your expression.: {:#?}", + exp + ), )), } } diff --git a/packages/yew-macro/src/props/prop_macro.rs b/packages/yew-macro/src/props/prop_macro.rs index 172b5f53be6..1ff4312276b 100644 --- a/packages/yew-macro/src/props/prop_macro.rs +++ b/packages/yew-macro/src/props/prop_macro.rs @@ -61,7 +61,11 @@ impl Parse for PropValue { impl From for Prop { fn from(prop_value: PropValue) -> Prop { let PropValue { label, value } = prop_value; - Prop { label, value } + Prop { + label, + value, + directive: None, + } } } diff --git a/packages/yew-macro/tests/html_macro/component-fail.stderr b/packages/yew-macro/tests/html_macro/component-fail.stderr index fb02ae65600..1a756718b44 100644 --- a/packages/yew-macro/tests/html_macro/component-fail.stderr +++ b/packages/yew-macro/tests/html_macro/component-fail.stderr @@ -249,7 +249,13 @@ help: escape `type` to use it as an identifier 85 | html! { }; | ++ -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 | 86 | html! { }; @@ -309,7 +315,24 @@ error: only one root html element is allowed (hint: you can wrap multiple html e 102 | html! { }; | ^^^^^^^^^^^^^^^ -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 | 106 | html! { }; diff --git a/packages/yew-macro/tests/html_macro/element-fail.stderr b/packages/yew-macro/tests/html_macro/element-fail.stderr index 089444ed8ba..08072df2e7f 100644 --- a/packages/yew-macro/tests/html_macro/element-fail.stderr +++ b/packages/yew-macro/tests/html_macro/element-fail.stderr @@ -142,61 +142,260 @@ error: dynamic closing tags must not have a body (hint: replace it with just ` }; | ^^^^^^^^ -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 | 83 | html! {
}; | ^^^^^^^^^^^^^^^^^^^^^^^^^ -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 | 84 | html! { }; | ^^ -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 | 85 | html! { }; | ^^ -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 | 86 | html! { }; | ^^^^^^^ -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 | 87 | html! { }; | ^^^^^^^^^^^ -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 | 88 | html! { }; | ^^^^^^^^^^^^^^^^^ -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 | 89 | html! { }; | ^^^^^^^ -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 | 90 | html! { }; | ^^ -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 | 91 | html! { }; | ^^ -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 | 92 | html! { }; diff --git a/packages/yew-macro/tests/html_macro/list-fail.stderr b/packages/yew-macro/tests/html_macro/list-fail.stderr index 2c24fca6132..49cda7d753d 100644 --- a/packages/yew-macro/tests/html_macro/list-fail.stderr +++ b/packages/yew-macro/tests/html_macro/list-fail.stderr @@ -1,65 +1,87 @@ 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! { <> }; | ^^ 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! { <><> }; | ^^ 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! { <><> }; | ^^ 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! { }; | ^^^ 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! { }; | ^^^ 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! { <><> }; | ^^^^^ error: expected a valid html element - --> $DIR/list-fail.rs:16:15 + --> tests/html_macro/list-fail.rs:16:15 | 16 | html! { <>invalid }; | ^^^^^^^ error: expected an expression following this equals sign - --> $DIR/list-fail.rs:18:17 + --> tests/html_macro/list-fail.rs:18:17 | 18 | html! { }; | ^^ -error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression. - --> $DIR/list-fail.rs:20:18 +error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: MethodCall( + 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! { }; | ^^^^^^^^^^^^^^^^^ 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! { }; | ^^^ error: fragments only accept the `key` prop - --> $DIR/list-fail.rs:25:14 + --> tests/html_macro/list-fail.rs:25:14 | 25 | html! { }; | ^^^^^^^^^ diff --git a/packages/yew/src/dom_bundle/btag/attributes.rs b/packages/yew/src/dom_bundle/btag/attributes.rs index ecd786e5494..040905ba7f1 100644 --- a/packages/yew/src/dom_bundle/btag/attributes.rs +++ b/packages/yew/src/dom_bundle/btag/attributes.rs @@ -2,13 +2,14 @@ use std::collections::HashMap; use std::ops::Deref; use indexmap::IndexMap; +use wasm_bindgen::JsValue; use web_sys::{Element, HtmlInputElement as InputElement, HtmlTextAreaElement as TextAreaElement}; use yew::AttrValue; use super::Apply; use crate::dom_bundle::BSubtree; use crate::virtual_dom::vtag::{InputFields, Value}; -use crate::virtual_dom::Attributes; +use crate::virtual_dom::{ApplyAttributeAs, Attributes}; impl Apply for Value { type Bundle = Self; @@ -87,23 +88,23 @@ impl Attributes { #[cold] fn apply_diff_index_maps( el: &Element, - new: &IndexMap, - old: &IndexMap, + new: &IndexMap, + old: &IndexMap, ) { for (key, value) in new.iter() { match old.get(key) { Some(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) { - Self::remove_attribute(el, key); + Self::remove(el, key, *apply_as); } } } @@ -112,17 +113,26 @@ impl Attributes { /// Works with any [Attributes] variants. #[cold] 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::*; 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 .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(), - IndexMap(m) => m.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect(), } } @@ -135,25 +145,42 @@ impl Attributes { Some(old) => old != new, None => true, } { - el.set_attribute(k, new).unwrap(); + Self::set(el, k, new.0, new.1); } } // Remove missing - for k in old.keys() { + for (k, (_, apply_as)) in old.iter() { 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) { - el.set_attribute(key, value).expect("invalid attribute key") + fn set(el: &Element, key: &str, value: &str, apply_as: ApplyAttributeAs) { + 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) { - el.remove_attribute(key) - .expect("could not remove attribute") + fn remove(el: &Element, key: &str, apply_as: ApplyAttributeAs) { + match apply_as { + 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 { match &self { Self::Static(arr) => { - for kv in arr.iter() { - Self::set_attribute(el, kv[0], kv[1]); + for (k, v, apply_as) in arr.iter() { + Self::set(el, *k, *v, *apply_as); } } Self::Dynamic { keys, values } => { for (k, v) in keys.iter().zip(values.iter()) { - if let Some(v) = v { - Self::set_attribute(el, k, v) + if let Some((v, apply_as)) = v { + Self::set(el, k, v, *apply_as) } } } Self::IndexMap(m) => { - for (k, v) in m.iter() { - Self::set_attribute(el, k, v) + for (k, (v, apply_as)) in m.iter() { + Self::set(el, k, v, *apply_as) } } } @@ -217,7 +244,7 @@ impl Apply for Attributes { } macro_rules! set { ($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), - (None, Some(_)) => { - Self::remove_attribute(el, key!()); + (None, Some(old)) => { + Self::remove(el, key!(), old.1); } (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(), + "", + "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! { } + } + + let output = gloo::utils::document().get_element_by_id("output").unwrap(); + yew::Renderer::::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" + ); + } +} diff --git a/packages/yew/src/dom_bundle/btag/mod.rs b/packages/yew/src/dom_bundle/btag/mod.rs index 55262c346e2..d504ea0386b 100644 --- a/packages/yew/src/dom_bundle/btag/mod.rs +++ b/packages/yew/src/dom_bundle/btag/mod.rs @@ -970,7 +970,7 @@ mod tests { .unwrap() .outer_html(), "
" - ) + ); } } diff --git a/packages/yew/src/virtual_dom/mod.rs b/packages/yew/src/virtual_dom/mod.rs index e7d9797b18c..f24a2db6931 100644 --- a/packages/yew/src/virtual_dom/mod.rs +++ b/packages/yew/src/virtual_dom/mod.rs @@ -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 #[derive(PartialEq, Eq, Clone, Debug)] pub enum Attributes { @@ -152,7 +160,7 @@ pub enum Attributes { /// /// Allows optimizing comparison to a simple pointer equality check and reducing allocations, /// 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 /// values. @@ -165,12 +173,12 @@ pub enum Attributes { /// Attribute values. Matches [keys](Attributes::Dynamic::keys). Optional attributes are /// designated by setting [None]. - values: Box<[Option]>, + values: Box<[Option<(AttrValue, ApplyAttributeAs)>]>, }, /// IndexMap is used to provide runtime attribute deduplication in cases where the html! macro /// was not used to guarantee it. - IndexMap(IndexMap), + IndexMap(IndexMap), } impl Attributes { @@ -183,19 +191,19 @@ impl Attributes { /// This function is suboptimal and does not inline well. Avoid on hot paths. pub fn iter<'a>(&'a self) -> Box + 'a> { 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( keys.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`. /// If the attributes are stored in the `Vec` variant, it will be converted. - pub fn get_mut_index_map(&mut self) -> &mut IndexMap { + pub fn get_mut_index_map(&mut self) -> &mut IndexMap { macro_rules! unpack { () => { match self { @@ -209,7 +217,11 @@ impl Attributes { match self { Self::IndexMap(m) => m, 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!() } Self::Dynamic { keys, values } => { @@ -227,7 +239,11 @@ impl Attributes { } impl From> for Attributes { - fn from(v: IndexMap) -> Self { + fn from(map: IndexMap) -> Self { + let v = map + .into_iter() + .map(|(k, v)| (k, (v, ApplyAttributeAs::Attribute))) + .collect(); Self::IndexMap(v) } } @@ -236,7 +252,7 @@ impl From> for Attributes { fn from(v: IndexMap<&'static str, AttrValue>) -> Self { let v = v .into_iter() - .map(|(k, v)| (AttrValue::Static(k), v)) + .map(|(k, v)| (AttrValue::Static(k), (v, ApplyAttributeAs::Attribute))) .collect(); Self::IndexMap(v) } diff --git a/packages/yew/src/virtual_dom/vtag.rs b/packages/yew/src/virtual_dom/vtag.rs index cf69128ec31..a4e423a4f59 100644 --- a/packages/yew/src/virtual_dom/vtag.rs +++ b/packages/yew/src/virtual_dom/vtag.rs @@ -9,7 +9,7 @@ use std::rc::Rc; 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}; /// 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: /// `value` and `checked`. pub fn add_attribute(&mut self, key: &'static str, value: impl Into) { - self.attributes - .get_mut_index_map() - .insert(AttrValue::Static(key), value.into()); + self.attributes.get_mut_index_map().insert( + AttrValue::Static(key), + (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) { + self.attributes.get_mut_index_map().insert( + AttrValue::Static(key), + (value.into(), ApplyAttributeAs::Property), + ); } /// Sets attributes to a virtual node. @@ -378,9 +389,10 @@ impl VTag { #[doc(hidden)] pub fn __macro_push_attr(&mut self, key: &'static str, value: impl IntoPropValue) { - self.attributes - .get_mut_index_map() - .insert(AttrValue::from(key), value.into_prop_value()); + self.attributes.get_mut_index_map().insert( + AttrValue::from(key), + (value.into_prop_value(), ApplyAttributeAs::Property), + ); } /// Add event listener on the [VTag]'s [Element](web_sys::Element). diff --git a/packages/yew/tests/suspense.rs b/packages/yew/tests/suspense.rs index 41980ac279a..969cc5fcbf6 100644 --- a/packages/yew/tests/suspense.rs +++ b/packages/yew/tests/suspense.rs @@ -15,7 +15,7 @@ use yew::platform::time::sleep; use yew::prelude::*; 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] async fn suspense_works() {