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

Generic functional components #1756

Merged
merged 18 commits into from
Feb 24, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions docs/concepts/function-components/attribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ title: #[function_component]
description: The #[function_component] attribute
---


The `#[function_component(_)]` turns a normal Rust function into a function component.
Functions with the attribute have to return `Html` and may take a single parameter for the type of props the component should accept.
`#[function_component(_)]` turns a normal Rust function into a function component.
Functions with the attribute have to return `Html` and may take a single parameter for the type of props the component should accept.
The parameter type needs to be a reference to a type which implements `Properties` and `PartialEq` (ex. `props: &MyProps`).
If the function doesn't have any parameters the resulting component doesn't accept any props.

The attribute doesn't replace your original function with a component. You need to provide a name as an input to the attribute which will be the identifier of the component.
Assuming you have a function called `chat_container` and you add the attribute `#[function_component(ChatContainer)]` you can use the component like this:

```rust
html! { <ChatContainer /> }
```
Expand All @@ -19,6 +19,7 @@ html! { <ChatContainer /> }

<!--DOCUSAURUS_CODE_TABS-->
<!--With props-->

```rust
#[derive(Properties, Clone, PartialEq)]
pub struct RenderedAtProps {
Expand All @@ -37,6 +38,7 @@ pub fn rendered_at(props: &RenderedAtProps) -> Html {
```

<!--Without props-->

```rust
#[function_component(App)]
fn app() -> Html {
Expand All @@ -46,7 +48,7 @@ fn app() -> Html {
let counter = Rc::clone(&counter);
Callback::from(move |_| set_counter(*counter + 1))
};

html! {
<div>
<button onclick=onclick>{ "Increment value" }</button>
Expand All @@ -58,4 +60,38 @@ fn app() -> Html {
}
}
```

<!--END_DOCUSAURUS_CODE_TABS-->

## Generic function components

The `#[function_component(_)]` attribute also works with generic functions for creating generic components.

```rust
#[derive(Properties, Clone, PartialEq)]
pub struct Props<T>
where T: Clone + PartialEq
{
data: T,
}

#[function_component(MyGenericComponent)]
pub fn my_generic_component<T>(props: &Props<T>) -> Html
where T: Clone + PartialEq + Display
{
html! {
<p>
{ props.data }
</p>
}
}

// used like this
html! {
<MyGenericComponent<i32> data=123 />
}
// or
html! {
<MyGenericComponent<Foo> data=foo />
}
```
30 changes: 24 additions & 6 deletions packages/yew-functional-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::Comma;
use syn::{
parse_macro_input, Attribute, Block, FnArg, Ident, Item, ItemFn, ReturnType, Type, Visibility,
parse_macro_input, Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type,
Visibility,
};

