Skip to content

Commit e2b91ca

Browse files
authored
Includes query parameters in rendered Link component (#2464)
* Add tests for Link component * Include query parameters in href of Link component * Add test cases for HashRouter and basename option * Fix test name, format, clippy * Ignore clippy warnings for test helpers
1 parent f39afdc commit e2b91ca

5 files changed

Lines changed: 236 additions & 3 deletions

File tree

packages/yew-router/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ js-sys = "0.3"
2323
gloo = { version = "0.6", features = ["futures"] }
2424
route-recognizer = "0.3"
2525
serde = "1"
26+
serde_urlencoded = "0.7.1"
2627

2728
[dependencies.web-sys]
2829
version = "0.3"

packages/yew-router/src/components/link.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use yew::virtual_dom::AttrValue;
77

88
use crate::navigator::NavigatorKind;
99
use crate::scope_ext::RouterScopeExt;
10+
use crate::utils;
1011
use crate::Routable;
1112

1213
/// Props for [`Link`]
@@ -86,6 +87,7 @@ where
8687
let LinkProps {
8788
classes,
8889
to,
90+
query,
8991
children,
9092
disabled,
9193
..
@@ -100,11 +102,15 @@ where
100102
.navigator()
101103
.expect_throw("failed to get navigator");
102104
let href: AttrValue = {
103-
let href = navigator.route_to_url(to);
105+
let pathname = navigator.route_to_url(to);
106+
let path = query
107+
.and_then(|query| serde_urlencoded::to_string(query).ok())
108+
.and_then(|query| utils::compose_path(&pathname, &query))
109+
.unwrap_or_else(|| pathname.to_string());
104110

105111
match navigator.kind() {
106-
NavigatorKind::Hash => format!("#{}", href).into(),
107-
_ => href,
112+
NavigatorKind::Hash => format!("#{}", path),
113+
_ => path,
108114
}
109115
.into()
110116
};

packages/yew-router/src/utils.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ pub fn fetch_base_url() -> Option<String> {
4141
}
4242
}
4343

44+
pub fn compose_path(pathname: &str, query: &str) -> Option<String> {
45+
gloo::utils::window()
46+
.location()
47+
.href()
48+
.ok()
49+
.and_then(|base| web_sys::Url::new_with_base(pathname, &base).ok())
50+
.map(|url| {
51+
url.set_search(query);
52+
format!("{}{}", url.pathname(), url.search())
53+
})
54+
}
55+
4456
#[cfg(test)]
4557
mod tests {
4658
use gloo::utils::document;
@@ -78,4 +90,17 @@ mod tests {
7890
.set_inner_html(r#"<base href="/base">"#);
7991
assert_eq!(fetch_base_url(), Some("/base".to_string()));
8092
}
93+
94+
#[test]
95+
fn test_compose_path() {
96+
assert_eq!(compose_path("/home", ""), Some("/home".to_string()));
97+
assert_eq!(
98+
compose_path("/path/to", "foo=bar"),
99+
Some("/path/to?foo=bar".to_string())
100+
);
101+
assert_eq!(
102+
compose_path("/events", "from=2019&to=2021"),
103+
Some("/events?from=2019&to=2021".to_string())
104+
);
105+
}
81106
}

packages/yew-router/tests/link.rs

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
use gloo::timers::future::sleep;
2+
use serde::{Deserialize, Serialize};
3+
use std::time::Duration;
4+
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
5+
use yew::functional::function_component;
6+
use yew::prelude::*;
7+
use yew_router::prelude::*;
8+
9+
mod utils;
10+
use utils::*;
11+
12+
wasm_bindgen_test_configure!(run_in_browser);
13+
14+
#[derive(Clone, Serialize, Deserialize, PartialEq)]
15+
struct PageParam {
16+
page: i32,
17+
}
18+
19+
#[derive(Clone, Serialize, Deserialize, PartialEq)]
20+
struct SearchParams {
21+
q: String,
22+
lang: Option<String>,
23+
}
24+
25+
impl SearchParams {
26+
fn new(q: &str) -> Self {
27+
Self {
28+
q: q.to_string(),
29+
lang: None,
30+
}
31+
}
32+
33+
fn new_with_lang(q: &str, lang: &str) -> Self {
34+
Self {
35+
q: q.to_string(),
36+
lang: Some(lang.to_string()),
37+
}
38+
}
39+
}
40+
41+
#[derive(Debug, Clone, Copy, PartialEq, Routable)]
42+
enum Routes {
43+
#[at("/posts")]
44+
Posts,
45+
#[at("/search")]
46+
Search,
47+
}
48+
49+
#[function_component(NavigationMenu)]
50+
fn navigation_menu() -> Html {
51+
html! {
52+
<ul>
53+
<li class="posts">
54+
<Link<Routes> to={Routes::Posts}>
55+
{ "Posts without parameters" }
56+
</Link<Routes>>
57+
</li>
58+
<li class="posts-page-2">
59+
<Link<Routes, PageParam> to={Routes::Posts} query={Some(PageParam { page: 2 })}>
60+
{ "Posts of 2nd page" }
61+
</Link<Routes, PageParam>>
62+
</li>
63+
<li class="search">
64+
<Link<Routes> to={Routes::Search}>
65+
{ "Search withfout parameters" }
66+
</Link<Routes>>
67+
</li>
68+
<li class="search-q">
69+
<Link<Routes, SearchParams> to={Routes::Search} query={Some(SearchParams::new("Rust"))}>
70+
{ "Search with keyword parameter" }
71+
</Link<Routes, SearchParams>>
72+
</li>
73+
<li class="search-q-lang">
74+
<Link<Routes, SearchParams> to={Routes::Search} query={Some(SearchParams::new_with_lang("Rust", "en_US"))}>
75+
{ "Search with keyword and language parameters" }
76+
</Link<Routes, SearchParams>>
77+
</li>
78+
</ul>
79+
}
80+
}
81+
82+
#[function_component(RootForBrowserRouter)]
83+
fn root_for_browser_router() -> Html {
84+
html! {
85+
<BrowserRouter>
86+
<NavigationMenu />
87+
</BrowserRouter>
88+
}
89+
}
90+
91+
#[test]
92+
async fn link_in_browser_router() {
93+
let div = gloo::utils::document().create_element("div").unwrap();
94+
let _ = div.set_attribute("id", "browser-router");
95+
let _ = gloo::utils::body().append_child(&div);
96+
yew::start_app_in_element::<RootForBrowserRouter>(div);
97+
98+
sleep(Duration::ZERO).await;
99+
100+
assert_eq!("/posts", link_href("#browser-router ul > li.posts > a"));
101+
assert_eq!(
102+
"/posts?page=2",
103+
link_href("#browser-router ul > li.posts-page-2 > a")
104+
);
105+
106+
assert_eq!("/search", link_href("#browser-router ul > li.search > a"));
107+
assert_eq!(
108+
"/search?q=Rust",
109+
link_href("#browser-router ul > li.search-q > a")
110+
);
111+
assert_eq!(
112+
"/search?q=Rust&lang=en_US",
113+
link_href("#browser-router ul > li.search-q-lang > a")
114+
);
115+
}
116+
117+
#[function_component(RootForBasename)]
118+
fn root_for_basename() -> Html {
119+
html! {
120+
<BrowserRouter basename="/base/">
121+
<NavigationMenu />
122+
</BrowserRouter>
123+
}
124+
}
125+
126+
#[test]
127+
async fn link_with_basename() {
128+
let div = gloo::utils::document().create_element("div").unwrap();
129+
let _ = div.set_attribute("id", "with-basename");
130+
let _ = gloo::utils::body().append_child(&div);
131+
yew::start_app_in_element::<RootForBasename>(div);
132+
133+
sleep(Duration::ZERO).await;
134+
135+
assert_eq!("/base/posts", link_href("#with-basename ul > li.posts > a"));
136+
assert_eq!(
137+
"/base/posts?page=2",
138+
link_href("#with-basename ul > li.posts-page-2 > a")
139+
);
140+
141+
assert_eq!(
142+
"/base/search",
143+
link_href("#with-basename ul > li.search > a")
144+
);
145+
assert_eq!(
146+
"/base/search?q=Rust",
147+
link_href("#with-basename ul > li.search-q > a")
148+
);
149+
assert_eq!(
150+
"/base/search?q=Rust&lang=en_US",
151+
link_href("#with-basename ul > li.search-q-lang > a")
152+
);
153+
}
154+
155+
#[function_component(RootForHashRouter)]
156+
fn root_for_hash_router() -> Html {
157+
html! {
158+
<HashRouter>
159+
<NavigationMenu />
160+
</HashRouter>
161+
}
162+
}
163+
164+
#[test]
165+
async fn link_in_hash_router() {
166+
let div = gloo::utils::document().create_element("div").unwrap();
167+
let _ = div.set_attribute("id", "hash-router");
168+
let _ = gloo::utils::body().append_child(&div);
169+
yew::start_app_in_element::<RootForHashRouter>(div);
170+
171+
sleep(Duration::ZERO).await;
172+
173+
assert_eq!("#/posts", link_href("#hash-router ul > li.posts > a"));
174+
assert_eq!(
175+
"#/posts?page=2",
176+
link_href("#hash-router ul > li.posts-page-2 > a")
177+
);
178+
179+
assert_eq!("#/search", link_href("#hash-router ul > li.search > a"));
180+
assert_eq!(
181+
"#/search?q=Rust",
182+
link_href("#hash-router ul > li.search-q > a")
183+
);
184+
assert_eq!(
185+
"#/search?q=Rust&lang=en_US",
186+
link_href("#hash-router ul > li.search-q-lang > a")
187+
);
188+
}

packages/yew-router/tests/utils.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use wasm_bindgen::JsCast;
22

3+
#[allow(dead_code)]
34
pub fn obtain_result_by_id(id: &str) -> String {
45
gloo::utils::document()
56
.get_element_by_id(id)
67
.expect("No result found. Most likely, the application crashed and burned")
78
.inner_html()
89
}
910

11+
#[allow(dead_code)]
1012
pub fn click(selector: &str) {
1113
gloo::utils::document()
1214
.query_selector(selector)
@@ -17,10 +19,21 @@ pub fn click(selector: &str) {
1719
.click();
1820
}
1921

22+
#[allow(dead_code)]
2023
pub fn history_length() -> u32 {
2124
gloo::utils::window()
2225
.history()
2326
.expect("No history found")
2427
.length()
2528
.expect("No history length found")
2629
}
30+
31+
#[allow(dead_code)]
32+
pub fn link_href(selector: &str) -> String {
33+
gloo::utils::document()
34+
.query_selector(selector)
35+
.expect("Failed to run query selector")
36+
.unwrap_or_else(|| panic!("No such link: {}", selector))
37+
.get_attribute("href")
38+
.expect("No href attribute")
39+
}

0 commit comments

Comments
 (0)