Skip to content

Commit

Permalink
Bind to properties instead of attributes by default (#2819)
Browse files Browse the repository at this point in the history
* Set to properties, not attributes

* fix tests

* Add tests

* enable disabled test, fmt

* Introduce @key syntax to forcefully set as attribute

* Everything compiles

* More tests

* id as property

* This was not meant to be committed

* Make test pass, fmt + clippy

* fucking rustfmt

* is this enough formatting

* that was not supposed to be commited

* apply review

* fmt

* fix CI

* will you be happy now, clippy?
  • Loading branch information
hamza1311 committed Aug 14, 2022
1 parent a4e7091 commit 4c35f95
Show file tree
Hide file tree
Showing 12 changed files with 601 additions and 125 deletions.
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
114 changes: 76 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(),
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,34 @@ 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 +273,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
47 changes: 37 additions & 10 deletions packages/yew-macro/src/props/prop.rs
Expand Up @@ -13,17 +13,27 @@ 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(PropDirective::ApplyAsProperty)
.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 +43,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 +57,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 +72,18 @@ 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 +103,11 @@ impl Prop {
}

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

Expand All @@ -105,10 +129,13 @@ 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
27 changes: 25 additions & 2 deletions packages/yew-macro/tests/html_macro/component-fail.stderr
Expand Up @@ -249,7 +249,13 @@ help: escape `type` to use it as an identifier
85 | html! { <Child r#type=0 /> };
| ++

error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Tuple(
ExprTuple {
attrs: [],
paren_token: Paren,
elems: [],
},
)
--> tests/html_macro/component-fail.rs:86:24
|
86 | html! { <Child ref=() /> };
Expand Down Expand Up @@ -309,7 +315,24 @@ error: only one root html element is allowed (hint: you can wrap multiple html e
102 | html! { <Child></Child><Child></Child> };
| ^^^^^^^^^^^^^^^

error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.
error: the property value must be either a literal or enclosed in braces. Consider adding braces around your expression.: Path(
ExprPath {
attrs: [],
qself: None,
path: Path {
leading_colon: None,
segments: [
PathSegment {
ident: Ident {
ident: "num",
span: #0 bytes(3894..3897),
},
arguments: None,
},
],
},
},
)
--> tests/html_macro/component-fail.rs:106:24
|
106 | html! { <Child int=num ..props /> };
Expand Down

0 comments on commit 4c35f95

Please sign in to comment.