This is unreleased documentation for Yew Next version.
For up-to-date documentation, see the latest version on docs.rs.

yew_macro/
function_component.rs

1use proc_macro2::{Span, TokenStream};
2use quote::{quote, ToTokens};
3use syn::parse::{Parse, ParseStream};
4use syn::punctuated::Punctuated;
5use syn::token::{Comma, Fn};
6use syn::{
7    parse_quote, parse_quote_spanned, visit_mut, Attribute, Block, FnArg, Generics, Ident, Item,
8    ItemFn, LitStr, ReturnType, Type, Visibility,
9};
10
11use crate::hook::BodyRewriter;
12
13#[derive(Clone)]
14pub struct FunctionComponent {
15    block: Box<Block>,
16    props_type: Box<Type>,
17    arg: FnArg,
18    generics: Generics,
19    vis: Visibility,
20    attrs: Vec<Attribute>,
21    name: Ident,
22    return_type: Box<Type>,
23    fn_token: Fn,
24
25    component_name: Option<Ident>,
26}
27
28impl Parse for FunctionComponent {
29    fn parse(input: ParseStream) -> syn::Result<Self> {
30        let parsed: Item = input.parse()?;
31
32        let func = match parsed {
33            Item::Fn(m) => m,
34
35            item => {
36                return Err(syn::Error::new_spanned(
37                    item,
38                    "`function_component` attribute can only be applied to functions",
39                ))
40            }
41        };
42
43        let ItemFn {
44            attrs,
45            vis,
46            sig,
47            block,
48        } = func;
49
50        if sig.generics.lifetimes().next().is_some() {
51            return Err(syn::Error::new_spanned(
52                sig.generics,
53                "function components can't have generic lifetime parameters",
54            ));
55        }
56
57        if sig.asyncness.is_some() {
58            return Err(syn::Error::new_spanned(
59                sig.asyncness,
60                "function components can't be async",
61            ));
62        }
63
64        if sig.constness.is_some() {
65            return Err(syn::Error::new_spanned(
66                sig.constness,
67                "const functions can't be function components",
68            ));
69        }
70
71        if sig.abi.is_some() {
72            return Err(syn::Error::new_spanned(
73                sig.abi,
74                "extern functions can't be function components",
75            ));
76        }
77
78        let return_type = match sig.output {
79            ReturnType::Default => {
80                return Err(syn::Error::new_spanned(
81                    sig,
82                    "function components must return `yew::Html` or `yew::HtmlResult`",
83                ))
84            }
85            ReturnType::Type(_, ty) => ty,
86        };
87
88        let mut inputs = sig.inputs.into_iter();
89        let arg = inputs
90            .next()
91            .unwrap_or_else(|| syn::parse_quote! { _: &() });
92
93        let ty = match &arg {
94            FnArg::Typed(arg) => match &*arg.ty {
95                Type::Reference(ty) => {
96                    if ty.lifetime.is_some() {
97                        return Err(syn::Error::new_spanned(
98                            &ty.lifetime,
99                            "reference must not have a lifetime",
100                        ));
101                    }
102
103                    if ty.mutability.is_some() {
104                        return Err(syn::Error::new_spanned(
105                            ty.mutability,
106                            "reference must not be mutable",
107                        ));
108                    }
109
110                    ty.elem.clone()
111                }
112                ty => {
113                    let msg = format!(
114                        "expected a reference to a `Properties` type (try: `&{}`)",
115                        ty.to_token_stream()
116                    );
117                    return Err(syn::Error::new_spanned(ty, msg));
118                }
119            },
120
121            FnArg::Receiver(_) => {
122                return Err(syn::Error::new_spanned(
123                    arg,
124                    "function components can't accept a receiver",
125                ));
126            }
127        };
128
129        // Checking after param parsing may make it a little inefficient
130        // but that's a requirement for better error messages in case of receivers
131        // `>0` because first one is already consumed.
132        if inputs.len() > 0 {
133            let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect();
134            return Err(syn::Error::new_spanned(
135                params,
136                "function components can accept at most one parameter for the props",
137            ));
138        }
139
140        Ok(Self {
141            props_type: ty,
142            block,
143            arg,
144            generics: sig.generics,
145            vis,
146            attrs,
147            name: sig.ident,
148            return_type,
149            fn_token: sig.fn_token,
150            component_name: None,
151        })
152    }
153}
154
155impl FunctionComponent {
156    /// Filters attributes that should be copied to component definition.
157    fn filter_attrs_for_component_struct(&self) -> Vec<Attribute> {
158        self.attrs
159            .iter()
160            .filter_map(|m| {
161                m.path()
162                    .get_ident()
163                    .and_then(|ident| match ident.to_string().as_str() {
164                        "doc" | "allow" => Some(m.clone()),
165                        _ => None,
166                    })
167            })
168            .collect()
169    }
170
171    /// Filters attributes that should be copied to the component impl block.
172    fn filter_attrs_for_component_impl(&self) -> Vec<Attribute> {
173        self.attrs
174            .iter()
175            .filter_map(|m| {
176                m.path()
177                    .get_ident()
178                    .and_then(|ident| match ident.to_string().as_str() {
179                        "allow" => Some(m.clone()),
180                        _ => None,
181                    })
182            })
183            .collect()
184    }
185
186    fn phantom_generics(&self) -> Punctuated<Ident, Comma> {
187        self.generics
188            .type_params()
189            .map(|ty_param| ty_param.ident.clone()) // create a new Punctuated sequence without any type bounds
190            .collect::<Punctuated<_, Comma>>()
191    }
192
193    fn merge_component_name(&mut self, name: FunctionComponentName) -> syn::Result<()> {
194        if let Some(ref m) = name.component_name {
195            if m == &self.name {
196                return Err(syn::Error::new_spanned(
197                    m,
198                    "the component must not have the same name as the function",
199                ));
200            }
201        }
202
203        self.component_name = name.component_name;
204
205        Ok(())
206    }
207
208    fn inner_fn_ident(&self) -> Ident {
209        if self.component_name.is_some() {
210            self.name.clone()
211        } else {
212            Ident::new("inner", Span::mixed_site())
213        }
214    }
215
216    fn component_name(&self) -> Ident {
217        self.component_name
218            .clone()
219            .unwrap_or_else(|| self.name.clone())
220    }
221
222    // We need to cast 'static on all generics for base component.
223    fn create_static_component_generics(&self) -> Generics {
224        let mut generics = self.generics.clone();
225
226        let where_clause = generics.make_where_clause();
227        for ty_generic in self.generics.type_params() {
228            let ident = &ty_generic.ident;
229            let bound = parse_quote_spanned! { ident.span() =>
230                #ident: 'static
231            };
232
233            where_clause.predicates.push(bound);
234        }
235
236        where_clause.predicates.push(parse_quote! { Self: 'static });
237
238        generics
239    }
240
241    /// Prints the impl fn.
242    fn print_inner_fn(&self) -> TokenStream {
243        let name = self.inner_fn_ident();
244        let FunctionComponent {
245            ref fn_token,
246            ref attrs,
247            ref block,
248            ref return_type,
249            ref generics,
250            ref arg,
251            ..
252        } = self;
253        let mut block = *block.clone();
254        let (impl_generics, _ty_generics, where_clause) = generics.split_for_impl();
255
256        // We use _ctx here so if the component does not use any hooks, the usused_vars lint will
257        // not be triggered.
258        let ctx_ident = Ident::new("_ctx", Span::mixed_site());
259
260        let mut body_rewriter = BodyRewriter::new(ctx_ident.clone());
261        visit_mut::visit_block_mut(&mut body_rewriter, &mut block);
262
263        quote! {
264            #(#attrs)*
265            #fn_token #name #impl_generics (#ctx_ident: &mut ::yew::functional::HookContext, #arg) -> #return_type
266            #where_clause
267            {
268                #block
269            }
270        }
271    }
272
273    fn print_base_component_impl(&self) -> TokenStream {
274        let component_name = self.component_name();
275        let props_type = &self.props_type;
276        let static_comp_generics = self.create_static_component_generics();
277
278        let (impl_generics, ty_generics, where_clause) = static_comp_generics.split_for_impl();
279
280        // TODO: replace with blanket implementation when specialisation becomes stable.
281        quote! {
282            #[automatically_derived]
283            impl #impl_generics ::yew::html::BaseComponent for #component_name #ty_generics #where_clause {
284                type Message = ();
285                type Properties = #props_type;
286
287                #[inline]
288                fn create(ctx: &::yew::html::Context<Self>) -> Self {
289                    Self {
290                        _marker: ::std::marker::PhantomData,
291                        function_component: ::yew::functional::FunctionComponent::<Self>::new(ctx),
292                    }
293                }
294
295                #[inline]
296                fn update(&mut self, _ctx: &::yew::html::Context<Self>, _msg: Self::Message) -> ::std::primitive::bool {
297                    true
298                }
299
300                #[inline]
301                fn changed(&mut self, _ctx: &::yew::html::Context<Self>, _old_props: &Self::Properties) -> ::std::primitive::bool {
302                    true
303                }
304
305                #[inline]
306                fn view(&self, ctx: &::yew::html::Context<Self>) -> ::yew::html::HtmlResult {
307                    ::yew::functional::FunctionComponent::<Self>::render(
308                        &self.function_component,
309                        ::yew::html::Context::<Self>::props(ctx)
310                    )
311                }
312
313                #[inline]
314                fn rendered(&mut self, _ctx: &::yew::html::Context<Self>, _first_render: ::std::primitive::bool) {
315                    ::yew::functional::FunctionComponent::<Self>::rendered(&self.function_component)
316                }
317
318                #[inline]
319                fn destroy(&mut self, _ctx: &::yew::html::Context<Self>) {
320                    ::yew::functional::FunctionComponent::<Self>::destroy(&self.function_component)
321                }
322
323                #[inline]
324                fn prepare_state(&self) -> ::std::option::Option<::std::string::String> {
325                    ::yew::functional::FunctionComponent::<Self>::prepare_state(&self.function_component)
326                }
327            }
328        }
329    }
330
331    fn print_debug_impl(&self) -> TokenStream {
332        let component_name = self.component_name();
333        let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
334
335        let component_name_lit = LitStr::new(&format!("{component_name}<_>"), Span::mixed_site());
336
337        quote! {
338            #[automatically_derived]
339            impl #impl_generics ::std::fmt::Debug for #component_name #ty_generics #where_clause {
340                fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
341                    ::std::write!(f, #component_name_lit)
342                }
343            }
344        }
345    }
346
347    fn print_fn_provider_impl(&self) -> TokenStream {
348        let func = self.print_inner_fn();
349        let component_impl_attrs = self.filter_attrs_for_component_impl();
350        let component_name = self.component_name();
351        let fn_name = self.inner_fn_ident();
352        let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
353        let props_type = &self.props_type;
354        let fn_generics = ty_generics.as_turbofish();
355
356        let component_props = Ident::new("props", Span::mixed_site());
357        let ctx_ident = Ident::new("ctx", Span::mixed_site());
358
359        quote! {
360            // we cannot disable any lints here because it will be applied to the function body
361            // as well.
362            #(#component_impl_attrs)*
363            impl #impl_generics ::yew::functional::FunctionProvider for #component_name #ty_generics #where_clause {
364                type Properties = #props_type;
365
366                fn run(#ctx_ident: &mut ::yew::functional::HookContext, #component_props: &Self::Properties) -> ::yew::html::HtmlResult {
367                    #func
368
369                    ::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#ctx_ident, #component_props))
370                }
371            }
372        }
373    }
374
375    fn print_struct_def(&self) -> TokenStream {
376        let component_attrs = self.filter_attrs_for_component_struct();
377        let component_name = self.component_name();
378
379        let generics = &self.generics;
380        let (_impl_generics, _ty_generics, where_clause) = self.generics.split_for_impl();
381        let phantom_generics = self.phantom_generics();
382        let vis = &self.vis;
383
384        quote! {
385            #(#component_attrs)*
386            #[allow(unused_parens)]
387            #vis struct #component_name #generics #where_clause {
388                _marker: ::std::marker::PhantomData<(#phantom_generics)>,
389                function_component: ::yew::functional::FunctionComponent<Self>,
390            }
391        }
392    }
393}
394
395pub struct FunctionComponentName {
396    component_name: Option<Ident>,
397}
398
399impl Parse for FunctionComponentName {
400    fn parse(input: ParseStream) -> syn::Result<Self> {
401        if input.is_empty() {
402            return Ok(Self {
403                component_name: None,
404            });
405        }
406
407        let component_name = input.parse()?;
408
409        Ok(Self {
410            component_name: Some(component_name),
411        })
412    }
413}
414
415pub fn function_component_impl(
416    name: FunctionComponentName,
417    mut component: FunctionComponent,
418) -> syn::Result<TokenStream> {
419    component.merge_component_name(name)?;
420
421    let base_comp_impl = component.print_base_component_impl();
422    let debug_impl = component.print_debug_impl();
423    let provider_fn_impl = component.print_fn_provider_impl();
424    let struct_def = component.print_struct_def();
425
426    let quoted = quote! {
427        #struct_def
428
429        #provider_fn_impl
430        #debug_impl
431        #base_comp_impl
432    };
433
434    Ok(quoted)
435}