Skip to content

Commit

Permalink
yew-router: Dynamic basename. (#3725)
Browse files Browse the repository at this point in the history
* yew-router: Dynamic basename.

* Revisions.

* Test location.path and navigator.basename match expectations at each step.

* Better coverage of edge case.
  • Loading branch information
finnbear authored Oct 12, 2024
1 parent 7be9d17 commit 3c7e3e3
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 17 deletions.
2 changes: 1 addition & 1 deletion packages/yew-router/src/navigator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"))
Expand Down
39 changes: 34 additions & 5 deletions packages/yew-router/src/router.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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();

Expand Down
159 changes: 148 additions & 11 deletions packages/yew-router/tests/link.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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;

Expand All @@ -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",
Expand All @@ -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)]
Expand All @@ -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;

Expand All @@ -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;
}

1 comment on commit 3c7e3e3

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yew master branch benchmarks (Lower is better)

Benchmark suite Current: 3c7e3e3 Previous: 7be9d17 Ratio
yew-hooks-v0.21.0-keyed 01_run1k 191.2 193.9 0.99
yew-hooks-v0.21.0-keyed 02_replace1k 218.6 222.1 0.98
yew-hooks-v0.21.0-keyed 03_update10th1k_x16 93.4 90.6 1.03
yew-hooks-v0.21.0-keyed 04_select1k 41.2 40 1.03
yew-hooks-v0.21.0-keyed 05_swap1k 105.5 105 1.00
yew-hooks-v0.21.0-keyed 06_remove-one-1k 79.7 76 1.05
yew-hooks-v0.21.0-keyed 07_create10k 2211.5 2179 1.01
yew-hooks-v0.21.0-keyed 08_create1k-after1k_x2 221.6 212.8 1.04
yew-hooks-v0.21.0-keyed 09_clear1k_x8 89.8 84.2 1.07
yew-hooks-v0.21.0-keyed 21_ready-memory 2.123171806335449 2.1212377548217773 1.00
yew-hooks-v0.21.0-keyed 22_run-memory 6.310580253601074 6.310763359069824 1.00
yew-hooks-v0.21.0-keyed 23_update5-memory 6.573078155517578 6.573081970214844 1.00
yew-hooks-v0.21.0-keyed 25_run-clear-memory 4.763222694396973 5.130159378051758 0.93
yew-hooks-v0.21.0-keyed 26_run-10k-memory 42.47025775909424 42.794925689697266 0.99
yew-hooks-v0.21.0-keyed 41_size-uncompressed 168 168 1
yew-hooks-v0.21.0-keyed 42_size-compressed 54.6 54.6 1
yew-hooks-v0.21.0-keyed 43_first-paint 444 435.8 1.02
yew-v0.21.0-keyed 01_run1k 192.2 195.1 0.99
yew-v0.21.0-keyed 02_replace1k 214.4 220.9 0.97
yew-v0.21.0-keyed 03_update10th1k_x16 71.5 75.3 0.95
yew-v0.21.0-keyed 04_select1k 16 16.9 0.95
yew-v0.21.0-keyed 05_swap1k 75.5 81.1 0.93
yew-v0.21.0-keyed 06_remove-one-1k 66.1 69.1 0.96
yew-v0.21.0-keyed 07_create10k 2235.9 2195.5 1.02
yew-v0.21.0-keyed 08_create1k-after1k_x2 212.3 213.6 0.99
yew-v0.21.0-keyed 09_clear1k_x8 86.2 87.5 0.99
yew-v0.21.0-keyed 21_ready-memory 2.1482934951782227 2.1193323135375977 1.01
yew-v0.21.0-keyed 22_run-memory 6.247132301330566 6.247616767883301 1.00
yew-v0.21.0-keyed 23_update5-memory 6.338102340698242 6.33714485168457 1.00
yew-v0.21.0-keyed 25_run-clear-memory 4.925570487976074 4.971579551696777 0.99
yew-v0.21.0-keyed 26_run-10k-memory 41.498477935791016 41.572007179260254 1.00
yew-v0.21.0-keyed 41_size-uncompressed 166 166 1
yew-v0.21.0-keyed 42_size-compressed 54.4 54.4 1
yew-v0.21.0-keyed 43_first-paint 442.6 449.8 0.98

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.