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

Allow creating components with the Component trait #213

Merged
merged 5 commits into from Aug 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -9,6 +9,7 @@ members = [
"examples/context",
"examples/counter",
"examples/hello",
"examples/higher-order-components",
"examples/iteration",
"examples/ssr",
"examples/todomvc",
Expand Down
24 changes: 19 additions & 5 deletions 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:

Expand Down Expand Up @@ -49,9 +48,24 @@ we want to render the following HTML:
The `p { ... }` creates a new `<p>` tag. The `"Hello, World!"` creates a new text node that is
nested within the `<p>` 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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>My first Sycamore app</title>
</head>
<body></body>
</html>
```

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.
Expand Down
20 changes: 10 additions & 10 deletions docs/versioned_docs/v0.5/getting_started/hello_world.md
Expand Up @@ -48,24 +48,24 @@ we want to render the following HTML:
The `p { ... }` creates a new `<p>` tag. The `"Hello, World!"` creates a new text node that is
nested within the `<p>` 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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My first Sycamore app</title>
</head>
<body></body>
<head>
<meta charset="utf-8" />
<title>My first Sycamore app</title>
</head>
<body></body>
</html>
```

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.
Expand Down
14 changes: 14 additions & 0 deletions 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"}
15 changes: 15 additions & 0 deletions examples/higher-order-components/index.html
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Higher Kinded Components</title>

<style>
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
</style>
</head>
<body></body>
</html>
29 changes: 29 additions & 0 deletions examples/higher-order-components/src/main.rs
@@ -0,0 +1,29 @@
use sycamore::component::Component;
use sycamore::prelude::*;

#[component(EnhancedComponent<G>)]
fn enhanced_component<C: Component<G, Props = i32>>() -> Template<G> {
template! {
div(class="enhanced-container") {
p { "Enhanced container start" }
C(42)
p { "Enhanced container end" }
}
}
}

#[component(NumberDisplayer<G>)]
fn number_displayer(prop: i32) -> Template<G> {
template! {
p { "My number is: " (prop) }
}
}

type EnhancedNumberDisplayer<G> = EnhancedComponent<G, NumberDisplayer<G>>;

fn main() {
console_error_panic_hook::set_once();
console_log::init_with_level(log::Level::Debug).unwrap();

sycamore::render(|| template! { EnhancedNumberDisplayer() });
}
58 changes: 44 additions & 14 deletions packages/sycamore-macro/src/component/mod.rs
Expand Up @@ -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,
};

Expand Down Expand Up @@ -167,22 +167,56 @@ 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
};

let ComponentFunction {
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::<Vec<_>>();
// 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::<TokenStream>();

if name == component_name {
return Err(syn::Error::new_spanned(
Expand All @@ -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
}
}
Expand Down
44 changes: 36 additions & 8 deletions 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 {
Expand All @@ -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)
})
}
};

Expand Down
2 changes: 0 additions & 2 deletions packages/sycamore-macro/tests/template/component-pass.rs
@@ -1,5 +1,3 @@


use sycamore::prelude::*;

#[component(Component<G>)]
Expand Down
14 changes: 12 additions & 2 deletions 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<G: GenericNode> {
/// 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<G>;
}
1 change: 1 addition & 0 deletions website/sitemap_index.xml
Expand Up @@ -49,6 +49,7 @@
<url><loc>https://sycamore-rs.netlify.app/examples/counter</loc><changefreq>monthly</changefreq><priority>0.5</priority></url>
<url><loc>https://sycamore-rs.netlify.app/examples/dist</loc><changefreq>monthly</changefreq><priority>0.5</priority></url>
<url><loc>https://sycamore-rs.netlify.app/examples/hello</loc><changefreq>monthly</changefreq><priority>0.5</priority></url>
<url><loc>https://sycamore-rs.netlify.app/examples/higher-order-components</loc><changefreq>monthly</changefreq><priority>0.5</priority></url>
<url><loc>https://sycamore-rs.netlify.app/examples/iteration</loc><changefreq>monthly</changefreq><priority>0.5</priority></url>
<url><loc>https://sycamore-rs.netlify.app/examples/ssr</loc><changefreq>monthly</changefreq><priority>0.5</priority></url>
<url><loc>https://sycamore-rs.netlify.app/examples/todomvc</loc><changefreq>monthly</changefreq><priority>0.5</priority></url>
Expand Down