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! { }; +}