diff --git a/Cargo.toml b/Cargo.toml index 0c5f8d3b..c4ebb918 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "examples/context", "examples/counter", "examples/hello", + "examples/higher-order-components", "examples/iteration", "examples/ssr", "examples/todomvc", diff --git a/docs/next/getting_started/hello_world.md b/docs/next/getting_started/hello_world.md index 9645d2aa..9afbca76 100644 --- a/docs/next/getting_started/hello_world.md +++ b/docs/next/getting_started/hello_world.md @@ -1,7 +1,6 @@ # Hello, World! -Sycamore tries to have as simple of an API as possible. In fact, the Hello World program in Sycamore -is but slightly longer than the console version! +Sycamore tries to have as simple of an API as possible. Here it is: @@ -49,9 +48,24 @@ we want to render the following HTML: The `p { ... }` creates a new `

` tag. The `"Hello, World!"` creates a new text node that is nested within the `

` tag. -There it is! To try it out, copy the Hello World code snippet to your `main.rs` file and run -`trunk serve` from your command prompt. Open up your browser at `localhost:8080` and you should see -_"Hello, World!"_ printed to the screen in all its glory. +There it is! Trunk just needs one thing to turn this into a website; a html source file to inject +the template into. Copy the following code to a file called `index.html` in the root of your crate +(alongside `Cargo.toml`): + +```html + + + + + My first Sycamore app + + + +``` + +To try it out, copy the Hello World code snippet to your `main.rs` file and run `trunk serve` from +your command prompt. Open up your browser at `localhost:8080` and you should see _"Hello, World!"_ +printed to the screen in all its glory. If you modify your code, Trunk should automatically rebuild your app. Just refresh your browser tab to see the latest changes. diff --git a/docs/versioned_docs/v0.5/getting_started/hello_world.md b/docs/versioned_docs/v0.5/getting_started/hello_world.md index e1710434..9afbca76 100644 --- a/docs/versioned_docs/v0.5/getting_started/hello_world.md +++ b/docs/versioned_docs/v0.5/getting_started/hello_world.md @@ -48,24 +48,24 @@ we want to render the following HTML: The `p { ... }` creates a new `

` tag. The `"Hello, World!"` creates a new text node that is nested within the `

