mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
yew-router: Dynamic basename. (#3725)
* yew-router: Dynamic basename. * Revisions. * Test location.path and navigator.basename match expectations at each step. * Better coverage of edge case.
This commit is contained in:
parent
7be9d17ec4
commit
3c7e3e3e79
@ -159,7 +159,7 @@ impl Navigator {
|
||||
pub(crate) fn prefix_basename<'a>(&self, route_s: &'a str) -> Cow<'a, str> {
|
||||
match self.basename() {
|
||||
Some(base) => {
|
||||
if route_s.is_empty() && route_s.is_empty() {
|
||||
if base.is_empty() && route_s.is_empty() {
|
||||
Cow::from("/")
|
||||
} else {
|
||||
Cow::from(format!("{base}{route_s}"))
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
//! Router Component.
|
||||
use std::borrow::Cow;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gloo::history::query::Raw;
|
||||
use yew::prelude::*;
|
||||
use yew::virtual_dom::AttrValue;
|
||||
|
||||
@ -72,16 +74,43 @@ fn base_router(props: &RouterProps) -> Html {
|
||||
basename,
|
||||
} = props.clone();
|
||||
|
||||
let basename = basename.map(|m| strip_slash_suffix(&m).to_owned());
|
||||
let navigator = Navigator::new(history.clone(), basename.clone());
|
||||
|
||||
let old_basename = use_mut_ref(|| Option::<String>::None);
|
||||
let mut old_basename = old_basename.borrow_mut();
|
||||
if basename != *old_basename {
|
||||
// If `old_basename` is `Some`, path is probably prefixed with `old_basename`.
|
||||
// If `old_basename` is `None`, path may or may not be prefixed with the new `basename`,
|
||||
// depending on whether this is the first render.
|
||||
let old_navigator = Navigator::new(
|
||||
history.clone(),
|
||||
old_basename.as_ref().or(basename.as_ref()).cloned(),
|
||||
);
|
||||
*old_basename = basename.clone();
|
||||
let location = history.location();
|
||||
let stripped = old_navigator.strip_basename(Cow::from(location.path()));
|
||||
let prefixed = navigator.prefix_basename(&stripped);
|
||||
|
||||
if prefixed != location.path() {
|
||||
history
|
||||
.replace_with_query(prefixed, Raw(location.query_str()))
|
||||
.unwrap_or_else(|never| match never {});
|
||||
} else {
|
||||
// Reaching here is possible if the page loads with the correct path, including the
|
||||
// initial basename. In that case, the new basename would be stripped and then
|
||||
// prefixed right back. While replacing the history would probably be harmless,
|
||||
// we might as well avoid doing it.
|
||||
}
|
||||
}
|
||||
|
||||
let navi_ctx = NavigatorContext { navigator };
|
||||
|
||||
let loc_ctx = use_reducer(|| LocationContext {
|
||||
location: history.location(),
|
||||
ctr: 0,
|
||||
});
|
||||
|
||||
let basename = basename.map(|m| strip_slash_suffix(&m).to_string());
|
||||
let navi_ctx = NavigatorContext {
|
||||
navigator: Navigator::new(history.clone(), basename),
|
||||
};
|
||||
|
||||
{
|
||||
let loc_ctx_dispatcher = loc_ctx.dispatcher();
|
||||
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use gloo::utils::window;
|
||||
use js_sys::{JsString, Object, Reflect};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure};
|
||||
use yew::functional::function_component;
|
||||
@ -47,8 +50,20 @@ enum Routes {
|
||||
Search,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Properties)]
|
||||
struct NavigationMenuProps {
|
||||
#[prop_or(None)]
|
||||
assertion: Option<fn(&Navigator, &Location)>,
|
||||
}
|
||||
|
||||
#[function_component(NavigationMenu)]
|
||||
fn navigation_menu() -> Html {
|
||||
fn navigation_menu(props: &NavigationMenuProps) -> Html {
|
||||
let navigator = use_navigator().unwrap();
|
||||
let location = use_location().unwrap();
|
||||
if let Some(assertion) = props.assertion {
|
||||
assertion(&navigator, &location);
|
||||
}
|
||||
|
||||
html! {
|
||||
<ul>
|
||||
<li class="posts">
|
||||
@ -89,12 +104,11 @@ fn root_for_browser_router() -> Html {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn link_in_browser_router() {
|
||||
let div = gloo::utils::document().create_element("div").unwrap();
|
||||
let _ = div.set_attribute("id", "browser-router");
|
||||
let _ = gloo::utils::body().append_child(&div);
|
||||
yew::Renderer::<RootForBrowserRouter>::with_root(div).render();
|
||||
let handle = yew::Renderer::<RootForBrowserRouter>::with_root(div).render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
@ -113,26 +127,77 @@ async fn link_in_browser_router() {
|
||||
"/search?q=Rust&lang=en_US",
|
||||
link_href("#browser-router ul > li.search-q-lang > a")
|
||||
);
|
||||
|
||||
handle.destroy();
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Properties)]
|
||||
struct BasenameProps {
|
||||
basename: Option<String>,
|
||||
assertion: fn(&Navigator, &Location),
|
||||
}
|
||||
|
||||
#[function_component(RootForBasename)]
|
||||
fn root_for_basename() -> Html {
|
||||
fn root_for_basename(props: &BasenameProps) -> Html {
|
||||
html! {
|
||||
<BrowserRouter basename="/base/">
|
||||
<NavigationMenu />
|
||||
<BrowserRouter basename={props.basename.clone()}>
|
||||
<NavigationMenu assertion={props.assertion}/>
|
||||
</BrowserRouter>
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn link_with_basename() {
|
||||
async fn link_with_basename(correct_initial_path: bool) {
|
||||
if correct_initial_path {
|
||||
let cookie = Object::new();
|
||||
Reflect::set(&cookie, &JsString::from("foo"), &JsString::from("bar")).unwrap();
|
||||
window()
|
||||
.history()
|
||||
.unwrap()
|
||||
.replace_state_with_url(&cookie, "", Some("/base/"))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
static RENDERS: AtomicU8 = AtomicU8::new(0);
|
||||
RENDERS.store(0, Ordering::Relaxed);
|
||||
|
||||
let div = gloo::utils::document().create_element("div").unwrap();
|
||||
let _ = div.set_attribute("id", "with-basename");
|
||||
let _ = gloo::utils::body().append_child(&div);
|
||||
yew::Renderer::<RootForBasename>::with_root(div).render();
|
||||
|
||||
let mut handle = yew::Renderer::<RootForBasename>::with_root_and_props(
|
||||
div,
|
||||
BasenameProps {
|
||||
basename: Some("/base/".to_owned()),
|
||||
assertion: |navigator, location| {
|
||||
RENDERS.fetch_add(1, Ordering::Relaxed);
|
||||
assert_eq!(navigator.basename(), Some("/base"));
|
||||
assert_eq!(location.path(), "/base/");
|
||||
},
|
||||
},
|
||||
)
|
||||
.render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
if correct_initial_path {
|
||||
// If the initial path was correct, the router shouldn't have mutated the history.
|
||||
assert_eq!(
|
||||
Reflect::get(
|
||||
&window().history().unwrap().state().unwrap(),
|
||||
&JsString::from("foo")
|
||||
)
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.as_deref(),
|
||||
Some("bar")
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
"/base/",
|
||||
gloo::utils::window().location().pathname().unwrap()
|
||||
);
|
||||
|
||||
assert_eq!("/base/posts", link_href("#with-basename ul > li.posts > a"));
|
||||
assert_eq!(
|
||||
"/base/posts?page=2",
|
||||
@ -151,6 +216,68 @@ async fn link_with_basename() {
|
||||
"/base/search?q=Rust&lang=en_US",
|
||||
link_href("#with-basename ul > li.search-q-lang > a")
|
||||
);
|
||||
|
||||
// Some(a) -> Some(b)
|
||||
handle.update(BasenameProps {
|
||||
basename: Some("/bayes/".to_owned()),
|
||||
assertion: |navigator, location| {
|
||||
RENDERS.fetch_add(1, Ordering::Relaxed);
|
||||
assert_eq!(navigator.basename(), Some("/bayes"));
|
||||
assert_eq!(location.path(), "/bayes/");
|
||||
},
|
||||
});
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
assert_eq!(
|
||||
"/bayes/",
|
||||
gloo::utils::window().location().pathname().unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"/bayes/posts",
|
||||
link_href("#with-basename ul > li.posts > a")
|
||||
);
|
||||
|
||||
// Some -> None
|
||||
handle.update(BasenameProps {
|
||||
basename: None,
|
||||
assertion: |navigator, location| {
|
||||
RENDERS.fetch_add(1, Ordering::Relaxed);
|
||||
assert_eq!(navigator.basename(), None);
|
||||
assert_eq!(location.path(), "/");
|
||||
},
|
||||
});
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
assert_eq!("/", gloo::utils::window().location().pathname().unwrap());
|
||||
|
||||
assert_eq!("/posts", link_href("#with-basename ul > li.posts > a"));
|
||||
|
||||
// None -> Some
|
||||
handle.update(BasenameProps {
|
||||
basename: Some("/bass/".to_string()),
|
||||
assertion: |navigator, location| {
|
||||
RENDERS.fetch_add(1, Ordering::Relaxed);
|
||||
assert_eq!(navigator.basename(), Some("/bass"));
|
||||
assert_eq!(location.path(), "/bass/");
|
||||
},
|
||||
});
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
assert_eq!(
|
||||
"/bass/",
|
||||
gloo::utils::window().location().pathname().unwrap()
|
||||
);
|
||||
|
||||
assert_eq!("/bass/posts", link_href("#with-basename ul > li.posts > a"));
|
||||
|
||||
handle.destroy();
|
||||
|
||||
// 1 initial, 1 rerender after initial, 3 props changes
|
||||
assert_eq!(RENDERS.load(Ordering::Relaxed), 5);
|
||||
}
|
||||
|
||||
#[function_component(RootForHashRouter)]
|
||||
@ -162,12 +289,11 @@ fn root_for_hash_router() -> Html {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
async fn link_in_hash_router() {
|
||||
let div = gloo::utils::document().create_element("div").unwrap();
|
||||
let _ = div.set_attribute("id", "hash-router");
|
||||
let _ = gloo::utils::body().append_child(&div);
|
||||
yew::Renderer::<RootForHashRouter>::with_root(div).render();
|
||||
let handle = yew::Renderer::<RootForHashRouter>::with_root(div).render();
|
||||
|
||||
sleep(Duration::ZERO).await;
|
||||
|
||||
@ -186,4 +312,15 @@ async fn link_in_hash_router() {
|
||||
"#/search?q=Rust&lang=en_US",
|
||||
link_href("#hash-router ul > li.search-q-lang > a")
|
||||
);
|
||||
|
||||
handle.destroy();
|
||||
}
|
||||
|
||||
// These cannot be run in concurrently because they all read/write the URL.
|
||||
#[test]
|
||||
async fn sequential_tests() {
|
||||
link_in_hash_router().await;
|
||||
link_in_browser_router().await;
|
||||
link_with_basename(false).await;
|
||||
link_with_basename(true).await;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user