Skip to content

Commit 0e82e2e

Browse files
Support query parameters in routes (#752)
* feat(router): support query and fragment * fix(router): remove CustomEvent web_sys feature * fix(router): add docs, run fmt, fix clippy warnings * fix(router): remove unnececery clones and dereferences * refactor(router): format * feat(router): add query params to router example * fix(router): bring back refresh and navigate_no_history * fix(router): change history & signal update order * chore(router): fmt * fix(router): change history & signal update order in navigate* functions --------- Co-authored-by: Luke Chu <[email protected]>
1 parent eb134c8 commit 0e82e2e

File tree

3 files changed

+146
-21
lines changed

3 files changed

+146
-21
lines changed

examples/router/src/main.rs

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use sycamore::prelude::*;
2-
use sycamore_router::{HistoryIntegration, Route, Router};
2+
use sycamore_router::{use_search_query, HistoryIntegration, Route, Router};
33

44
#[derive(Route, Clone)]
55
enum AppRoutes {
@@ -11,6 +11,8 @@ enum AppRoutes {
1111
Wildcard { path: Vec<String> },
1212
#[to("/uint-capture/<unit>")]
1313
Unit(u32),
14+
#[to("/query-params")]
15+
QueryParams,
1416
#[not_found]
1517
NotFound,
1618
}
@@ -32,6 +34,8 @@ fn App() -> View {
3234
br {}
3335
a(href="/uint-capture/42") {"Unit: 42"}
3436
br {}
37+
a(href="/query-params") {"Query Params"}
38+
br {}
3539
a(href="/not-found") {"Not Found"}
3640
br {}
3741

@@ -51,6 +55,14 @@ fn App() -> View {
5155
AppRoutes::Unit(unit) => view! {
5256
h1 { "Unit: " (unit) }
5357
},
58+
AppRoutes::QueryParams => {
59+
let q = use_search_query("q");
60+
view! {
61+
h1 { "Query Params" }
62+
a(href="?q=a") { "A" } a(href="?q=b") { "B" }
63+
p { "Query: " (q.get_clone().unwrap_or_default()) }
64+
}
65+
}
5466
AppRoutes::NotFound => view! {
5567
h1 { "Not Found" }
5668
},

packages/sycamore-router/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ features = [
2929
"PopStateEvent",
3030
"Url",
3131
"Window",
32+
"UrlSearchParams",
3233
]
3334
version = "0.3.60"

packages/sycamore-router/src/router.rs

+132-20
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use std::cell::Cell;
2+
use std::collections::HashMap;
23
use std::marker::PhantomData;
34
use std::rc::Rc;
45

56
use sycamore::prelude::*;
67
use wasm_bindgen::prelude::*;
7-
use web_sys::{Element, HtmlAnchorElement, HtmlBaseElement, KeyboardEvent};
8+
use web_sys::js_sys::Array;
9+
use web_sys::{Element, Event, HtmlAnchorElement, HtmlBaseElement, KeyboardEvent, UrlSearchParams};
810

911
use crate::Route;
1012

@@ -24,6 +26,7 @@ pub trait Integration {
2426

2527
thread_local! {
2628
static PATHNAME: Cell<Option<Signal<String>>> = const { Cell::new(None) };
29+
static QUERY: Cell<Option<Signal<()>>> = const { Cell::new(None) };
2730
}
2831

2932
/// A router integration that uses the
@@ -78,28 +81,55 @@ impl Integration for HistoryIntegration {
7881
let origin = a.origin();
7982
let a_pathname = a.pathname();
8083
let hash = a.hash();
84+
let query = a.search();
8185

8286
let meta_keys_pressed = meta_keys_pressed(ev.unchecked_ref::<KeyboardEvent>());
8387
if !meta_keys_pressed && location.origin() == Ok(origin) {
8488
if location.pathname().as_ref() != Ok(&a_pathname) {
8589
// Same origin, different path. Navigate to new page.
8690
ev.prevent_default();
8791
PATHNAME.with(|pathname| {
92+
// Update History API.
93+
let history = window().history().unwrap_throw();
94+
history
95+
.push_state_with_url(&JsValue::UNDEFINED, "", Some(&a_pathname))
96+
.unwrap_throw();
97+
window().scroll_to_with_x_and_y(0.0, 0.0);
98+
8899
let pathname = pathname.get().unwrap_throw();
89100
let path = a_pathname
90101
.strip_prefix(&base_pathname())
91102
.unwrap_or(&a_pathname);
92103
pathname.set(path.to_string());
93-
94-
// Update History API.
104+
});
105+
} else if location.search().as_ref() != Ok(&query) {
106+
// Same origin, same pathname, different query.
107+
ev.prevent_default();
108+
let history = window().history().unwrap_throw();
109+
if query.is_empty() {
110+
history
111+
.push_state_with_url(&JsValue::UNDEFINED, "", Some(&a.href()))
112+
.unwrap_throw();
113+
} else {
95114
let history = window().history().unwrap_throw();
96115
history
97-
.push_state_with_url(&JsValue::UNDEFINED, "", Some(&a_pathname))
116+
.push_state_with_url(&JsValue::UNDEFINED, "", Some(&query))
98117
.unwrap_throw();
99-
window().scroll_to_with_x_and_y(0.0, 0.0);
100-
});
118+
}
119+
QUERY.with(|query| query.get().unwrap_throw().update(|_| {}));
101120
} else if location.hash().as_ref() != Ok(&hash) {
102-
// Same origin, same pathname, different hash. Use default browser behavior.
121+
// Same origin, same pathname, same query, different hash. Use default
122+
// browser behavior.
123+
if hash.is_empty() {
124+
ev.prevent_default();
125+
let history = window().history().unwrap_throw();
126+
history
127+
.push_state_with_url(&JsValue::UNDEFINED, "", Some(&a.href()))
128+
.unwrap_throw();
129+
window()
130+
.dispatch_event(&Event::new("hashchange").unwrap())
131+
.unwrap_throw();
132+
}
103133
} else {
104134
// Same page. Do nothing.
105135
ev.prevent_default();
@@ -233,6 +263,7 @@ where
233263
let path = integration.current_pathname();
234264
let path = path.strip_prefix(&base_pathname).unwrap_or(&path);
235265
pathname.set(Some(create_signal(path.to_string())));
266+
QUERY.set(Some(create_signal(())));
236267
});
237268
let pathname = PATHNAME.with(|p| p.get().unwrap_throw());
238269

@@ -329,16 +360,16 @@ pub fn navigate(url: &str) {
329360
"navigate can only be used with a Router"
330361
);
331362

332-
let pathname = pathname.get().unwrap_throw();
333-
let path = url.strip_prefix(&base_pathname()).unwrap_or(url);
334-
pathname.set(path.to_string());
335-
336363
// Update History API.
337364
let history = window().history().unwrap_throw();
338365
history
339366
.push_state_with_url(&JsValue::UNDEFINED, "", Some(url))
340367
.unwrap_throw();
341368
window().scroll_to_with_x_and_y(0.0, 0.0);
369+
370+
let pathname = pathname.get().unwrap_throw();
371+
let path = url.strip_prefix(&base_pathname()).unwrap_or(url);
372+
pathname.set(path.to_string());
342373
});
343374
}
344375

@@ -357,16 +388,16 @@ pub fn navigate_replace(url: &str) {
357388
"navigate_replace can only be used with a Router"
358389
);
359390

360-
let pathname = pathname.get().unwrap_throw();
361-
let path = url.strip_prefix(&base_pathname()).unwrap_or(url);
362-
pathname.set(path.to_string());
363-
364391
// Update History API.
365392
let history = window().history().unwrap_throw();
366393
history
367394
.replace_state_with_url(&JsValue::UNDEFINED, "", Some(url))
368395
.unwrap_throw();
369396
window().scroll_to_with_x_and_y(0.0, 0.0);
397+
398+
let pathname = pathname.get().unwrap_throw();
399+
let path = url.strip_prefix(&base_pathname()).unwrap_or(url);
400+
pathname.set(path.to_string());
370401
});
371402
}
372403

