Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option<...> props are optional by default. #315

Merged
merged 3 commits into from Mar 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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