Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add syntax to bind to properties instead of attributes #2819

Merged
merged 18 commits into from Aug 14, 2022
Merged
22 changes: 18 additions & 4 deletions examples/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions examples/counter_functional/src/main.rs
Expand Up @@ -14,13 +14,13 @@ fn App() -> Html {
Callback::from(move |_| state.set(*state - 1))
};

html!(
html! {
<>
<p> {"current count: "} {*state} </p>
<button onclick={incr_counter}> {"+"} </button>
<button onclick={decr_counter}> {"-"} </button>
<p> {"current count: "} {*state} </p>
<button onclick={incr_counter}> {"+"} </button>
<button onclick={decr_counter}> {"-"} </button>
</>
)
}
}

fn main() {
Expand Down
110 changes: 72 additions & 38 deletions packages/yew-macro/src/html_tree/html_element.rs
Expand Up @@ -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};

Expand Down Expand Up @@ -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(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For properties, maybe we should apply a snake_case to camelCase conversion here like web_sys.
We only need to do this with html! macro as at runtime property names are set as string.

// snake case as we assume a it's like a field in Properties for an html element.
html! { <custom-element ~some_prop={"value"} /> };

// camel case as it's a string.
el.add_property("someProp", "value");

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is a good idea. We don't convert snake_case to kebab-case for attributes so this will be just another differentiator between the two. We can add it for both though, but that is for a future PR

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();
Expand Down Expand Up @@ -196,6 +215,7 @@ impl ToTokens for HtmlElement {
__yew_classes
}
}),
None,
))
}
ClassesForm::Single(classes) => {
Expand All @@ -207,6 +227,7 @@ impl ToTokens for HtmlElement {
Some((
LitStr::new("class", lit.span()),
Value::Static(quote! { #lit }),
None,
))
}
}
Expand All @@ -216,21 +237,30 @@ 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<TokenStream> {
fn try_into_static(src: &[(LitStr, Value, Option<PropDirective>)]) -> Option<TokenStream> {
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),*]) })
Expand All @@ -239,10 +269,14 @@ impl ToTokens for HtmlElement {
let attrs = normal_attrs
.chain(boolean_attrs)
.chain(class_attr)
.collect::<Vec<(LitStr, Value)>>();
.collect::<Vec<(LitStr, Value, Option<PropDirective>)>>();
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),*],
Expand Down
38 changes: 28 additions & 10 deletions packages/yew-macro/src/props/prop.rs
Expand Up @@ -13,17 +13,24 @@ 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<PropDirective>,
pub label: HtmlDashedName,
/// Punctuation between `label` and `value`.
pub value: Expr,
}
impl Parse for Prop {
fn parse(input: ParseStream) -> syn::Result<Self> {
let directive = input.parse::<Token![~]>().map(|parsed| PropDirective::ApplyAsProperty(parsed)).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)
}
}
}
Expand All @@ -33,7 +40,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<Self> {
fn parse_shorthand_prop_assignment(
input: ParseStream,
directive: Option<PropDirective>,
) -> syn::Result<Self> {
let value;
let _brace = braced!(value in input);
let expr = value.parse::<Expr>()?;
Expand All @@ -44,7 +54,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,
Expand All @@ -59,11 +69,15 @@ 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<Self> {
fn parse_prop_assignment(input: ParseStream, directive: Option<PropDirective>) -> syn::Result<Self> {
let label = input.parse::<HtmlDashedName>()?;
let equals = input.parse::<Token![=]>().map_err(|_| {
syn::Error::new_spanned(
Expand All @@ -83,7 +97,11 @@ impl Prop {
}

let value = parse_prop_value(input)?;
Ok(Self { label, value })
Ok(Self {
label,
value,
directive,
})
}
}

Expand All @@ -105,10 +123,10 @@ fn parse_prop_value(input: &ParseBuffer) -> syn::Result<Expr> {

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),
)),
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/yew-macro/src/props/prop_macro.rs
Expand Up @@ -61,7 +61,11 @@ impl Parse for PropValue {
impl From<PropValue> for Prop {
fn from(prop_value: PropValue) -> Prop {
let PropValue { label, value } = prop_value;
Prop { label, value }
Prop {
label,
value,
directive: None,
}
}
}

Expand Down