From 9912c32ffdebd1acfc8f40a4e2a2cb1881cd4475 Mon Sep 17 00:00:00 2001 From: Adrian Wannenmacher Date: Thu, 15 Dec 2022 15:45:19 +0100 Subject: [PATCH] add web hash history integration --- packages/router-core/Cargo.toml | 3 +- packages/router-core/src/history/mod.rs | 5 + packages/router-core/src/history/web.rs | 8 +- packages/router-core/src/history/web_hash.rs | 153 +++++++++++++++++++ 4 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 packages/router-core/src/history/web_hash.rs diff --git a/packages/router-core/Cargo.toml b/packages/router-core/Cargo.toml index 8ef8c4fdb..255a0182a 100644 --- a/packages/router-core/Cargo.toml +++ b/packages/router-core/Cargo.toml @@ -17,9 +17,10 @@ serde = { version = "1.0.147", optional = true } serde_urlencoded = { version = "0.7.1", optional = true } url = "2.3.1" urlencoding = "2.1.2" +wasm-bindgen = { version = "0.2.83", optional = true } web-sys = { version = "0.3.60", optional = true } [features] regex = ["dep:regex"] serde = ["dep:serde", "serde_urlencoded"] -web = ["gloo", "web-sys"] +web = ["gloo", "wasm-bindgen", "web-sys"] diff --git a/packages/router-core/src/history/mod.rs b/packages/router-core/src/history/mod.rs index 01768579c..9a18a1337 100644 --- a/packages/router-core/src/history/mod.rs +++ b/packages/router-core/src/history/mod.rs @@ -8,6 +8,11 @@ use std::sync::Arc; +#[cfg(feature = "web")] +mod web_hash; +#[cfg(feature = "web")] +pub use web_hash::*; + mod memory; pub use memory::*; diff --git a/packages/router-core/src/history/web.rs b/packages/router-core/src/history/web.rs index ab90fa1cc..13f293928 100644 --- a/packages/router-core/src/history/web.rs +++ b/packages/router-core/src/history/web.rs @@ -31,9 +31,9 @@ pub struct WebHistory { impl WebHistory { /// Create a new [`WebHistory`] with a prefix. #[must_use] - pub fn with_prefix(prefix: String) -> Self { + pub fn with_prefix(prefix: impl Into) -> Self { Self { - prefix: Some(prefix), + prefix: Some(prefix.into()), ..Default::default() } } @@ -105,8 +105,8 @@ impl HistoryProvider for WebHistory { fn external(&mut self, url: String) -> bool { match self.window.location().set_href(&url) { Ok(_) => true, - Err(_) => { - error!("`WebHistory` failed to navigate to external target: {url}"); + Err(e) => { + error!("failed to navigate to external url (`{url}): {e:?}"); false } } diff --git a/packages/router-core/src/history/web_hash.rs b/packages/router-core/src/history/web_hash.rs new file mode 100644 index 000000000..b80479ec2 --- /dev/null +++ b/packages/router-core/src/history/web_hash.rs @@ -0,0 +1,153 @@ +use gloo::{events::EventListener, utils::window}; +use log::error; +use url::Url; +use wasm_bindgen::JsValue; +use web_sys::{History, Window}; + +use super::HistoryProvider; + +const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/"; + +/// A [`HistoryProvider`] that integrates with a browser via the [History API]. It uses the URLs +/// hash instead of its path. +/// +/// Early web applications used the hash to store the current path because there was no other way +/// for them to interact with the history without triggering a browser navigation, as the +/// [History API] did not yet exist. While this implementation could have been written that way, it +/// was not, because no browser supports WebAssembly without the [History API]. +/// +/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API +pub struct WebHashHistory { + history: History, + window: Window, +} + +impl Default for WebHashHistory { + fn default() -> Self { + let window = window(); + + Self { + history: window.history().expect("window has history"), + window, + } + } +} + +impl WebHashHistory { + fn join_url_to_hash(&self, path: String) -> Option { + if path.starts_with("//") { + error!("cannot navigate to paths starting with `//`, got `{path}`"); + return None; + } + + let url = match self.url() { + Some(c) => match c.join(&path) { + Ok(new) => new, + Err(e) => { + error!("failed to join location with target: {e}"); + return None; + } + }, + None => { + error!("current location unknown"); + return None; + } + }; + + Some(format!( + "#{path}{query}", + path = url.path(), + query = url.query().map(|q| format!("?{q}")).unwrap_or_default() + )) + } + + fn url(&self) -> Option { + let mut path = self.window.location().hash().ok()?; + + if path.starts_with('#') { + path.remove(0); + } + + if path.starts_with('/') { + path.remove(0); + } + + match Url::parse(&format!("{INITIAL_URL}/{path}")) { + Ok(url) => Some(url), + Err(e) => { + error!("failed to parse hash path: {e}"); + None + } + } + } +} + +impl HistoryProvider for WebHashHistory { + fn current_path(&self) -> String { + self.url() + .map(|url| url.path().to_string()) + .unwrap_or(String::from("/")) + } + + fn current_query(&self) -> Option { + self.url().and_then(|url| url.query().map(String::from)) + } + + fn current_prefix(&self) -> Option { + Some(String::from("#")) + } + + fn go_back(&mut self) { + if let Err(e) = self.history.back() { + error!("failed to go back: {e:?}") + } + } + + fn go_forward(&mut self) { + if let Err(e) = self.history.forward() { + error!("failed to go forward: {e:?}") + } + } + + fn push(&mut self, path: String) { + let hash = match self.join_url_to_hash(path) { + Some(hash) => hash, + None => return, + }; + + if let Err(e) = self + .history + .push_state_with_url(&JsValue::NULL, "", Some(&hash)) + { + error!("failed to push state: {e:?}"); + } + } + + fn replace(&mut self, path: String) { + let hash = match self.join_url_to_hash(path) { + Some(hash) => hash, + None => return, + }; + + if let Err(e) = self + .history + .replace_state_with_url(&JsValue::NULL, "", Some(&hash)) + { + error!("failed to replace state: {e:?}"); + } + } + + fn external(&mut self, url: String) -> bool { + match self.window.location().set_href(&url) { + Ok(_) => true, + Err(e) => { + error!("failed to navigate to external url (`{url}): {e:?}"); + false + } + } + } + + fn updater(&mut self, callback: std::sync::Arc) { + let listener = EventListener::new(&self.window, "popstate", move |_| (*callback)()); + } +}