perseus/template/core/
state_setters.rs

1#[cfg(engine)]
2use super::super::fn_types::*;
3use super::TemplateInner;
4#[cfg(engine)]
5use crate::errors::*;
6use crate::{
7    reactor::Reactor,
8    state::{AnyFreeze, MakeRx, MakeUnrx, UnreactiveState},
9};
10#[cfg(engine)]
11use http::HeaderMap;
12use serde::{de::DeserializeOwned, Serialize};
13use sycamore::prelude::BoundedScope;
14use sycamore::prelude::{create_child_scope, create_ref};
15#[cfg(engine)]
16use sycamore::web::SsrNode;
17use sycamore::{prelude::Scope, view::View, web::Html};
18
19impl<G: Html> TemplateInner<G> {
20    // The view functions below are shadowed for widgets, and therefore these
21    // definitions only apply to templates, not capsules!
22
23    /// Sets the template rendering function to use, if the template takes
24    /// state. Templates that do not take state should use `.template()`
25    /// instead.
26    ///
27    /// The closure wrapping this performs will automatically handle suspense
28    /// state.
29    // Generics are swapped here for nicer manual specification
30    pub fn view_with_state<I, F>(mut self, val: F) -> Self
31    where
32        // The state is made reactive on the child
33        F: for<'app, 'child> Fn(BoundedScope<'app, 'child>, &'child I) -> View<G>
34            + Send
35            + Sync
36            + 'static,
37        I: MakeUnrx + AnyFreeze + Clone,
38        I::Unrx: MakeRx<Rx = I> + Serialize + DeserializeOwned + Send + Sync + Clone + 'static,
39    {
40        self.view = Box::new(
41            #[allow(unused_variables)]
42            move |app_cx, preload_info, template_state, path| {
43                let reactor = Reactor::<G>::from_cx(app_cx);
44                // This will handle frozen/active state prioritization, etc.
45                let intermediate_state =
46                    reactor.get_page_state::<I::Unrx>(&path, template_state)?;
47                // Run the user's code in a child scope so any effects they start are killed
48                // when the page ends (otherwise we basically get a series of
49                // continuous pseudo-memory leaks, which can also cause accumulations of
50                // listeners on things like the router state)
51                let mut view = View::empty();
52                let disposer = ::sycamore::reactive::create_child_scope(app_cx, |child_cx| {
53                    // Compute suspended states
54                    #[cfg(any(client, doc))]
55                    intermediate_state.compute_suspense(child_cx);
56
57                    view = val(child_cx, create_ref(child_cx, intermediate_state));
58                });
59                Ok((view, disposer))
60            },
61        );
62        self
63    }
64    /// Sets the template rendering function to use, if the template takes
65    /// unreactive state.
66    pub fn view_with_unreactive_state<F, S>(mut self, val: F) -> Self
67    where
68        F: Fn(Scope, S) -> View<G> + Send + Sync + 'static,
69        S: MakeRx + Serialize + DeserializeOwned + UnreactiveState + 'static,
70        <S as MakeRx>::Rx: AnyFreeze + Clone + MakeUnrx<Unrx = S>,
71    {
72        self.view = Box::new(
73            #[allow(unused_variables)]
74            move |app_cx, preload_info, template_state, path| {
75                let reactor = Reactor::<G>::from_cx(app_cx);
76                // This will handle frozen/active state prioritization, etc.
77                let intermediate_state = reactor.get_page_state::<S>(&path, template_state)?;
78                let mut view = View::empty();
79                let disposer = create_child_scope(app_cx, |child_cx| {
80                    // We go back from the unreactive state type wrapper to the base type (since
81                    // it's unreactive)
82                    view = val(child_cx, intermediate_state.make_unrx());
83                });
84                Ok((view, disposer))
85            },
86        );
87        self
88    }
89
90    /// Sets the template rendering function to use for templates that take no
91    /// state. Templates that do take state should use
92    /// `.template_with_state()` instead.
93    pub fn view<F>(mut self, val: F) -> Self
94    where
95        F: Fn(Scope) -> View<G> + Send + Sync + 'static,
96    {
97        self.view = Box::new(move |app_cx, _preload_info, _template_state, path| {
98            let reactor = Reactor::<G>::from_cx(app_cx);
99            // Declare that this page/widget will never take any state to enable full
100            // caching
101            reactor.register_no_state(&path, false);
102
103            // Nicely, if this is a widget, this means there need be no network requests
104            // at all!
105            let mut view = View::empty();
106            let disposer = ::sycamore::reactive::create_child_scope(app_cx, |child_cx| {
107                view = val(child_cx);
108            });
109            Ok((view, disposer))
110        });
111        self
112    }
113
114    /// Sets the document `<head>` rendering function to use. The [`View`]
115    /// produced by this will only be rendered on the engine-side, and will
116    /// *not* be reactive (since it only contains metadata).
117    ///
118    /// This is for heads that do require state. Those that do not should use
119    /// `.head()` instead.
120    #[cfg(engine)]
121    pub fn head_with_state<S, V>(
122        mut self,
123        val: impl Fn(Scope, S) -> V + Send + Sync + 'static,
124    ) -> Self
125    where
126        S: Serialize + DeserializeOwned + MakeRx + 'static,
127        V: Into<GeneratorResult<View<SsrNode>>>,
128    {
129        let template_name = self.get_path();
130        self.head = Some(Box::new(move |cx, template_state| {
131            // Make sure now that there is actually state
132            if template_state.is_empty() {
133                return Err(ClientError::InvariantError(ClientInvariantError::NoState).into());
134            }
135            // Declare a type on the untyped state (this doesn't perform any conversions,
136            // but the type we declare may be invalid)
137            let typed_state = template_state.change_type::<S>();
138
139            let state =
140                match typed_state.into_concrete() {
141                    Ok(state) => state,
142                    Err(err) => {
143                        return Err(ClientError::InvariantError(
144                            ClientInvariantError::InvalidState { source: err },
145                        )
146                        .into())
147                    }
148                };
149
150            let template_name = template_name.clone();
151            val(cx, state)
152                .into()
153                .into_server_result("head", template_name)
154        }));
155        self
156    }
157    /// Sets the document `<head>` rendering function to use. The [`View`]
158    /// produced by this will only be rendered on the engine-side, and will
159    /// *not* be reactive (since it only contains metadata).
160    ///
161    /// This is for heads that do require state. Those that do not should use
162    /// `.head()` instead.
163    #[cfg(any(client, doc))]
164    pub fn head_with_state(self, _val: impl Fn() + 'static) -> Self {
165        self
166    }
167
168    /// Sets the function to set headers. This will override Perseus' inbuilt
169    /// header defaults. This should only be used when your header-setting
170    /// requires knowing the state.
171    #[cfg(engine)]
172    pub fn set_headers_with_state<S, V>(
173        mut self,
174        val: impl Fn(Scope, S) -> V + Send + Sync + 'static,
175    ) -> Self
176    where
177        S: Serialize + DeserializeOwned + MakeRx + 'static,
178        V: Into<GeneratorResult<HeaderMap>>,
179    {
180        let template_name = self.get_path();
181        self.set_headers = Some(Box::new(move |cx, template_state| {
182            // Make sure now that there is actually state
183            if template_state.is_empty() {
184                return Err(ClientError::InvariantError(ClientInvariantError::NoState).into());
185            }
186            // Declare a type on the untyped state (this doesn't perform any conversions,
187            // but the type we declare may be invalid)
188            let typed_state = template_state.change_type::<S>();
189
190            let state =
191                match typed_state.into_concrete() {
192                    Ok(state) => state,
193                    Err(err) => {
194                        return Err(ClientError::InvariantError(
195                            ClientInvariantError::InvalidState { source: err },
196                        )
197                        .into())
198                    }
199                };
200
201            let template_name = template_name.clone();
202            val(cx, state)
203                .into()
204                .into_server_result("set_headers", template_name)
205        }));
206        self
207    }
208    /// Sets the function to set headers. This will override Perseus' inbuilt
209    /// header defaults. This should only be used when your header-setting
210    /// requires knowing the state.
211    #[cfg(any(client, doc))]
212    pub fn set_headers_with_state(self, _val: impl Fn() + 'static) -> Self {
213        self
214    }
215}