` tag. -There it is! Trunk just needs one thing to turn this into a website; a html source file to inject the -template into. Copy the following code to a file called `index.html` in the root of your crate +There it is! Trunk just needs one thing to turn this into a website; a html source file to inject +the template into. Copy the following code to a file called `index.html` in the root of your crate (alongside `Cargo.toml`): ```html - - - My first Sycamore app - - + + + My first Sycamore app + + ``` -To try it out, copy the Hello World code snippet to your `main.rs` file and run -`trunk serve` from your command prompt. Open up your browser at `localhost:8080` and you should see -_"Hello, World!"_ printed to the screen in all its glory. +To try it out, copy the Hello World code snippet to your `main.rs` file and run `trunk serve` from +your command prompt. Open up your browser at `localhost:8080` and you should see _"Hello, World!"_ +printed to the screen in all its glory. If you modify your code, Trunk should automatically rebuild your app. Just refresh your browser tab to see the latest changes. diff --git a/examples/higher-order-components/Cargo.toml b/examples/higher-order-components/Cargo.toml new file mode 100644 index 00000000..4bac158f --- /dev/null +++ b/examples/higher-order-components/Cargo.toml @@ -0,0 +1,14 @@ +[package] +authors = ["Luke Chu <37006668+lukechu10@users.noreply.github.com>"] +edition = "2018" +name = "higher-order-components" +publish = false +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +console_error_panic_hook = "0.1.6" +console_log = "0.2.0" +log = "0.4.14" +sycamore = {path = "../../packages/sycamore"} diff --git a/examples/higher-order-components/index.html b/examples/higher-order-components/index.html new file mode 100644 index 00000000..e8af15b2 --- /dev/null +++ b/examples/higher-order-components/index.html @@ -0,0 +1,15 @@ + + + + + + Higher Kinded Components + + + + + diff --git a/examples/higher-order-components/src/main.rs b/examples/higher-order-components/src/main.rs new file mode 100644 index 00000000..18271a7d --- /dev/null +++ b/examples/higher-order-components/src/main.rs @@ -0,0 +1,29 @@ +use sycamore::component::Component; +use sycamore::prelude::*; + +#[component(EnhancedComponent)] +fn enhanced_component>() -> Template { + template! { + div(class="enhanced-container") { + p { "Enhanced container start" } + C(42) + p { "Enhanced container end" } + } + } +} + +#[component(NumberDisplayer)] +fn number_displayer(prop: i32) -> Template { + template! { + p { "My number is: " (prop) } + } +} + +type EnhancedNumberDisplayer = EnhancedComponent>; + +fn main() { + console_error_panic_hook::set_once(); + console_log::init_with_level(log::Level::Debug).unwrap(); + + sycamore::render(|| template! { EnhancedNumberDisplayer() }); +} diff --git a/packages/sycamore-macro/src/component/mod.rs b/packages/sycamore-macro/src/component/mod.rs index d80084d3..5a70f798 100644 --- a/packages/sycamore-macro/src/component/mod.rs +++ b/packages/sycamore-macro/src/component/mod.rs @@ -2,7 +2,7 @@ use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::parse::{Parse, ParseStream}; use syn::{ - Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, Result, ReturnType, Type, TypeParam, + Attribute, Block, FnArg, GenericParam, Generics, Ident, Item, ItemFn, Result, ReturnType, Type, Visibility, }; @@ -167,7 +167,7 @@ pub fn component_impl( let component_name_str = component_name.to_string(); let generic_node_ty = generic_node_ty.type_params().next().unwrap(); - let generic_node: TypeParam = syn::parse_quote! { + let generic_node: GenericParam = syn::parse_quote! { #generic_node_ty: ::sycamore::generic_node::GenericNode }; @@ -175,14 +175,48 @@ pub fn component_impl( block, props_type: _, arg, - generics, + mut generics, vis, attrs, name, return_type, } = component; - let (impl_generics, _ty_generics, where_clause) = generics.split_for_impl(); + let prop_ty = match &arg { + FnArg::Receiver(_) => unreachable!(), + FnArg::Typed(pat_ty) => &pat_ty.ty, + }; + + // Add the GenericNode type param to generics. + let first_generic_param_index = generics + .params + .iter() + .enumerate() + .find(|(_, param)| matches!(param, GenericParam::Type(_) | GenericParam::Const(_))) + .map(|(i, _)| i); + if let Some(first_generic_param_index) = first_generic_param_index { + generics + .params + .insert(first_generic_param_index, generic_node); + } else { + generics.params.push(generic_node); + } + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + // Generics for the PhantomData. + let phantom_generics = ty_generics + .clone() + .into_token_stream() + .into_iter() + .collect::>(); + // Throw away first and last TokenTree to get rid of angle brackets. + let phantom_generics_len = phantom_generics.len(); + let phantom_generics = phantom_generics + .into_iter() + .take(phantom_generics_len.saturating_sub(1)) + .skip(1) + .collect::(); if name == component_name { return Err(syn::Error::new_spanned( @@ -193,23 +227,19 @@ pub fn component_impl( let quoted = quote! { #(#attrs)* - #vis struct #component_name<#generic_node> { + #vis struct #component_name#generics { #[doc(hidden)] - _marker: ::std::marker::PhantomData<#generic_node_ty>, + _marker: ::std::marker::PhantomData<(#phantom_generics)>, } - impl<#generic_node> ::sycamore::component::Component<#generic_node_ty> - for #component_name<#generic_node_ty> + impl#impl_generics ::sycamore::component::Component::<#generic_node_ty> for #component_name#ty_generics + #where_clause { #[cfg(debug_assertions)] const NAME: &'static ::std::primitive::str = #component_name_str; - } + type Props = #prop_ty; - impl<#generic_node> #component_name<#generic_node_ty> { - #[doc(hidden)] - pub fn __create_component#impl_generics(#arg) -> #return_type - #where_clause - { + fn __create_component(#arg) -> #return_type{ #block } } diff --git a/packages/sycamore-macro/src/template/component.rs b/packages/sycamore-macro/src/template/component.rs index b89ca07e..ead0bf70 100644 --- a/packages/sycamore-macro/src/template/component.rs +++ b/packages/sycamore-macro/src/template/component.rs @@ -1,12 +1,12 @@ use std::mem; use proc_macro2::TokenStream; -use quote::{quote_spanned, ToTokens}; +use quote::{quote, quote_spanned, ToTokens}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::token::{Comma, Paren}; -use syn::{parenthesized, Expr, Path, Result}; +use syn::{parenthesized, parse_quote, Expr, GenericArgument, Path, Result}; /// Components are identical to function calls. pub struct Component { @@ -31,19 +31,47 @@ impl ToTokens for Component { let Component { path, paren, args } = self; let mut path = path.clone(); + let generic_arg: GenericArgument = parse_quote! { _ }; let generics = mem::take(&mut path.segments.last_mut().unwrap().arguments); + let generics = match generics { + syn::PathArguments::None => quote! {}, + syn::PathArguments::AngleBracketed(mut generics) => { + if !generics.args.is_empty() { + // Add the GenericNode type param to generics. + let first_generic_param_index = generics + .args + .iter() + .enumerate() + .find(|(_, arg)| { + matches!(arg, GenericArgument::Type(_) | GenericArgument::Const(_)) + }) + .map(|(i, _)| i); + if let Some(first_generic_param_index) = first_generic_param_index { + generics.args.insert(first_generic_param_index, generic_arg); + } else { + generics.args.push(generic_arg); + } + } + generics.into_token_stream() + } + syn::PathArguments::Parenthesized(_) => unreachable!(), + }; let quoted = if args.empty_or_trailing() { quote_spanned! { paren.span=> - ::sycamore::reactive::untrack(|| - #path::<_>::__create_component#generics(()) - ) + ::sycamore::reactive::untrack(|| { + #[allow(unused_imports)] + use ::sycamore::component::Component as __Component; + #path#generics::__create_component(()) + }) } } else { quote_spanned! { path.span()=> - ::sycamore::reactive::untrack(|| - #path::<_>::__create_component#generics(#args) - ) + ::sycamore::reactive::untrack(|| { + #[allow(unused_imports)] + use ::sycamore::component::Component as __Component; + #path#generics::__create_component(#args) + }) } }; diff --git a/packages/sycamore-macro/tests/template/component-pass.rs b/packages/sycamore-macro/tests/template/component-pass.rs index e8ef6f80..b565bcad 100644 --- a/packages/sycamore-macro/tests/template/component-pass.rs +++ b/packages/sycamore-macro/tests/template/component-pass.rs @@ -1,5 +1,3 @@ - - use sycamore::prelude::*; #[component(Component)] diff --git a/packages/sycamore/src/component.rs b/packages/sycamore/src/component.rs index 940867ad..5832689d 100644 --- a/packages/sycamore/src/component.rs +++ b/packages/sycamore/src/component.rs @@ -1,10 +1,20 @@ -//! The definition for the [`Component`] trait. +//! The definition of the [`Component`] trait. use crate::generic_node::GenericNode; +use crate::prelude::Template; /// Trait that is implemented by components. Should not be implemented manually. Use the /// [`component`](sycamore_macro::component) macro instead. pub trait Component { - /// The name of the component (for use in debug mode). + /// The name of the component (for use in debug mode). In release mode, this will default to + /// `"UnnamedComponent"` const NAME: &'static str = "UnnamedComponent"; + /// The type of the properties passed to the component. + type Props; + + /// Create a new component with an instance of the properties. + /// + /// The double underscores (`__`) are to prevent conflicts with other trait methods. This is + /// because we cannot use fully qualified syntax here because it prevents type inference. + fn __create_component(props: Self::Props) -> Template; } diff --git a/website/sitemap_index.xml b/website/sitemap_index.xml index 18d15e86..b81113f4 100644 --- a/website/sitemap_index.xml +++ b/website/sitemap_index.xml @@ -49,6 +49,7 @@ https://sycamore-rs.netlify.app/examples/countermonthly0.5 https://sycamore-rs.netlify.app/examples/distmonthly0.5 https://sycamore-rs.netlify.app/examples/hellomonthly0.5 +https://sycamore-rs.netlify.app/examples/higher-order-componentsmonthly0.5 https://sycamore-rs.netlify.app/examples/iterationmonthly0.5 https://sycamore-rs.netlify.app/examples/ssrmonthly0.5 https://sycamore-rs.netlify.app/examples/todomvcmonthly0.5