diff --git a/packages/yew-macro/src/html_tree/html_element.rs b/packages/yew-macro/src/html_tree/html_element.rs
index ee588305de6..4830a0a48e6 100644
--- a/packages/yew-macro/src/html_tree/html_element.rs
+++ b/packages/yew-macro/src/html_tree/html_element.rs
@@ -4,10 +4,10 @@ use quote::{quote, quote_spanned, ToTokens};
use syn::buffer::Cursor;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
-use syn::{Expr, Ident, Lit, LitStr, Token};
+use syn::{Expr, ExprLit, Ident, Lit, LitStr, Token};
use super::{HtmlChildrenTree, HtmlDashedName, TagTokens};
-use crate::props::{ElementProps, Prop, PropDirective};
+use crate::props::{ElementProps, Prop, PropDirective, PropLabel};
use crate::stringify::{Stringify, Value};
use crate::{is_ide_completion, non_capitalized_ascii, Peek, PeekValue};
@@ -138,6 +138,30 @@ impl ToTokens for HtmlElement {
// other attributes
let attributes = {
+ #[derive(Clone)]
+ enum Key {
+ Static(LitStr),
+ Dynamic(Expr),
+ }
+
+ impl From<&PropLabel> for Key {
+ fn from(value: &PropLabel) -> Self {
+ match value {
+ PropLabel::Static(dashed_name) => Self::Static(dashed_name.to_lit_str()),
+ PropLabel::Dynamic(expr) => Self::Dynamic(expr.clone()),
+ }
+ }
+ }
+
+ impl ToTokens for Key {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ tokens.extend(match self {
+ Key::Static(dashed_name) => quote! { #dashed_name },
+ Key::Dynamic(expr) => quote! { #expr },
+ });
+ }
+ }
+
let normal_attrs = attributes.iter().map(
|Prop {
label,
@@ -146,7 +170,7 @@ impl ToTokens for HtmlElement {
..
}| {
(
- label.to_lit_str(),
+ Key::from(label),
value.optimize_literals_tagged(),
*directive,
)
@@ -159,26 +183,30 @@ impl ToTokens for HtmlElement {
directive,
..
}| {
- let key = label.to_lit_str();
+ let key = Key::from(label);
+ let lit = match &key {
+ Key::Static(lit) => lit,
+ Key::Dynamic(_) => unreachable!(),
+ };
Some((
key.clone(),
match value {
Expr::Lit(e) => match &e.lit {
Lit::Bool(b) => Value::Static(if b.value {
- quote! { #key }
+ quote! { #lit }
} else {
return None;
}),
_ => Value::Dynamic(quote_spanned! {value.span()=> {
::yew::utils::__ensure_type::<::std::primitive::bool>(#value);
- #key
+ #lit
}}),
},
expr => Value::Dynamic(
quote_spanned! {expr.span().resolved_at(Span::call_site())=>
if #expr {
::std::option::Option::Some(
- ::yew::virtual_dom::AttrValue::Static(#key)
+ ::yew::virtual_dom::AttrValue::Static(#lit)
)
} else {
::std::option::Option::None
@@ -200,7 +228,7 @@ impl ToTokens for HtmlElement {
None
} else {
Some((
- LitStr::new("class", lit.span()),
+ Key::Static(LitStr::new("class", lit.span())),
Value::Static(quote! { #lit }),
None,
))
@@ -209,7 +237,7 @@ impl ToTokens for HtmlElement {
None => {
let expr = &classes.value;
Some((
- LitStr::new("class", classes.label.span()),
+ Key::Static(LitStr::new("class", classes.label.span())),
Value::Dynamic(quote! {
::std::convert::Into::<::yew::html::Classes>::into(#expr)
}),
@@ -219,15 +247,13 @@ impl ToTokens for HtmlElement {
});
/// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Static`
- fn try_into_static(
- src: &[(LitStr, Value, Option)],
- ) -> Option {
- if src
- .iter()
- .any(|(_, _, d)| matches!(d, Some(PropDirective::ApplyAsProperty(_))))
- {
+ fn try_into_static(src: &[(Key, Value, Option)]) -> Option {
+ if src.iter().any(|(k, _, d)| {
+ matches!(k, Key::Dynamic(_))
+ || matches!(d, Some(PropDirective::ApplyAsProperty(_)))
+ }) {
// don't try to make a static attribute list if there are any properties to
- // assign
+ // assign or any labels are dynamic
return None;
}
let mut kv = Vec::with_capacity(src.len());
@@ -252,13 +278,24 @@ impl ToTokens for HtmlElement {
Some(quote! { ::yew::virtual_dom::Attributes::Static(&[#(#kv),*]) })
}
- let attrs = normal_attrs
- .chain(boolean_attrs)
- .chain(class_attr)
- .collect::)>>();
- try_into_static(&attrs).unwrap_or_else(|| {
- let keys = attrs.iter().map(|(k, ..)| quote! { #k });
- let values = attrs.iter().map(|(_, v, directive)| {
+ /// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Dynamic`
+ fn try_into_dynamic(
+ src: &[(Key, Value, Option)],
+ ) -> Option {
+ if src.iter().any(|(k, ..)| {
+ !matches!(
+ k,
+ Key::Dynamic(Expr::Lit(ExprLit {
+ lit: Lit::Str(_),
+ ..
+ })) | Key::Static(_)
+ )
+ }) {
+ // use IndexMap if there are any dynamic-expr labels
+ return None;
+ }
+ let keys = src.iter().map(|(k, ..)| quote! { #k });
+ let values = src.iter().map(|(_, v, directive)| {
let value = match directive {
Some(PropDirective::ApplyAsProperty(token)) => {
quote_spanned!(token.span()=> ::std::option::Option::Some(
@@ -276,11 +313,50 @@ impl ToTokens for HtmlElement {
};
quote! { #value }
});
- quote! {
+ Some(quote! {
::yew::virtual_dom::Attributes::Dynamic{
keys: &[#(#keys),*],
values: ::std::boxed::Box::new([#(#values),*]),
}
+ })
+ }
+
+ let attrs = normal_attrs
+ .chain(boolean_attrs)
+ .chain(class_attr)
+ .collect::)>>();
+ try_into_static(&attrs).or_else(|| try_into_dynamic(&attrs)).unwrap_or_else(|| {
+ let results = attrs.iter()
+ .map(|(k, v, directive)| {
+ let value = match directive {
+ Some(PropDirective::ApplyAsProperty(token)) => {
+ quote_spanned!(token.span()=> ::std::option::Option::Some(
+ ::yew::virtual_dom::AttributeOrProperty::Property(
+ ::std::convert::Into::into(#v)
+ ))
+ )
+ }
+ None => {
+ let value = wrap_attr_value(v);
+ quote! {
+ ::std::option::Option::map(#value, ::yew::virtual_dom::AttributeOrProperty::Attribute)
+ }
+ },
+ };
+ quote! { (::std::convert::Into::into(#k), #value) }
+ });
+ quote! {
+ ::yew::virtual_dom::Attributes::IndexMap(
+ ::std::rc::Rc::new(
+ ::std::iter::Iterator::collect(
+ ::std::iter::Iterator::filter_map(
+ ::std::iter::IntoIterator::into_iter([#(#results),*]),
+ // FIXME verify if i understood it correctly
+ |(k, v)| v.map(|v| (k, v))
+ )
+ )
+ )
+ )
}
})
};
@@ -289,7 +365,8 @@ impl ToTokens for HtmlElement {
quote! { ::yew::virtual_dom::listeners::Listeners::None }
} else {
let listeners_it = listeners.iter().map(|Prop { label, value, .. }| {
- let name = &label.name;
+ // TODO: consider making a `ListenerProp` that has dashed name's name and value
+ let name = &<&HtmlDashedName>::try_from(label).unwrap().name;
quote! {
::yew::html::#name::Wrapper::__macro_new(#value)
}
diff --git a/packages/yew-macro/src/html_tree/html_list.rs b/packages/yew-macro/src/html_tree/html_list.rs
index 301b533a92c..58d228239db 100644
--- a/packages/yew-macro/src/html_tree/html_list.rs
+++ b/packages/yew-macro/src/html_tree/html_list.rs
@@ -134,11 +134,14 @@ impl Parse for HtmlListProps {
return Err(input.error("only a single `key` prop is allowed on a fragment"));
}
- if prop.label.to_ascii_lowercase_string() != "key" {
- return Err(syn::Error::new_spanned(
- prop.label,
- "fragments only accept the `key` prop",
- ));
+ match String::try_from(&prop.label) {
+ Ok(label) if label.eq_ignore_ascii_case("key") => {}
+ _ => {
+ return Err(syn::Error::new_spanned(
+ prop.label,
+ "fragments only accept the `key` prop",
+ ))
+ }
}
Some(prop.value)
diff --git a/packages/yew-macro/src/html_tree/lint/mod.rs b/packages/yew-macro/src/html_tree/lint/mod.rs
index bbb34f24648..cf166f9f680 100644
--- a/packages/yew-macro/src/html_tree/lint/mod.rs
+++ b/packages/yew-macro/src/html_tree/lint/mod.rs
@@ -45,10 +45,12 @@ where
///
/// Attribute names are lowercased before being compared (so pass "href" for `name` and not "HREF").
fn get_attribute<'a>(props: &'a ElementProps, name: &str) -> Option<&'a Prop> {
- props
- .attributes
- .iter()
- .find(|item| item.label.eq_ignore_ascii_case(name))
+ props.attributes.iter().find(|item| {
+ matches!(
+ String::try_from(&item.label),
+ Ok(label) if label.eq_ignore_ascii_case(name)
+ )
+ })
}
/// Lints to check if anchor (``) tags have valid `href` attributes defined.
diff --git a/packages/yew-macro/src/props/component.rs b/packages/yew-macro/src/props/component.rs
index 3c0984e611c..9d06381cf53 100644
--- a/packages/yew-macro/src/props/component.rs
+++ b/packages/yew-macro/src/props/component.rs
@@ -7,7 +7,7 @@ use syn::spanned::Spanned;
use syn::token::DotDot;
use syn::Expr;
-use super::{Prop, Props, SpecialProps, CHILDREN_LABEL};
+use super::{Prop, PropLabel, Props, SpecialProps, CHILDREN_LABEL};
struct BaseExpr {
pub dot_dot: DotDot,
@@ -190,15 +190,12 @@ impl TryFrom for ComponentProps {
fn validate(props: Props) -> Result {
props.check_no_duplicates()?;
- props.check_all(|prop| {
- if !prop.label.extended.is_empty() {
- Err(syn::Error::new_spanned(
- &prop.label,
- "expected a valid Rust identifier",
- ))
- } else {
- Ok(())
- }
+ props.check_all(|prop| match &prop.label {
+ PropLabel::Static(dashed_name) if dashed_name.extended.is_empty() => Ok(()),
+ _ => Err(syn::Error::new_spanned(
+ &prop.label,
+ "components expect valid Rust identifiers for their property names",
+ )),
})?;
Ok(props)
diff --git a/packages/yew-macro/src/props/element.rs b/packages/yew-macro/src/props/element.rs
index 17a90e3f9db..7ec68769c29 100644
--- a/packages/yew-macro/src/props/element.rs
+++ b/packages/yew-macro/src/props/element.rs
@@ -19,14 +19,18 @@ impl Parse for ElementProps {
fn parse(input: ParseStream) -> syn::Result {
let mut props = input.parse::()?;
- let listeners =
- props.drain_filter(|prop| LISTENER_SET.contains(prop.label.to_string().as_str()));
+ let listeners = props.drain_filter(|prop| {
+ matches!(String::try_from(&prop.label),
+ Ok(prop) if LISTENER_SET.contains(prop.as_str()))
+ });
// Multiple listener attributes are allowed, but no others
props.check_no_duplicates()?;
- let booleans =
- props.drain_filter(|prop| BOOLEAN_SET.contains(prop.label.to_string().as_str()));
+ let booleans = props.drain_filter(|prop| {
+ matches!(String::try_from(&prop.label),
+ Ok(prop) if BOOLEAN_SET.contains(prop.as_str()))
+ });
let classes = props.pop("class");
let value = props.pop("value");
diff --git a/packages/yew-macro/src/props/prop.rs b/packages/yew-macro/src/props/prop.rs
index f1c0ad48079..7a78cde7745 100644
--- a/packages/yew-macro/src/props/prop.rs
+++ b/packages/yew-macro/src/props/prop.rs
@@ -2,11 +2,14 @@ use std::convert::TryFrom;
use std::ops::{Deref, DerefMut};
use proc_macro2::{Spacing, Span, TokenStream, TokenTree};
-use quote::{quote, quote_spanned};
+use quote::{quote, quote_spanned, ToTokens};
use syn::parse::{Parse, ParseBuffer, ParseStream};
use syn::spanned::Spanned;
use syn::token::Brace;
-use syn::{braced, Block, Expr, ExprBlock, ExprMacro, ExprPath, ExprRange, Stmt, Token};
+use syn::{
+ braced, parse_quote, Block, Expr, ExprBlock, ExprMacro, ExprPath, ExprRange, LitStr, Stmt,
+ Token,
+};
use crate::html_tree::HtmlDashedName;
use crate::stringify::Stringify;
@@ -16,9 +19,85 @@ pub enum PropDirective {
ApplyAsProperty(Token![~]),
}
+pub enum PropLabel {
+ Static(HtmlDashedName),
+ Dynamic(Expr),
+}
+
+impl From for PropLabel {
+ fn from(value: HtmlDashedName) -> Self {
+ Self::Static(value)
+ }
+}
+
+impl From for PropLabel {
+ fn from(value: LitStr) -> Self {
+ Self::Dynamic(parse_quote! { #value })
+ }
+}
+
+impl TryFrom for HtmlDashedName {
+ type Error = ();
+
+ fn try_from(value: PropLabel) -> Result {
+ use PropLabel::*;
+ match value {
+ Static(dashed_name) => Ok(dashed_name),
+ Dynamic(_) => Err(()),
+ }
+ }
+}
+
+impl<'a> TryFrom<&'a PropLabel> for &'a HtmlDashedName {
+ type Error = ();
+
+ fn try_from(value: &'a PropLabel) -> Result {
+ use PropLabel::*;
+ match value {
+ Static(dashed_name) => Ok(dashed_name),
+ Dynamic(_) => Err(()),
+ }
+ }
+}
+
+impl TryFrom for String {
+ type Error = ();
+
+ fn try_from(value: PropLabel) -> Result {
+ HtmlDashedName::try_from(value).map(|dashed_name| dashed_name.to_string())
+ }
+}
+
+impl TryFrom<&PropLabel> for String {
+ type Error = ();
+
+ fn try_from(value: &PropLabel) -> Result {
+ <&HtmlDashedName>::try_from(value).map(|dashed_name| dashed_name.to_string())
+ }
+}
+
+impl PartialEq for PropLabel {
+ fn eq(&self, other: &PropLabel) -> bool {
+ match (self, other) {
+ (Self::Static(l), Self::Static(r)) => l == r,
+ // NOTE: Dynamic props may repeat
+ _ => false,
+ }
+ }
+}
+
+impl ToTokens for PropLabel {
+ fn to_tokens(&self, tokens: &mut TokenStream) {
+ tokens.extend(match self {
+ PropLabel::Static(dashed_name) => quote! {#dashed_name},
+ PropLabel::Dynamic(expr) => quote! {#expr}, // FIXME this probably wanted its group back
+ });
+ }
+}
+
pub struct Prop {
pub directive: Option,
- pub label: HtmlDashedName,
+ pub label: PropLabel,
/// Punctuation between `label` and `value`.
pub value: Expr,
}
@@ -30,7 +109,9 @@ impl Parse for Prop {
.map(PropDirective::ApplyAsProperty)
.ok();
if input.peek(Brace) {
- Self::parse_shorthand_prop_assignment(input, directive)
+ Self::parse_shorthand_or_expr_dynamic_prop_assignment(input, directive)
+ } else if input.peek(LitStr) {
+ Self::parse_literal_dynamic_prop_assignment(input, directive)
} else {
Self::parse_prop_assignment(input, directive)
}
@@ -39,16 +120,31 @@ impl Parse for Prop {
/// Helpers for parsing props
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(
+ /// Parse a prop using the shorthand syntax `{value}`, short for `value={value}`,
+ /// or using the `{label}={value}` dynamic label syntax.
+ ///
+ /// Shorthand syntax only allows for labels with no hyphens,
+ /// as it would otherwise create an ambiguity in the syntax.
+ fn parse_shorthand_or_expr_dynamic_prop_assignment(
input: ParseStream,
directive: Option,
) -> syn::Result {
let value;
let _brace = braced!(value in input);
let expr = value.parse::()?;
+
+ // dynamic here
+ if input.peek(Token![=]) {
+ input.parse::().unwrap();
+ let value = parse_prop_value(input)?;
+ return Ok(Self {
+ label: PropLabel::Dynamic(expr),
+ value,
+ directive,
+ });
+ }
+ // otherwise, shorthand
+
let label = if let Expr::Path(ExprPath {
ref attrs,
qself: None,
@@ -72,12 +168,43 @@ impl Prop {
}?;
Ok(Self {
- label,
+ label: label.into(),
value: expr,
directive,
})
}
+ /// Parse a prop of the form `"label"={value}`
+ fn parse_literal_dynamic_prop_assignment(
+ input: ParseStream,
+ directive: Option,
+ ) -> syn::Result {
+ let label = input.parse::()?;
+ let equals = input.parse::().map_err(|_| {
+ let display = label.stringify();
+ syn::Error::new_spanned(
+ &label,
+ format!(
+ "`{display}` doesn't have a value. (hint: set the value to `true` or `false` \
+ for boolean attributes)"
+ ),
+ )
+ })?;
+ if input.is_empty() {
+ return Err(syn::Error::new_spanned(
+ equals,
+ "expected an expression following this equals sign",
+ ));
+ }
+
+ let value = parse_prop_value(input)?;
+ Ok(Self {
+ label: label.into(),
+ value,
+ directive,
+ })
+ }
+
/// Parse a prop of the form `label={value}`
fn parse_prop_assignment(
input: ParseStream,
@@ -102,7 +229,7 @@ impl Prop {
let value = parse_prop_value(input)?;
Ok(Self {
- label,
+ label: label.into(),
value,
directive,
})
@@ -226,12 +353,16 @@ impl PropList {
}
fn position(&self, key: &str) -> Option {
- self.0.iter().position(|it| it.label.to_string() == key)
+ self.0.iter().position(
+ |it| matches!(String::try_from(&it.label), Ok(dashed_name) if dashed_name == key),
+ )
}
/// Get the first prop with the given key.
pub fn get_by_label(&self, key: &str) -> Option<&Prop> {
- self.0.iter().find(|it| it.label.to_string() == key)
+ self.0
+ .iter()
+ .find(|it| matches!(String::try_from(&it.label), Ok(dashed_name) if dashed_name == key))
}
/// Pop the first prop with the given key.
@@ -292,7 +423,7 @@ impl PropList {
&prop.label,
format!(
"`{}` can only be specified once but is given here again",
- prop.label
+ String::try_from(&prop.label).unwrap()
),
)
}))
diff --git a/packages/yew-macro/src/props/prop_macro.rs b/packages/yew-macro/src/props/prop_macro.rs
index 2bddcebceb0..2ef67b54791 100644
--- a/packages/yew-macro/src/props/prop_macro.rs
+++ b/packages/yew-macro/src/props/prop_macro.rs
@@ -8,7 +8,7 @@ use syn::spanned::Spanned;
use syn::token::Brace;
use syn::{Expr, Token, TypePath};
-use super::{ComponentProps, Prop, PropList, Props};
+use super::{ComponentProps, Prop, PropLabel, PropList, Props};
use crate::html_tree::HtmlDashedName;
/// Pop from `Punctuated` without leaving it in a state where it has trailing punctuation.
@@ -45,6 +45,7 @@ struct PropValue {
label: HtmlDashedName,
value: Expr,
}
+
impl Parse for PropValue {
fn parse(input: ParseStream) -> syn::Result {
let label = input.parse()?;
@@ -62,7 +63,7 @@ impl From for Prop {
fn from(prop_value: PropValue) -> Prop {
let PropValue { label, value } = prop_value;
Prop {
- label,
+ label: PropLabel::Static(label),
value,
directive: None,
}
@@ -74,6 +75,7 @@ struct PropsExpr {
_brace_token: Brace,
fields: Punctuated,
}
+
impl Parse for PropsExpr {
fn parse(input: ParseStream) -> syn::Result {
let mut ty: TypePath = input.parse()?;
@@ -103,6 +105,7 @@ pub struct PropsMacroInput {
ty: TypePath,
props: ComponentProps,
}
+
impl Parse for PropsMacroInput {
fn parse(input: ParseStream) -> syn::Result {
let PropsExpr { ty, fields, .. } = input.parse()?;
@@ -121,6 +124,7 @@ impl Parse for PropsMacroInput {
})
}
}
+
impl ToTokens for PropsMacroInput {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self { ty, props } = self;
diff --git a/packages/yew-macro/tests/html_macro/component-fail.stderr b/packages/yew-macro/tests/html_macro/component-fail.stderr
index 440ffed4c64..3d960d2564e 100644
--- a/packages/yew-macro/tests/html_macro/component-fail.stderr
+++ b/packages/yew-macro/tests/html_macro/component-fail.stderr
@@ -259,7 +259,7 @@ error: the property value must be either a literal or enclosed in braces. Consid
86 | html! { };
| ^^
-error: expected a valid Rust identifier
+error: components expect valid Rust identifiers for their property names
--> tests/html_macro/component-fail.rs:87:20
|
87 | html! { };
diff --git a/packages/yew-macro/tests/html_macro/dyn-prop-fail.rs b/packages/yew-macro/tests/html_macro/dyn-prop-fail.rs
new file mode 100644
index 00000000000..fbd453add65
--- /dev/null
+++ b/packages/yew-macro/tests/html_macro/dyn-prop-fail.rs
@@ -0,0 +1,70 @@
+#![no_implicit_prelude]
+
+// Shadow primitives
+#[allow(non_camel_case_types)]
+pub struct bool;
+#[allow(non_camel_case_types)]
+pub struct char;
+#[allow(non_camel_case_types)]
+pub struct f32;
+#[allow(non_camel_case_types)]
+pub struct f64;
+#[allow(non_camel_case_types)]
+pub struct i128;
+#[allow(non_camel_case_types)]
+pub struct i16;
+#[allow(non_camel_case_types)]
+pub struct i32;
+#[allow(non_camel_case_types)]
+pub struct i64;
+#[allow(non_camel_case_types)]
+pub struct i8;
+#[allow(non_camel_case_types)]
+pub struct isize;
+#[allow(non_camel_case_types)]
+pub struct str;
+#[allow(non_camel_case_types)]
+pub struct u128;
+#[allow(non_camel_case_types)]
+pub struct u16;
+#[allow(non_camel_case_types)]
+pub struct u32;
+#[allow(non_camel_case_types)]
+pub struct u64;
+#[allow(non_camel_case_types)]
+pub struct u8;
+#[allow(non_camel_case_types)]
+pub struct usize;
+
+#[derive(::yew::Properties, ::std::cmp::PartialEq)]
+pub struct SimpleProps {
+ pub test: ::std::string::String,
+}
+
+pub struct Simple;
+impl ::yew::Component for Simple {
+ type Message = ();
+ type Properties = SimpleProps;
+
+ fn create(_ctx: &::yew::Context) -> Self {
+ ::std::unimplemented!()
+ }
+
+ fn view(&self, _ctx: &::yew::Context) -> ::yew::Html {
+ ::std::unimplemented!()
+ }
+}
+
+pub struct Fail;
+
+fn main() {
+ let dyn_prop = || Fail;
+
+ _ = ::yew::html! { };
+ _ = ::yew::html! { };
+
+ _ = ::yew::html! { };
+ _ = ::yew::html! { };
+ _ = ::yew::html! { };
+ _ = ::yew::html! { };
+}
diff --git a/packages/yew-macro/tests/html_macro/dyn-prop-fail.stderr b/packages/yew-macro/tests/html_macro/dyn-prop-fail.stderr
new file mode 100644
index 00000000000..ed5dfb60bfa
--- /dev/null
+++ b/packages/yew-macro/tests/html_macro/dyn-prop-fail.stderr
@@ -0,0 +1,51 @@
+error: components expect valid Rust identifiers for their property names
+ --> tests/html_macro/dyn-prop-fail.rs:66:32
+ |
+66 | _ = ::yew::html! { };
+ | ^^^^^^
+
+error: components expect valid Rust identifiers for their property names
+ --> tests/html_macro/dyn-prop-fail.rs:67:32
+ |
+67 | _ = ::yew::html! { };
+ | ^^^^^^
+
+error: components expect valid Rust identifiers for their property names
+ --> tests/html_macro/dyn-prop-fail.rs:68:34
+ |
+68 | _ = ::yew::html! { };
+ | ^^^^^^
+
+error: components expect valid Rust identifiers for their property names
+ --> tests/html_macro/dyn-prop-fail.rs:69:34
+ |
+69 | _ = ::yew::html! { };
+ | ^^^^^^^^^^
+
+error[E0277]: the trait bound `implicit_clone::unsync::IString: From` is not satisfied
+ --> tests/html_macro/dyn-prop-fail.rs:63:9
+ |
+63 | _ = ::yew::html! { };
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From` is not implemented for `implicit_clone::unsync::IString`
+ |
+ = help: the following other types implement trait `From`:
+ >
+ >>
+ >>
+ >
+ = note: required because of the requirements on the impl of `Into` for `Fail`
+ = note: this error originates in the macro `::yew::html` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error[E0277]: the trait bound `implicit_clone::unsync::IString: From` is not satisfied
+ --> tests/html_macro/dyn-prop-fail.rs:64:9
+ |
+64 | _ = ::yew::html! { };
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `From` is not implemented for `implicit_clone::unsync::IString`
+ |
+ = help: the following other types implement trait `From`:
+ >
+ >>
+ >>
+ >
+ = note: required because of the requirements on the impl of `Into` for `Fail`
+ = note: this error originates in the macro `::yew::html` (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/packages/yew-macro/tests/html_macro/dyn-prop-pass.rs b/packages/yew-macro/tests/html_macro/dyn-prop-pass.rs
new file mode 100644
index 00000000000..4ffc3a25669
--- /dev/null
+++ b/packages/yew-macro/tests/html_macro/dyn-prop-pass.rs
@@ -0,0 +1,55 @@
+#![no_implicit_prelude]
+
+// Shadow primitives
+#[allow(non_camel_case_types)]
+pub struct bool;
+#[allow(non_camel_case_types)]
+pub struct char;
+#[allow(non_camel_case_types)]
+pub struct f32;
+#[allow(non_camel_case_types)]
+pub struct f64;
+#[allow(non_camel_case_types)]
+pub struct i128;
+#[allow(non_camel_case_types)]
+pub struct i16;
+#[allow(non_camel_case_types)]
+pub struct i32;
+#[allow(non_camel_case_types)]
+pub struct i64;
+#[allow(non_camel_case_types)]
+pub struct i8;
+#[allow(non_camel_case_types)]
+pub struct isize;
+#[allow(non_camel_case_types)]
+pub struct str;
+#[allow(non_camel_case_types)]
+pub struct u128;
+#[allow(non_camel_case_types)]
+pub struct u16;
+#[allow(non_camel_case_types)]
+pub struct u32;
+#[allow(non_camel_case_types)]
+pub struct u64;
+#[allow(non_camel_case_types)]
+pub struct u8;
+#[allow(non_camel_case_types)]
+pub struct usize;
+
+fn main() {
+ // basic example from https://htmx.org/attributes/hx-on/
+
+ // repeating attrs is not valid HTMX nor HTML,
+ // but valid html! (any checks can happen only during runtime)
+
+ // literal
+ _ = ::yew::html! { };
+ _ = ::yew::html! { };
+ _ = ::yew::html! { };
+ _ = ::yew::html! { };
+
+ // expr
+ let dyn_prop = || ::std::string::ToString::to_string("hx-on:click");
+ _ = ::yew::html! { };
+ _ = ::yew::html! { };
+}