diff --git a/docs/guide/src/components/propsmacro.md b/docs/guide/src/components/propsmacro.md index 83b01a3ea..a86a453e2 100644 --- a/docs/guide/src/components/propsmacro.md +++ b/docs/guide/src/components/propsmacro.md @@ -131,23 +131,29 @@ Borrowed Props cannot be safely memoized. However, this is not a problem – Dio ## Optional Props -You can easily create optional fields by attaching the `optional` modifier to a field: +You can easily create optional fields by using the `Option<…>` type for a field: ```rust #[derive(Props, PartialEq)] struct MyProps { name: String, - #[props(optional)] description: Option } fn Demo(cx: MyProps) -> Element { - todo!() + let text = match cx.props.description { + Some(d) => d, // if a value is provided + None => "No description" // if the prop is omitted + }; + + cx.render(rsx! { + "{name}": "{text}" + }) } ``` - -Then, we can completely omit the description field when calling the component: +In this example ˋnameˋ is a required prop and ˋdescriptionˋ is optional. +This means we can completely omit the description field when calling the component: ```rust rsx!{ @@ -157,15 +163,39 @@ rsx!{ } } ``` +Additionally if we provide a value we don't have to wrap it with ˋSome(…)ˋ. This is done automatically for us: -The `optional` modifier is a combination of two separate modifiers: `default` and `strip_option`. The full list of modifiers includes: +ˋˋˋrust +rsx!{ + Demo { + name: "Thing".to_string(), + description: "This is explains it".to_string(), + } +} +ˋˋˋ + +If you want to make a prop required even though it is of type ˋOptionˋ you can provide the ˋ!optionalˋ modifier: + +ˋˋˋrust +#[derive(Props, PartialEq)] +struct MyProps { + name: String, + + #[props(!optional)] + description: Option +} +ˋˋˋ + +This can be especially useful if you have a type alias named ˋOptionˋ in the current scope. + +For more information on how tags work, check out the [TypedBuilder](https://github.com/idanarye/rust-typed-builder) crate. However, all attributes for props in Dioxus are flattened (no need for `setter` syntax) and the `optional` field is new. The `optional` modifier is a combination of two separate modifiers: `default` and `strip_option` and it is automatically detected on ˋOption<…>ˋ types. + +The full list of Dioxus' modifiers includes: - `default` - automatically add the field using its `Default` implementation -- `strip_option` - automatically wrap values at the call site in `Some` - `optional` - alias for `default` and `strip_option` - `into` - automatically call `into` on the value at the callsite -For more information on how tags work, check out the [TypedBuilder](https://github.com/idanarye/rust-typed-builder) crate. However, all attributes for props in Dioxus are flattened (no need for `setter` syntax) and the `optional` field is new. ## The `inline_props` macro diff --git a/examples/optional_props.rs b/examples/optional_props.rs index 9485a2aeb..3c47b3aac 100644 --- a/examples/optional_props.rs +++ b/examples/optional_props.rs @@ -14,37 +14,38 @@ fn app(cx: Scope) -> Element { cx.render(rsx! { Button { a: "asd".to_string(), - c: Some("asd".to_string()), - d: "asd".to_string(), + c: "asd".to_string(), + d: Some("asd".to_string()), e: "asd".to_string(), } }) } +type SthElse = Option; + #[derive(Props, PartialEq)] struct ButtonProps { a: String, #[props(default)] - b: Option, + b: String, - #[props(default)] c: Option, - #[props(default, strip_option)] + #[props(!optional)] d: Option, #[props(optional)] - e: Option, + e: SthElse, } fn Button(cx: Scope) -> Element { cx.render(rsx! { button { - "{cx.props.a}" - "{cx.props.b:?}" - "{cx.props.c:?}" - "{cx.props.d:?}" + "{cx.props.a} | " + "{cx.props.b:?} | " + "{cx.props.c:?} | " + "{cx.props.d:?} | " "{cx.props.e:?}" } }) diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index d569e711a..6c65c5944 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -43,27 +43,21 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result { syn::Fields::Unnamed(_) => { return Err(Error::new( ast.span(), - "TypedBuilder is not supported for tuple structs", + "Props is not supported for tuple structs", )) } syn::Fields::Unit => { return Err(Error::new( ast.span(), - "TypedBuilder is not supported for unit structs", + "Props is not supported for unit structs", )) } }, syn::Data::Enum(_) => { - return Err(Error::new( - ast.span(), - "TypedBuilder is not supported for enums", - )) + return Err(Error::new(ast.span(), "Props is not supported for enums")) } syn::Data::Union(_) => { - return Err(Error::new( - ast.span(), - "TypedBuilder is not supported for unions", - )) + return Err(Error::new(ast.span(), "Props is not supported for unions")) } }; Ok(data) @@ -169,6 +163,7 @@ mod util { } mod field_info { + use crate::props::type_from_inside_option; use proc_macro2::TokenStream; use quote::quote; use syn::parse::Error; @@ -202,6 +197,16 @@ mod field_info { Some(syn::parse(quote!(Default::default()).into()).unwrap()); } + // auto detect optional + let strip_option_auto = builder_attr.strip_option + || !builder_attr.ignore_option + && type_from_inside_option(&field.ty, true).is_some(); + if !builder_attr.strip_option && strip_option_auto { + builder_attr.strip_option = true; + builder_attr.default = + Some(syn::parse(quote!(Default::default()).into()).unwrap()); + } + Ok(FieldInfo { ordinal, name, @@ -236,31 +241,8 @@ mod field_info { .into() } - pub fn type_from_inside_option(&self) -> Option<&syn::Type> { - let path = if let syn::Type::Path(type_path) = self.ty { - if type_path.qself.is_some() { - return None; - } else { - &type_path.path - } - } else { - return None; - }; - let segment = path.segments.last()?; - if segment.ident != "Option" { - return None; - } - let generic_params = - if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments { - generic_params - } else { - return None; - }; - if let syn::GenericArgument::Type(ty) = generic_params.args.first()? { - Some(ty) - } else { - None - } + pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> { + type_from_inside_option(self.ty, check_option_name) } } @@ -271,6 +253,7 @@ mod field_info { pub skip: bool, pub auto_into: bool, pub strip_option: bool, + pub ignore_option: bool, } impl FieldBuilderAttr { @@ -427,8 +410,9 @@ mod field_info { self.auto_into = false; Ok(()) } - "strip_option" => { + "optional" => { self.strip_option = false; + self.ignore_option = true; Ok(()) } _ => Err(Error::new_spanned(path, "Unknown setting".to_owned())), @@ -446,6 +430,33 @@ mod field_info { } } +fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&syn::Type> { + let path = if let syn::Type::Path(type_path) = ty { + if type_path.qself.is_some() { + return None; + } else { + &type_path.path + } + } else { + return None; + }; + let segment = path.segments.last()?; + if check_option_name && segment.ident != "Option" { + return None; + } + let generic_params = + if let syn::PathArguments::AngleBracketed(generic_params) = &segment.arguments { + generic_params + } else { + return None; + }; + if let syn::GenericArgument::Type(ty) = generic_params.args.first()? { + Some(ty) + } else { + None + } +} + mod struct_info { use proc_macro2::TokenStream; use quote::quote; @@ -766,7 +777,7 @@ Finally, call `.build()` to create the instance of `{name}`. // NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of // nesting is different so we have to do this little dance. let arg_type = if field.builder_attr.strip_option { - let internal_type = field.type_from_inside_option().ok_or_else(|| { + let internal_type = field.type_from_inside_option(false).ok_or_else(|| { Error::new_spanned( &field_type, "can't `strip_option` - field is not `Option<...>`", diff --git a/packages/router/src/components/redirect.rs b/packages/router/src/components/redirect.rs index a1d2d7727..7a7d85be2 100644 --- a/packages/router/src/components/redirect.rs +++ b/packages/router/src/components/redirect.rs @@ -27,7 +27,6 @@ pub struct RedirectProps<'a> { /// // Relative path /// Redirect { from: "", to: "../" } /// ``` - #[props(optional)] pub from: Option<&'a str>, } diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index d7df377f1..85adfa422 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -20,7 +20,6 @@ pub struct RouterProps<'a> { /// /// This will be used to trim any latent segments from the URL when your app is /// not deployed to the root of the domain. - #[props(optional)] pub base_url: Option<&'a str>, /// Hook into the router when the route is changed. @@ -33,7 +32,6 @@ pub struct RouterProps<'a> { /// /// This is useful if you don't want to repeat the same `active_class` prop value in every Link. /// By default set to `"active"`. - #[props(default, strip_option)] pub active_class: Option<&'a str>, }