Skip to content

Commit 7600c95

Browse files
committed
feat(templates): ✨ added context to templates if they're beeing rendered on the server or client
Closes #26.
1 parent a413e85 commit 7600c95

File tree

7 files changed

+72
-8
lines changed

7 files changed

+72
-8
lines changed

docs/0.2.x/src/templates/intro.md

+10
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,13 @@ One niche case is defining a route like this: `/<locale>/about`. In this case, t
4747
It's perfectly possible in Perseus to define one template for `/post` (and its children) and a different one for `/post/new`. In fact, this is exactly what [the showcase example](https://github.com/arctic-hen7/perseus/tree/main/examples/showcase) does, and you can check it out for inspiration. This is based on a simple idea: **more specific templates win** the routing contest.
4848

4949
There is one use-case though that requires a bit more fiddling: having a different template for the root path. A very common use-case for this would be having one template for `/posts`'s children (one URl for each blog post) and a different template for `/posts` itself that lists all available posts. Currently, the only way to do this is to define a property on the `posts` template that will be `true` if you're rendering for that root, and then to conditionally render the list of posts. Otherwise, you would render the given post content. This does require a lot of `Option<T>`s, but they could be safely unwrapped (data passing in Perseus is logical and safe).
50+
51+
## Checking Render Context
52+
53+
It's often necessary to make sure you're only running some logic on the client-side, particularly anything to do with `web_sys`, which will `panic!` if used on the server. Because Perseus renders your templates in both environments, you'll need to explicitly check if you want to do something only on the client (like get an authentication token from a cookie). This can be done trivially with the `is_client!` macro, which does exactly what it says on the tin. Here's an example from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/basic/src/templates/about.rs):
54+
55+
```rust,no_run,no_playground
56+
{{#include ../../../../examples/basic/src/templates/about.rs}}
57+
```
58+
59+
This is a very contrived example, but what you should note if you try this is the flash from `server` to `client`, because the page is pre-rendered on the server and then hydrated on the client. This is an important principle of Perseus, and you should be aware of this potential flashing (easily solved by a less contrived example).

docs/next/src/templates/intro.md

+10
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,13 @@ One niche case is defining a route like this: `/<locale>/about`. In this case, t
4747
It's perfectly possible in Perseus to define one template for `/post` (and its children) and a different one for `/post/new`. In fact, this is exactly what [the showcase example](https://github.com/arctic-hen7/perseus/tree/main/examples/showcase) does, and you can check it out for inspiration. This is based on a simple idea: **more specific templates win** the routing contest.
4848

