diff --git a/examples/store/src/post.rs b/examples/store/src/post.rs
index 398b825a81d..2b8f2d02282 100644
--- a/examples/store/src/post.rs
+++ b/examples/store/src/post.rs
@@ -71,7 +71,7 @@ impl Component for Post {
{ format!("Post #{}", self.id) }
{text}
-
+
diff --git a/examples/store/src/text_input.rs b/examples/store/src/text_input.rs
index e0ac230e614..1f52f0c746e 100644
--- a/examples/store/src/text_input.rs
+++ b/examples/store/src/text_input.rs
@@ -57,7 +57,7 @@ impl Component for TextInput {
html! {
&Ident {
+ match &self.attr {
+ PropAttr::Required { wrapped_name } => wrapped_name,
+ _ => &self.name,
+ }
+ }
+
/// Used to transform the `PropWrapper` struct into `Properties`
pub fn to_field_setter(&self) -> proc_macro2::TokenStream {
let name = &self.name;
match &self.attr {
PropAttr::Required { wrapped_name } => {
quote! {
- #name: self.wrapped.#wrapped_name.unwrap(),
+ #name: ::std::option::Option::unwrap(self.wrapped.#wrapped_name),
}
}
- _ => {
+ PropAttr::Option => {
quote! {
#name: self.wrapped.#name,
}
}
+ PropAttr::PropOr(value) => {
+ quote_spanned! {value.span()=>
+ #name: ::std::option::Option::unwrap_or(self.wrapped.#name, #value),
+ }
+ }
+ PropAttr::PropOrElse(func) => {
+ quote_spanned! {func.span()=>
+ #name: ::std::option::Option::unwrap_or_else(self.wrapped.#name, #func),
+ }
+ }
+ PropAttr::PropOrDefault => {
+ quote! {
+ #name: ::std::option::Option::unwrap_or_default(self.wrapped.#name),
+ }
+ }
}
}
/// Wrap all required props in `Option`
pub fn to_field_def(&self) -> proc_macro2::TokenStream {
let ty = &self.ty;
+ let wrapped_name = self.wrapped_name();
match &self.attr {
- PropAttr::Required { wrapped_name } => {
+ PropAttr::Option => {
quote! {
- #wrapped_name: ::std::option::Option<#ty>,
+ #wrapped_name: #ty,
}
}
_ => {
- let name = &self.name;
quote! {
- #name: #ty,
+ #wrapped_name: ::std::option::Option<#ty>,
}
}
}
@@ -74,32 +98,9 @@ impl PropField {
/// All optional props must implement the `Default` trait
pub fn to_default_setter(&self) -> proc_macro2::TokenStream {
- match &self.attr {
- PropAttr::Required { wrapped_name } => {
- quote! {
- #wrapped_name: ::std::option::Option::None,
- }
- }
- PropAttr::PropOr(value) => {
- let name = &self.name;
- let span = value.span();
- quote_spanned! {span=>
- #name: #value,
- }
- }
- PropAttr::PropOrElse(func) => {
- let name = &self.name;
- let span = func.span();
- quote_spanned! {span=>
- #name: (#func)(),
- }
- }
- PropAttr::PropOrDefault => {
- let name = &self.name;
- quote! {
- #name: ::std::default::Default::default(),
- }
- }
+ let wrapped_name = self.wrapped_name();
+ quote! {
+ #wrapped_name: ::std::option::Option::None,
}
}
@@ -115,8 +116,8 @@ impl PropField {
PropAttr::Required { wrapped_name } => {
quote! {
#[doc(hidden)]
- #vis fn #name(mut self, #name: #ty) -> #builder_name<#generic_arguments> {
- self.wrapped.#wrapped_name = ::std::option::Option::Some(#name);
+ #vis fn #name(mut self, #name: impl ::yew::html::IntoPropValue<#ty>) -> #builder_name<#generic_arguments> {
+ self.wrapped.#wrapped_name = ::std::option::Option::Some(#name.into_prop_value());
#builder_name {
wrapped: self.wrapped,
_marker: ::std::marker::PhantomData,
@@ -124,11 +125,20 @@ impl PropField {
}
}
}
+ PropAttr::Option => {
+ quote! {
+ #[doc(hidden)]
+ #vis fn #name(mut self, #name: impl ::yew::html::IntoPropValue<#ty>) -> #builder_name<#generic_arguments> {
+ self.wrapped.#name = #name.into_prop_value();
+ self
+ }
+ }
+ }
_ => {
quote! {
#[doc(hidden)]
- #vis fn #name(mut self, #name: #ty) -> #builder_name<#generic_arguments> {
- self.wrapped.#name = #name;
+ #vis fn #name(mut self, #name: impl ::yew::html::IntoPropValue<#ty>) -> #builder_name<#generic_arguments> {
+ self.wrapped.#name = ::std::option::Option::Some(#name.into_prop_value());
self
}
}
@@ -154,6 +164,12 @@ impl PropField {
} else {
unreachable!()
}
+ } else if matches!(
+ &named_field.ty,
+ Type::Path(TypePath { path, .. })
+ if path.segments.len() == 1 && path.segments[0].ident == "Option"
+ ) {
+ Ok(PropAttr::Option)
} else {
let ident = named_field.ident.as_ref().unwrap();
let wrapped_name = Ident::new(&format!("{}_wrapper", ident), Span::call_site());
diff --git a/packages/yew-macro/src/html_tree/html_element.rs b/packages/yew-macro/src/html_tree/html_element.rs
index e4c2057993d..70a05c8835a 100644
--- a/packages/yew-macro/src/html_tree/html_element.rs
+++ b/packages/yew-macro/src/html_tree/html_element.rs
@@ -1,7 +1,7 @@
use super::{HtmlChildrenTree, HtmlDashedName, TagTokens};
use crate::props::{ClassesForm, ElementProps, Prop};
use crate::stringify::Stringify;
-use crate::{non_capitalized_ascii, stringify, Peek, PeekValue};
+use crate::{non_capitalized_ascii, Peek, PeekValue};
use boolinator::Boolinator;
use proc_macro2::{Delimiter, TokenStream};
use quote::{quote, quote_spanned, ToTokens};
@@ -132,203 +132,124 @@ impl ToTokens for HtmlElement {
// attributes with special treatment
- let node_ref = node_ref
- .as_ref()
- .map(|attr| {
- let value = &attr.value;
- quote! { #value }
- })
- .unwrap_or(quote! { ::std::default::Default::default() });
- let key = key
- .as_ref()
- .map(|attr| {
- let value = &attr.value;
+ let set_node_ref = node_ref.as_ref().map(|attr| {
+ let value = &attr.value;
+ quote! {
+ #vtag.node_ref = #value;
+ }
+ });
+ let set_key = key.as_ref().map(|attr| {
+ let value = attr.value.optimize_literals();
+ quote! {
+ #vtag.__macro_set_key(#value);
+ }
+ });
+ let set_value = value.as_ref().map(|attr| {
+ let value = attr.value.optimize_literals();
+ quote! {
+ #vtag.set_value(#value);
+ }
+ });
+ let set_kind = kind.as_ref().map(|attr| {
+ let value = attr.value.optimize_literals();
+ quote! {
+ #vtag.set_kind(#value);
+ }
+ });
+ let set_checked = checked.as_ref().map(|attr| {
+ let value = &attr.value;
+ quote! {
+ #vtag.set_checked(#value);
+ }
+ });
+
+ // other attributes
+
+ let set_attributes = {
+ let normal_attrs = attributes.iter().map(|Prop { label, value, .. }| {
+ let key = label.to_lit_str();
+ let value = value.optimize_literals();
quote! {
- ::std::option::Option::Some
- (::std::convert::Into::<::yew::virtual_dom::Key>::into(#value))
- }
- })
- .unwrap_or(quote! { ::std::option::Option::None });
- let value = value
- .as_ref()
- .map(|attr| {
- let value = &attr.value;
- if attr.question_mark.is_some() {
- quote_spanned! {value.span()=>
- ::std::option::Option::as_ref(&(#value))
- .map(::std::string::ToString::to_string)
- }
- } else {
- quote_spanned! {value.span()=>
- ::std::option::Option::Some(::std::string::ToString::to_string(&(#value)))
- }
+ ::yew::virtual_dom::PositionalAttr::new(#key, #value)
}
- })
- .unwrap_or(quote! { ::std::option::Option::None });
- let kind = kind
- .as_ref()
- .map(|attr| {
- let value = &attr.value;
- if attr.question_mark.is_some() {
- let sr = stringify::stringify_option_at_runtime(value);
- quote_spanned! {value.span()=>
- ::std::option::Option::map(
- #sr,
- |v| ::std::convert::Into::<::std::borrow::Cow<'static, str>>::into(v),
- )
- }
- } else {
- let sr = value.stringify();
- quote_spanned! {value.span()=>
- ::std::option::Option::Some
- (::std::convert::Into::<::std::borrow::Cow<'static, str>>::into(#sr))
- }
- }
- })
- .unwrap_or(quote! { ::std::option::Option::None });
- let checked = checked
- .as_ref()
- .map(|attr| {
- let value = &attr.value;
- quote_spanned! {value.span()=> #value}
- })
- .unwrap_or(quote! { false });
-
- // normal attributes
-
- let mut attributes = attributes
- .iter()
- .map(
- |Prop {
- label,
- question_mark,
- value,
- ..
- }| {
- let key = label.to_lit_str();
- if question_mark.is_some() {
- let sr = stringify::stringify_option_at_runtime(value);
- quote! {
- ::yew::virtual_dom::PositionalAttr(#key, #sr)
- }
- } else {
- let sr = value.stringify();
- quote! {
- ::yew::virtual_dom::PositionalAttr::new(#key, #sr)
- }
- }
- },
- )
- .collect::
>();
-
- attributes.extend(booleans.iter().map(|Prop { label, value, .. }| {
- let label_str = label.to_lit_str();
- let sr = label.stringify();
- quote_spanned! {value.span()=> {
- if #value {
- ::yew::virtual_dom::PositionalAttr::new(#label_str, #sr)
- } else {
- ::yew::virtual_dom::PositionalAttr::new_placeholder(#label_str)
+ });
+ let boolean_attrs = booleans.iter().map(|Prop { label, value, .. }| {
+ let key = label.to_lit_str();
+ quote! {
+ ::yew::virtual_dom::PositionalAttr::new_boolean(#key, #value)
}
- }}
- }));
-
- match classes {
- Some(ClassesForm::Tuple(classes)) => {
- let span = classes.span();
- let classes: Vec<_> = classes.elems.iter().collect();
- let n = classes.len();
- let sr = stringify::stringify_at_runtime(quote! { __yew_classes });
-
- let deprecation_warning = quote_spanned! {span=>
- #[deprecated(
- note = "the use of `(...)` with the attribute `class` is deprecated and will be removed in version 0.19. Use the `classes!` macro instead."
- )]
- fn deprecated_use_of_class() {}
-
- if false {
- deprecated_use_of_class();
+ });
+ let class_attr = classes.as_ref().and_then(|classes| match classes {
+ ClassesForm::Tuple(classes) => {
+ let span = classes.span();
+ let classes: Vec<_> = classes.elems.iter().collect();
+ let n = classes.len();
+
+ let deprecation_warning = quote_spanned! {span=>
+ #[deprecated(
+ note = "the use of `(...)` with the attribute `class` is deprecated and will be removed in version 0.19. Use the `classes!` macro instead."
+ )]
+ fn deprecated_use_of_class() {}
+
+ if false {
+ deprecated_use_of_class();
+ };
};
- };
- attributes.push(quote! {{
- let mut __yew_classes = ::yew::html::Classes::with_capacity(#n);
- #(__yew_classes.push(#classes);)*
+ Some(quote! {
+ {
+ let mut __yew_classes = ::yew::html::Classes::with_capacity(#n);
+ #(__yew_classes.push(#classes);)*
- #deprecation_warning
+ #deprecation_warning
- if !__yew_classes.is_empty() {
- ::yew::virtual_dom::PositionalAttr::new("class", #sr)
- } else {
- ::yew::virtual_dom::PositionalAttr::new_placeholder("class")
- }
- }});
- }
- Some(ClassesForm::Single(classes)) => match classes.try_into_lit() {
- Some(lit) => {
- if !lit.value().is_empty() {
- let sr = lit.stringify();
- attributes.push(quote! {
- ::yew::virtual_dom::PositionalAttr::new("class", #sr)
- });
- }
+ ::yew::virtual_dom::PositionalAttr::new("class", __yew_classes)
+ }
+ })
}
- None => {
- let sr = stringify::stringify_at_runtime(quote! { __yew_classes });
- attributes.push(quote! {{
- let __yew_classes = ::std::convert::Into::<::yew::html::Classes>::into(#classes);
- if !__yew_classes.is_empty() {
- ::yew::virtual_dom::PositionalAttr::new("class", #sr)
+ ClassesForm::Single(classes) => match classes.try_into_lit() {
+ Some(lit) => {
+ if lit.value().is_empty() {
+ None
} else {
- ::yew::virtual_dom::PositionalAttr::new_placeholder("class")
+ let sr = lit.stringify();
+ Some(quote! { ::yew::virtual_dom::PositionalAttr::new("class", #sr) })
}
- }});
+ }
+ None => {
+ Some(quote! { ::yew::virtual_dom::PositionalAttr::new("class", ::std::convert::Into::<::yew::html::Classes>::into(#classes)) })
+ }
}
- },
- None => (),
+ });
+
+ let attrs = normal_attrs.chain(boolean_attrs).chain(class_attr);
+ quote! {
+ #vtag.attributes = ::yew::virtual_dom::Attributes::Vec(::std::vec![#(#attrs),*]);
+ }
};
- let listeners = if listeners.is_empty() {
- quote! { ::std::vec![] }
- } else if listeners.iter().any(|attr| attr.question_mark.is_some()) {
- let listeners = listeners
- .iter()
- .map(
- |Prop {
- label,
- question_mark,
- value,
- ..
- }| {
- let name = &label.name;
-
- if question_mark.is_some() {
- let ident = Ident::new("__yew_listener", name.span());
- let listener = to_wrapped_listener(name, &ident);
- quote_spanned! {value.span()=>
- ::std::option::Option::map(#value, |#ident| #listener)
- }
- } else {
- let listener = to_wrapped_listener(name, value);
- quote_spanned! {value.span()=> Some(#listener)}
- }
- },
- )
- .collect::>();
- quote! {{
- use ::std::iter::{Iterator, IntoIterator};
-
- ::std::vec![#(#listeners),*]
- .into_iter()
- .filter_map(|l| l)
- .collect::<::std::vec::Vec<::std::rc::Rc>>()
- }}
+ let set_listeners = if listeners.is_empty() {
+ None
+ } else {
+ let listeners_it = listeners.iter().map(|Prop { label, value, .. }| {
+ let name = &label.name;
+ quote! {
+ ::yew::html::#name::Wrapper::__macro_new(#value)
+ }
+ });
+
+ Some(quote! {
+ #vtag.__macro_set_listeners(::std::vec![#(#listeners_it),*]);
+ })
+ };
+
+ let add_children = if children.is_empty() {
+ None
} else {
- let listeners_it = listeners
- .iter()
- .map(|Prop { label, value, .. }| to_wrapped_listener(&label.name, value))
- .collect::>();
- quote! { ::std::vec![#(#listeners_it),*] }
+ Some(quote! {
+ #[allow(clippy::redundant_clone, unused_braces)]
+ #vtag.add_children(#children);
+ })
};
// These are the runtime-checks exclusive to dynamic tags.
@@ -336,7 +257,6 @@ impl ToTokens for HtmlElement {
let dyn_tag_runtime_checks = if matches!(&name, TagName::Expr(_)) {
// when Span::source_file Span::start get stabilised or yew-macro introduces a nightly feature flag
// we should expand the panic message to contain the exact location of the dynamic tag.
- let sr = stringify::stringify_at_runtime(quote! { __yew_v });
Some(quote! {
// check void element
if !#vtag.children.is_empty() {
@@ -353,11 +273,8 @@ impl ToTokens for HtmlElement {
match #vtag.tag() {
"input" | "textarea" => {}
_ => {
- if let ::std::option::Option::Some(__yew_v) = #vtag.value.take() {
- #vtag.__macro_push_attribute("value", #sr);
- } else {
- #vtag.__macro_push_attribute_placeholder("value");
- };
+ let __yew_v = #vtag.value.take();
+ #vtag.__macro_push_attr(::yew::virtual_dom::PositionalAttr::new("value", __yew_v));
}
}
})
@@ -367,38 +284,29 @@ impl ToTokens for HtmlElement {
tokens.extend(quote_spanned! {name.span()=>
{
- #[allow(clippy::redundant_clone, unused_braces)]
- let mut #vtag = ::yew::virtual_dom::VTag::__new_complete(
- #name_sr,
- #node_ref,
- #key,
- #value,
- #kind,
- #checked,
- ::yew::virtual_dom::Attributes::Vec(::std::vec![#(#attributes),*]),
- #listeners,
- ::yew::virtual_dom::VList{
- key: ::std::option::Option::None,
- children: #children,
- },
- );
+ #[allow(unused_braces)]
+ let mut #vtag = ::yew::virtual_dom::VTag::new(#name_sr);
+
+ #set_node_ref
+ #set_key
+ #set_value
+ #set_kind
+ #set_checked
+ #set_attributes
+ #set_listeners
+
+ #add_children
#dyn_tag_runtime_checks
- #[allow(unused_braces)]
- <::yew::virtual_dom::VNode as ::std::convert::From<_>>::from(#vtag)
+ {
+ use ::std::convert::From;
+ ::yew::virtual_dom::VNode::from(#vtag)
+ }
}
});
}
}
-fn to_wrapped_listener(name: &Ident, value: impl ToTokens) -> TokenStream {
- quote_spanned! {value.span()=>
- ::std::rc::Rc::new(::yew::html::#name::Wrapper::new(
- <::yew::virtual_dom::VTag as ::yew::virtual_dom::Transformer<_, _>>::transform(#value),
- )) as ::std::rc::Rc::
- }
-}
-
struct DynamicName {
at: Token![@],
expr: Option,
diff --git a/packages/yew-macro/src/html_tree/html_list.rs b/packages/yew-macro/src/html_tree/html_list.rs
index d3fb07c9d60..a9a75ce93f0 100644
--- a/packages/yew-macro/src/html_tree/html_list.rs
+++ b/packages/yew-macro/src/html_tree/html_list.rs
@@ -64,9 +64,7 @@ impl ToTokens for HtmlList {
} = &self;
let key = if let Some(key) = &open.props.key {
- quote_spanned! {key.span()=>
- ::std::option::Option::Some(::std::convert::Into::<::yew::virtual_dom::Key>::into(#key))
- }
+ quote_spanned! {key.span()=> ::std::option::Option::Some(::std::convert::Into::<::yew::virtual_dom::Key>::into(#key))}
} else {
quote! { ::std::option::Option::None }
};
@@ -139,8 +137,6 @@ impl Parse for HtmlListProps {
));
}
- prop.ensure_not_optional()?;
-
Some(prop.value)
};
diff --git a/packages/yew-macro/src/props/component.rs b/packages/yew-macro/src/props/component.rs
index 592d02ffd61..40891356ce8 100644
--- a/packages/yew-macro/src/props/component.rs
+++ b/packages/yew-macro/src/props/component.rs
@@ -138,13 +138,10 @@ impl ComponentProps {
let build_props = match self {
Self::List(props) => {
let set_props = props.iter().map(|Prop { label, value, .. }| {
- quote_spanned! {value.span()=> .#label(
- #[allow(unused_braces)]
- <::yew::virtual_dom::VComp as ::yew::virtual_dom::Transformer<_, _>>::transform(
- #value
- )
- )}
- });
+ quote_spanned! {value.span()=>
+ .#label(#value)
+ }
+ });
let set_children = children_renderer.map(|children| {
Some(quote_spanned! {props_ty.span()=>
@@ -200,12 +197,7 @@ impl TryFrom for ComponentProps {
fn try_from(props: Props) -> Result {
props.check_no_duplicates()?;
props.check_all(|prop| {
- if prop.question_mark.is_some() {
- Err(syn::Error::new_spanned(
- &prop.label,
- "optional attributes are only supported on elements. Components can use `Option` properties to accomplish the same thing.",
- ))
- } else if !prop.label.extended.is_empty() {
+ if !prop.label.extended.is_empty() {
Err(syn::Error::new_spanned(
&prop.label,
"expected a valid Rust identifier",
diff --git a/packages/yew-macro/src/props/element.rs b/packages/yew-macro/src/props/element.rs
index 3edeab3ad37..4d95c2a9093 100644
--- a/packages/yew-macro/src/props/element.rs
+++ b/packages/yew-macro/src/props/element.rs
@@ -41,23 +41,13 @@ impl Parse for ElementProps {
let booleans =
props.drain_filter(|prop| BOOLEAN_SET.contains(prop.label.to_string().as_str()));
- booleans.check_all(|prop| {
- if prop.question_mark.is_some() {
- Err(syn::Error::new_spanned(
- &prop.label,
- "boolean attributes don't support being used as an optional attribute (hint: a value of false results in the attribute not being set)"
- ))
- } else {
- Ok(())
- }
- })?;
let classes = props
- .pop_nonoptional("class")?
+ .pop("class")
.map(|prop| ClassesForm::from_expr(prop.value));
let value = props.pop("value");
let kind = props.pop("type");
- let checked = props.pop_nonoptional("checked")?;
+ let checked = props.pop("checked");
let SpecialProps { node_ref, key } = props.special;
diff --git a/packages/yew-macro/src/props/prop.rs b/packages/yew-macro/src/props/prop.rs
index 0036e6c6838..716dd2e0488 100644
--- a/packages/yew-macro/src/props/prop.rs
+++ b/packages/yew-macro/src/props/prop.rs
@@ -1,6 +1,6 @@
use crate::html_tree::HtmlDashedName;
use proc_macro2::TokenStream;
-use quote::{quote, ToTokens};
+use quote::ToTokens;
use std::{
cmp::Ordering,
convert::TryFrom,
@@ -26,40 +26,13 @@ impl ToTokens for PropPunct {
pub struct Prop {
pub label: HtmlDashedName,
- pub question_mark: Option,
/// Punctuation between `label` and `value`.
pub punct: Option,
pub value: Expr,
}
-impl Prop {
- /// Checks if the prop uses the optional attribute syntax.
- /// If it does, an error is returned.
- pub fn ensure_not_optional(&self) -> syn::Result<()> {
- let Self {
- label,
- question_mark,
- punct,
- ..
- } = self;
- if question_mark.is_some() {
- let msg = format!(
- "`{}` does not support being used as an optional attribute",
- label
- );
- // include `?=` in the span
- Err(syn::Error::new_spanned(
- quote! { #label#question_mark#punct },
- msg,
- ))
- } else {
- Ok(())
- }
- }
-}
impl Parse for Prop {
fn parse(input: ParseStream) -> syn::Result {
let label = input.parse::()?;
- let question_mark = input.parse::().ok();
let equals = input.parse::().map_err(|_| {
syn::Error::new_spanned(
&label,
@@ -75,7 +48,6 @@ impl Parse for Prop {
let value = input.parse::()?;
Ok(Self {
label,
- question_mark,
punct: Some(PropPunct::Eq(equals)),
value,
})
@@ -141,16 +113,6 @@ impl SortedPropList {
Ok(prop)
}
- /// Pop the prop with the given key and error if it uses the optional attribute syntax.
- pub fn pop_nonoptional(&mut self, key: &str) -> syn::Result