Skip to content

Commit

Permalink
Generic functional components (#1756)
Browse files Browse the repository at this point in the history
* Generic functional components

* Add some tests

* Clippy

* Fix generic-props-fail snapshot

* Add docs for generic function components

* Add some more docs

* Fix trait bounds in docs

* Fix docs

* Improve lifetime error messages

* Fix parsing for const generics

* Use fully qualified path for pass test

* Remove TODO message

* Suggested change

Co-authored-by: Simon <[email protected]>

* Update test

Co-authored-by: Simon <[email protected]>

* Update test

Co-authored-by: Simon <[email protected]>

* Update stderr snapshots

* Combine quote implementations

* Fix warning about type alias bounds

Co-authored-by: Simon <[email protected]>
  • Loading branch information
lukechu10 and siku2 authored Feb 24, 2021
1 parent 08864f5 commit ee8eae1
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 33 deletions.
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>
// }
// }

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

0 comments on commit ee8eae1

Please sign in to comment.