Skip to content

Commit 4c9c1be

Browse files
committed
fix: made page state store work with multiple pages in single template
This is fine if you're using the `#[template(...)]` macro, but if you aren't then you'll need to change some things.
1 parent 3b2401b commit 4c9c1be

File tree

8 files changed

+165
-48
lines changed

8 files changed

+165
-48
lines changed

examples/rx_state/src/about.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ pub fn about_page() -> View<G> {
1111
let pss = get_render_ctx!().page_state_store;
1212
// Get the state from the index page
1313
// If the user hasn't visited there yet, this won't exist
14-
let username = match pss.get::<IndexPropsRx>() {
14+
// The index page is just an empty string
15+
let username = match pss.get::<IndexPropsRx>("") {
1516
Some(IndexPropsRx { username }) => username,
1617
None => Signal::new("".to_string()),
1718
};

packages/perseus-macro/src/head.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ pub fn head_impl(input: HeadFn) -> TokenStream {
120120
if arg.is_some() {
121121
// There's an argument that will be provided as a `String`, so the wrapper will deserialize it
122122
quote! {
123-
#vis fn #name(props: ::std::option::Option<::std::string::String>) -> ::sycamore::prelude::View<::sycamore::prelude::SsrNode> {
123+
#vis fn #name(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<::sycamore::prelude::SsrNode> {
124124
// The user's function, with Sycamore component annotations and the like preserved
125125
// We know this won't be async because Sycamore doesn't allow that
126126
#(#attrs)*
@@ -129,14 +129,14 @@ pub fn head_impl(input: HeadFn) -> TokenStream {
129129
}
130130
#name(
131131
// If there are props, they will always be provided, the compiler just doesn't know that
132-
::serde_json::from_str(&props.unwrap()).unwrap()
132+
::serde_json::from_str(&props.state.unwrap()).unwrap()
133133
)
134134
}
135135
}
136136
} else {
137137
// There are no arguments
138138
quote! {
139-
#vis fn #name(props: ::std::option::Option<::std::string::String>) -> ::sycamore::prelude::View<::sycamore::prelude::SsrNode> {
139+
#vis fn #name(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<::sycamore::prelude::SsrNode> {
140140
// The user's function, with Sycamore component annotations and the like preserved
141141
// We know this won't be async because Sycamore doesn't allow that
142142
#(#attrs)*

packages/perseus-macro/src/template.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ pub fn template_impl(input: TemplateFn, component_name: Ident) -> TokenStream {
120120
if arg.is_some() {
121121
// There's an argument that will be provided as a `String`, so the wrapper will deserialize it
122122
quote! {
123-
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::std::option::Option<::std::string::String>) -> ::sycamore::prelude::View<G> {
123+
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
124124
// The user's function, with Sycamore component annotations and the like preserved
125125
// We know this won't be async because Sycamore doesn't allow that
126126
#(#attrs)*
@@ -130,15 +130,15 @@ pub fn template_impl(input: TemplateFn, component_name: Ident) -> TokenStream {
130130
::sycamore::prelude::view! {
131131
#component_name(
132132
// If there are props, they will always be provided, the compiler just doesn't know that
133-
::serde_json::from_str(&props.unwrap()).unwrap()
133+
::serde_json::from_str(&props.state.unwrap()).unwrap()
134134
)
135135
}
136136
}
137137
}
138138
} else {
139139
// There are no arguments
140140
quote! {
141-
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::std::option::Option<::std::string::String>) -> ::sycamore::prelude::View<G> {
141+
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
142142
// The user's function, with Sycamore component annotations and the like preserved
143143
// We know this won't be async because Sycamore doesn't allow that
144144
#(#attrs)*
@@ -200,7 +200,7 @@ pub fn template_with_rx_state_impl(input: TemplateFn, attr_args: AttributeArgs)
200200
// There's an argument that will be provided as a `String`, so the wrapper will deserialize it
201201
// We'll also make it reactive and potentially add it to the global store
202202
quote! {
203-
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::std::option::Option<::std::string::String>) -> ::sycamore::prelude::View<G> {
203+
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
204204
// The user's function, with Sycamore component annotations and the like preserved
205205
// We know this won't be async because Sycamore doesn't allow that
206206
#(#attrs)*
@@ -214,14 +214,14 @@ pub fn template_with_rx_state_impl(input: TemplateFn, attr_args: AttributeArgs)
214214
// If they are, we'll use them (so state persists for templates across the whole app)
215215
// TODO Isolate this for pages
216216
let mut pss = ::perseus::get_render_ctx!().page_state_store;
217-
match pss.get() {
217+
match pss.get(&props.path) {
218218
Some(old_state) => old_state,
219219
None => {
220220
// If there are props, they will always be provided, the compiler just doesn't know that
221221
// If the user is using this macro, they sure should be using `#[make_rx(...)]` or similar!
222-
let rx_props = ::serde_json::from_str::<#unrx_ty>(&props.unwrap()).unwrap().make_rx();
222+
let rx_props = ::serde_json::from_str::<#unrx_ty>(&props.state.unwrap()).unwrap().make_rx();
223223
// They aren't in there, so insert them
224-
pss.add(rx_props.clone());
224+
pss.add(&props.path, rx_props.clone());
225225
rx_props
226226
}
227227
}
@@ -233,7 +233,7 @@ pub fn template_with_rx_state_impl(input: TemplateFn, attr_args: AttributeArgs)
233233
} else {
234234
// There are no arguments
235235
quote! {
236-
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::std::option::Option<::std::string::String>) -> ::sycamore::prelude::View<G> {
236+
#vis fn #name<G: ::sycamore::prelude::Html>(props: ::perseus::templates::PageProps) -> ::sycamore::prelude::View<G> {
237237
// The user's function, with Sycamore component annotations and the like preserved
238238
// We know this won't be async because Sycamore doesn't allow that
239239
#(#attrs)*

packages/perseus/src/build.rs

+42-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::errors::*;
44
use crate::locales::Locales;
55
use crate::router::RouterState;
66
use crate::state::PageStateStore;
7-
use crate::templates::TemplateMap;
7+
use crate::templates::{PageProps, TemplateMap};
88
use crate::translations_manager::TranslationsManager;
99
use crate::translator::Translator;
1010
use crate::{
@@ -94,13 +94,20 @@ async fn gen_state_for_path(
9494
Some(stripped) => stripped.to_string(),
9595
None => full_path_without_locale,
9696
};
97-
// Add the current locale to the front of that na dencode it as a URL so we can store a flat series of files
97+
// Add the current locale to the front of that and dencode it as a URL so we can store a flat series of files
9898
// BUG: insanely nested paths won't work whatsoever if the filename is too long, maybe hash instead?
9999
let full_path_encoded = format!(
100100
"{}-{}",
101101
translator.get_locale(),
102102
urlencoding::encode(&full_path_without_locale)
103103
);
104+
// And we'll need the full path with the locale for the `PageProps`
105+
// If it's `xx-XX`, we should just have it without the locale (this may be interacted with by users)
106+
let locale = translator.get_locale();
107+
let full_path_with_locale = match locale.as_str() {
108+
"xx-XX" => full_path_without_locale.clone(),
109+
locale => format!("{}/{}", locale, &full_path_without_locale),
110+
};
104111

105112
// Handle static initial state generation
106113
// We'll only write a static state if one is explicitly generated
@@ -120,7 +127,10 @@ async fn gen_state_for_path(
120127
// Prerender the template using that state
121128
let prerendered = sycamore::render_to_string(|| {
122129
template.render_for_template(
123-
Some(initial_state.clone()),
130+
PageProps {
131+
path: full_path_with_locale.clone(),
132+
state: Some(initial_state.clone()),
133+
},
124134
translator,
125135
true,
126136
RouterState::default(),
@@ -133,7 +143,13 @@ async fn gen_state_for_path(
133143
.await?;
134144
// Prerender the document `<head>` with that state
135145
// If the page also uses request state, amalgamation will be applied as for the normal content
136-
let head_str = template.render_head_str(Some(initial_state), translator);
146+
let head_str = template.render_head_str(
147+
PageProps {
148+
path: full_path_with_locale.clone(),
149+
state: Some(initial_state.clone()),
150+
},
151+
translator,
152+
);
137153
mutable_store
138154
.write(
139155
&format!("static/{}.head.html", full_path_encoded),
@@ -155,7 +171,10 @@ async fn gen_state_for_path(
155171
// Prerender the template using that state
156172
let prerendered = sycamore::render_to_string(|| {
157173
template.render_for_template(
158-
Some(initial_state.clone()),
174+
PageProps {
175+
path: full_path_with_locale.clone(),
176+
state: Some(initial_state.clone()),
177+
},
159178
translator,
160179
true,
161180
RouterState::default(),
@@ -168,7 +187,13 @@ async fn gen_state_for_path(
168187
.await?;
169188
// Prerender the document `<head>` with that state
170189
// If the page also uses request state, amalgamation will be applied as for the normal content
171-
let head_str = template.render_head_str(Some(initial_state), translator);
190+
let head_str = template.render_head_str(
191+
PageProps {
192+
path: full_path_with_locale.clone(),
193+
state: Some(initial_state),
194+
},
195+
translator,
196+
);
172197
immutable_store
173198
.write(
174199
&format!("static/{}.head.html", full_path_encoded),
@@ -200,14 +225,23 @@ async fn gen_state_for_path(
200225
if template.is_basic() {
201226
let prerendered = sycamore::render_to_string(|| {
202227
template.render_for_template(
203-
None,
228+
PageProps {
229+
path: full_path_with_locale.clone(),
230+
state: None,
231+
},
204232
translator,
205233
true,
206234
RouterState::default(),
207235
PageStateStore::default(),
208236
)
209237
});
210-
let head_str = template.render_head_str(None, translator);
238+
let head_str = template.render_head_str(
239+
PageProps {
240+
path: full_path_with_locale,
241+
state: None,
242+
},
243+
translator,
244+
);
211245
// Write that prerendered HTML to a static file
212246
immutable_store
213247
.write(&format!("static/{}.html", full_path_encoded), &prerendered)

packages/perseus/src/page_state_store.rs

+32-10
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,37 @@ use std::cell::RefCell;
33
use std::collections::HashMap;
44
use std::rc::Rc;
55

6+
/// A key type for the `PageStateStore` that denotes both a page's state type and its URL.
7+
#[derive(Hash, PartialEq, Eq)]
8+
pub struct PageStateKey {
9+
state_type: TypeId,
10+
url: String,
11+
}
12+
613
/// A container for page state in Perseus. This is designed as a context store, in which one of each type can be stored. Therefore, it acts very similarly to Sycamore's context system,
714
/// though it's specifically designed for each page to store one reactive properties object. In theory, you could interact with this entirely independently of Perseus' state interface,
815
/// though this isn't recommended.
16+
///
17+
/// Note that the same pages in different locales will have different entries here. If you need to store state for a page across locales, you should use the global state system instead. For apps
18+
/// not using i18n, the page URL will not include any locale.
919
// TODO Make this work with multiple pages for a single template
1020
#[derive(Default, Clone)]
1121
pub struct PageStateStore {
1222
/// A map of type IDs to anything, allowing one storage of each type (each type is intended to a properties `struct` for a template). Entries must be `Clone`able becasue we assume them
1323
/// to be `Signal`s or `struct`s composed of `Signal`s.
1424
// Technically, this should be `Any + Clone`, but that's not possible without something like `dyn_clone`, and we don't need it because we can restrict on the methods instead!
15-
map: Rc<RefCell<HashMap<TypeId, Box<dyn Any>>>>,
25+
map: Rc<RefCell<HashMap<PageStateKey, Box<dyn Any>>>>,
1626
}
1727
impl PageStateStore {
18-
/// Gets an element out of the state by its type.
19-
pub fn get<T: Any + Clone>(&self) -> Option<T> {
28+
/// Gets an element out of the state by its type and URL.
29+
pub fn get<T: Any + Clone>(&self, url: &str) -> Option<T> {
2030
let type_id = TypeId::of::<T>();
31+
let key = PageStateKey {
32+
state_type: type_id,
33+
url: url.to_string(),
34+
};
2135
let map = self.map.borrow();
22-
map.get(&type_id).map(|val| {
36+
map.get(&key).map(|val| {
2337
if let Some(val) = val.downcast_ref::<T>() {
2438
(*val).clone()
2539
} else {
@@ -28,16 +42,24 @@ impl PageStateStore {
2842
}
2943
})
3044
}
31-
/// Adds a new element to the state by its type. Any existing element with the same type will be silently overriden (use `.contains()` to check first if needed).
32-
pub fn add<T: Any + Clone>(&mut self, val: T) {
45+
/// Adds a new element to the state by its type and URL. Any existing element with the same type and URL will be silently overriden (use `.contains()` to check first if needed).
46+
pub fn add<T: Any + Clone>(&mut self, url: &str, val: T) {
3347
let type_id = TypeId::of::<T>();
48+
let key = PageStateKey {
49+
state_type: type_id,
50+
url: url.to_string(),
51+
};
3452
let mut map = self.map.borrow_mut();
35-
map.insert(type_id, Box::new(val));
53+
map.insert(key, Box::new(val));
3654
}
37-
/// Checks if the state contains the element of the given type.
38-
pub fn contains<T: Any + Clone>(&self) -> bool {
55+
/// Checks if the state contains the element of the given type for the given page.
56+
pub fn contains<T: Any + Clone>(&self, url: &str) -> bool {
3957
let type_id = TypeId::of::<T>();
40-
self.map.borrow().contains_key(&type_id)
58+
let key = PageStateKey {
59+
state_type: type_id,
60+
url: url.to_string(),
61+
};
62+
self.map.borrow().contains_key(&key)
4163
}
4264
}
4365

0 commit comments

Comments
 (0)