Skip to content

Commit 2c39a3b

Browse files
fix: allow bare None for Option props in html! macro (#4021)
The html! macro now detects bare `None` and bypasses IntoPropValue, avoiding type inference ambiguity when multiple Option<X> -> Option<Y> impls exist. Fixes #3747.
1 parent 148656f commit 2c39a3b

3 files changed

Lines changed: 158 additions & 5 deletions

File tree

packages/yew-macro/src/derive_props/field.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,28 @@ use proc_macro2::{Ident, Span};
55
use quote::{format_ident, quote, quote_spanned};
66
use syn::parse::Result;
77
use syn::spanned::Spanned;
8-
use syn::{parse_quote, Attribute, Error, Expr, Field, GenericParam, Generics, Type, Visibility};
8+
use syn::{
9+
parse_quote, Attribute, Error, Expr, Field, GenericArgument, GenericParam, Generics,
10+
PathArguments, Type, Visibility,
11+
};
912

1013
use super::should_preserve_attr;
1114
use crate::derive_props::generics::push_type_param;
1215

16+
fn is_option_type(ty: &Type) -> bool {
17+
if let Type::Path(type_path) = ty {
18+
if let Some(segment) = type_path.path.segments.last() {
19+
if segment.ident == "Option" {
20+
if let PathArguments::AngleBracketed(args) = &segment.arguments {
21+
return args.args.len() == 1
22+
&& matches!(args.args.first(), Some(GenericArgument::Type(_)));
23+
}
24+
}
25+
}
26+
}
27+
false
28+
}
29+
1330
#[allow(clippy::large_enum_variant)]
1431
#[derive(PartialEq, Eq)]
1532
pub enum PropAttr {
@@ -130,9 +147,24 @@ impl PropField {
130147
) -> proc_macro2::TokenStream {
131148
let Self { name, ty, attr, .. } = self;
132149
let token_ty = Ident::new("__YewTokenTy", Span::mixed_site());
150+
let none_fn_name = format_ident!("{}_none", name, span = Span::mixed_site());
133151
let build_fn = match attr {
134152
PropAttr::Required { wrapped_name } => {
135153
let check_struct = self.to_check_name(props_name);
154+
let none_setter = if is_option_type(ty) {
155+
quote! {
156+
#[doc(hidden)]
157+
#vis fn #none_fn_name<#token_ty>(
158+
&mut self,
159+
token: #token_ty,
160+
) -> #check_struct< #token_ty > {
161+
self.wrapped.#wrapped_name = ::std::option::Option::Some(::std::option::Option::None);
162+
#check_struct ( ::std::marker::PhantomData )
163+
}
164+
}
165+
} else {
166+
quote! {}
167+
};
136168
quote! {
137169
#[doc(hidden)]
138170
#vis fn #name<#token_ty>(
@@ -143,9 +175,25 @@ impl PropField {
143175
self.wrapped.#wrapped_name = ::std::option::Option::Some(value.into_prop_value());
144176
#check_struct ( ::std::marker::PhantomData )
145177
}
178+
179+
#none_setter
146180
}
147181
}
148182
_ => {
183+
let none_setter = if is_option_type(ty) {
184+
quote! {
185+
#[doc(hidden)]
186+
#vis fn #none_fn_name<#token_ty>(
187+
&mut self,
188+
token: #token_ty,
189+
) -> #token_ty {
190+
self.wrapped.#name = ::std::option::Option::Some(::std::option::Option::None);
191+
token
192+
}
193+
}
194+
} else {
195+
quote! {}
196+
};
149197
quote! {
150198
#[doc(hidden)]
151199
#vis fn #name<#token_ty>(
@@ -156,6 +204,8 @@ impl PropField {
156204
self.wrapped.#name = ::std::option::Option::Some(value.into_prop_value());
157205
token
158206
}
207+
208+
#none_setter
159209
}
160210
}
161211
};

packages/yew-macro/src/props/component.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ use syn::Expr;
99

1010
use super::{Prop, Props, SpecialProps, CHILDREN_LABEL};
1111