@@ -383,17 +414,18 @@ pub fn navigate_no_history(url: &str) {
383414
"navigate_no_history can only be used with a Router"
384415
);
385416

417+
window().scroll_to_with_x_and_y(0.0, 0.0);
418+
386419
let pathname = pathname.get().unwrap_throw();
387420
let path = url.strip_prefix(&base_pathname()).unwrap_or(url);
388421
pathname.set(path.to_string());
389-
390-
window().scroll_to_with_x_and_y(0.0, 0.0);
391422
});
392423
}
393424

394425
/// Preform a "soft" refresh of the current page.
395426
///
396-
/// Unlike a "hard" refresh which corresponds to clicking on the refresh button, this simply forces a re-render of the view for the current page.
427+
/// Unlike a "hard" refresh which corresponds to clicking on the refresh button, this simply forces
428+
/// a re-render of the view for the current page.
397429
///
398430
/// # Panic
399431
/// This function will `panic!()` if a [`Router`] has not yet been created.
@@ -404,12 +436,92 @@ pub fn refresh() {
404436
"refresh can only be used with a Router"
405437
);
406438

407-
pathname.get().unwrap_throw().update(|_| {});
408-
409439
window().scroll_to_with_x_and_y(0.0, 0.0);
440+
441+
pathname.get().unwrap_throw().update(|_| {});
410442
});
411443
}
412444