4949
There is one use-case though that requires a bit more fiddling: having a different template for the root path. A very common use-case for this would be having one template for `/posts`'s children (one URl for each blog post) and a different template for `/posts` itself that lists all available posts. Currently, the only way to do this is to define a property on the `posts` template that will be `true` if you're rendering for that root, and then to conditionally render the list of posts. Otherwise, you would render the given post content. This does require a lot of `Option<T>`s, but they could be safely unwrapped (data passing in Perseus is logical and safe).
50+
51+
## Checking Render Context
52+
53+
It's often necessary to make sure you're only running some logic on the client-side, particularly anything to do with `web_sys`, which will `panic!` if used on the server. Because Perseus renders your templates in both environments, you'll need to explicitly check if you want to do something only on the client (like get an authentication token from a cookie). This can be done trivially with the `is_client!` macro, which does exactly what it says on the tin. Here's an example from [here](https://github.com/arctic-hen7/perseus/blob/main/examples/basic/src/templates/about.rs):
54+
55+
```rust,no_run,no_playground
56+
{{#include ../../../../examples/basic/src/templates/about.rs}}
57+
```
58+
59+
This is a very contrived example, but what you should note if you try this is the flash from `server` to `client`, because the page is pre-rendered on the server and then hydrated on the client. This is an important principle of Perseus, and you should be aware of this potential flashing (easily solved by a less contrived example).

examples/basic/src/templates/about.rs

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
use perseus::Template;
1+
use perseus::{is_client, Template};
22
use std::rc::Rc;
33
use sycamore::prelude::{component, template, GenericNode, Template as SycamoreTemplate};
44

55
#[component(AboutPage<G>)]
66
pub fn about_page() -> SycamoreTemplate<G> {
77
template! {
88
p { "About." }
9+
p {
10+
(
11+
format!(
12+
"This is currently being run on the {}.",
13+
if is_client!() {
14+
"client"
15+
} else {
16+
"server"
17+
}
18+
)
19+
)
20+
}
921
}
1022
}
1123

packages/perseus/src/build.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,11 @@ pub async fn build_template(
6767
.await?;
6868
// Prerender the template using that state
6969
let prerendered = sycamore::render_to_string(|| {
70-
template.render_for_template(Some(initial_state.clone()), Rc::clone(&translator))
70+
template.render_for_template(
71+
Some(initial_state.clone()),
72+
Rc::clone(&translator),
73+
true,
74+
)
7175
});
7276
// Write that prerendered HTML to a static file
7377
config_manager
@@ -104,7 +108,7 @@ pub async fn build_template(
104108
// It's safe to add a property to the render options here because `.is_basic()` will only return true if path generation is not being used (or anything else)
105109
if template.is_basic() {
106110
let prerendered = sycamore::render_to_string(|| {
107-
template.render_for_template(None, Rc::clone(&translator))
111+
template.render_for_template(None, Rc::clone(&translator), true)
108112
});
109113
let head_str = template.render_head_str(None, Rc::clone(&translator));
110114
// Write that prerendered HTML to a static file

packages/perseus/src/serve.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ async fn render_request_state(
6969
let state = Some(template.get_request_state(path.to_string(), req).await?);
7070
// Use that to render the static HTML
7171
let html = sycamore::render_to_string(|| {
72-
template.render_for_template(state.clone(), Rc::clone(&translator))
72+
template.render_for_template(state.clone(), Rc::clone(&translator), true)
7373
});
7474
let head = template.render_head_str(state.clone(), Rc::clone(&translator));
7575

@@ -142,7 +142,7 @@ async fn revalidate(
142142
.await?,
143143
);
144144
let html = sycamore::render_to_string(|| {
145-
template.render_for_template(state.clone(), Rc::clone(&translator))
145+
template.render_for_template(state.clone(), Rc::clone(&translator), true)
146146
});
147147
let head = template.render_head_str(state.clone(), Rc::clone(&translator));
148148
// Handle revalidation, we need to parse any given time strings into datetimes
@@ -254,7 +254,7 @@ pub async fn get_page_for_template(
254254
// We need to generate and cache this page for future usage
255255
let state = Some(template.get_build_state(path.to_string()).await?);
256256
let html_val = sycamore::render_to_string(|| {
257-
template.render_for_template(state.clone(), Rc::clone(&translator))
257+
template.render_for_template(state.clone(), Rc::clone(&translator), true)
258258
});
259259
let head_val = template.render_head_str(state.clone(), Rc::clone(&translator));
260260
// Handle revalidation, we need to parse any given time strings into datetimes

packages/perseus/src/shell.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ pub async fn app_shell(
259259
// BUG (Sycamore): this will double-render if the component is just text (no nodes)
260260
sycamore::hydrate_to(
261261
// This function provides translator context as needed
262-
|| template.render_for_template(state, Rc::clone(&translator)),
262+
|| template.render_for_template(state, Rc::clone(&translator), false),
263263
&container_rx_elem,
264264
);
265265
checkpoint("page_interactive");
@@ -341,6 +341,7 @@ pub async fn app_shell(
341341
template.render_for_template(
342342
page_data.state,
343343
Rc::clone(&translator),
344+
false,
344345
)
345346
},
346347
&container_rx_elem,

packages/perseus/src/template.rs

+28-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ use std::rc::Rc;
1313
use sycamore::context::{ContextProvider, ContextProviderProps};
1414
use sycamore::prelude::{template, GenericNode, Template as SycamoreTemplate};
1515

16+
/// Used to encapsulate whether or not a template is running on the client or server. We use a `struct` so as not to interfere with
17+
/// any user-set context.
18+
#[derive(Clone, Debug)]
19+
pub struct RenderCtx {
20+
/// Whether or not we're being executed on the server-side.
21+
pub is_server: bool,
22+
}
23+
1624
/// Represents all the different states that can be generated for a single template, allowing amalgamation logic to be run with the knowledge
1725
/// of what did what (rather than blindly working on a vector).
1826
#[derive(Default)]
@@ -209,12 +217,22 @@ impl<G: GenericNode> Template<G> {
209217
&self,
210218
props: Option<String>,
211219
translator: Rc<Translator>,
220+
is_server: bool,
212221
) -> SycamoreTemplate<G> {
213222
template! {
214223
// We provide the translator through context, which avoids having to define a separate variable for every translation due to Sycamore's `template!` macro taking ownership with `move` closures
215224
ContextProvider(ContextProviderProps {
216225
value: Rc::clone(&translator),
217-
children: || (self.template)(props)
226+
children: || template! {
227+
// We then provide whether this is being rendered on the client or the server as a context element
228+
// This allows easy specification of client-side-only logic without having to worry about `web_sys` panicking
229+
ContextProvider(ContextProviderProps {
230+
value: RenderCtx {
231+
is_server
232+
},
233+
children: || (self.template)(props)
234+
})
235+
}
218236
})
219237
}
220238
}
@@ -473,5 +491,14 @@ macro_rules! get_templates_map {
473491
};
474492
}
475493

494+
/// Checks in a template if the code is being run on client-side or the server-side. This uses Sycamore context, and so will only work
495+
/// in a reactive scope.
496+
#[macro_export]
497+
macro_rules! is_client {
498+
() => {
499+
!::sycamore::context::use_context::<::perseus::template::RenderCtx>().is_server;
500+
};
501+
}
502+
476503
/// A type alias for a `HashMap` of `Template`s.
477504
pub type TemplateMap<G> = HashMap<String, Template<G>>;

0 commit comments

Comments
 (0)