Intigrate macro with router hooks and components

This commit is contained in:
Evan Almloff committed 2023-05-22 16:42:32 -05:00
1 parent ef6551a6cd
commit e4b8fbfafe
37 files changed
+1442 -3009

No files matched your search

+230
View File
@@ -0,0 +1,230 @@
use std::str::FromStr;
use crate::routable::Routable;
use super::HistoryProvider;
/// A [`HistoryProvider`] that stores all navigation information in memory.
pub struct MemoryHistory<R: Routable> {
current: Option<R>,
history: Vec<R>,
future: Vec<R>,
}
impl<R: Routable> MemoryHistory<R> {
/// Create a [`MemoryHistory`] starting at `path`.
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::with_initial_path("/some/path").unwrap();
/// assert_eq!(history.current_path(), "/some/path");
/// assert_eq!(history.can_go_back(), false);
/// ```
pub fn with_initial_path(path: impl Into<String>) -> Result<Self, <R as FromStr>::Err> {
let path = path.into();
Ok(Self {
current: Some(R::from_str(&path)?),
..Default::default()
})
}
}
impl<R: Routable> Default for MemoryHistory<R> {
fn default() -> Self {
Self {
current: None,
history: Vec::new(),
future: Vec::new(),
}
}
}
impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
fn current_route(&self) -> &R {
self.current.as_ref().expect("current route is not set")
}
fn can_go_back(&self) -> bool {
!self.history.is_empty()
}
fn go_back(&mut self) {
if let Some(last) = self.history.pop() {
let new = self.current.replace(last);
if let Some(new) = new {
self.future.push(new);
}
}
}
fn can_go_forward(&self) -> bool {
!self.future.is_empty()
}
fn go_forward(&mut self) {
if let Some(next) = self.future.pop() {
let old = self.current.replace(next);
if let Some(old) = old {
self.history.push(old);
}
}
}
fn push(&mut self, new: R) {
if let Some(current) = self.current.take() {
self.history.push(current);
}
self.current = Some(new);
self.future.clear();
}
fn replace(&mut self, path: R) {
self.current = Some(path);
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// #[test]
// fn default() {
// let mem = MemoryHistory::default();
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// assert_eq!(mem.history, Vec::<String>::new());
// assert_eq!(mem.future, Vec::<String>::new());
// }
// #[test]
// fn with_initial_path() {
// let mem = MemoryHistory::with_initial_path("something").unwrap();
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}something")).unwrap()
// );
// assert_eq!(mem.history, Vec::<String>::new());
// assert_eq!(mem.future, Vec::<String>::new());
// }
// #[test]
// fn with_initial_path_with_leading_slash() {
// let mem = MemoryHistory::with_initial_path("/something").unwrap();
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}something")).unwrap()
// );
// assert_eq!(mem.history, Vec::<String>::new());
// assert_eq!(mem.future, Vec::<String>::new());
// }
// #[test]
// fn can_go_back() {
// let mut mem = MemoryHistory::default();
// assert!(!mem.can_go_back());
// mem.push(String::from("/test"));
// assert!(mem.can_go_back());
// }
// #[test]
// fn go_back() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// mem.go_back();
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// assert!(mem.history.is_empty());
// assert_eq!(mem.future, vec![format!("{INITIAL_URL}test")]);
// }
// #[test]
// fn can_go_forward() {
// let mut mem = MemoryHistory::default();
// assert!(!mem.can_go_forward());
// mem.push(String::from("/test"));
// mem.go_back();
// assert!(mem.can_go_forward());
// }
// #[test]
// fn go_forward() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// mem.go_back();
// mem.go_forward();
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}test")).unwrap()
// );
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
// assert!(mem.future.is_empty());
// }
// #[test]
// fn push() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}test")).unwrap()
// );
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
// assert!(mem.future.is_empty());
// }
// #[test]
// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
// #[cfg(debug_assertions)]
// fn push_debug() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("//test"));
// }
// #[test]
// #[cfg(not(debug_assertions))]
// fn push_release() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("//test"));
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// assert!(mem.history.is_empty())
// }
// #[test]
// fn replace() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// mem.push(String::from("/other"));
// mem.go_back();
// mem.replace(String::from("/replace"));
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}replace")).unwrap()
// );
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
// assert_eq!(mem.future, vec![format!("{INITIAL_URL}other")]);
// }
// #[test]
// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
// #[cfg(debug_assertions)]
// fn replace_debug() {
// let mut mem = MemoryHistory::default();
// mem.replace(String::from("//test"));
// }
// #[test]
// #[cfg(not(debug_assertions))]
// fn replace_release() {
// let mut mem = MemoryHistory::default();
// mem.replace(String::from("//test"));
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// }
// }
+193
View File
@@ -0,0 +1,193 @@
//! History Integration
//!
//! dioxus-router-core relies on so-called [`HistoryProvider`]s to store the current URL, and possibly a
//! history (i.e. a browsers back button) and future (i.e. a browsers forward button).
//!
//! To integrate dioxus-router-core with a any type of history, all you have to do is implement the
//! [`HistoryProvider`] trait. dioxus-router-core also comes with some (for now one) default implementations.
use std::sync::Arc;
mod memory;
pub use memory::*;
#[cfg(feature = "web")]
mod web;
#[cfg(feature = "web")]
pub use web::*;
#[cfg(feature = "web")]
mod web_hash;
#[cfg(feature = "web")]
pub use web_hash::*;
use crate::routable::Routable;
#[cfg(feature = "web")]
pub(crate) mod web_scroll;
/// An integration with some kind of navigation history.
///
/// Depending on your use case, your implementation may deviate from the described procedure. This
/// is fine, as long as both `current_path` and `current_query` match the described format.
///
/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
/// The described behaviors are designed to mimic a web browser, which most users should already
/// know. Deviations might confuse them.
pub trait HistoryProvider<R: Routable> {
/// Get the path of the current URL.
///
/// **Must start** with `/`. **Must _not_ contain** the prefix.
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.current_path(), "/");
///
/// history.push(String::from("/path"));
/// assert_eq!(history.current_path(), "/path");
/// ```
#[must_use]
fn current_route(&self) -> &R;
/// Get the current path prefix of the URL.
///
/// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a
/// dioxus-router-core-routed application is not running on `/`. The [`HistoryProvider`] is responsible
/// for removing the prefix from the dioxus-router-core-internal path, and also for adding it back in
/// during navigation. This functions value is only used for creating `href`s (e.g. for SSR or
/// display (but not navigation) in a web app).
fn current_prefix(&self) -> Option<String> {
None
}
/// Check whether there is a previous page to navigate back to.
///
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.can_go_back(), false);
///
/// history.push(String::from("/some-other-page"));
/// assert_eq!(history.can_go_back(), true);
/// ```
#[must_use]
fn can_go_back(&self) -> bool {
true
}
/// Go back to a previous page.
///
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
/// might be called, even if `can_go_back` returns [`false`].
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.current_path(), "/");
///
/// history.go_back();
/// assert_eq!(history.current_path(), "/");
///
/// history.push(String::from("/some-other-page"));
/// assert_eq!(history.current_path(), "/some-other-page");
///
/// history.go_back();
/// assert_eq!(history.current_path(), "/");
/// ```
fn go_back(&mut self);
/// Check whether there is a future page to navigate forward to.
///
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.can_go_forward(), false);
///
/// history.push(String::from("/some-other-page"));
/// assert_eq!(history.can_go_forward(), false);
///
/// history.go_back();
/// assert_eq!(history.can_go_forward(), true);
/// ```
#[must_use]
fn can_go_forward(&self) -> bool {
true
}
/// Go forward to a future page.
///
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
/// might be called, even if `can_go_forward` returns [`false`].
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// history.push(String::from("/some-other-page"));
/// assert_eq!(history.current_path(), "/some-other-page");
///
/// history.go_back();
/// assert_eq!(history.current_path(), "/");
///
/// history.go_forward();
/// assert_eq!(history.current_path(), "/some-other-page");
/// ```
fn go_forward(&mut self);
/// Go to another page.
///
/// This should do three things:
/// 1. Merge the current URL with the `path` parameter (which may also include a query part).
/// 2. Remove the previous URL to the navigation history.
/// 3. Clear the navigation future.
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.current_path(), "/");
///
/// history.push(String::from("/some-other-page"));
/// assert_eq!(history.current_path(), "/some-other-page");
/// assert!(history.can_go_back());
/// ```
fn push(&mut self, route: R);
/// Replace the current page with another one.
///
/// This should merge the current URL with the `path` parameter (which may also include a query
/// part). In contrast to the `push` function, the navigation history and future should stay
/// untouched.
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.current_path(), "/");
///
/// history.replace(String::from("/some-other-page"));
/// assert_eq!(history.current_path(), "/some-other-page");
/// assert!(!history.can_go_back());
/// ```
fn replace(&mut self, path: R);
/// Navigate to an external URL.
///
/// This should navigate to an external URL, which isn't controlled by the router. If a
/// [`HistoryProvider`] cannot do that, it should return [`false`], otherwise [`true`].
///
/// Returning [`false`] will cause the router to handle the external navigation failure.
#[allow(unused_variables)]
fn external(&mut self, url: String) -> bool {
false
}
/// Provide the [`HistoryProvider`] with an update callback.
///
/// Some [`HistoryProvider`]s may receive URL updates from outside the router. When such
/// updates are received, they should call `callback`, which will cause the router to update.
#[allow(unused_variables)]
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {}
}
+192
View File
@@ -0,0 +1,192 @@
use std::sync::{Arc, Mutex};
use gloo::{events::EventListener, render::AnimationFrame};
use log::error;
use web_sys::{window, History, ScrollRestoration, Window};
use super::{
web_scroll::{top_left, update_history, update_scroll},
HistoryProvider,
};
/// A [`HistoryProvider`] that integrates with a browser via the [History API].
///
/// # Prefix
/// This [`HistoryProvider`] supports a prefix, which can be used for web apps that aren't located
/// at the root of their domain.
///
/// Application developers are responsible for ensuring that right after the prefix comes a `/`. If
/// that is not the case, this [`HistoryProvider`] will replace the first character after the prefix
/// with one.
///
/// Application developers are responsible for not rendering the router if the prefix is not present
/// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added.
///
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
pub struct WebHistory<R: Serialize + DeserializeOwned> {
do_scroll_restoration: bool,
history: History,
listener_navigation: Option<EventListener>,
#[allow(dead_code)]
listener_scroll: Option<EventListener>,
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
prefix: Option<String>,
window: Window,
}
impl WebHistory {
/// Create a new [`WebHistory`].
///
/// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
/// state. It'll also set the browsers scroll restoration to `manual`.
pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
let window = window().expect("access to `window`");
let history = window.history().expect("`window` has access to `history`");
let listener_scroll = match do_scroll_restoration {
true => {
history
.set_scroll_restoration(ScrollRestoration::Manual)
.expect("`history` can set scroll restoration");
let w = window.clone();
let h = history.clone();
let document = w.document().expect("`window` has access to `document`");
Some(EventListener::new(&document, "scroll", move |_| {
update_history(&w, &h);
}))
}
false => None,
};
Self {
do_scroll_restoration,
history,
listener_navigation: None,
listener_scroll,
listener_animation_frame: Default::default(),
prefix: prefix,
window,
}
}
}
impl HistoryProvider for WebHistory {
fn current_route(&self) -> String {
let path = self
.window
.location()
.pathname()
.unwrap_or_else(|_| String::from("/"));
match &self.prefix {
None => path.to_string(),
Some(prefix) => path
.starts_with(prefix)
.then(|| path.split_at(prefix.len()).1)
.unwrap_or("/")
.to_string(),
}
}
fn current_query(&self) -> Option<String> {
self.window
.location()
.search()
.ok()
.map(|mut q| {
if q.starts_with('?') {
q.remove(0);
}
q
})
.and_then(|q| q.is_empty().then_some(q))
}
fn current_prefix(&self) -> Option<String> {
self.prefix.clone()
}
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 path = match &self.prefix {
None => path,
Some(prefix) => format!("{prefix}{path}"),
};
let state = match self.do_scroll_restoration {
true => top_left(),
false => self.history.state().unwrap_or_default(),
};
let nav = self.history.push_state_with_url(&state, "", Some(&path));
match nav {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => error!("failed to push state: {e:?}"),
}
}
fn replace(&mut self, path: String) {
let path = match &self.prefix {
None => path,
Some(prefix) => format!("{prefix}{path}"),
};
let state = match self.do_scroll_restoration {
true => top_left(),
false => self.history.state().unwrap_or_default(),
};
let nav = self.history.replace_state_with_url(&state, "", Some(&path));
match nav {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => 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<dyn Fn() + Send + Sync>) {
let w = self.window.clone();
let h = self.history.clone();
let s = self.listener_animation_frame.clone();
let d = self.do_scroll_restoration;
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
(*callback)();
if d {
let mut s = s.lock().expect("unpoisoned scroll mutex");
*s = Some(update_scroll(&w, &h));
}
}));
}
}
+220
View File
@@ -0,0 +1,220 @@
use std::sync::{Arc, Mutex};
use gloo::{events::EventListener, render::AnimationFrame, utils::window};
use log::error;
use url::Url;
use web_sys::{History, ScrollRestoration, Window};
use super::{
web_scroll::{top_left, update_history, update_scroll},
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<R: Serialize + DeserializeOwned> {
do_scroll_restoration: bool,
history: History,
listener_navigation: Option<EventListener>,
#[allow(dead_code)]
listener_scroll: Option<EventListener>,
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
window: Window,
}
impl WebHashHistory {
/// Create a new [`WebHashHistory`].
///
/// If `do_scroll_restoration` is [`true`], [`WebHashHistory`] will take control of the history
/// state. It'll also set the browsers scroll restoration to `manual`.
pub fn new(do_scroll_restoration: bool) -> Self {
let window = window();
let history = window.history().expect("`window` has access to `history`");
history
.set_scroll_restoration(ScrollRestoration::Manual)
.expect("`history` can set scroll restoration");
let listener_scroll = match do_scroll_restoration {
true => {
history
.set_scroll_restoration(ScrollRestoration::Manual)
.expect("`history` can set scroll restoration");
let w = window.clone();
let h = history.clone();
let document = w.document().expect("`window` has access to `document`");
Some(EventListener::new(&document, "scroll", move |_| {
update_history(&w, &h);
}))
}
false => None,
};
Self {
do_scroll_restoration,
history,
listener_navigation: None,
listener_scroll,
listener_animation_frame: Default::default(),
window,
}
}
}
impl WebHashHistory {
fn join_url_to_hash(&self, path: String) -> Option<String> {
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<Url> {
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_route(&self) -> String {
self.url()
.map(|url| url.path().to_string())
.unwrap_or(String::from("/"))
}
fn current_query(&self) -> Option<String> {
self.url().and_then(|url| url.query().map(String::from))
}
fn current_prefix(&self) -> Option<String> {
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,
};
let state = match self.do_scroll_restoration {
true => top_left(),
false => self.history.state().unwrap_or_default(),
};
let nav = self.history.push_state_with_url(&state, "", Some(&hash));
match nav {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => 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,
};
let state = match self.do_scroll_restoration {
true => top_left(),
false => self.history.state().unwrap_or_default(),
};
let nav = self.history.replace_state_with_url(&state, "", Some(&hash));
match nav {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => 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<dyn Fn() + Send + Sync>) {
let w = self.window.clone();
let h = self.history.clone();
let s = self.listener_animation_frame.clone();
let d = self.do_scroll_restoration;
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
(*callback)();
if d {
let mut s = s.lock().expect("unpoisoned scroll mutex");
*s = Some(update_scroll(&w, &h));
}
}));
}
}
+37
View File
@@ -0,0 +1,37 @@
use gloo::render::{request_animation_frame, AnimationFrame};
use log::error;
use serde::{Deserialize, Serialize};
use wasm_bindgen::JsValue;
use web_sys::{History, Window};
#[derive(Debug, Default, Deserialize, Serialize)]
pub(crate) struct ScrollPosition {
x: f64,
y: f64,
}
pub(crate) fn top_left() -> JsValue {
serde_wasm_bindgen::to_value(&ScrollPosition::default()).unwrap()
}
pub(crate) fn update_history(window: &Window, history: &History) {
let position = serde_wasm_bindgen::to_value(&ScrollPosition {
x: window.scroll_x().unwrap_or_default(),
y: window.scroll_y().unwrap_or_default(),
})
.unwrap();
if let Err(e) = history.replace_state(&position, "") {
error!("failed to update scroll position: {e:?}");
}
}
pub(crate) fn update_scroll(window: &Window, history: &History) -> AnimationFrame {
let ScrollPosition { x, y } = history
.state()
.map(|state| serde_wasm_bindgen::from_value(state).unwrap_or_default())
.unwrap_or_default();
let w = window.clone();
request_animation_frame(move |_| w.scroll_to_with_x_and_y(x, y))
}