diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index 490aea844..774eb0ead 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -46,6 +46,9 @@ fn main() { /// This type alias specifies the type for you so you don't need to write "None as Option<()>" const NONE_ELEMENT: Option<()> = None; +use core::{fmt, str::FromStr}; +use std::fmt::Display; + use baller::Baller; use dioxus::prelude::*; @@ -187,6 +190,15 @@ fn app(cx: Scope) -> Element { text: "using functionc all syntax" ) + // Components can be geneirc too + // This component takes i32 type to give you typed input + TypedInput::> {} + // Type inference can be used too + TypedInput { initial: 10.0 } + // geneircs with the `inline_props` macro + label(text: "hello geneirc world!") + label(text: 99.9) + // helper functions // Single values must be wrapped in braces or `Some` to satisfy `IntoIterator` [helper(&cx, "hello world!")] @@ -227,9 +239,35 @@ pub fn Taller<'a>(cx: Scope<'a, TallerProps<'a>>) -> Element { }) } +#[derive(Props, PartialEq)] +pub struct TypedInputProps { + #[props(optional, default)] + initial: Option, +} + +#[allow(non_snake_case)] +pub fn TypedInput(_: Scope>) -> Element +where + T: FromStr + fmt::Display, + ::Err: std::fmt::Display, +{ + todo!() +} + #[inline_props] fn with_inline<'a>(cx: Scope<'a>, text: &'a str) -> Element { cx.render(rsx! { p { "{text}" } }) } + +// generic component with inline_props too +#[inline_props] +fn label(cx: Scope, text: T) -> Element +where + T: Display, +{ + cx.render(rsx! { + p { "{text}" } + }) +} diff --git a/packages/core-macro/src/inlineprops.rs b/packages/core-macro/src/inlineprops.rs index 782d13a48..05d9d5240 100644 --- a/packages/core-macro/src/inlineprops.rs +++ b/packages/core-macro/src/inlineprops.rs @@ -17,6 +17,7 @@ pub struct InlinePropsBody { pub inputs: Punctuated, // pub fields: FieldsNamed, pub output: ReturnType, + pub where_clause: Option, pub block: Box, } @@ -28,7 +29,7 @@ impl Parse for InlinePropsBody { let fn_token = input.parse()?; let ident = input.parse()?; - let generics = input.parse()?; + let generics: Generics = input.parse()?; let content; let paren_token = syn::parenthesized!(content in input); @@ -47,6 +48,11 @@ impl Parse for InlinePropsBody { let output = input.parse()?; + let where_clause = input + .peek(syn::token::Where) + .then(|| input.parse()) + .transpose()?; + let block = input.parse()?; Ok(Self { @@ -57,6 +63,7 @@ impl Parse for InlinePropsBody { paren_token, inputs, output, + where_clause, block, cx_token, attrs, @@ -73,6 +80,7 @@ impl ToTokens for InlinePropsBody { generics, inputs, output, + where_clause, block, cx_token, attrs, @@ -136,12 +144,16 @@ impl ToTokens for InlinePropsBody { out_tokens.append_all(quote! { #modifiers #[allow(non_camel_case_types)] - #vis struct #struct_name #struct_generics { + #vis struct #struct_name #struct_generics + #where_clause + { #(#fields),* } #(#attrs)* - #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output { + #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics>) #output + #where_clause + { let #struct_name { #(#field_names),* } = &cx.props; #block } diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 931933386..8407a3733 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -29,152 +29,7 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token /// /// ## Complete Reference Guide: /// ``` -/// const Example: Component = |cx| { -/// let formatting = "formatting!"; -/// let formatting_tuple = ("a", "b"); -/// let lazy_fmt = format_args!("lazily formatted text"); -/// cx.render(rsx! { -/// div { -/// // Elements -/// div {} -/// h1 {"Some text"} -/// h1 {"Some text with {formatting}"} -/// h1 {"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"} -/// h2 { -/// "Multiple" -/// "Text" -/// "Blocks" -/// "Use comments as separators in html" -/// } -/// div { -/// h1 {"multiple"} -/// h2 {"nested"} -/// h3 {"elements"} -/// } -/// div { -/// class: "my special div" -/// h1 {"Headers and attributes!"} -/// } -/// div { -/// // pass simple rust expressions in -/// class: lazy_fmt, -/// id: format_args!("attributes can be passed lazily with std::fmt::Arguments"), -/// div { -/// class: { -/// const WORD: &str = "expressions"; -/// format_args!("Arguments can be passed in through curly braces for complex {}", WORD) -/// } -/// } -/// } -/// -/// // Expressions can be used in element position too: -/// {rsx!(p { "More templating!" })} -/// {html!(

"Even HTML templating!!"

)} -/// -/// // Iterators -/// {(0..10).map(|i| rsx!(li { "{i}" }))} -/// {{ -/// let data = std::collections::HashMap::<&'static str, &'static str>::new(); -/// // Iterators *should* have keys when you can provide them. -/// // Keys make your app run faster. Make sure your keys are stable, unique, and predictable. -/// // Using an "ID" associated with your data is a good idea. -/// data.into_iter().map(|(k, v)| rsx!(li { key: "{k}" "{v}" })) -/// }} -/// -/// // Matching -/// {match true { -/// true => rsx!(h1 {"Top text"}), -/// false => rsx!(h1 {"Bottom text"}) -/// }} -/// -/// // Conditional rendering -/// // Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals. -/// // You can convert a bool condition to rsx! with .then and .or -/// {true.then(|| rsx!(div {}))} -/// -/// // True conditions -/// {if true { -/// rsx!(h1 {"Top text"}) -/// } else { -/// rsx!(h1 {"Bottom text"}) -/// }} -/// -/// // returning "None" is a bit noisy... but rare in practice -/// {None as Option<()>} -/// -/// // Use the Dioxus type-alias for less noise -/// {NONE_ELEMENT} -/// -/// // can also just use empty fragments -/// Fragment {} -/// -/// // Fragments let you insert groups of nodes without a parent. -/// // This lets you make components that insert elements as siblings without a container. -/// div {"A"} -/// Fragment { -/// div {"B"} -/// div {"C"} -/// Fragment { -/// "D" -/// Fragment { -/// "heavily nested fragments is an antipattern" -/// "they cause Dioxus to do unnecessary work" -/// "don't use them carelessly if you can help it" -/// } -/// } -/// } -/// -/// // Components -/// // Can accept any paths -/// // Notice how you still get syntax highlighting and IDE support :) -/// Baller {} -/// baller::Baller { } -/// crate::baller::Baller {} -/// -/// // Can take properties -/// Taller { a: "asd" } -/// -/// // Can take optional properties -/// Taller { a: "asd" } -/// -/// // Can pass in props directly as an expression -/// {{ -/// let props = TallerProps {a: "hello"}; -/// rsx!(Taller { ..props }) -/// }} -/// -/// // Spreading can also be overridden manually -/// Taller { -/// ..TallerProps { a: "ballin!" } -/// a: "not ballin!" -/// } -/// -/// // Can take children too! -/// Taller { a: "asd", div {"hello world!"} } -/// } -/// }) -/// }; -/// -/// mod baller { -/// use super::*; -/// pub struct BallerProps {} -/// -/// /// This component totally balls -/// pub fn Baller(cx: Scope) -> DomTree { -/// todo!() -/// } -/// } -/// -/// #[derive(Debug, PartialEq, Props)] -/// pub struct TallerProps { -/// a: &'static str, -/// } -/// -/// /// This component is taller than most :) -/// pub fn Taller(cx: Scope) -> DomTree { -/// let b = true; -/// todo!() -/// } +#[doc = include_str!("../../../examples/rsx_usage.rs")] /// ``` #[proc_macro_error::proc_macro_error] #[proc_macro] diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 6c65c5944..92772df00 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -653,7 +653,9 @@ Finally, call `.build()` to create the instance of `{name}`. } } - impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{ + impl #impl_generics dioxus::prelude::Properties for #name #ty_generics + #b_generics_where_extras_predicates + { type Builder = #builder_name #generics_with_empty; const IS_STATIC: bool = #is_static; fn builder() -> Self::Builder { diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 5b0a38b87..91c8c4903 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -18,19 +18,56 @@ use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ ext::IdentExt, parse::{Parse, ParseBuffer, ParseStream}, - token, Expr, Ident, LitStr, Result, Token, + token, AngleBracketedGenericArguments, Expr, Ident, LitStr, PathArguments, Result, Token, }; pub struct Component { pub name: syn::Path, + pub prop_gen_args: Option, pub body: Vec, pub children: Vec, pub manual_props: Option, } +impl Component { + pub fn validate_component_path(path: &syn::Path) -> Result<()> { + // ensure path segments doesn't have PathArguments, only the last + // segment is allowed to have one. + if path + .segments + .iter() + .take(path.segments.len() - 1) + .any(|seg| seg.arguments != PathArguments::None) + { + component_path_cannot_have_arguments!(path); + } + + // ensure last segment only have value of None or AngleBracketed + if !matches!( + path.segments.last().unwrap().arguments, + PathArguments::None | PathArguments::AngleBracketed(_) + ) { + invalid_component_path!(path); + } + + Ok(()) + } +} + impl Parse for Component { fn parse(stream: ParseStream) -> Result { - let name = syn::Path::parse_mod_style(stream)?; + let mut name = stream.parse::()?; + Component::validate_component_path(&name)?; + + // extract the path arguments from the path into prop_gen_args + let prop_gen_args = name.segments.last_mut().and_then(|seg| { + if let PathArguments::AngleBracketed(args) = seg.arguments.clone() { + seg.arguments = PathArguments::None; + Some(args) + } else { + None + } + }); let content: ParseBuffer; @@ -64,6 +101,7 @@ impl Parse for Component { Ok(Self { name, + prop_gen_args, body, children, manual_props, @@ -74,6 +112,7 @@ impl Parse for Component { impl ToTokens for Component { fn to_tokens(&self, tokens: &mut TokenStream2) { let name = &self.name; + let prop_gen_args = &self.prop_gen_args; let mut has_key = None; @@ -101,7 +140,10 @@ impl ToTokens for Component { }} } None => { - let mut toks = quote! { fc_to_builder(#name) }; + let mut toks = match prop_gen_args { + Some(gen_args) => quote! { fc_to_builder #gen_args(#name) }, + None => quote! { fc_to_builder(#name) }, + }; for field in &self.body { match field.name.to_string().as_str() { "key" => { diff --git a/packages/core-macro/src/rsx/errors.rs b/packages/core-macro/src/rsx/errors.rs index 27e2c7f98..476f42eff 100644 --- a/packages/core-macro/src/rsx/errors.rs +++ b/packages/core-macro/src/rsx/errors.rs @@ -13,3 +13,19 @@ macro_rules! attr_after_element { ) }; } + +macro_rules! component_path_cannot_have_arguments { + ($span:expr) => { + proc_macro_error::abort!( + $span, + "expected a path without arguments"; + help = "try remove the path arguments" + ) + }; +} + +macro_rules! invalid_component_path { + ($span:expr) => { + proc_macro_error::abort!($span, "Invalid component path syntax") + }; +} diff --git a/packages/core-macro/src/rsx/node.rs b/packages/core-macro/src/rsx/node.rs index d5d701767..1f1c19131 100644 --- a/packages/core-macro/src/rsx/node.rs +++ b/packages/core-macro/src/rsx/node.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseStream}, - token, Expr, LitStr, Result, Token, + token, Expr, LitStr, Result, }; /* @@ -28,39 +28,49 @@ impl Parse for BodyNode { return Ok(BodyNode::Text(stream.parse()?)); } - // div {} -> el - // Div {} -> comp - if stream.peek(syn::Ident) && stream.peek2(token::Brace) { - if stream - .fork() - .parse::()? - .to_string() - .chars() - .next() - .unwrap() - .is_ascii_uppercase() - { - return Ok(BodyNode::Component(stream.parse()?)); - } else { - return Ok(BodyNode::Element(stream.parse::()?)); + let body_stream = stream.fork(); + if let Ok(path) = body_stream.parse::() { + // this is an Element if path match of: + // - one ident + // - followed by `{` + // - 1st char is lowercase + // + // example: + // div {} + if let Some(ident) = path.get_ident() { + if body_stream.peek(token::Brace) + && ident + .to_string() + .chars() + .next() + .unwrap() + .is_ascii_lowercase() + { + return Ok(BodyNode::Element(stream.parse::()?)); + } } - } - // component() -> comp - // ::component {} -> comp - // ::component () -> comp - if (stream.peek(syn::Ident) && stream.peek2(token::Paren)) - || (stream.peek(Token![::])) - || (stream.peek(Token![:]) && stream.peek2(Token![:])) - { - return Ok(BodyNode::Component(stream.parse::()?)); - } + // Otherwise this should be Component, allowed syntax: + // - syn::Path + // - PathArguments can only apper in last segment + // - followed by `{` or `(`, note `(` cannot be used with one ident + // + // example + // Div {} + // Div () + // ::Div {} + // crate::Div {} + // component() + // ::component {} + // ::component () + // crate::component{} + // crate::component() + // Input:: {} + // crate::Input:: {} + if body_stream.peek(token::Brace) || body_stream.peek(token::Paren) { + Component::validate_component_path(&path)?; - // crate::component{} -> comp - // crate::component() -> comp - if let Ok(pat) = stream.fork().parse::() { - if pat.segments.len() > 1 { - return Ok(BodyNode::Component(stream.parse::()?)); + return Ok(BodyNode::Component(stream.parse()?)); } }