Skip to content

Commit

Permalink
Merge pull request #315 from Synphonyte/master
Browse files Browse the repository at this point in the history
Option<...> props are optional by default.
  • Loading branch information
jkelleyrtp committed Mar 20, 2022
2 parents 7170d8e + a2825fb commit d3ac3db
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 58 deletions.
46 changes: 38 additions & 8 deletions docs/guide/src/components/propsmacro.md
Expand Up @@ -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!{
Expand All @@ -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

Expand Down
21 changes: 11 additions & 10 deletions examples/optional_props.rs
Expand Up @@ -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:?}"
}
})
Expand Down
85 changes: 48 additions & 37 deletions packages/core-macro/src/props/mod.rs
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -271,6 +253,7 @@ mod field_info {
pub skip: bool,
pub auto_into: bool,
pub strip_option: bool,
pub ignore_option: bool,
}

impl FieldBuilderAttr {
Expand Down Expand Up @@ -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())),
Expand All @@ -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;
Expand Down Expand Up @@ -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<...>`",
Expand Down
1 change: 0 additions & 1 deletion packages/router/src/components/redirect.rs
Expand Up @@ -27,7 +27,6 @@ pub struct RedirectProps<'a> {
/// // Relative path
/// Redirect { from: "", to: "../" }
/// ```
#[props(optional)]
pub from: Option<&'a str>,
}

Expand Down
2 changes: 0 additions & 2 deletions packages/router/src/components/router.rs
Expand Up @@ -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.
Expand All @@ -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>,
}

Expand Down

0 comments on commit d3ac3db

Please sign in to comment.