瀏覽代碼

Merge pull request #315 from Synphonyte/master

Option<...> props are optional by default.
Jon Kelley 3 年之前
父節點
當前提交
d3ac3db296

+ 38 - 8
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<String>
 }
 
 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:
+
+ˋˋˋ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<String>
+}
+ˋˋˋ
+
+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 `optional` modifier is a combination of two separate modifiers: `default` and `strip_option`. The full list of modifiers includes:
+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
 

+ 11 - 10
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<T> = Option<T>;
+
 #[derive(Props, PartialEq)]
 struct ButtonProps {
     a: String,
 
     #[props(default)]
-    b: Option<String>,
+    b: String,
 
-    #[props(default)]
     c: Option<String>,
 
-    #[props(default, strip_option)]
+    #[props(!optional)]
     d: Option<String>,
 
     #[props(optional)]
-    e: Option<String>,
+    e: SthElse<String>,
 }
 
 fn Button(cx: Scope<ButtonProps>) -> 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:?}"
         }
     })

+ 48 - 37
packages/core-macro/src/props/mod.rs

@@ -43,27 +43,21 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
             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<...>`",

+ 0 - 1
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>,
 }
 

+ 0 - 2
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>,
 }