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 support for optional attributes on HTML elements/tags #1433

Closed
wants to merge 11 commits into from
15 changes: 15 additions & 0 deletions docs/concepts/html/elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ html! {
```
<!--END_DOCUSAURUS_CODE_TABS-->

## Optional attributes for HTML elements

Most HTML attributes can be marked as optional by placing a `?` in front of
the `=` sign. This makes them accept the same type of value as otherwise, but
wrapped in an `Option<T>`:
```rust
let maybe_id = Some("foobar");

html! {
<div id?=maybe_id></div>
}
```
If the attribute is set to a `None` value, it will behave as though the
attribute wasn't set.
ilyvion marked this conversation as resolved.
Show resolved Hide resolved

## Classes

There are a number of convenient ways to specify classes for an element:
Expand Down
9 changes: 8 additions & 1 deletion yew-macro/src/html_tree/html_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ impl ToTokens for HtmlComponent {

let init_props = match &props.prop_type {
PropType::List(list_props) => {
let set_props = list_props.iter().map(|HtmlProp { label, value }| {
let set_props = list_props.iter().map(|HtmlProp { label, question_mark: _, value }| {
ilyvion marked this conversation as resolved.
Show resolved Hide resolved
quote_spanned! { value.span()=> .#label(
<::yew::virtual_dom::VComp as ::yew::virtual_dom::Transformer<_, _>>::transform(
#value
Expand Down Expand Up @@ -405,6 +405,13 @@ impl Parse for Props {
return Err(syn::Error::new_spanned(&prop.label, "expected identifier"));
}

if prop.question_mark.is_some() {
return Err(syn::Error::new_spanned(
&prop.label,
"optional attributes are only supported on HTML tags. Yew components can use `Option<T>` properties to accomplish the same thing.",
));
}

match props.prop_type {
ref mut prop_type @ PropType::None => {
*prop_type = PropType::List(vec![prop]);
Expand Down
2 changes: 1 addition & 1 deletion yew-macro/src/html_tree/html_dashed_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@ impl ToTokens for HtmlDashedName {
let dashes = extended.iter().map(|(dash, _)| quote! {#dash});
let idents = extended.iter().map(|(_, ident)| quote! {#ident});
let extended = quote! { #(#dashes#idents)* };
tokens.extend(quote! {#name#extended});
tokens.extend(quote! { #name#extended });
}
}
9 changes: 8 additions & 1 deletion yew-macro/src/html_tree/html_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ impl PeekValue<()> for HtmlListOpen {
// make sure it's either a property (key=value) or it's immediately closed
if let Some((_, cursor)) = HtmlDashedName::peek(cursor) {
let (punct, _) = cursor.punct()?;
(punct.as_char() == '=').as_option()
(punct.as_char() == '=' || punct.as_char() == '?').as_option()
} else {
let (punct, _) = cursor.punct()?;
(punct.as_char() == '>').as_option()
Expand Down Expand Up @@ -136,6 +136,13 @@ impl Parse for HtmlListProps {
));
}

if prop.question_mark.is_some() {
return Err(syn::Error::new_spanned(
prop.label,
"The 'key' attribute does not support being used as an optional attribute",
ilyvion marked this conversation as resolved.
Show resolved Hide resolved
));
}

Some(prop.value)
};

Expand Down
12 changes: 11 additions & 1 deletion yew-macro/src/html_tree/html_prop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use syn::{Expr, Token};

pub struct HtmlProp {
pub label: HtmlPropLabel,
pub question_mark: Option<Token![?]>,
pub value: Expr,
}

Expand All @@ -19,6 +20,11 @@ impl PeekValue<()> for HtmlProp {
impl Parse for HtmlProp {
fn parse(input: ParseStream) -> ParseResult<Self> {
let label = input.parse::<HtmlPropLabel>()?;
let question_mark = if input.peek(Token![?]) {
Some(input.parse::<Token![?]>()?)
ilyvion marked this conversation as resolved.
Show resolved Hide resolved
} else {
None
};
let equals = input
.parse::<Token![=]>()
.map_err(|_| syn::Error::new_spanned(&label, "this prop doesn't have a value"))?;
Expand All @@ -31,7 +37,11 @@ impl Parse for HtmlProp {
let value = input.parse::<Expr>()?;
// backwards compat
let _ = input.parse::<Token![,]>();
Ok(HtmlProp { label, value })
Ok(HtmlProp {
ilyvion marked this conversation as resolved.
Show resolved Hide resolved
label,
question_mark,
value,
})
}
}

Expand Down
135 changes: 110 additions & 25 deletions yew-macro/src/html_tree/html_tag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,32 +136,90 @@ impl ToTokens for HtmlTag {
} = &attributes;

let vtag = Ident::new("__yew_vtag", tag_name.span());
let attr_pairs = attributes.iter().map(|TagAttribute { label, value }| {
let attr_pairs = attributes.iter().map(|TagAttribute { label, question_mark, value }| {
let label_str = label.to_string();
quote_spanned! {value.span()=> (#label_str.to_owned(), (#value).to_string()) }
});
let set_booleans = booleans.iter().map(|TagAttribute { label, value }| {
let label_str = label.to_string();
quote_spanned! {value.span()=>
if #value {
#vtag.add_attribute(&#label_str, &#label_str);
if question_mark.is_some() {
quote_spanned! {value.span()=>
{
let __yew_value = ::std::option::Option::as_ref(&(#value)).map(::std::string::ToString::to_string);
(::std::string::String::from(#label_str), __yew_value)
}
}
} else {
quote_spanned! {value.span()=>
{
(::std::string::String::from(#label_str), ::std::option::Option::Some(::std::string::ToString::to_string(&#value)))
}
}
}
});
let set_booleans = booleans.iter().map(
|TagAttribute {
label,
question_mark: _,
value,
}| {
ilyvion marked this conversation as resolved.
Show resolved Hide resolved
let label_str = label.to_string();
quote_spanned! {value.span()=>
{
if #value {
#vtag.add_attribute(#label_str, &#label_str);
}
}
}
},
);
let set_kind = kind.iter().map(|kind| {
quote_spanned! {kind.span()=> #vtag.set_kind(&(#kind)); }
let value = &kind.value;
if kind.question_mark.is_some() {
quote_spanned! {value.span()=>
{
if let ::std::option::Option::Some(__yew_kind) = ::std::option::Option::as_ref(&(#value)) {
siku2 marked this conversation as resolved.
Show resolved Hide resolved
#vtag.set_kind(__yew_kind)
}
}
}
} else {
quote_spanned! {value.span()=> #vtag.set_kind(&(#value)); }
}
});
let set_value = value.iter().map(|value| {
quote_spanned! {value.span()=> #vtag.set_value(&(#value)); }
let value_value = &value.value;
if value.question_mark.is_some() {
quote_spanned! {value_value.span()=>
{
if let ::std::option::Option::Some(__yew_value) = ::std::option::Option::as_ref(&(#value_value)) {
#vtag.set_value(__yew_value);
}
}
}
} else {
quote_spanned! {value_value.span()=> #vtag.set_value(&(#value_value)); }
}
});
let add_href = href.iter().map(|href| {
quote_spanned! {href.span()=>
let __yew_href: ::yew::html::Href = (#href).into();
#vtag.add_attribute("href", &__yew_href);
let value = &href.value;
if href.question_mark.is_some() {
quote_spanned! {value.span()=>
{
if let ::std::option::Option::Some(__yew_href) = #value {
let __yew_href: ::yew::html::Href = __yew_href.into();
siku2 marked this conversation as resolved.
Show resolved Hide resolved
#vtag.add_attribute("href", &__yew_href);
}
}
}
} else {
quote_spanned! {value.span()=>
{
let __yew_href: ::yew::html::Href = (#value).into();
#vtag.add_attribute("href", &__yew_href);
}
}
}
});
let set_checked = checked.iter().map(|checked| {
quote_spanned! {checked.span()=> #vtag.set_checked(#checked); }
let value = &checked.value;
quote_spanned! {value.span()=> #vtag.set_checked(#value); }
});
let set_classes = classes.iter().map(|classes_form| match classes_form {
ClassesForm::Tuple(classes) => quote! {
Expand Down Expand Up @@ -191,13 +249,27 @@ impl ToTokens for HtmlTag {
let name = &listener.label.name;
let callback = &listener.value;

quote_spanned! {name.span()=> {
::yew::html::#name::Wrapper::new(
<::yew::virtual_dom::VTag as ::yew::virtual_dom::Transformer<_, _>>::transform(
#callback
)
)
}}
if listener.question_mark.is_some() {
quote_spanned! {name.span()=>
{
::std::option::Option::map(#callback, |__yew_callback| {
::yew::html::#name::Wrapper::new(
<::yew::virtual_dom::VTag as ::yew::virtual_dom::Transformer<_, _>>::transform(
__yew_callback,
),
)
})
}
}
} else {
quote_spanned! {name.span()=> {
::std::option::Option::Some(::yew::html::#name::Wrapper::new(
<::yew::virtual_dom::VTag as ::yew::virtual_dom::Transformer<_, _>>::transform(
#callback
)
))
}}
}
});

// These are the runtime-checks exclusive to dynamic tags.
Expand Down Expand Up @@ -241,8 +313,20 @@ impl ToTokens for HtmlTag {
#(#set_classes)*
#(#set_node_ref)*
#(#set_key)*
#vtag.add_attributes(vec![#(#attr_pairs),*]);
#vtag.add_listeners(vec![#(::std::rc::Rc::new(#listeners)),*]);
#vtag.add_attributes({
let mut attributes = vec![];
#(if let (l, Some(v)) = #attr_pairs {
attributes.push((l, v));
})*
attributes
});
#vtag.add_listeners({
let mut listeners: ::std::vec::Vec<::std::rc::Rc<dyn ::yew::virtual_dom::Listener>> = vec![];
#(if let Some(l) = #listeners {
listeners.push(::std::rc::Rc::new(l));
})*
listeners
});
#vtag.add_children(#children);
#dyn_tag_runtime_checks
::yew::virtual_dom::VNode::from(#vtag)
Expand Down Expand Up @@ -383,10 +467,11 @@ impl Parse for HtmlTagOpen {
match name.to_ascii_lowercase_string().as_str() {
"input" | "textarea" => {}
_ => {
if let Some(value) = attributes.value.take() {
if let Some(attribute) = attributes.value.take() {
attributes.attributes.push(TagAttribute {
label: HtmlDashedName::new(Ident::new("value", Span::call_site())),
value,
question_mark: attribute.question_mark,
value: attribute.value,
});
}
}
Expand Down
61 changes: 53 additions & 8 deletions yew-macro/src/html_tree/html_tag/tag_attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ pub struct TagAttributes {
pub listeners: Vec<TagAttribute>,
pub classes: Option<ClassesForm>,
pub booleans: Vec<TagAttribute>,
pub value: Option<Expr>,
pub kind: Option<Expr>,
pub checked: Option<Expr>,
pub value: Option<TagAttribute>,
pub kind: Option<TagAttribute>,
pub checked: Option<TagAttribute>,
pub node_ref: Option<Expr>,
pub key: Option<Expr>,
pub href: Option<Expr>,
pub href: Option<TagAttribute>,
}

pub enum ClassesForm {
Expand Down Expand Up @@ -242,11 +242,11 @@ impl TagAttributes {
drained
}

fn remove_attr(attrs: &mut Vec<TagAttribute>, name: &str) -> Option<Expr> {
fn remove_attr(attrs: &mut Vec<TagAttribute>, name: &str) -> Option<TagAttribute> {
let mut i = 0;
while i < attrs.len() {
if attrs[i].label.to_string() == name {
return Some(attrs.remove(i).value);
return Some(attrs.remove(i));
} else {
i += 1;
}
Expand Down Expand Up @@ -307,14 +307,59 @@ impl Parse for TagAttributes {
i += 1;
}
let booleans = TagAttributes::drain_boolean(&mut attributes);
for boolean in &booleans {
if boolean.question_mark.is_some() {
return Err(syn::Error::new_spanned(
&boolean.label,
format!(
"The '{}' attribute does not support being used as an optional attribute",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"The '{}' attribute does not support being used as an optional attribute",
"the '{}' attribute does not support being used as an optional attribute",

Copy link
Member

Choose a reason for hiding this comment

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

I think this error message should be more along the lines of: "boolean attributes don't support being used as an option attribute".
The current error might give the impression that only the attribute at hand isn't supported but in actuality no boolean attributes are.

boolean.label
),
));
}
}

let classes =
TagAttributes::remove_attr(&mut attributes, "class").map(TagAttributes::map_classes);
let classes = TagAttributes::remove_attr(&mut attributes, "class");
if let Some(classes) = classes.as_ref() {
if classes.question_mark.is_some() {
return Err(syn::Error::new_spanned(
&classes.label,
"The 'class' attribute does not support being used as an optional attribute",
ilyvion marked this conversation as resolved.
Show resolved Hide resolved
));
}
}
let classes = classes.map(|a| TagAttributes::map_classes(a.value));
let value = TagAttributes::remove_attr(&mut attributes, "value");
let kind = TagAttributes::remove_attr(&mut attributes, "type");
let checked = TagAttributes::remove_attr(&mut attributes, "checked");
if let Some(checked) = checked.as_ref() {
siku2 marked this conversation as resolved.
Show resolved Hide resolved
if checked.question_mark.is_some() {
return Err(syn::Error::new_spanned(
&checked.label,
"The 'checked' attribute does not support being used as an optional attribute",
ilyvion marked this conversation as resolved.
Show resolved Hide resolved
));
}
}
let node_ref = TagAttributes::remove_attr(&mut attributes, "ref");
if let Some(node_ref) = node_ref.as_ref() {
siku2 marked this conversation as resolved.
Show resolved Hide resolved
if node_ref.question_mark.is_some() {
return Err(syn::Error::new_spanned(
&node_ref.label,
"The 'ref' attribute does not support being used as an optional attribute",
ilyvion marked this conversation as resolved.
Show resolved Hide resolved
));
}
}
let node_ref = node_ref.map(|n| n.value);
let key = TagAttributes::remove_attr(&mut attributes, "key");
if let Some(key) = key.as_ref() {
siku2 marked this conversation as resolved.
Show resolved Hide resolved
if key.question_mark.is_some() {
return Err(syn::Error::new_spanned(
&key.label,
"The 'key' attribute does not support being used as an optional attribute",
ilyvion marked this conversation as resolved.
Show resolved Hide resolved
));
}
}
let key = key.map(|k| k.value);

let href = TagAttributes::remove_attr(&mut attributes, "href");

Expand Down
2 changes: 2 additions & 0 deletions yew-macro/tests/macro/html-component-fail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ fn compile_fail() {
<span>{ 1 }</span>
<span>{ 2 }</span>
};

html! { <TestComponent value?="not_supported" /> };
}

fn main() {}
Loading