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 `
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
|
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() {