12+
fn is_none_expr(expr: &Expr) -> bool {
13+
matches!(
14+
expr,
15+
Expr::Path(syn::ExprPath {
16+
attrs,
17+
qself: None,
18+
path,
19+
}) if attrs.is_empty() && path.is_ident("None")
20+
)
21+
}
22+
1223
struct BaseExpr {
1324
pub dot_dot: DotDot,
1425
pub expr: Expr,
@@ -100,8 +111,18 @@ impl ComponentProps {
100111
let #token_ident = ::yew::html::AssertAllProps;
101112
};
102113
let set_props = self.props.iter().map(|Prop { label, value, .. }| {
103-
quote_spanned! {value.span()=>
104-
let #token_ident = #builder_ident.#label(#token_ident, #value);
114+
if is_none_expr(value) {
115+
let none_setter = Ident::new(
116+
&format!("{}_none", label),
117+
label.span().resolved_at(Span::mixed_site()),
118+
);
119+
quote_spanned! {value.span()=>
120+
let #token_ident = #builder_ident.#none_setter(#token_ident);
121+
}
122+
} else {
123+
quote_spanned! {value.span()=>
124+
let #token_ident = #builder_ident.#label(#token_ident, #value);
125+
}
105126
}
106127
});
107128
let set_children = children_renderer.map(|children| {
@@ -125,8 +146,14 @@ impl ComponentProps {
125146
Some(expr) => {
126147
let ident = Ident::new("__yew_props", props_ty.span());
127148
let set_props = self.props.iter().map(|Prop { label, value, .. }| {
128-
quote_spanned! {value.span().resolved_at(Span::call_site())=>
129-
#ident.#label = ::yew::html::IntoPropValue::into_prop_value(#value);
149+
if is_none_expr(value) {
150+
quote_spanned! {value.span().resolved_at(Span::call_site())=>
151+
#ident.#label = ::std::option::Option::None;
152+
}
153+
} else {
154+
quote_spanned! {value.span().resolved_at(Span::call_site())=>
155+
#ident.#label = ::yew::html::IntoPropValue::into_prop_value(#value);
156+
}
130157
}
131158
});
132159
let set_children = children_renderer.map(|children| {

packages/yew/src/html/conversion/into_prop_value.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,4 +570,80 @@ mod test {
570570
let _ = html! { <Child>{&attr_value}</Child> };
571571
}
572572
}
573+
574+
#[test]
575+
fn test_bare_none_option_string_prop() {
576+
use crate::prelude::*;
577+
578+
#[derive(PartialEq, Properties)]
579+
pub struct Props {
580+
pub foo: Option<String>,
581+
}
582+
583+
#[component]
584+
fn Comp(_props: &Props) -> Html {
585+
html! {}
586+
}
587+
588+
let _ = html! { <Comp foo={None} /> };
589+
let _ = html! { <Comp foo="hello" /> };
590+
let _ = html! { <Comp foo={Some("hello")} /> };
591+
}
592+
593+
#[test]
594+
fn test_bare_none_option_attr_value_prop() {
595+
use crate::prelude::*;
596+
597+
#[derive(PartialEq, Properties)]
598+
pub struct Props {
599+
pub foo: Option<AttrValue>,
600+
}
601+
602+
#[component]
603+
fn Comp(_props: &Props) -> Html {
604+
html! {}
605+
}
606+
607+
let _ = html! { <Comp foo={None} /> };
608+
let _ = html! { <Comp foo="hello" /> };
609+
let _ = html! { <Comp foo={AttrValue::from("hello")} /> };
610+
}
611+
612+
#[test]
613+
fn test_bare_none_option_html_prop() {
614+
use crate::prelude::*;
615+
616+
#[derive(PartialEq, Properties)]
617+
pub struct Props {
618+
pub title: Option<Html>,
619+
}
620+
621+
#[component]
622+
fn Comp(_props: &Props) -> Html {
623+
html! {}
624+
}
625+
626+
let _ = html! { <Comp title={None} /> };
627+
let _ = html! { <Comp title={Option::<Html>::None} /> };
628+
}
629+
630+
#[test]
631+
fn test_bare_none_optional_prop_with_default() {
632+
use crate::prelude::*;
633+
634+
#[derive(PartialEq, Properties)]
635+
pub struct Props {
636+
#[prop_or_default]
637+
pub foo: Option<String>,
638+
}
639+
640+
#[component]
641+
fn Comp(_props: &Props) -> Html {
642+
html! {}
643+
}
644+
645+
let _ = html! { <Comp foo={None} /> };
646+
let _ = html! { <Comp foo="hello" /> };
647+
let _ = html! { <Comp /> };
648+
}
573649
}

0 commit comments

Comments
 (0)