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