445+
/// Creates a ReadSignal that tracks the url query provided.
446+
pub fn use_search_query(query: &'static str) -> ReadSignal<Option<String>> {
447+
PATHNAME.with(|pathname| {
448+
assert!(
449+
pathname.get().is_some(),
450+
"create_query can only be used with a Router"
451+
);
452+
453+
let pathname = pathname.get().unwrap_throw();
454+
455+
create_memo(move || {
456+
QUERY.with(|query| query.get().unwrap_throw()).track();
457+
pathname.track();
458+
UrlSearchParams::new_with_str(&window().location().search().unwrap_throw())
459+
.unwrap_throw()
460+
.get(query)
461+
})
462+
})
463+
}
464+
465+
/// Creates a ReadSignal that tracks the url query string.
466+
pub fn use_search_queries() -> ReadSignal<HashMap<String, String>> {
467+
PATHNAME.with(|pathname| {
468+
assert!(
469+
pathname.get().is_some(),
470+
"create_queries can only be used with a Router"
471+
);
472+
473+
let pathname = pathname.get().unwrap_throw();
474+
475+
create_memo(move || {
476+
QUERY.with(|query| query.get().unwrap_throw()).track();
477+
pathname.track();
478+
UrlSearchParams::new_with_str(&window().location().search().unwrap_throw())
479+
.unwrap_throw()
480+
.entries()
481+
.into_iter()
482+
.map(|e| {
483+
let e: Array = e.unwrap_throw().into();
484+
let e = e
485+
.into_iter()
486+
.map(|s| s.as_string().unwrap_throw())
487+
.collect::<Vec<String>>();
488+
(e[0].clone(), e[1].clone())
489+
})
490+
.collect()
491+
})
492+
})
493+
}
494+
495+
/// Creates a ReadSignal that tracks the url fragment.
496+
pub fn use_location_hash() -> ReadSignal<String> {
497+
PATHNAME.with(|pathname| {
498+
assert!(
499+
pathname.get().is_some(),
500+
"create_fragment can only be used with a Router"
501+
);
502+
503+
let pathname = pathname.get().unwrap_throw();
504+
505+
let on_hashchange = create_signal(());
506+
window()
507+
.add_event_listener_with_callback(
508+
"hashchange",
509+
Closure::wrap(Box::new(move || {
510+
on_hashchange.update(|_| {});
511+
}) as Box<dyn FnMut()>)
512+
.into_js_value()
513+
.unchecked_ref(),
514+
)
515+
.unwrap_throw();
516+
517+
create_memo(move || {
518+
on_hashchange.track();
519+
pathname.track();
520+
window().location().hash().unwrap_throw()
521+
})
522+
})
523+
}
524+
413525
fn meta_keys_pressed(kb_event: &KeyboardEvent) -> bool {
414526
kb_event.meta_key() || kb_event.ctrl_key() || kb_event.shift_key() || kb_event.alt_key()
415527
}

0 commit comments

Comments
 (0)