struct FunctionComponent {
block: Box<Block>,
props_type: Box<Type>,
arg: FnArg,
generics: Generics,
vis: Visibility,
attrs: Vec<Attribute>,
name: Ident,
Expand All @@ -29,10 +33,10 @@ impl Parse for FunctionComponent {
block,
} = func;

if !sig.generics.params.is_empty() {
if sig.generics.lifetimes().next().is_some() {
return Err(syn::Error::new_spanned(
sig.generics,
"function components can't contain generics",
"function components can't have generic lifetime parameters",
));
}

Expand Down Expand Up @@ -123,6 +127,7 @@ impl Parse for FunctionComponent {
props_type: ty,
block,
arg,
generics: sig.generics,
vis,
attrs,
name: sig.ident,
Expand Down Expand Up @@ -176,12 +181,15 @@ fn function_component_impl(
block,
props_type,
arg,
generics,
vis,
attrs,
name: function_name,
return_type,
} = component;

let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

if function_name == component_name {
return Err(syn::Error::new_spanned(
component_name,
Expand All @@ -191,12 +199,20 @@ fn function_component_impl(

let ret_type = quote_spanned!(return_type.span()=> ::yew::html::Html);

let phantom_generics = generics
.type_params()
.map(|ty_param| ty_param.ident.clone()) // create a new Punctuated sequence without any type bounds
.collect::<Punctuated<_, Comma>>();

let quoted = quote! {
#[doc(hidden)]
#[allow(non_camel_case_types)]
#vis struct #function_name;
#[allow(unused_parens)]
#vis struct #function_name #impl_generics {
_marker: ::std::marker::PhantomData<(#phantom_generics)>,
}

impl ::yew_functional::FunctionProvider for #function_name {
impl #impl_generics ::yew_functional::FunctionProvider for #function_name #ty_generics #where_clause {
type TProps = #props_type;

fn run(#arg) -> #ret_type {
Expand All @@ -205,7 +221,9 @@ fn function_component_impl(
}

#(#attrs)*
#vis type #component_name = ::yew_functional::FunctionComponent<#function_name>;
#[allow(type_alias_bounds)]
#vis type #component_name #impl_generics = ::yew_functional::FunctionComponent<#function_name #ty_generics>;
};

Ok(quoted)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
use yew::prelude::*;
use yew_functional::function_component;

#[derive(Clone, Properties, PartialEq)]
struct Props {
a: usize,
}

#[function_component(Comp)]
const fn comp<P: Properties>(props: &P) -> Html {
html! {
<p>
{ props.a }
</p>
}
}

fn main() {}
use yew::prelude::*;
use yew_functional::function_component;

#[derive(Clone, Properties, PartialEq)]
struct Props {
a: usize,
}

#[function_component(Comp)]
fn comp<'a>(props: &'a Props) -> Html {

html! {
<p>
{ props.a }
</p>
}
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: function components can't have generic lifetime parameters
--> $DIR/generic-lifetime-fail.rs:10:8
|
10 | fn comp<'a>(props: &'a Props) -> Html {
| ^^^^
40 changes: 40 additions & 0 deletions packages/yew-functional-macro/tests/function_attr/generic-pass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#[derive(Clone, ::yew::Properties, PartialEq)]
struct Props {
a: usize,
}

#[::yew_functional::function_component(Comp)]
fn comp<P>(_props: &P) -> ::yew::Html
where
P: ::yew::Properties + PartialEq,
{
::yew::html! {
<p></p>
}
}

#[::yew_functional::function_component(Comp1)]
fn comp1<T1, T2>(_props: &()) -> ::yew::Html {
::yew::html! {
<p></p>
}
}

// TODO: uncomment when min_const_generics are in stable and Rust version in CI is bumped
// #[::yew_functional::function_component(ConstGenerics)]
// fn const_generics<const N: i32>() -> ::yew::Html {
// ::yew::html! {
// <div>
// { N }
// </div>
// }
// }
lukechu10 marked this conversation as resolved.
Show resolved Hide resolved

fn compile_pass() {
::yew::html! { <Comp<Props> a=10 /> };
::yew::html! { <Comp1<usize, usize> /> };

// ::yew::html! { <ConstGenerics<10> };
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use yew::prelude::*;
use yew_functional::function_component;

#[derive(Clone, Properties, PartialEq)]
struct Props {
a: usize,
}

#[function_component(Comp)]
fn comp<P>(_props: &P) -> Html
where
P: Properties + PartialEq,
{
html! {
<p></p>
}
}

struct MissingTypeBounds;

fn compile_fail() {
// missing prop 'a'
html! { <Comp<Props> /> };

// invalid type parameter
html! { <Comp<INVALID> /> };
// parameter doesn't match bounds
html! { <Comp<MissingTypeBounds> /> };

// missing type param
html! { <Comp /> };
}

fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
error[E0412]: cannot find type `INVALID` in this scope
--> $DIR/generic-props-fail.rs:26:19
|
21 | fn compile_fail() {
| - help: you might be missing a type parameter: `<INVALID>`
...
26 | html! { <Comp<INVALID> /> };
| ^^^^^^^ not found in this scope

error[E0599]: no method named `build` found for struct `PropsBuilder<PropsBuilderStep_missing_required_prop_a>` in the current scope
--> $DIR/generic-props-fail.rs:23:14
|
4 | #[derive(Clone, Properties, PartialEq)]
| ---------- method `build` not found for this
...
23 | html! { <Comp<Props> /> };
| ^^^^ method not found in `PropsBuilder<PropsBuilderStep_missing_required_prop_a>`
|
= help: items from traits can only be used if the trait is implemented and in scope
= note: the following trait defines an item `build`, perhaps you need to implement it:
candidate #1: `proc_macro::bridge::server::TokenStreamBuilder`

error[E0599]: no function or associated item named `new` found for struct `yew::virtual_dom::vcomp::VChild<yew_functional::FunctionComponent<comp<MissingTypeBounds>>>` in the current scope
--> $DIR/generic-props-fail.rs:28:14
|
28 | html! { <Comp<MissingTypeBounds> /> };
| ^^^^ function or associated item not found in `yew::virtual_dom::vcomp::VChild<yew_functional::FunctionComponent<comp<MissingTypeBounds>>>`
|
::: $WORKSPACE/packages/yew-functional/src/lib.rs
|
| pub struct FunctionComponent<T: FunctionProvider + 'static> {
| ----------------------------------------------------------- doesn't satisfy `_: yew::html::component::Component`
|
= note: the method `new` exists but the following trait bounds were not satisfied:
`yew_functional::FunctionComponent<comp<MissingTypeBounds>>: yew::html::component::Component`

error[E0277]: the trait bound `MissingTypeBounds: yew::html::component::properties::Properties` is not satisfied
--> $DIR/generic-props-fail.rs:28:14
|
28 | html! { <Comp<MissingTypeBounds> /> };
| ^^^^ the trait `yew::html::component::properties::Properties` is not implemented for `MissingTypeBounds`
|
= note: required because of the requirements on the impl of `yew_functional::FunctionProvider` for `comp<MissingTypeBounds>`

error[E0277]: can't compare `MissingTypeBounds` with `MissingTypeBounds`
--> $DIR/generic-props-fail.rs:28:14
|
28 | html! { <Comp<MissingTypeBounds> /> };
| ^^^^ no implementation for `MissingTypeBounds == MissingTypeBounds`
|
= help: the trait `std::cmp::PartialEq` is not implemented for `MissingTypeBounds`
= note: required because of the requirements on the impl of `yew_functional::FunctionProvider` for `comp<MissingTypeBounds>`

error[E0107]: wrong number of type arguments: expected 1, found 0
--> $DIR/generic-props-fail.rs:31:14
|
31 | html! { <Comp /> };
| ^^^^ expected 1 type argument