Skip to content

Commit 92a3d7c

Browse files
feat(yew-macro): Dynamic Prop Labels (#3509)
1 parent 3b7aa83 commit 92a3d7c

19 files changed

Lines changed: 735 additions & 66 deletions

File tree

packages/yew-macro/src/html_tree/html_element.rs

Lines changed: 102 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ use quote::{quote, quote_spanned, ToTokens};
44
use syn::buffer::Cursor;
55
use syn::parse::{Parse, ParseStream};
66
use syn::spanned::Spanned;
7-
use syn::{Expr, Ident, Lit, LitStr, Token};
7+
use syn::{Expr, ExprLit, Ident, Lit, LitStr, Token};
88

99
use super::{HtmlChildrenTree, HtmlDashedName, TagTokens};
10-
use crate::props::{ElementProps, Prop, PropDirective};
10+
use crate::props::{ElementProps, Prop, PropDirective, PropLabel};
1111
use crate::stringify::{Stringify, Value};
1212
use crate::{is_ide_completion, non_capitalized_ascii, Peek, PeekValue};
1313

@@ -198,6 +198,30 @@ impl ToTokens for HtmlElement {
198198
// other attributes
199199

200200
let attributes = {
201+
#[derive(Clone)]
202+
enum Key {
203+
Static(LitStr),
204+
Dynamic(Expr),
205+
}
206+
207+
impl From<&PropLabel> for Key {
208+
fn from(value: &PropLabel) -> Self {
209+
match value {
210+
PropLabel::Static(dashed_name) => Self::Static(dashed_name.to_lit_str()),
211+
PropLabel::Dynamic(expr) => Self::Dynamic(expr.clone()),
212+
}
213+
}
214+
}
215+
216+
impl ToTokens for Key {
217+
fn to_tokens(&self, tokens: &mut TokenStream) {
218+
match self {
219+
Key::Static(dashed_name) => dashed_name.to_tokens(tokens),
220+
Key::Dynamic(expr) => expr.to_tokens(tokens),
221+
}
222+
}
223+
}
224+
201225
let normal_attrs = attributes.iter().map(
202226
|Prop {
203227
label,
@@ -206,7 +230,7 @@ impl ToTokens for HtmlElement {
206230
..
207231
}| {
208232
(
209-
label.to_lit_str(),
233+
Key::from(label),
210234
value.optimize_literals_tagged(),
211235
*directive,
212236
)
@@ -219,26 +243,30 @@ impl ToTokens for HtmlElement {
219243
directive,
220244
..
221245
}| {
222-
let key = label.to_lit_str();
246+
let key = Key::from(label);
247+
let lit = match &key {
248+
Key::Static(lit) => lit,
249+
Key::Dynamic(_) => unreachable!(),
250+
};
223251
Some((
224252
key.clone(),
225253
match value {
226254
Expr::Lit(e) => match &e.lit {
227255
Lit::Bool(b) => Value::Static(if b.value {
228-
quote! { #key }
256+
quote! { #lit }
229257
} else {
230258
return None;
231259
}),
232260
_ => Value::Dynamic(quote_spanned! {value.span()=> {
233261
::yew::utils::__ensure_type::<::std::primitive::bool>(#value);
234-
#key
262+
#lit
235263
}}),
236264
},
237265
expr => Value::Dynamic(
238266
quote_spanned! {expr.span().resolved_at(Span::call_site())=>
239267
if #expr {
240268
::std::option::Option::Some(
241-
::yew::virtual_dom::AttrValue::Static(#key)
269+
::yew::virtual_dom::AttrValue::Static(#lit)
242270
)
243271
} else {
244272
::std::option::Option::None
@@ -260,7 +288,7 @@ impl ToTokens for HtmlElement {
260288
None
261289
} else {
262290
Some((
263-
LitStr::new("class", lit.span()),
291+
Key::Static(LitStr::new("class", lit.span())),
264292
Value::Static(quote! { #lit }),
265293
None,
266294
))
@@ -269,7 +297,7 @@ impl ToTokens for HtmlElement {
269297
None => {
270298
let expr = &classes.value;
271299
Some((
272-
LitStr::new("class", classes.label.span()),
300+
Key::Static(LitStr::new("class", classes.label.span())),
273301
Value::Dynamic(quote! {
274302
::std::convert::Into::<::yew::html::Classes>::into(#expr)
275303
}),
@@ -279,15 +307,13 @@ impl ToTokens for HtmlElement {
279307
});
280308

281309
/// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Static`
282-
fn try_into_static(
283-
src: &[(LitStr, Value, Option<PropDirective>)],
284-
) -> Option<TokenStream> {
285-
if src
286-
.iter()
287-
.any(|(_, _, d)| matches!(d, Some(PropDirective::ApplyAsProperty(_))))
288-
{
310+
fn try_into_static(src: &[(Key, Value, Option<PropDirective>)]) -> Option<TokenStream> {
311+
if src.iter().any(|(k, _, d)| {
312+
matches!(k, Key::Dynamic(_))
313+
|| matches!(d, Some(PropDirective::ApplyAsProperty(_)))
314+
}) {
289315
// don't try to make a static attribute list if there are any properties to
290-
// assign
316+
// assign or any labels are dynamic
291317
return None;
292318
}
293319
let mut kv = Vec::with_capacity(src.len());
@@ -312,13 +338,24 @@ impl ToTokens for HtmlElement {
312338
Some(quote! { ::yew::virtual_dom::Attributes::Static(&[#(#kv),*]) })
313339
}
314340

315-
let attrs = normal_attrs
316-
.chain(boolean_attrs)
317-
.chain(class_attr)
318-
.collect::<Vec<(LitStr, Value, Option<PropDirective>)>>();
319-
try_into_static(&attrs).unwrap_or_else(|| {
320-
let keys = attrs.iter().map(|(k, ..)| quote! { #k });
321-
let values = attrs.iter().map(|(_, v, directive)| {
341+
/// Try to turn attribute list into a `::yew::virtual_dom::Attributes::Dynamic`
342+
fn try_into_dynamic(
343+
src: &[(Key, Value, Option<PropDirective>)],
344+
) -> Option<TokenStream> {
345+
if src.iter().any(|(k, ..)| {
346+
!matches!(
347+
k,
348+
Key::Dynamic(Expr::Lit(ExprLit {
349+
lit: Lit::Str(_),
350+
..
351+
})) | Key::Static(_)
352+
)
353+
}) {
354+
// use IndexMap if there are any dynamic-expr labels
355+
return None;
356+
}
357+
let keys = src.iter().map(|(k, ..)| quote! { #k });
358+
let values = src.iter().map(|(_, v, directive)| {
322359
let value = match directive {
323360
Some(PropDirective::ApplyAsProperty(token)) => {
324361
quote_spanned!(token.span()=> ::std::option::Option::Some(
@@ -336,11 +373,49 @@ impl ToTokens for HtmlElement {
336373
};
337374
quote! { #value }
338375
});
339-
quote! {
376+
Some(quote! {
340377
::yew::virtual_dom::Attributes::Dynamic{
341378
keys: &[#(#keys),*],
342379
values: ::std::boxed::Box::new([#(#values),*]),
343380
}
381+
})
382+
}
383+
384+
let attrs = normal_attrs
385+
.chain(boolean_attrs)
386+
.chain(class_attr)
387+
.collect::<Vec<(Key, Value, Option<PropDirective>)>>();
388+
try_into_static(&attrs).or_else(|| try_into_dynamic(&attrs)).unwrap_or_else(|| {
389+
let results = attrs.iter()
390+
.map(|(k, v, directive)| {
391+
let value = match directive {
392+
Some(PropDirective::ApplyAsProperty(token)) => {
393+
quote_spanned!(token.span()=> ::std::option::Option::Some(
394+
::yew::virtual_dom::AttributeOrProperty::Property(
395+
::std::convert::Into::into(#v)
396+
))
397+
)
398+
}
399+
None => {
400+
let value = wrap_attr_value(v);
401+
quote! {
402+
::std::option::Option::map(#value, ::yew::virtual_dom::AttributeOrProperty::Attribute)
403+
}
404+
},
405+
};
406+
quote! { (::std::convert::Into::into(#k), #value) }
407+
});
408+
quote! {
409+
::yew::virtual_dom::Attributes::IndexMap(
410+
::std::rc::Rc::new(
411+
::std::iter::Iterator::collect(
412+
::std::iter::Iterator::filter_map(
413+
::std::iter::IntoIterator::into_iter([#(#results),*]),
414+
|(k, v)| v.map(|v| (k, v))
415+
)
416+
)
417+
)
418+
)
344419
}
345420
})
346421
};
@@ -349,7 +424,8 @@ impl ToTokens for HtmlElement {
349424
quote! { ::yew::virtual_dom::listeners::Listeners::None }
350425
} else {
351426
let listeners_it = listeners.iter().map(|Prop { label, value, .. }| {
352-
let name = &label.name;
427+
// TODO: consider making a `ListenerProp` that has dashed name's name and value
428+
let name = &<&HtmlDashedName>::try_from(label).unwrap().name;
353429
quote! {
354430
::yew::html::#name::Wrapper::__macro_new(#value)
355431
}

packages/yew-macro/src/html_tree/html_list.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,14 @@ impl Parse for HtmlListProps {
141141
return Err(input.error("only a single `key` prop is allowed on a fragment"));
142142
}
143143

144-
if prop.label.to_ascii_lowercase_string() != "key" {
145-
return Err(syn::Error::new_spanned(
146-
prop.label,
147-
"fragments only accept the `key` prop",
148-
));
144+
match String::try_from(&prop.label) {
145+
Ok(label) if label.eq_ignore_ascii_case("key") => {}
146+
_ => {
147+
return Err(syn::Error::new_spanned(
148+
prop.label,
149+
"fragments only accept the `key` prop",
150+
))
151+
}
149152
}
150153

151154
Some(prop.value)

packages/yew-macro/src/html_tree/lint/mod.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ where
4747
///
4848
/// Attribute names are lowercased before being compared (so pass "href" for `name` and not "HREF").
4949
fn get_attribute<'a>(props: &'a ElementProps, name: &str) -> Option<&'a Prop> {
50-
props
51-
.attributes
52-
.iter()
53-
.find(|item| item.label.eq_ignore_ascii_case(name))
50+
props.attributes.iter().find(|item| {
51+
matches!(
52+
String::try_from(&item.label),
53+
Ok(label) if label.eq_ignore_ascii_case(name)
54+
)
55+
})
5456
}
5557

5658
/// Lints to check if anchor (`<a>`) tags have valid `href` attributes defined.

packages/yew-macro/src/props/component.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ use syn::spanned::Spanned;
77
use syn::token::DotDot;
88
use syn::Expr;
99

10-
use super::{Prop, Props, SpecialProps, CHILDREN_LABEL};
10+
use super::{Prop, PropLabel, Props, SpecialProps, CHILDREN_LABEL};
11+
use crate::html_tree::HtmlDashedName;
1112

1213
fn is_none_expr(expr: &Expr) -> bool {
1314
matches!(
@@ -112,8 +113,9 @@ impl ComponentProps {
112113
};
113114
let set_props = self.props.iter().map(|Prop { label, value, .. }| {
114115
if is_none_expr(value) {
116+
let name = <&HtmlDashedName>::try_from(label).unwrap();
115117
let none_setter = Ident::new(
116-
&format!("{}_none", label),
118+
&format!("{}_none", name),
117119
label.span().resolved_at(Span::mixed_site()),
118120
);
119121
quote_spanned! {value.span()=>
@@ -217,15 +219,12 @@ impl TryFrom<Props> for ComponentProps {
217219

218220
fn validate(props: Props) -> Result<Props, syn::Error> {
219221
props.check_no_duplicates()?;
220-
props.check_all(|prop| {
221-
if !prop.label.extended.is_empty() {
222-
Err(syn::Error::new_spanned(
223-
&prop.label,
224-
"expected a valid Rust identifier",
225-
))
226-
} else {
227-
Ok(())
228-
}
222+
props.check_all(|prop| match &prop.label {
223+
PropLabel::Static(dashed_name) if dashed_name.extended.is_empty() => Ok(()),
224+
_ => Err(syn::Error::new_spanned(
225+
&prop.label,
226+
"components expect valid Rust identifiers for their property names",
227+
)),
229228
})?;
230229

231230
Ok(props)

packages/yew-macro/src/props/element.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,18 @@ impl Parse for ElementProps {
2020
fn parse(input: ParseStream) -> syn::Result<Self> {
2121
let mut props = input.parse::<Props>()?;
2222

23-
let listeners =
24-
props.drain_filter(|prop| LISTENER_SET.contains(prop.label.to_string().as_str()));
23+
let listeners = props.drain_filter(|prop| {
24+
matches!(String::try_from(&prop.label),
25+
Ok(prop) if LISTENER_SET.contains(prop.as_str()))
26+
});
2527

2628
// Multiple listener attributes are allowed, but no others
2729
props.check_no_duplicates()?;
2830

29-
let booleans =
30-
props.drain_filter(|prop| BOOLEAN_SET.contains(prop.label.to_string().as_str()));
31+
let booleans = props.drain_filter(|prop| {
32+
matches!(String::try_from(&prop.label),
33+
Ok(prop) if BOOLEAN_SET.contains(prop.as_str()))
34+
});
3135

3236
let classes = props.pop("class");
3337
let value = props.pop("value");

0 commit comments

Comments
 (0)