Merge pull request #315 from Synphonyte/master
Option<...> props are optional by default.
This commit is contained in:
5 files changed
+97
-58
No files matched your search
@@ -131,23 +131,29 @@ Borrowed Props cannot be safely memoized. However, this is not a problem – Dio
|
|||||||
|
|
||||||
## Optional Props
|
## 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
|
```rust
|
||||||
#[derive(Props, PartialEq)]
|
#[derive(Props, PartialEq)]
|
||||||
struct MyProps {
|
struct MyProps {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
#[props(optional)]
|
|
||||||
description: Option<String>
|
description: Option<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
fn Demo(cx: MyProps) -> Element {
|
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}"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
In this example ˋnameˋ is a required prop and ˋdescriptionˋ is optional.
|
||||||
Then, we can completely omit the description field when calling the component:
|
This means we can completely omit the description field when calling the component:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
rsx!{
|
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<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 full list of Dioxus' modifiers includes:
|
||||||
|
|
||||||
- `default` - automatically add the field using its `Default` implementation
|
- `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`
|
- `optional` - alias for `default` and `strip_option`
|
||||||
- `into` - automatically call `into` on the value at the callsite
|
- `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
|
## The `inline_props` macro
|
||||||
|
|
||||||
|
|||||||
+11
-10
@@ -14,37 +14,38 @@ fn app(cx: Scope) -> Element {
|
|||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
Button {
|
Button {
|
||||||
a: "asd".to_string(),
|
a: "asd".to_string(),
|
||||||
c: Some("asd".to_string()),
|
c: "asd".to_string(),
|
||||||
d: "asd".to_string(),
|
d: Some("asd".to_string()),
|
||||||
e: "asd".to_string(),
|
e: "asd".to_string(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SthElse<T> = Option<T>;
|
||||||
|
|
||||||
#[derive(Props, PartialEq)]
|
#[derive(Props, PartialEq)]
|
||||||
struct ButtonProps {
|
struct ButtonProps {
|
||||||
a: String,
|
a: String,
|
||||||
|
|
||||||
#[props(default)]
|
#[props(default)]
|
||||||
b: Option<String>,
|
b: String,
|
||||||
|
|
||||||
#[props(default)]
|
|
||||||
c: Option<String>,
|
c: Option<String>,
|
||||||
|
|
||||||
#[props(default, strip_option)]
|
#[props(!optional)]
|
||||||
d: Option<String>,
|
d: Option<String>,
|
||||||
|
|
||||||
#[props(optional)]
|
#[props(optional)]
|
||||||
e: Option<String>,
|
e: SthElse<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn Button(cx: Scope<ButtonProps>) -> Element {
|
fn Button(cx: Scope<ButtonProps>) -> Element {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
button {
|
button {
|
||||||
"{cx.props.a}"
|
"{cx.props.a} | "
|
||||||
"{cx.props.b:?}"
|
"{cx.props.b:?} | "
|
||||||
"{cx.props.c:?}"
|
"{cx.props.c:?} | "
|
||||||
"{cx.props.d:?}"
|
"{cx.props.d:?} | "
|
||||||
"{cx.props.e:?}"
|
"{cx.props.e:?}"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -43,27 +43,21 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result<TokenStream, Error> {
|
|||||||
syn::Fields::Unnamed(_) => {
|
syn::Fields::Unnamed(_) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
ast.span(),
|
ast.span(),
|
||||||
"TypedBuilder is not supported for tuple structs",
|
"Props is not supported for tuple structs",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
syn::Fields::Unit => {
|
syn::Fields::Unit => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
ast.span(),
|
ast.span(),
|
||||||
"TypedBuilder is not supported for unit structs",
|
"Props is not supported for unit structs",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
syn::Data::Enum(_) => {
|
syn::Data::Enum(_) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(ast.span(), "Props is not supported for enums"))
|
||||||
ast.span(),
|
|
||||||
"TypedBuilder is not supported for enums",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
syn::Data::Union(_) => {
|
syn::Data::Union(_) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(ast.span(), "Props is not supported for unions"))
|
||||||
ast.span(),
|
|
||||||
"TypedBuilder is not supported for unions",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(data)
|
Ok(data)
|
||||||
@@ -169,6 +163,7 @@ mod util {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod field_info {
|
mod field_info {
|
||||||
|
use crate::props::type_from_inside_option;
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::parse::Error;
|
use syn::parse::Error;
|
||||||
@@ -202,6 +197,16 @@ mod field_info {
|
|||||||
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
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 {
|
Ok(FieldInfo {
|
||||||
ordinal,
|
ordinal,
|
||||||
name,
|
name,
|
||||||
@@ -236,31 +241,8 @@ mod field_info {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn type_from_inside_option(&self) -> Option<&syn::Type> {
|
pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> {
|
||||||
let path = if let syn::Type::Path(type_path) = self.ty {
|
type_from_inside_option(self.ty, check_option_name)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +253,7 @@ mod field_info {
|
|||||||
pub skip: bool,
|
pub skip: bool,
|
||||||
pub auto_into: bool,
|
pub auto_into: bool,
|
||||||
pub strip_option: bool,
|
pub strip_option: bool,
|
||||||
|
pub ignore_option: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FieldBuilderAttr {
|
impl FieldBuilderAttr {
|
||||||
@@ -427,8 +410,9 @@ mod field_info {
|
|||||||
self.auto_into = false;
|
self.auto_into = false;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"strip_option" => {
|
"optional" => {
|
||||||
self.strip_option = false;
|
self.strip_option = false;
|
||||||
|
self.ignore_option = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
_ => Err(Error::new_spanned(path, "Unknown setting".to_owned())),
|
_ => 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 {
|
mod struct_info {
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
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
|
// 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.
|
// nesting is different so we have to do this little dance.
|
||||||
let arg_type = if field.builder_attr.strip_option {
|
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(
|
Error::new_spanned(
|
||||||
&field_type,
|
&field_type,
|
||||||
"can't `strip_option` - field is not `Option<...>`",
|
"can't `strip_option` - field is not `Option<...>`",
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ pub struct RedirectProps<'a> {
|
|||||||
/// // Relative path
|
/// // Relative path
|
||||||
/// Redirect { from: "", to: "../" }
|
/// Redirect { from: "", to: "../" }
|
||||||
/// ```
|
/// ```
|
||||||
#[props(optional)]
|
|
||||||
pub from: Option<&'a str>,
|
pub from: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ pub struct RouterProps<'a> {
|
|||||||
///
|
///
|
||||||
/// This will be used to trim any latent segments from the URL when your app is
|
/// This will be used to trim any latent segments from the URL when your app is
|
||||||
/// not deployed to the root of the domain.
|
/// not deployed to the root of the domain.
|
||||||
#[props(optional)]
|
|
||||||
pub base_url: Option<&'a str>,
|
pub base_url: Option<&'a str>,
|
||||||
|
|
||||||
/// Hook into the router when the route is changed.
|
/// 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.
|
/// This is useful if you don't want to repeat the same `active_class` prop value in every Link.
|
||||||
/// By default set to `"active"`.
|
/// By default set to `"active"`.
|
||||||
#[props(default, strip_option)]
|
|
||||||
pub active_class: Option<&'a str>,
|
pub active_class: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user