diff --git a/packages/yew-macro/src/derive_props/field.rs b/packages/yew-macro/src/derive_props/field.rs
index 09ee9809048..6898acc89cf 100644
--- a/packages/yew-macro/src/derive_props/field.rs
+++ b/packages/yew-macro/src/derive_props/field.rs
@@ -5,11 +5,28 @@ use proc_macro2::{Ident, Span};
use quote::{format_ident, quote, quote_spanned};
use syn::parse::Result;
use syn::spanned::Spanned;
-use syn::{parse_quote, Attribute, Error, Expr, Field, GenericParam, Generics, Type, Visibility};
+use syn::{
+ parse_quote, Attribute, Error, Expr, Field, GenericArgument, GenericParam, Generics,
+ PathArguments, Type, Visibility,
+};
use super::should_preserve_attr;
use crate::derive_props::generics::push_type_param;
+fn is_option_type(ty: &Type) -> bool {
+ if let Type::Path(type_path) = ty {
+ if let Some(segment) = type_path.path.segments.last() {
+ if segment.ident == "Option" {
+ if let PathArguments::AngleBracketed(args) = &segment.arguments {
+ return args.args.len() == 1
+ && matches!(args.args.first(), Some(GenericArgument::Type(_)));
+ }
+ }
+ }
+ }
+ false
+}
+
#[allow(clippy::large_enum_variant)]
#[derive(PartialEq, Eq)]
pub enum PropAttr {
@@ -130,9 +147,24 @@ impl PropField {
) -> proc_macro2::TokenStream {
let Self { name, ty, attr, .. } = self;
let token_ty = Ident::new("__YewTokenTy", Span::mixed_site());
+ let none_fn_name = format_ident!("{}_none", name, span = Span::mixed_site());
let build_fn = match attr {
PropAttr::Required { wrapped_name } => {
let check_struct = self.to_check_name(props_name);
+ let none_setter = if is_option_type(ty) {
+ quote! {
+ #[doc(hidden)]
+ #vis fn #none_fn_name<#token_ty>(
+ &mut self,
+ token: #token_ty,
+ ) -> #check_struct< #token_ty > {
+ self.wrapped.#wrapped_name = ::std::option::Option::Some(::std::option::Option::None);
+ #check_struct ( ::std::marker::PhantomData )
+ }
+ }
+ } else {
+ quote! {}
+ };
quote! {
#[doc(hidden)]
#vis fn #name<#token_ty>(
@@ -143,9 +175,25 @@ impl PropField {
self.wrapped.#wrapped_name = ::std::option::Option::Some(value.into_prop_value());
#check_struct ( ::std::marker::PhantomData )
}
+
+ #none_setter
}
}
_ => {
+ let none_setter = if is_option_type(ty) {
+ quote! {
+ #[doc(hidden)]
+ #vis fn #none_fn_name<#token_ty>(
+ &mut self,
+ token: #token_ty,
+ ) -> #token_ty {
+ self.wrapped.#name = ::std::option::Option::Some(::std::option::Option::None);
+ token
+ }
+ }
+ } else {
+ quote! {}
+ };
quote! {
#[doc(hidden)]
#vis fn #name<#token_ty>(
@@ -156,6 +204,8 @@ impl PropField {
self.wrapped.#name = ::std::option::Option::Some(value.into_prop_value());
token
}
+
+ #none_setter
}
}
};
diff --git a/packages/yew-macro/src/props/component.rs b/packages/yew-macro/src/props/component.rs
index f2eed2a5955..ea401732ba4 100644
--- a/packages/yew-macro/src/props/component.rs
+++ b/packages/yew-macro/src/props/component.rs
@@ -9,6 +9,17 @@ use syn::Expr;
use super::{Prop, Props, SpecialProps, CHILDREN_LABEL};
+fn is_none_expr(expr: &Expr) -> bool {
+ matches!(
+ expr,
+ Expr::Path(syn::ExprPath {
+ attrs,
+ qself: None,
+ path,
+ }) if attrs.is_empty() && path.is_ident("None")
+ )
+}
+
struct BaseExpr {
pub dot_dot: DotDot,
pub expr: Expr,
@@ -100,8 +111,18 @@ impl ComponentProps {
let #token_ident = ::yew::html::AssertAllProps;
};
let set_props = self.props.iter().map(|Prop { label, value, .. }| {
- quote_spanned! {value.span()=>
- let #token_ident = #builder_ident.#label(#token_ident, #value);
+ if is_none_expr(value) {
+ let none_setter = Ident::new(
+ &format!("{}_none", label),
+ label.span().resolved_at(Span::mixed_site()),
+ );
+ quote_spanned! {value.span()=>
+ let #token_ident = #builder_ident.#none_setter(#token_ident);
+ }
+ } else {
+ quote_spanned! {value.span()=>
+ let #token_ident = #builder_ident.#label(#token_ident, #value);
+ }
}
});
let set_children = children_renderer.map(|children| {
@@ -125,8 +146,14 @@ impl ComponentProps {
Some(expr) => {
let ident = Ident::new("__yew_props", props_ty.span());
let set_props = self.props.iter().map(|Prop { label, value, .. }| {
- quote_spanned! {value.span().resolved_at(Span::call_site())=>
- #ident.#label = ::yew::html::IntoPropValue::into_prop_value(#value);
+ if is_none_expr(value) {
+ quote_spanned! {value.span().resolved_at(Span::call_site())=>
+ #ident.#label = ::std::option::Option::None;
+ }
+ } else {
+ quote_spanned! {value.span().resolved_at(Span::call_site())=>
+ #ident.#label = ::yew::html::IntoPropValue::into_prop_value(#value);
+ }
}
});
let set_children = children_renderer.map(|children| {
diff --git a/packages/yew/src/html/conversion/into_prop_value.rs b/packages/yew/src/html/conversion/into_prop_value.rs
index 343ef2a28d4..05f8a99bcb4 100644
--- a/packages/yew/src/html/conversion/into_prop_value.rs
+++ b/packages/yew/src/html/conversion/into_prop_value.rs
@@ -570,4 +570,80 @@ mod test {
let _ = html! { {&attr_value} };
}
}
+
+ #[test]
+ fn test_bare_none_option_string_prop() {
+ use crate::prelude::*;
+
+ #[derive(PartialEq, Properties)]
+ pub struct Props {
+ pub foo: Option,
+ }
+
+ #[component]
+ fn Comp(_props: &Props) -> Html {
+ html! {}
+ }
+
+ let _ = html! { };
+ let _ = html! { };
+ let _ = html! { };
+ }
+
+ #[test]
+ fn test_bare_none_option_attr_value_prop() {
+ use crate::prelude::*;
+
+ #[derive(PartialEq, Properties)]
+ pub struct Props {
+ pub foo: Option,
+ }
+
+ #[component]
+ fn Comp(_props: &Props) -> Html {
+ html! {}
+ }
+
+ let _ = html! { };
+ let _ = html! { };
+ let _ = html! { };
+ }
+
+ #[test]
+ fn test_bare_none_option_html_prop() {
+ use crate::prelude::*;
+
+ #[derive(PartialEq, Properties)]
+ pub struct Props {
+ pub title: Option,
+ }
+
+ #[component]
+ fn Comp(_props: &Props) -> Html {
+ html! {}
+ }
+
+ let _ = html! { };
+ let _ = html! { ::None} /> };
+ }
+
+ #[test]
+ fn test_bare_none_optional_prop_with_default() {
+ use crate::prelude::*;
+
+ #[derive(PartialEq, Properties)]
+ pub struct Props {
+ #[prop_or_default]
+ pub foo: Option,
+ }
+
+ #[component]
+ fn Comp(_props: &Props) -> Html {
+ html! {}
+ }
+
+ let _ = html! { };
+ let _ = html! { };
+ let _ = html! { };
+ }
}