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 @disabled={false}> {"current count: "} {*state} </p>
<button onclick={incr_counter}> {"+"} </button>
<button onclick={decr_counter}> {"-"} </button>
</>
)
}
}

fn main() {
Expand Down
110 changes: 73 additions & 37 deletions packages/yew-macro/src/html_tree/html_element.rs
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,
is_forced_attribute,
..
}| {
(
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(),
*is_forced_attribute,
)
},
);
let boolean_attrs = booleans.iter().filter_map(
|Prop {
label,
value,
is_forced_attribute,
..
}| {
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
}
},
),
},
*is_forced_attribute,
))
},
);
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
}
}),
false,
))
}
ClassesForm::Single(classes) => {
Expand All @@ -207,6 +227,7 @@ impl ToTokens for HtmlElement {
Some((
LitStr::new("class", lit.span()),
Value::Static(quote! { #lit }),
false,
))
}
}
Expand All @@ -216,21 +237,27 @@ impl ToTokens for HtmlElement {
Value::Dynamic(quote! {
::std::convert::Into::<::yew::html::Classes>::into(#classes)
}),
false,
))
}
}
}
});

/// 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, bool)]) -> Option<TokenStream> {
let mut kv = Vec::with_capacity(src.len());
for (k, v) in src.iter() {
for (k, v, is_forced_attribute) in src.iter() {
let v = match v {
Value::Static(v) => quote! { #v },
Value::Dynamic(_) => return None,
};
kv.push(quote! { [ #k, #v ] });
let apply_as = if *is_forced_attribute {
quote! { ::yew::virtual_dom::ApplyAttributeAs::Attribute }
} else {
quote! { ::yew::virtual_dom::ApplyAttributeAs::Property }
};
kv.push(quote! { ( #k, #v, #apply_as ) });
}

Some(quote! { ::yew::virtual_dom::Attributes::Static(&[#(#kv),*]) })
Expand All @@ -239,10 +266,19 @@ impl ToTokens for HtmlElement {
let attrs = normal_attrs
.chain(boolean_attrs)
.chain(class_attr)
.collect::<Vec<(LitStr, Value)>>();
.collect::<Vec<(LitStr, Value, bool)>>();
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, is_forced_attribute)| {
let apply_as = if *is_forced_attribute {
quote! { ::yew::virtual_dom::ApplyAttributeAs::Attribute }
} else {
quote! { ::yew::virtual_dom::ApplyAttributeAs::Property }
};
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
27 changes: 20 additions & 7 deletions packages/yew-macro/src/props/prop.rs
Expand Up @@ -14,16 +14,18 @@ use crate::html_tree::HtmlDashedName;
use crate::stringify::Stringify;

pub struct Prop {
pub is_forced_attribute: bool,
hamza1311 marked this conversation as resolved.
Show resolved Hide resolved
pub label: HtmlDashedName,
/// Punctuation between `label` and `value`.
pub value: Expr,
}
impl Parse for Prop {
fn parse(input: ParseStream) -> syn::Result<Self> {
let at = input.parse::<Token![@]>().map(|_| true).unwrap_or(false);
Copy link
Member

Choose a reason for hiding this comment

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

Let's discuss some syntax and why I think using @ is not not intuitive.

  • @{} is used for dynamic tags already. We're currently not supporting dynamic attribute names, but if we were to, I'd reuse the syntax there. Since this feature does something else, I find it confusing.
  • vue syntax calls this style of modifying what a prop assignment means a "directive". There's a directive for callbacks, i.e. @click = v-on:click = a callback on the click event. And there's a directive for attribute binding, :href = v-bind:href = sets the attribute of the element.
  • Rust has alternative syntax for something akin to directives: Attributes

My preferred solution: use Token![:], i.e. :attr-name. An eventual dynamic attribute name would look like :@{expr}={value}. When we want to support further directives, we could use #[attr] attr-name={value} and #[on] event={handler} (not so sure about a short-hand form for event handlers, $click?).

Can we also support a syntax such as .prop-name (or #[prop] prop-name) to explicitly assign to a property? This is important for the complication with the property reflecting an attribute not being always named the same. See below on the comment about "class".

Copy link
Member Author

Choose a reason for hiding this comment

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

if input.peek(Brace) {
Self::parse_shorthand_prop_assignment(input)
Self::parse_shorthand_prop_assignment(input, at)
} else {
Self::parse_prop_assignment(input)
Self::parse_prop_assignment(input, at)
}
}
}
Expand All @@ -33,7 +35,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,
is_forced_attribute: bool,
) -> syn::Result<Self> {
let value;
let _brace = braced!(value in input);
let expr = value.parse::<Expr>()?;
Expand All @@ -44,7 +49,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 +64,15 @@ impl Prop {
));
}?;

Ok(Self { label, value: expr })
Ok(Self {
label,
value: expr,
is_forced_attribute,
})
}

/// Parse a prop of the form `label={value}`
fn parse_prop_assignment(input: ParseStream) -> syn::Result<Self> {
fn parse_prop_assignment(input: ParseStream, is_forced_attribute: bool) -> syn::Result<Self> {
let label = input.parse::<HtmlDashedName>()?;
let equals = input.parse::<Token![=]>().map_err(|_| {
syn::Error::new_spanned(
Expand All @@ -83,7 +92,11 @@ impl Prop {
}

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

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,
is_forced_attribute: false,
}
}
}

Expand Down