Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions examples/counter_functional/src/main.rs
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Copy Markdown
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
Copy Markdown
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,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading