Skip to content

Commit

Permalink
Merge pull request #385 from MuhannadAlrusayni/master
Browse files Browse the repository at this point in the history
feat: Support generic components in `rsx!()` macro
  • Loading branch information
jkelleyrtp authored Jun 11, 2022
2 parents ea4eb21 + dded91a commit 03973f6
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 184 deletions.
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

0 comments on commit 03973f6

Please sign in to comment.