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

feat: Support generic components in rsx!() macro #385

Merged
merged 10 commits into from
Jun 11, 2022
38 changes: 38 additions & 0 deletions examples/rsx_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down Expand Up @@ -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::<TypedInputProps<i32>> {}
// 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!")]
Expand Down Expand Up @@ -227,9 +239,35 @@ pub fn Taller<'a>(cx: Scope<'a, TallerProps<'a>>) -> Element {
})
}

#[derive(Props, PartialEq)]
pub struct TypedInputProps<T> {
#[props(optional, default)]
initial: Option<T>,
}

#[allow(non_snake_case)]
pub fn TypedInput<T>(_: Scope<TypedInputProps<T>>) -> Element
where
T: FromStr + fmt::Display,
<T as FromStr>::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<T>(cx: Scope, text: T) -> Element
where
T: Display,
{
cx.render(rsx! {
p { "{text}" }
})
}
18 changes: 15 additions & 3 deletions packages/core-macro/src/inlineprops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub struct InlinePropsBody {
pub inputs: Punctuated<FnArg, Token![,]>,
// pub fields: FieldsNamed,
pub output: ReturnType,
pub where_clause: Option<WhereClause>,
pub block: Box<Block>,
}

Expand All @@ -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);
Expand All @@ -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 {
Expand All @@ -57,6 +63,7 @@ impl Parse for InlinePropsBody {
paren_token,
inputs,
output,
where_clause,
block,
cx_token,
attrs,
Expand All @@ -73,6 +80,7 @@ impl ToTokens for InlinePropsBody {
generics,
inputs,
output,
where_clause,
block,
cx_token,
attrs,
Expand Down Expand Up @@ -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
}
Expand Down
147 changes: 1 addition & 146 deletions packages/core-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(<p>"Even HTML templating!!"</p>)}
///
/// // 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<TallerProps>) -> DomTree {
/// let b = true;
/// todo!()
/// }
#[doc = include_str!("../../../examples/rsx_usage.rs")]
/// ```
#[proc_macro_error::proc_macro_error]
#[proc_macro]
Expand Down
4 changes: 3 additions & 1 deletion packages/core-macro/src/props/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
48 changes: 45 additions & 3 deletions packages/core-macro/src/rsx/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AngleBracketedGenericArguments>,
pub body: Vec<ComponentField>,
pub children: Vec<BodyNode>,
pub manual_props: Option<Expr>,
}

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<Self> {
let name = syn::Path::parse_mod_style(stream)?;
let mut name = stream.parse::<syn::Path>()?;
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;

Expand Down Expand Up @@ -64,6 +101,7 @@ impl Parse for Component {

Ok(Self {
name,
prop_gen_args,
body,
children,
manual_props,
Expand All @@ -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;

Expand Down Expand Up @@ -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" => {
Expand Down
16 changes: 16 additions & 0 deletions packages/core-macro/src/rsx/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
};
}
Loading