Intigrate macro with router hooks and components
This commit is contained in:
1 parent
ef6551a6cd
commit
e4b8fbfafe
37 files changed
+1442
-3009
No files matched your search
@@ -3,7 +3,6 @@ members = [
|
|||||||
"packages/dioxus",
|
"packages/dioxus",
|
||||||
"packages/core",
|
"packages/core",
|
||||||
"packages/core-macro",
|
"packages/core-macro",
|
||||||
"packages/router-core",
|
|
||||||
"packages/router-macro",
|
"packages/router-macro",
|
||||||
"packages/router",
|
"packages/router",
|
||||||
"packages/html",
|
"packages/html",
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "dioxus-router-core"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
async-lock = "2.7.0"
|
|
||||||
either = "1.8.0"
|
|
||||||
futures-channel = "0.3.25"
|
|
||||||
futures-util = "0.3.25"
|
|
||||||
gloo = { version = "0.8.0", optional = true }
|
|
||||||
log = "0.4.17"
|
|
||||||
regex = { version = "1.6.0", optional = true }
|
|
||||||
serde = { version = "1.0.147", optional = true, features = ["derive"] }
|
|
||||||
serde-wasm-bindgen = { version = "0.4.5", 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 = ["ScrollRestoration"]}
|
|
||||||
dioxus = { path = "../dioxus" }
|
|
||||||
dioxus-router-macro = { path = "../router-macro" }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
regex = ["dep:regex"]
|
|
||||||
serde = ["dep:serde", "serde_urlencoded"]
|
|
||||||
web = ["gloo", "dep:serde", "serde-wasm-bindgen", "wasm-bindgen", "web-sys"]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# Dioxus Router Core
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use url::{ParseError, Url};
|
|
||||||
|
|
||||||
use super::HistoryProvider;
|
|
||||||
|
|
||||||
const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
|
|
||||||
|
|
||||||
/// A [`HistoryProvider`] that stores all navigation information in memory.
|
|
||||||
pub struct MemoryHistory {
|
|
||||||
current: Url,
|
|
||||||
history: Vec<String>,
|
|
||||||
future: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MemoryHistory {
|
|
||||||
/// 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, ParseError> {
|
|
||||||
let mut path = path.into();
|
|
||||||
if path.starts_with('/') {
|
|
||||||
path.remove(0);
|
|
||||||
}
|
|
||||||
let url = Url::parse(&format!("{INITIAL_URL}{path}"))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
current: url,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MemoryHistory {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
current: Url::from_str(INITIAL_URL).unwrap(),
|
|
||||||
history: Vec::new(),
|
|
||||||
future: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HistoryProvider for MemoryHistory {
|
|
||||||
fn current_path(&self) -> String {
|
|
||||||
self.current.path().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_query(&self) -> Option<String> {
|
|
||||||
self.current.query().map(|q| q.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_go_back(&self) -> bool {
|
|
||||||
!self.history.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn go_back(&mut self) {
|
|
||||||
if let Some(last) = self.history.pop() {
|
|
||||||
self.future.push(self.current.to_string());
|
|
||||||
self.current = Url::parse(&last).unwrap(/* past URLs are always valid */);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_go_forward(&self) -> bool {
|
|
||||||
!self.future.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn go_forward(&mut self) {
|
|
||||||
if let Some(next) = self.future.pop() {
|
|
||||||
self.history.push(self.current.to_string());
|
|
||||||
self.current = Url::parse(&next).unwrap(/* future URLs are always valid */);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push(&mut self, path: String) {
|
|
||||||
let wrong = path.starts_with("//");
|
|
||||||
debug_assert!(
|
|
||||||
!wrong,
|
|
||||||
"cannot navigate to paths starting with \"//\": {path}"
|
|
||||||
);
|
|
||||||
if wrong {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(new) = self.current.join(&path) {
|
|
||||||
self.history.push(self.current.to_string());
|
|
||||||
self.current = new;
|
|
||||||
self.future.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace(&mut self, path: String) {
|
|
||||||
let wrong = path.starts_with("//");
|
|
||||||
debug_assert!(
|
|
||||||
!wrong,
|
|
||||||
"cannot navigate to paths starting with \"//\": {path}"
|
|
||||||
);
|
|
||||||
if wrong {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(new) = self.current.join(&path) {
|
|
||||||
self.current = new;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
|
||||||
// #![forbid(missing_docs)]
|
|
||||||
|
|
||||||
pub mod history;
|
|
||||||
|
|
||||||
pub mod router;
|
|
||||||
pub use router::*;
|
|
||||||
|
|
||||||
// pub mod navigation;
|
|
||||||
|
|
||||||
// mod navigator;
|
|
||||||
// pub use navigator::*;
|
|
||||||
|
|
||||||
// mod service;
|
|
||||||
// pub use service::*;
|
|
||||||
|
|
||||||
// mod state;
|
|
||||||
// pub use state::*;
|
|
||||||
|
|
||||||
// mod utils {
|
|
||||||
// mod sitemap;
|
|
||||||
// pub use sitemap::*;
|
|
||||||
// }
|
|
||||||
|
|
||||||
/// A collection of useful types most applications might need.
|
|
||||||
pub mod prelude {
|
|
||||||
// pub use crate::navigation::*;
|
|
||||||
|
|
||||||
/// An external navigation failure.
|
|
||||||
///
|
|
||||||
/// These occur when the router tries to navigate to a [`NavigationTarget::External`] and the
|
|
||||||
/// [`HistoryProvider`](crate::history::HistoryProvider) doesn't support that.
|
|
||||||
pub struct FailureExternalNavigation;
|
|
||||||
|
|
||||||
/// A named navigation failure.
|
|
||||||
///
|
|
||||||
/// These occur when the router tries to navigate to a [`NavigationTarget::Named`] and the
|
|
||||||
/// specified [`Name`](crate::Name) is unknown, or a parameter is missing.
|
|
||||||
pub struct FailureNamedNavigation;
|
|
||||||
|
|
||||||
/// A redirection limit breach.
|
|
||||||
///
|
|
||||||
/// These occur when the router tries to navigate to any target, but encounters 25 consecutive
|
|
||||||
/// redirects.
|
|
||||||
pub struct FailureRedirectionLimit;
|
|
||||||
|
|
||||||
/// The [`Name`](crate::Name) equivalent of `/`.
|
|
||||||
pub struct RootIndex;
|
|
||||||
}
|
|
||||||
@@ -1,448 +0,0 @@
|
|||||||
//! Types pertaining to navigation.
|
|
||||||
|
|
||||||
use std::{collections::HashMap, str::FromStr};
|
|
||||||
|
|
||||||
use url::{ParseError, Url};
|
|
||||||
|
|
||||||
/// A target for the router to navigate to.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum NavigationTarget<R: Routable> {
|
|
||||||
/// An internal path that the router can navigate to by itself.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
|
||||||
/// let explicit = NavigationTarget::Internal(String::from("/internal"));
|
|
||||||
/// let implicit: NavigationTarget = "/internal".into();
|
|
||||||
/// assert_eq!(explicit, implicit);
|
|
||||||
/// ```
|
|
||||||
Internal(R),
|
|
||||||
/// An external target that the router doesn't control.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
|
||||||
/// let explicit = NavigationTarget::External(String::from("https://dioxuslabs.com/"));
|
|
||||||
/// let implicit: NavigationTarget = "https://dioxuslabs.com/".into();
|
|
||||||
/// assert_eq!(explicit, implicit);
|
|
||||||
/// ```
|
|
||||||
External(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NavigationTarget {
|
|
||||||
/// Create a new [`NavigationTarget::Named`] with `N` as the name.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
|
||||||
/// let target = NavigationTarget::named::<bool>();
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
|
|
||||||
/// for brevity. It is recommend to use types with descriptive names, and create unit structs if
|
|
||||||
/// needed.
|
|
||||||
pub fn named<N: 'static>() -> Self {
|
|
||||||
Self::Named {
|
|
||||||
name: Name::of::<N>(),
|
|
||||||
parameters: HashMap::new(),
|
|
||||||
query: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a parameter to a [`NavigationTarget::Named`].
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
|
||||||
/// let target = NavigationTarget::named::<bool>().parameter::<bool>("some parameter");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
|
|
||||||
/// for brevity. It is recommend to use types with descriptive names, and create unit structs if
|
|
||||||
/// needed.
|
|
||||||
///
|
|
||||||
/// # Error Handling
|
|
||||||
/// 1. An error occurs if `self` is any other [`NavigationTarget`] variant than
|
|
||||||
/// [`NavigationTarget::Named`]. In _debug mode_ this will trigger a panic. In _release mode_
|
|
||||||
/// nothing will happen.
|
|
||||||
/// 2. Parameters need to be unique within the [`NavigationTarget`]. In _debug mode_ the
|
|
||||||
/// second call with a duplicate name will panic. In _release mode_, all calls after the
|
|
||||||
/// first will be ignored.
|
|
||||||
pub fn parameter<N: 'static>(mut self, value: impl Into<String>) -> Self {
|
|
||||||
let n = Name::of::<N>();
|
|
||||||
|
|
||||||
if let Self::Named {
|
|
||||||
name,
|
|
||||||
parameters,
|
|
||||||
query: _,
|
|
||||||
} = &mut self
|
|
||||||
{
|
|
||||||
debug_assert!(
|
|
||||||
!parameters.contains_key(&n),
|
|
||||||
"duplicate parameter: {name} - {n}",
|
|
||||||
);
|
|
||||||
parameters.entry(n).or_insert_with(|| value.into());
|
|
||||||
} else {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
panic!("parameter only available for named target: {n}");
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a parameter to a [`NavigationTarget::Named`].
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
|
||||||
/// let target = NavigationTarget::named::<bool>().query("some=query");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Error Handling
|
|
||||||
/// 1. An error occurs if `self` is any other [`NavigationTarget`] variant than
|
|
||||||
/// [`NavigationTarget::Named`]. In _debug mode_ this will trigger a panic. In _release mode_
|
|
||||||
/// nothing will happen.
|
|
||||||
/// 2. This function may only be called once per [`NavigationTarget`]. In _debug mode_, the
|
|
||||||
/// second call will panic. In _release mode_, all calls after the first will be ignored.
|
|
||||||
pub fn query(mut self, query: impl Into<Query>) -> Self {
|
|
||||||
if let Self::Named {
|
|
||||||
name,
|
|
||||||
parameters: _,
|
|
||||||
query: q,
|
|
||||||
} = &mut self
|
|
||||||
{
|
|
||||||
debug_assert!(q.is_none(), "query cannot be changed: {name}",);
|
|
||||||
q.get_or_insert(query.into());
|
|
||||||
} else {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
panic!("query only available for named target",);
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new [`NavigationTarget::Named`] with `N` as the name.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus_router_core::navigation::named;
|
|
||||||
/// let target = named::<bool>();
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
|
|
||||||
/// for brevity. It is recommend to use types with descriptive names, and create unit structs if
|
|
||||||
/// needed.
|
|
||||||
///
|
|
||||||
/// This is a shortcut for [`NavigationTarget`]s `named` function.
|
|
||||||
pub fn named<T: 'static>() -> NavigationTarget {
|
|
||||||
NavigationTarget::named::<T>()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for NavigationTarget {
|
|
||||||
type Err = ParseError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
match Url::parse(s) {
|
|
||||||
Ok(_) => Ok(Self::External(s.to_string())),
|
|
||||||
Err(ParseError::RelativeUrlWithoutBase) => Ok(Self::Internal(s.to_string())),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Into<String>> From<T> for NavigationTarget {
|
|
||||||
fn from(v: T) -> Self {
|
|
||||||
let v = v.into();
|
|
||||||
v.clone().parse().unwrap_or(Self::Internal(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A representation of a query string.
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Query {
|
|
||||||
/// A basic query string.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus_router_core::navigation::Query;
|
|
||||||
/// let explicit = Query::Single(String::from("some=query"));
|
|
||||||
/// let implicit: Query = "some=query".into();
|
|
||||||
/// assert_eq!(explicit, implicit);
|
|
||||||
/// ```
|
|
||||||
Single(String),
|
|
||||||
/// A query separated into key-value-pairs.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus_router_core::navigation::Query;
|
|
||||||
/// let explicit = Query::List(vec![(String::from("some"), String::from("query"))]);
|
|
||||||
/// let implicit: Query = vec![("some", "query")].into();
|
|
||||||
/// assert_eq!(explicit, implicit);
|
|
||||||
/// ```
|
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
List(Vec<(String, String)>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Query {
|
|
||||||
/// Create a [`Query`] from a [`Serialize`](serde::Serialize)able object.
|
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
pub fn from_serde(query: impl serde::Serialize) -> Result<Self, serde_urlencoded::ser::Error> {
|
|
||||||
serde_urlencoded::to_string(query).map(|q| Self::Single(q))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for Query {
|
|
||||||
fn from(v: String) -> Self {
|
|
||||||
Self::Single(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for Query {
|
|
||||||
fn from(v: &str) -> Self {
|
|
||||||
v.to_string().into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
impl<T: Into<String>> From<Vec<(T, T)>> for Query {
|
|
||||||
fn from(v: Vec<(T, T)>) -> Self {
|
|
||||||
Self::List(v.into_iter().map(|(a, b)| (a.into(), b.into())).collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn named_enum() {
|
|
||||||
assert_eq!(
|
|
||||||
NavigationTarget::named::<bool>(),
|
|
||||||
NavigationTarget::Named {
|
|
||||||
name: Name::of::<bool>(),
|
|
||||||
parameters: HashMap::new(),
|
|
||||||
query: None
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn named_func() {
|
|
||||||
assert_eq!(
|
|
||||||
named::<bool>(),
|
|
||||||
NavigationTarget::Named {
|
|
||||||
name: Name::of::<bool>(),
|
|
||||||
parameters: HashMap::new(),
|
|
||||||
query: None
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parameter() {
|
|
||||||
assert_eq!(
|
|
||||||
named::<bool>()
|
|
||||||
.parameter::<i32>("integer")
|
|
||||||
.parameter::<u32>("unsigned"),
|
|
||||||
NavigationTarget::Named {
|
|
||||||
name: Name::of::<bool>(),
|
|
||||||
parameters: {
|
|
||||||
let mut r = HashMap::new();
|
|
||||||
r.insert(Name::of::<i32>(), "integer".to_string());
|
|
||||||
r.insert(Name::of::<u32>(), "unsigned".to_string());
|
|
||||||
r
|
|
||||||
},
|
|
||||||
query: None
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic = "duplicate parameter: bool - i32"]
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
fn parameter_duplicate_debug() {
|
|
||||||
named::<bool>()
|
|
||||||
.parameter::<i32>("integer")
|
|
||||||
.parameter::<i32>("duplicate");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
fn parameter_duplicate_release() {
|
|
||||||
assert_eq!(
|
|
||||||
named::<bool>()
|
|
||||||
.parameter::<i32>("integer")
|
|
||||||
.parameter::<i32>("duplicate"),
|
|
||||||
NavigationTarget::Named {
|
|
||||||
name: Name::of::<bool>(),
|
|
||||||
parameters: {
|
|
||||||
let mut r = HashMap::new();
|
|
||||||
r.insert(Name::of::<i32>(), "integer".to_string());
|
|
||||||
r
|
|
||||||
},
|
|
||||||
query: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic = "parameter only available for named target: i32"]
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
fn parameter_internal_debug() {
|
|
||||||
NavigationTarget::from("/test").parameter::<i32>("integer");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
fn parameter_internal_release() {
|
|
||||||
assert_eq!(
|
|
||||||
NavigationTarget::from("/test").parameter::<i32>("integer"),
|
|
||||||
NavigationTarget::from("/test")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic = "parameter only available for named target: i32"]
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
fn parameter_external_debug() {
|
|
||||||
NavigationTarget::from("https://dioxuslabs.com/").parameter::<i32>("integer");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
fn parameter_external_release() {
|
|
||||||
assert_eq!(
|
|
||||||
NavigationTarget::from("https://dioxuslabs.com/").parameter::<i32>("integer"),
|
|
||||||
NavigationTarget::from("https://dioxuslabs.com/")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn query() {
|
|
||||||
assert_eq!(
|
|
||||||
named::<bool>().query("test"),
|
|
||||||
NavigationTarget::Named {
|
|
||||||
name: Name::of::<bool>(),
|
|
||||||
parameters: HashMap::new(),
|
|
||||||
query: Some(Query::Single("test".to_string()))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic = "query cannot be changed: bool"]
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
fn query_multiple_debug() {
|
|
||||||
named::<bool>().query("test").query("other");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
fn query_multiple_release() {
|
|
||||||
assert_eq!(
|
|
||||||
named::<bool>().query("test").query("other"),
|
|
||||||
NavigationTarget::Named {
|
|
||||||
name: Name::of::<bool>(),
|
|
||||||
parameters: HashMap::new(),
|
|
||||||
query: Some(Query::Single("test".to_string()))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic = "query only available for named target"]
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
fn query_internal_debug() {
|
|
||||||
NavigationTarget::from("/test").query("test");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
fn query_internal_release() {
|
|
||||||
assert_eq!(
|
|
||||||
NavigationTarget::from("/test").query("test"),
|
|
||||||
NavigationTarget::from("/test")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic = "query only available for named target"]
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
fn query_external_debug() {
|
|
||||||
NavigationTarget::from("https://dioxuslabs.com/").query("test");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
fn query_external_release() {
|
|
||||||
assert_eq!(
|
|
||||||
NavigationTarget::from("https://dioxuslabs.com/").query("test"),
|
|
||||||
NavigationTarget::from("https://dioxuslabs.com/")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn target_parse_internal() {
|
|
||||||
assert_eq!(
|
|
||||||
"/test".parse::<NavigationTarget>(),
|
|
||||||
Ok(NavigationTarget::Internal("/test".to_string()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn target_parse_external() {
|
|
||||||
assert_eq!(
|
|
||||||
"https://dioxuslabs.com/".parse::<NavigationTarget>(),
|
|
||||||
Ok(NavigationTarget::External(
|
|
||||||
"https://dioxuslabs.com/".to_string()
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn target_from_str_internal() {
|
|
||||||
assert_eq!(
|
|
||||||
NavigationTarget::from("/test"),
|
|
||||||
NavigationTarget::Internal("/test".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn target_from_str_external() {
|
|
||||||
assert_eq!(
|
|
||||||
NavigationTarget::from("https://dioxuslabs.com/"),
|
|
||||||
NavigationTarget::External("https://dioxuslabs.com/".to_string())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn target_from_string_internal() {
|
|
||||||
assert_eq!(
|
|
||||||
NavigationTarget::from("/test".to_string()),
|
|
||||||
NavigationTarget::Internal("/test".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn target_from_string_external() {
|
|
||||||
assert_eq!(
|
|
||||||
NavigationTarget::from("https://dioxuslabs.com/".to_string()),
|
|
||||||
NavigationTarget::External("https://dioxuslabs.com/".to_string())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn query_from_string() {
|
|
||||||
assert_eq!(
|
|
||||||
Query::from("test".to_string()),
|
|
||||||
Query::Single("test".to_string())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn query_from_str() {
|
|
||||||
assert_eq!(Query::from("test"), Query::Single("test".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
fn query_form_vec() {
|
|
||||||
assert_eq!(
|
|
||||||
Query::from(vec![("test", "1234")]),
|
|
||||||
Query::List(vec![("test".to_string(), "1234".to_string())])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
use futures_channel::mpsc::UnboundedSender;
|
|
||||||
|
|
||||||
use crate::{navigation::NavigationTarget, RouterMessage};
|
|
||||||
|
|
||||||
/// A [`Navigator`] allowing for programmatic navigation.
|
|
||||||
///
|
|
||||||
/// The [`Navigator`] is not guaranteed to be able to trigger a navigation. When and if a navigation
|
|
||||||
/// is actually handled depends on the UI library.
|
|
||||||
pub struct Navigator<I> {
|
|
||||||
sender: UnboundedSender<RouterMessage<I>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I> Navigator<I> {
|
|
||||||
/// Go back to the previous location.
|
|
||||||
///
|
|
||||||
/// Will fail silently if there is no previous location to go to.
|
|
||||||
pub fn go_back(&self) {
|
|
||||||
let _ = self.sender.unbounded_send(RouterMessage::GoBack);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Go back to the next location.
|
|
||||||
///
|
|
||||||
/// Will fail silently if there is no next location to go to.
|
|
||||||
pub fn go_forward(&self) {
|
|
||||||
let _ = self.sender.unbounded_send(RouterMessage::GoForward);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a new location.
|
|
||||||
///
|
|
||||||
/// The previous location will be available to go back to.
|
|
||||||
pub fn push(&self, target: impl Into<NavigationTarget>) {
|
|
||||||
let _ = self
|
|
||||||
.sender
|
|
||||||
.unbounded_send(RouterMessage::Push(target.into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace the current location.
|
|
||||||
///
|
|
||||||
/// The previous location will **not** be available to go back to.
|
|
||||||
pub fn replace(&self, target: impl Into<NavigationTarget>) {
|
|
||||||
let _ = self
|
|
||||||
.sender
|
|
||||||
.unbounded_send(RouterMessage::Replace(target.into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I> From<UnboundedSender<RouterMessage<I>>> for Navigator<I> {
|
|
||||||
fn from(sender: UnboundedSender<RouterMessage<I>>) -> Self {
|
|
||||||
Self { sender }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use futures_channel::mpsc::{unbounded, UnboundedReceiver};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn prepare() -> (Navigator<()>, UnboundedReceiver<RouterMessage<()>>) {
|
|
||||||
let (sender, receiver) = unbounded();
|
|
||||||
(Navigator::from(sender), receiver)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn go_back() {
|
|
||||||
let (n, mut s) = prepare();
|
|
||||||
n.go_back();
|
|
||||||
|
|
||||||
assert_eq!(s.try_next().unwrap(), Some(RouterMessage::GoBack));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn go_forward() {
|
|
||||||
let (n, mut s) = prepare();
|
|
||||||
n.go_forward();
|
|
||||||
|
|
||||||
assert_eq!(s.try_next().unwrap(), Some(RouterMessage::GoForward));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn push() {
|
|
||||||
let (n, mut s) = prepare();
|
|
||||||
let target = NavigationTarget::from("https://dioxuslabs.com/");
|
|
||||||
n.push(target.clone());
|
|
||||||
|
|
||||||
assert_eq!(s.try_next().unwrap(), Some(RouterMessage::Push(target)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn replace() {
|
|
||||||
let (n, mut s) = prepare();
|
|
||||||
let target = NavigationTarget::from("https://dioxuslabs.com/");
|
|
||||||
n.replace(target.clone());
|
|
||||||
|
|
||||||
assert_eq!(s.try_next().unwrap(), Some(RouterMessage::Replace(target)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,288 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::{BTreeMap, HashMap, HashSet},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use either::Either;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
navigation::NavigationTarget, routes::ContentAtom, segments::NameMap, utils::resolve_target,
|
|
||||||
Name,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The current state of the router.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RouterState<T: Clone> {
|
|
||||||
/// Whether there is a previous page to navigate back to.
|
|
||||||
///
|
|
||||||
/// Even if this is [`true`], there might not be a previous page. However, it is nonetheless
|
|
||||||
/// safe to tell the router to go back.
|
|
||||||
pub can_go_back: bool,
|
|
||||||
/// Whether there is a future page to navigate forward to.
|
|
||||||
///
|
|
||||||
/// Even if this is [`true`], there might not be a future page. However, it is nonetheless safe
|
|
||||||
/// to tell the router to go forward.
|
|
||||||
pub can_go_forward: bool,
|
|
||||||
|
|
||||||
/// The current path.
|
|
||||||
pub path: String,
|
|
||||||
/// The current query.
|
|
||||||
pub query: Option<String>,
|
|
||||||
/// The current prefix.
|
|
||||||
pub prefix: Option<String>,
|
|
||||||
|
|
||||||
/// The names of currently active routes.
|
|
||||||
pub names: HashSet<Name>,
|
|
||||||
/// The current path parameters.
|
|
||||||
pub parameters: HashMap<Name, String>,
|
|
||||||
pub(crate) name_map: Arc<NameMap>,
|
|
||||||
|
|
||||||
/// The current main content.
|
|
||||||
///
|
|
||||||
/// This should only be used by UI integration crates, and not by applications.
|
|
||||||
pub content: Vec<ContentAtom<T>>,
|
|
||||||
/// The current named content.
|
|
||||||
///
|
|
||||||
/// This should only be used by UI integration crates, and not by applications.
|
|
||||||
pub named_content: BTreeMap<Name, Vec<ContentAtom<T>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone> RouterState<T> {
|
|
||||||
/// Get a parameter.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus_router_core::{RouterState, Name};
|
|
||||||
/// let mut state = RouterState::<&'static str>::default();
|
|
||||||
/// assert_eq!(state.parameter::<bool>(), None);
|
|
||||||
///
|
|
||||||
/// // Do not do this! For illustrative purposes only!
|
|
||||||
/// state.parameters.insert(Name::of::<bool>(), String::from("some parameter"));
|
|
||||||
/// assert_eq!(state.parameter::<bool>(), Some("some parameter".to_string()));
|
|
||||||
/// ```
|
|
||||||
pub fn parameter<N: 'static>(&self) -> Option<String> {
|
|
||||||
self.parameters.get(&Name::of::<N>()).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the `href` for the `target`.
|
|
||||||
pub fn href(&self, target: &NavigationTarget) -> String {
|
|
||||||
match resolve_target(&self.name_map, target) {
|
|
||||||
Either::Left(Either::Left(i)) => match &self.prefix {
|
|
||||||
Some(p) => format!("{p}{i}"),
|
|
||||||
None => i,
|
|
||||||
},
|
|
||||||
Either::Left(Either::Right(n)) => {
|
|
||||||
// the following assert currently cannot trigger, as resolve_target (or more
|
|
||||||
// precisely resolve_name, which is called by resolve_targe) will panic in debug
|
|
||||||
debug_assert!(false, "requested href for unknown name or parameter: {n}");
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
Either::Right(e) => e,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether the `target` is currently active.
|
|
||||||
///
|
|
||||||
/// # Normal mode
|
|
||||||
/// 1. For internal targets wrapping an absolute path, the current path has to start with it.
|
|
||||||
/// 2. For internal targets wrapping a relative path, it has to match the last current segment
|
|
||||||
/// exactly.
|
|
||||||
/// 3. For named targets, the provided name needs to be active.
|
|
||||||
/// 4. For external targets [`false`].
|
|
||||||
///
|
|
||||||
/// # Exact mode
|
|
||||||
/// 1. For internal targets, the current path must match the wrapped path exactly.
|
|
||||||
/// 2. For named targets, the provided name needs to be active and all parameters need to match
|
|
||||||
/// exactly.
|
|
||||||
/// 3. For external targets [`false`].
|
|
||||||
pub fn is_at(&self, target: &NavigationTarget, exact: bool) -> bool {
|
|
||||||
match target {
|
|
||||||
NavigationTarget::Internal(i) => {
|
|
||||||
if exact {
|
|
||||||
i == &self.path
|
|
||||||
} else if i.starts_with('/') {
|
|
||||||
self.path.starts_with(i)
|
|
||||||
} else if let Some((_, s)) = self.path.rsplit_once('/') {
|
|
||||||
s == i
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NavigationTarget::Named {
|
|
||||||
name,
|
|
||||||
parameters,
|
|
||||||
query: _,
|
|
||||||
} => {
|
|
||||||
if !self.names.contains(name) {
|
|
||||||
false
|
|
||||||
} else if exact {
|
|
||||||
for (k, v) in parameters {
|
|
||||||
match self.parameters.get(k) {
|
|
||||||
Some(p) if p != v => return false,
|
|
||||||
None => return false,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NavigationTarget::External(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// manual impl required because derive macro requires default for T unnecessarily
|
|
||||||
impl<T: Clone> Default for RouterState<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
can_go_back: Default::default(),
|
|
||||||
can_go_forward: Default::default(),
|
|
||||||
path: Default::default(),
|
|
||||||
query: Default::default(),
|
|
||||||
prefix: Default::default(),
|
|
||||||
names: Default::default(),
|
|
||||||
parameters: Default::default(),
|
|
||||||
name_map: Default::default(),
|
|
||||||
content: Default::default(),
|
|
||||||
named_content: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::{navigation::named, prelude::RootIndex};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn href_internal() {
|
|
||||||
let state = RouterState::<&str> {
|
|
||||||
prefix: Some(String::from("/prefix")),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(state.href(&"/test".into()), String::from("/prefix/test"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn href_named() {
|
|
||||||
let state = RouterState::<&str> {
|
|
||||||
name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
|
|
||||||
prefix: Some(String::from("/prefix")),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(state.href(&named::<RootIndex>()), String::from("/prefix/"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic = "named navigation to unknown name: bool"]
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
fn href_named_debug() {
|
|
||||||
let state = RouterState::<&str> {
|
|
||||||
name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
|
|
||||||
prefix: Some(String::from("/prefix")),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
state.href(&named::<bool>());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
fn href_named_release() {
|
|
||||||
let state = RouterState::<&str> {
|
|
||||||
name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
|
|
||||||
prefix: Some(String::from("/prefix")),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(state.href(&named::<bool>()), String::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn href_external() {
|
|
||||||
let state = RouterState::<&str> {
|
|
||||||
prefix: Some(String::from("/prefix")),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
state.href(&"https://dioxuslabs.com/".into()),
|
|
||||||
String::from("https://dioxuslabs.com/")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_at_internal_absolute() {
|
|
||||||
let state = test_state();
|
|
||||||
|
|
||||||
assert!(!state.is_at(&"/levels".into(), false));
|
|
||||||
assert!(!state.is_at(&"/levels".into(), true));
|
|
||||||
|
|
||||||
assert!(state.is_at(&"/test".into(), false));
|
|
||||||
assert!(!state.is_at(&"/test".into(), true));
|
|
||||||
|
|
||||||
assert!(state.is_at(&"/test/with/some/nested/levels".into(), false));
|
|
||||||
assert!(state.is_at(&"/test/with/some/nested/levels".into(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_at_internal_relative() {
|
|
||||||
let state = test_state();
|
|
||||||
|
|
||||||
assert!(state.is_at(&"levels".into(), false));
|
|
||||||
assert!(!state.is_at(&"levels".into(), true));
|
|
||||||
|
|
||||||
assert!(!state.is_at(&"test".into(), false));
|
|
||||||
assert!(!state.is_at(&"test".into(), true));
|
|
||||||
|
|
||||||
assert!(!state.is_at(&"test/with/some/nested/levels".into(), false));
|
|
||||||
assert!(!state.is_at(&"test/with/some/nested/levels".into(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_at_named() {
|
|
||||||
let state = test_state();
|
|
||||||
|
|
||||||
assert!(!state.is_at(&named::<RootIndex>(), false));
|
|
||||||
assert!(!state.is_at(&named::<RootIndex>(), true));
|
|
||||||
|
|
||||||
assert!(state.is_at(&named::<bool>(), false));
|
|
||||||
assert!(state.is_at(&named::<bool>(), true));
|
|
||||||
|
|
||||||
assert!(state.is_at(&named::<bool>().parameter::<bool>("test"), false));
|
|
||||||
assert!(state.is_at(&named::<bool>().parameter::<bool>("test"), true));
|
|
||||||
|
|
||||||
assert!(state.is_at(&named::<bool>().parameter::<i8>("test"), false));
|
|
||||||
assert!(!state.is_at(&named::<bool>().parameter::<i8>("test"), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_at_external() {
|
|
||||||
let state = test_state();
|
|
||||||
|
|
||||||
assert!(!state.is_at(&"https://dioxuslabs.com/".into(), false));
|
|
||||||
assert!(!state.is_at(&"https://dioxuslabs.com/".into(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_state() -> RouterState<&'static str> {
|
|
||||||
RouterState {
|
|
||||||
path: String::from("/test/with/some/nested/levels"),
|
|
||||||
names: {
|
|
||||||
let mut r = HashSet::new();
|
|
||||||
r.insert(Name::of::<bool>());
|
|
||||||
r
|
|
||||||
},
|
|
||||||
parameters: {
|
|
||||||
let mut r = HashMap::new();
|
|
||||||
r.insert(Name::of::<bool>(), String::from("test"));
|
|
||||||
r
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
use dioxus_router_core::*;
|
|
||||||
use dioxus_router_macro::*;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn Route1(cx: Scope, dynamic: String) -> Element {
|
|
||||||
render! {
|
|
||||||
div{
|
|
||||||
"Route1: {dynamic}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn Route2(cx: Scope) -> Element {
|
|
||||||
render! {
|
|
||||||
div{
|
|
||||||
"Route2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn Route3(cx: Scope, dynamic: u32) -> Element {
|
|
||||||
render! {
|
|
||||||
div{
|
|
||||||
"Route3: {dynamic}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn Route4(cx: Scope, number1: u32, number2: u32) -> Element {
|
|
||||||
render! {
|
|
||||||
div{
|
|
||||||
"Route4: {number1} {number2}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn Route5(cx: Scope, query: String) -> Element {
|
|
||||||
render! {
|
|
||||||
div{
|
|
||||||
"Route5: {query}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn Route6(cx: Scope, extra: Vec<String>) -> Element {
|
|
||||||
render! {
|
|
||||||
div{
|
|
||||||
"Route5: {extra:?}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline_props]
|
|
||||||
fn Nested(cx: Scope, nested: String) -> Element {
|
|
||||||
render! {
|
|
||||||
div{
|
|
||||||
"Nested: {nested:?}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[routable]
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
enum Route {
|
|
||||||
#[route("/:dynamic" Route1)]
|
|
||||||
Route1 { dynamic: String },
|
|
||||||
#[route("/:number1/:number2" Route4)]
|
|
||||||
Route4 { number1: u32, number2: u32 },
|
|
||||||
#[nest("/:nested" nested { nested: String } Nested)]
|
|
||||||
#[route("/" Route2)]
|
|
||||||
Route2 {},
|
|
||||||
// #[redirect("/:dynamic/hello_world")]
|
|
||||||
#[route("/:dynamic" Route3)]
|
|
||||||
Route3 { dynamic: u32 },
|
|
||||||
#[end_nest]
|
|
||||||
#[route("/?:query" Route5)]
|
|
||||||
Route5 { query: String },
|
|
||||||
#[route("/:...extra" Route6)]
|
|
||||||
Route6 { extra: Vec<String> },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn display_works() {
|
|
||||||
let route = Route::Route1 {
|
|
||||||
dynamic: "hello".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(route.to_string(), "/hello");
|
|
||||||
|
|
||||||
let route = Route::Route3 {
|
|
||||||
nested: "hello_world".to_string(),
|
|
||||||
dynamic: 1234,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(route.to_string(), "/hello_world/1234");
|
|
||||||
|
|
||||||
let route = Route::Route1 {
|
|
||||||
dynamic: "hello_world2".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(route.to_string(), "/hello_world2");
|
|
||||||
|
|
||||||
let route = Route::Route4 {
|
|
||||||
number1: 1234,
|
|
||||||
number2: 5678,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(route.to_string(), "/1234/5678");
|
|
||||||
|
|
||||||
let route = Route::Route5 {
|
|
||||||
query: "hello".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(route.to_string(), "/?hello");
|
|
||||||
|
|
||||||
let route = Route::Route6 {
|
|
||||||
extra: vec!["hello".to_string(), "world".to_string()],
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(route.to_string(), "/hello/world");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_string_works() {
|
|
||||||
let w = "/hello";
|
|
||||||
assert_eq!(
|
|
||||||
Route::from_str(w),
|
|
||||||
Ok(Route::Route1 {
|
|
||||||
dynamic: "hello".to_string()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
let w = "/hello/";
|
|
||||||
assert_eq!(
|
|
||||||
Route::from_str(w),
|
|
||||||
Ok(Route::Route1 {
|
|
||||||
dynamic: "hello".to_string()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let w = "/hello_world/1234";
|
|
||||||
assert_eq!(
|
|
||||||
Route::from_str(w),
|
|
||||||
Ok(Route::Route3 {
|
|
||||||
nested: "hello_world".to_string(),
|
|
||||||
dynamic: 1234
|
|
||||||
})
|
|
||||||
);
|
|
||||||
let w = "/hello_world/1234/";
|
|
||||||
assert_eq!(
|
|
||||||
Route::from_str(w),
|
|
||||||
Ok(Route::Route3 {
|
|
||||||
nested: "hello_world".to_string(),
|
|
||||||
dynamic: 1234
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let w = "/hello_world2";
|
|
||||||
assert_eq!(
|
|
||||||
Route::from_str(w),
|
|
||||||
Ok(Route::Route1 {
|
|
||||||
dynamic: "hello_world2".to_string()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let w = "/?x=1234&y=hello";
|
|
||||||
assert_eq!(
|
|
||||||
Route::from_str(w),
|
|
||||||
Ok(Route::Route5 {
|
|
||||||
query: "x=1234&y=hello".to_string()
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let w = "/hello_world/hello_world/hello_world";
|
|
||||||
assert_eq!(
|
|
||||||
Route::from_str(w),
|
|
||||||
Ok(Route::Route6 {
|
|
||||||
extra: vec![
|
|
||||||
"hello_world".to_string(),
|
|
||||||
"hello_world".to_string(),
|
|
||||||
"hello_world".to_string()
|
|
||||||
]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn round_trip() {
|
|
||||||
// Route1
|
|
||||||
let string = "hello_world2";
|
|
||||||
let route = Route::Route1 {
|
|
||||||
dynamic: string.to_string(),
|
|
||||||
};
|
|
||||||
assert_eq!(Route::from_str(&route.to_string()), Ok(route));
|
|
||||||
|
|
||||||
// Route2
|
|
||||||
for num1 in 0..100 {
|
|
||||||
for num2 in 0..100 {
|
|
||||||
let route = Route::Route3 {
|
|
||||||
nested: format!("number{num1}"),
|
|
||||||
dynamic: num2,
|
|
||||||
};
|
|
||||||
assert_eq!(Route::from_str(&route.to_string()), Ok(route));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route3
|
|
||||||
for num1 in 0..100 {
|
|
||||||
for num2 in 0..100 {
|
|
||||||
let route = Route::Route4 {
|
|
||||||
number1: num1,
|
|
||||||
number2: num2,
|
|
||||||
};
|
|
||||||
assert_eq!(Route::from_str(&route.to_string()), Ok(route));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route4
|
|
||||||
let string = "x=1234&y=hello";
|
|
||||||
let route = Route::Route5 {
|
|
||||||
query: string.to_string(),
|
|
||||||
};
|
|
||||||
assert_eq!(Route::from_str(&route.to_string()), Ok(route));
|
|
||||||
|
|
||||||
// Route5
|
|
||||||
let route = Route::Route6 {
|
|
||||||
extra: vec![
|
|
||||||
"hello_world".to_string(),
|
|
||||||
"hello_world".to_string(),
|
|
||||||
"hello_world".to_string(),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
assert_eq!(Route::from_str(&route.to_string()), Ok(route));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {}
|
|
||||||
@@ -190,7 +190,7 @@ impl RouteEnum {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for #name {
|
impl std::str::FromStr for #name {
|
||||||
type Err = RouteParseError<#error_name>;
|
type Err = dioxus_router::routable::RouteParseError<#error_name>;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
let route = s.strip_prefix('/').unwrap_or(s);
|
let route = s.strip_prefix('/').unwrap_or(s);
|
||||||
@@ -200,7 +200,7 @@ impl RouteEnum {
|
|||||||
|
|
||||||
#(#tokens)*
|
#(#tokens)*
|
||||||
|
|
||||||
Err(RouteParseError {
|
Err(dioxus_router::routable::RouteParseError {
|
||||||
attempted_routes: errors,
|
attempted_routes: errors,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -289,7 +289,7 @@ impl RouteEnum {
|
|||||||
let index_iter = 0..layers.len();
|
let index_iter = 0..layers.len();
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
impl Routable for #name where Self: Clone {
|
impl dioxus_router::routable::Routable for #name where Self: Clone {
|
||||||
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> {
|
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> {
|
||||||
let myself = self.clone();
|
let myself = self.clone();
|
||||||
match level {
|
match level {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ impl Nest {
|
|||||||
}
|
}
|
||||||
RouteSegment::Dynamic(ident, ty) => {
|
RouteSegment::Dynamic(ident, ty) => {
|
||||||
let missing_error = segment.missing_error_name().unwrap();
|
let missing_error = segment.missing_error_name().unwrap();
|
||||||
error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegment>::Err) });
|
error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) });
|
||||||
display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
|
display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
|
||||||
error_variants.push(quote! { #missing_error });
|
error_variants.push(quote! { #missing_error });
|
||||||
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
|
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ impl QuerySegment {
|
|||||||
let ident = &self.ident;
|
let ident = &self.ident;
|
||||||
let ty = &self.ty;
|
let ty = &self.ty;
|
||||||
quote! {
|
quote! {
|
||||||
let #ident = <#ty as dioxus_router_core::router::FromQuery>::from_query(query);
|
let #ident = <#ty as dioxus_router::routable::FromQuery>::from_query(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,13 +203,13 @@ impl Route {
|
|||||||
}
|
}
|
||||||
RouteSegment::Dynamic(ident, ty) => {
|
RouteSegment::Dynamic(ident, ty) => {
|
||||||
let missing_error = segment.missing_error_name().unwrap();
|
let missing_error = segment.missing_error_name().unwrap();
|
||||||
error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegment>::Err) });
|
error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) });
|
||||||
display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
|
display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
|
||||||
error_variants.push(quote! { #missing_error });
|
error_variants.push(quote! { #missing_error });
|
||||||
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
|
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
|
||||||
}
|
}
|
||||||
RouteSegment::CatchAll(ident, ty) => {
|
RouteSegment::CatchAll(ident, ty) => {
|
||||||
error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegments>::Err) });
|
error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegments>::Err) });
|
||||||
display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
|
display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ impl RouteSegment {
|
|||||||
{
|
{
|
||||||
let mut segments = segments.clone();
|
let mut segments = segments.clone();
|
||||||
let parsed = if let Some(segment) = segments.next() {
|
let parsed = if let Some(segment) = segments.next() {
|
||||||
<#ty as dioxus_router_core::router::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
|
<#ty as dioxus_router::routable::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
|
||||||
} else {
|
} else {
|
||||||
Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#missing_error_name))
|
Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#missing_error_name))
|
||||||
};
|
};
|
||||||
@@ -101,7 +101,7 @@ impl RouteSegment {
|
|||||||
let parsed = {
|
let parsed = {
|
||||||
let mut segments = segments.clone();
|
let mut segments = segments.clone();
|
||||||
let segments: Vec<_> = segments.collect();
|
let segments: Vec<_> = segments.collect();
|
||||||
<#ty as dioxus_router_core::router::FromRouteSegments>::from_route_segments(&segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
|
<#ty as dioxus_router::routable::FromRouteSegments>::from_route_segments(&segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
|
||||||
};
|
};
|
||||||
match parsed {
|
match parsed {
|
||||||
Ok(#name) => {
|
Ok(#name) => {
|
||||||
|
|||||||
@@ -12,19 +12,16 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.66"
|
anyhow = "1.0.66"
|
||||||
async-lock = "2.7.0"
|
|
||||||
dioxus = { path="../dioxus" }
|
dioxus = { path="../dioxus" }
|
||||||
dioxus-router-core = { path = "../router-core"}
|
dioxus-router-macro = { path="../router-macro" }
|
||||||
futures-channel = "0.3.25"
|
|
||||||
futures-util = "0.3.25"
|
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
|
serde = { version = "1.0.163", features = ["derive"] }
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
|
url = "2.3.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
regex = ["dioxus-router-core/regex"]
|
|
||||||
serde = ["dioxus-router-core/serde"]
|
|
||||||
wasm_test = []
|
wasm_test = []
|
||||||
web = ["dioxus-router-core/web"]
|
web = []
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dioxus = { path = "../dioxus" }
|
dioxus = { path = "../dioxus" }
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
use crate::{components::Link, hooks::use_route};
|
use crate::{components::Link, hooks::use_route, navigation::NavigationTarget, routable::Routable};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_router_core::prelude::{named, FailureExternalNavigation as FENName, RootIndex};
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn FailureExternalNavigation(cx: Scope) -> Element {
|
pub fn FailureExternalNavigation<R: Routable + Clone>(cx: Scope) -> Element {
|
||||||
let state = use_route(cx).expect(
|
let href = use_route::<R>(cx).expect(
|
||||||
"`FailureExternalNavigation` can only be mounted by the router itself, \
|
"`FailureExternalNavigation` can only be mounted by the router itself, \
|
||||||
since it is not exposed",
|
since it is not exposed",
|
||||||
);
|
);
|
||||||
let href = state.parameter::<FENName>().expect(
|
|
||||||
"`FailureExternalNavigation` cannot be mounted without receiving its parameter, \
|
|
||||||
since it is not exposed",
|
|
||||||
);
|
|
||||||
|
|
||||||
render! {
|
render! {
|
||||||
h1 { "External Navigation Failure!" }
|
h1 { "External Navigation Failure!" }
|
||||||
@@ -28,7 +23,7 @@ pub fn FailureExternalNavigation(cx: Scope) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn FailureNamedNavigation(cx: Scope) -> Element {
|
pub fn FailureNamedNavigation<R: Routable + Clone>(cx: Scope) -> Element {
|
||||||
render! {
|
render! {
|
||||||
h1 { "Named Navigation Failure!" }
|
h1 { "Named Navigation Failure!" }
|
||||||
p {
|
p {
|
||||||
@@ -40,15 +35,15 @@ pub fn FailureNamedNavigation(cx: Scope) -> Element {
|
|||||||
"We are sorry for the inconvenience. The link below may help to fix the problem, but "
|
"We are sorry for the inconvenience. The link below may help to fix the problem, but "
|
||||||
"there is no guarantee."
|
"there is no guarantee."
|
||||||
}
|
}
|
||||||
Link {
|
Link::<R> {
|
||||||
target: named::<RootIndex>(),
|
target: NavigationTarget::External("https://google.com".into()),
|
||||||
"Click here to try to fix the failure."
|
"Click here to try to fix the failure."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn FailureRedirectionLimit(cx: Scope) -> Element {
|
pub fn FailureRedirectionLimit<R: Routable + Clone>(cx: Scope) -> Element {
|
||||||
render! {
|
render! {
|
||||||
h1 { "Redirection Limit Failure!" }
|
h1 { "Redirection Limit Failure!" }
|
||||||
p {
|
p {
|
||||||
@@ -60,8 +55,8 @@ pub fn FailureRedirectionLimit(cx: Scope) -> Element {
|
|||||||
"We are sorry for the inconvenience. The link below may help to fix the problem, but "
|
"We are sorry for the inconvenience. The link below may help to fix the problem, but "
|
||||||
"there is no guarantee."
|
"there is no guarantee."
|
||||||
}
|
}
|
||||||
Link {
|
Link::<R> {
|
||||||
target: named::<RootIndex>(),
|
target: NavigationTarget::External("https://google.com".into()),
|
||||||
"Click here to try to fix the failure."
|
"Click here to try to fix the failure."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_router_core::RouterMessage;
|
|
||||||
use log::error;
|
use log::error;
|
||||||
|
|
||||||
use crate::utils::use_router_internal::use_router_internal;
|
use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
|
||||||
|
|
||||||
/// The properties for a [`GoBackButton`] or a [`GoForwardButton`].
|
/// The properties for a [`GoBackButton`] or a [`GoForwardButton`].
|
||||||
#[derive(Debug, Props)]
|
#[derive(Debug, Props)]
|
||||||
@@ -52,11 +51,11 @@ pub struct HistoryButtonProps<'a> {
|
|||||||
/// # );
|
/// # );
|
||||||
/// ```
|
/// ```
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
pub fn GoBackButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||||
let HistoryButtonProps { children } = cx.props;
|
let HistoryButtonProps { children } = cx.props;
|
||||||
|
|
||||||
// hook up to router
|
// hook up to router
|
||||||
let router = match use_router_internal(cx) {
|
let router = match use_router_internal::<R>(cx) {
|
||||||
Some(r) => r,
|
Some(r) => r,
|
||||||
#[allow(unreachable_code)]
|
#[allow(unreachable_code)]
|
||||||
None => {
|
None => {
|
||||||
@@ -67,20 +66,14 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let state = loop {
|
|
||||||
if let Some(state) = router.state.try_read() {
|
|
||||||
break state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let sender = router.sender.clone();
|
|
||||||
|
|
||||||
let disabled = !state.can_go_back;
|
let disabled = !router.can_go_back();
|
||||||
|
|
||||||
render! {
|
render! {
|
||||||
button {
|
button {
|
||||||
disabled: "{disabled}",
|
disabled: "{disabled}",
|
||||||
prevent_default: "onclick",
|
prevent_default: "onclick",
|
||||||
onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoBack); },
|
onclick: move |_| router.go_back(),
|
||||||
children
|
children
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,11 +120,11 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
|||||||
/// # );
|
/// # );
|
||||||
/// ```
|
/// ```
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
pub fn GoForwardButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||||
let HistoryButtonProps { children } = cx.props;
|
let HistoryButtonProps { children } = cx.props;
|
||||||
|
|
||||||
// hook up to router
|
// hook up to router
|
||||||
let router = match use_router_internal(cx) {
|
let router = match use_router_internal::<R>(cx) {
|
||||||
Some(r) => r,
|
Some(r) => r,
|
||||||
#[allow(unreachable_code)]
|
#[allow(unreachable_code)]
|
||||||
None => {
|
None => {
|
||||||
@@ -142,20 +135,14 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let state = loop {
|
|
||||||
if let Some(state) = router.state.try_read() {
|
|
||||||
break state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let sender = router.sender.clone();
|
|
||||||
|
|
||||||
let disabled = !state.can_go_back;
|
let disabled = !router.can_go_back();
|
||||||
|
|
||||||
render! {
|
render! {
|
||||||
button {
|
button {
|
||||||
disabled: "{disabled}",
|
disabled: "{disabled}",
|
||||||
prevent_default: "onclick",
|
prevent_default: "onclick",
|
||||||
onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoForward); },
|
onclick: move |_| router.go_forward(),
|
||||||
children
|
children
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_router_core::{navigation::NavigationTarget, RouterMessage};
|
|
||||||
use log::error;
|
use log::error;
|
||||||
|
|
||||||
|
use crate::navigation::NavigationTarget;
|
||||||
|
use crate::routable::Routable;
|
||||||
use crate::utils::use_router_internal::use_router_internal;
|
use crate::utils::use_router_internal::use_router_internal;
|
||||||
|
|
||||||
/// The properties for a [`Link`].
|
/// The properties for a [`Link`].
|
||||||
#[derive(Props)]
|
#[derive(Props)]
|
||||||
pub struct LinkProps<'a> {
|
pub struct LinkProps<'a, R: Routable> {
|
||||||
/// A class to apply to the generate HTML anchor tag if the `target` route is active.
|
/// A class to apply to the generate HTML anchor tag if the `target` route is active.
|
||||||
pub active_class: Option<&'a str>,
|
pub active_class: Option<&'a str>,
|
||||||
/// The children to render within the generated HTML anchor tag.
|
/// The children to render within the generated HTML anchor tag.
|
||||||
@@ -18,11 +19,6 @@ pub struct LinkProps<'a> {
|
|||||||
/// If `active_class` is [`Some`] and the `target` route is active, `active_class` will be
|
/// If `active_class` is [`Some`] and the `target` route is active, `active_class` will be
|
||||||
/// appended at the end of `class`.
|
/// appended at the end of `class`.
|
||||||
pub class: Option<&'a str>,
|
pub class: Option<&'a str>,
|
||||||
/// Require the __exact__ `target` to be active, for the [`Link`] to be active.
|
|
||||||
///
|
|
||||||
/// See [`RouterState::is_at`](dioxus_router_core::RouterState::is_at) for more details.
|
|
||||||
#[props(default)]
|
|
||||||
pub exact: bool,
|
|
||||||
/// The id attribute for the generated HTML anchor tag.
|
/// The id attribute for the generated HTML anchor tag.
|
||||||
pub id: Option<&'a str>,
|
pub id: Option<&'a str>,
|
||||||
/// When [`true`], the `target` route will be opened in a new tab.
|
/// When [`true`], the `target` route will be opened in a new tab.
|
||||||
@@ -46,22 +42,21 @@ pub struct LinkProps<'a> {
|
|||||||
pub rel: Option<&'a str>,
|
pub rel: Option<&'a str>,
|
||||||
/// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
|
/// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
|
||||||
#[props(into)]
|
#[props(into)]
|
||||||
pub target: NavigationTarget,
|
pub target: NavigationTarget<R>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for LinkProps<'_> {
|
impl<R: Routable> Debug for LinkProps<'_, R> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("LinkProps")
|
f.debug_struct("LinkProps")
|
||||||
.field("active_class", &self.active_class)
|
.field("active_class", &self.active_class)
|
||||||
.field("children", &self.children)
|
.field("children", &self.children)
|
||||||
.field("class", &self.class)
|
.field("class", &self.class)
|
||||||
.field("exact", &self.exact)
|
|
||||||
.field("id", &self.id)
|
.field("id", &self.id)
|
||||||
.field("new_tab", &self.new_tab)
|
.field("new_tab", &self.new_tab)
|
||||||
.field("onclick", &self.onclick.as_ref().map(|_| "onclick is set"))
|
.field("onclick", &self.onclick.as_ref().map(|_| "onclick is set"))
|
||||||
.field("onclick_only", &self.onclick_only)
|
.field("onclick_only", &self.onclick_only)
|
||||||
.field("rel", &self.rel)
|
.field("rel", &self.rel)
|
||||||
.field("target", &self.target)
|
.field("target", &self.target.to_string())
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,12 +125,11 @@ impl Debug for LinkProps<'_> {
|
|||||||
/// # );
|
/// # );
|
||||||
/// ```
|
/// ```
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
pub fn Link<'a, R: Routable + Clone>(cx: Scope<'a, LinkProps<'a, R>>) -> Element {
|
||||||
let LinkProps {
|
let LinkProps {
|
||||||
active_class,
|
active_class,
|
||||||
children,
|
children,
|
||||||
class,
|
class,
|
||||||
exact,
|
|
||||||
id,
|
id,
|
||||||
new_tab,
|
new_tab,
|
||||||
onclick,
|
onclick,
|
||||||
@@ -145,7 +139,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
|||||||
} = cx.props;
|
} = cx.props;
|
||||||
|
|
||||||
// hook up to router
|
// hook up to router
|
||||||
let router = match use_router_internal(cx) {
|
let router = match use_router_internal::<R>(cx) {
|
||||||
Some(r) => r,
|
Some(r) => r,
|
||||||
#[allow(unreachable_code)]
|
#[allow(unreachable_code)]
|
||||||
None => {
|
None => {
|
||||||
@@ -156,19 +150,19 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let state = loop {
|
|
||||||
if let Some(state) = router.state.try_read() {
|
|
||||||
break state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let sender = router.sender.clone();
|
|
||||||
|
|
||||||
let href = state.href(target);
|
let current_route = router.current();
|
||||||
|
let href = current_route.to_string();
|
||||||
let ac = active_class
|
let ac = active_class
|
||||||
.and_then(|active_class| {
|
.and_then(|active_class| match target {
|
||||||
state
|
NavigationTarget::Internal(target) => {
|
||||||
.is_at(target, *exact)
|
if href == target.to_string() {
|
||||||
.then(|| format!(" {active_class}"))
|
Some(format!(" {active_class}"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
@@ -186,7 +180,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
|||||||
let do_default = onclick.is_none() || !onclick_only;
|
let do_default = onclick.is_none() || !onclick_only;
|
||||||
let action = move |event| {
|
let action = move |event| {
|
||||||
if do_default && is_router_nav {
|
if do_default && is_router_nav {
|
||||||
let _ = sender.unbounded_send(RouterMessage::Push(target.clone()));
|
router.push(target.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(handler) = onclick {
|
if let Some(handler) = onclick {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
use crate::{contexts::outlet::OutletContext, routable::Routable};
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
/// An outlet for the current content.
|
||||||
|
///
|
||||||
|
/// Only works as descendant of a component calling [`use_router`], otherwise it will be inactive.
|
||||||
|
///
|
||||||
|
/// The [`Outlet`] is aware of how many [`Outlet`]s it is nested within. It will render the content
|
||||||
|
/// of the active route that is __exactly as deep__.
|
||||||
|
///
|
||||||
|
/// [`use_router`]: crate::hooks::use_router
|
||||||
|
///
|
||||||
|
/// # Panic
|
||||||
|
/// - When the [`Outlet`] is not nested within another component calling the [`use_router`] hook,
|
||||||
|
/// but only in debug builds.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// # use dioxus_router::prelude::*;
|
||||||
|
/// fn App(cx: Scope) -> Element {
|
||||||
|
/// use_router(
|
||||||
|
/// &cx,
|
||||||
|
/// &|| RouterConfiguration {
|
||||||
|
/// synchronous: true, // asynchronicity not needed for doc test
|
||||||
|
/// ..Default::default()
|
||||||
|
/// },
|
||||||
|
/// &|| Segment::content(comp(Content))
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// render! {
|
||||||
|
/// h1 { "App" }
|
||||||
|
/// Outlet { } // The content component will be rendered here
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// fn Content(cx: Scope) -> Element {
|
||||||
|
/// render! {
|
||||||
|
/// p { "Content" }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// #
|
||||||
|
/// # let mut vdom = VirtualDom::new(App);
|
||||||
|
/// # let _ = vdom.rebuild();
|
||||||
|
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Content</p>");
|
||||||
|
/// ```
|
||||||
|
pub fn Outlet<R: Routable + Clone>(cx: Scope) -> Element {
|
||||||
|
OutletContext::render::<R>(cx)
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use log::error;
|
||||||
|
use std::{cell::RefCell, str::FromStr};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
history::HistoryProvider,
|
||||||
|
prelude::{outlet::OutletContext, RouterContext},
|
||||||
|
routable::Routable,
|
||||||
|
router_cfg::RouterConfiguration,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The props for [`Router`].
|
||||||
|
#[derive(Props)]
|
||||||
|
pub struct RouterProps<R: Routable> {
|
||||||
|
#[props(into)]
|
||||||
|
initial_url: Option<String>,
|
||||||
|
#[props(default, into)]
|
||||||
|
config: RefCell<Option<RouterConfiguration<R>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> PartialEq for RouterProps<R> {
|
||||||
|
fn eq(&self, _: &Self) -> bool {
|
||||||
|
// prevent the router from re-rendering when the initial url or config changes
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A component that renders the current route.
|
||||||
|
pub fn Router<R: Routable + Clone, H: HistoryProvider<R> + Default + 'static>(
|
||||||
|
cx: Scope<RouterProps<R>>,
|
||||||
|
) -> Element
|
||||||
|
where
|
||||||
|
<R as FromStr>::Err: std::fmt::Display,
|
||||||
|
{
|
||||||
|
let router = use_context_provider(cx, || {
|
||||||
|
#[allow(unreachable_code, unused_variables)]
|
||||||
|
if let Some(outer) = cx.consume_context::<RouterContext<R>>() {
|
||||||
|
let msg = "Router components should not be nested within each other";
|
||||||
|
error!("{msg}, inner will be inactive and transparent");
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
panic!("{}", msg);
|
||||||
|
}
|
||||||
|
let router = RouterContext::new(
|
||||||
|
cx.props.config.take().unwrap_or_default(),
|
||||||
|
cx.schedule_update_any(),
|
||||||
|
);
|
||||||
|
if let Some(initial) = cx.props.initial_url.as_ref() {
|
||||||
|
router.replace(
|
||||||
|
initial
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_else(|_| panic!("failed to parse initial url")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
router
|
||||||
|
});
|
||||||
|
|
||||||
|
use_context_provider(cx, || OutletContext { current_level: 1 });
|
||||||
|
|
||||||
|
router.current().render(cx, 0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use crate::{hooks::use_route, routable::Routable};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct OutletContext {
|
||||||
|
pub current_level: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn use_outlet_context(cx: &ScopeState) -> &OutletContext {
|
||||||
|
let outlet_context = use_context(cx).unwrap();
|
||||||
|
outlet_context
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutletContext {
|
||||||
|
pub(crate) fn render<R: Routable + Clone>(cx: &ScopeState) -> Element<'_> {
|
||||||
|
let outlet = use_outlet_context(cx);
|
||||||
|
let current_level = outlet.current_level;
|
||||||
|
cx.provide_context({
|
||||||
|
OutletContext {
|
||||||
|
current_level: current_level + 1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
use_route::<R>(cx)
|
||||||
|
.expect("Outlet must be inside of a router")
|
||||||
|
.render(cx, current_level)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,809 @@
|
|||||||
use std::sync::Arc;
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
sync::{Arc, RwLock, RwLockWriteGuard},
|
||||||
|
};
|
||||||
|
|
||||||
use async_lock::RwLock;
|
use dioxus::prelude::*;
|
||||||
use dioxus::{core::Component, prelude::ScopeId};
|
|
||||||
use dioxus_router_core::{RouterMessage, RouterState};
|
|
||||||
use futures_channel::mpsc::UnboundedSender;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
use crate::{
|
||||||
pub(crate) struct RouterContext {
|
history::HistoryProvider, navigation::NavigationTarget, routable::Routable,
|
||||||
pub(crate) state: Arc<RwLock<RouterState<Component>>>,
|
router_cfg::RouterConfiguration,
|
||||||
pub(crate) sender: UnboundedSender<RouterMessage<ScopeId>>,
|
};
|
||||||
|
|
||||||
|
/// An error that can occur when navigating.
|
||||||
|
pub enum NavigationFailure<R: Routable> {
|
||||||
|
/// The router failed to navigate to an external URL.
|
||||||
|
External(String),
|
||||||
|
/// The router failed to navigate to an internal URL.
|
||||||
|
Internal(<R as std::str::FromStr>::Err),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A function the router will call after every routing update.
|
||||||
|
pub type RoutingCallback<R> = Arc<dyn Fn(RouterContext<R>) -> Option<NavigationTarget<R>>>;
|
||||||
|
|
||||||
|
struct MutableRouterState<R>
|
||||||
|
where
|
||||||
|
R: Routable,
|
||||||
|
{
|
||||||
|
/// Whether there is a previous page to navigate back to.
|
||||||
|
///
|
||||||
|
/// Even if this is [`true`], there might not be a previous page. However, it is nonetheless
|
||||||
|
/// safe to tell the router to go back.
|
||||||
|
can_go_back: bool,
|
||||||
|
/// Whether there is a future page to navigate forward to.
|
||||||
|
///
|
||||||
|
/// Even if this is [`true`], there might not be a future page. However, it is nonetheless safe
|
||||||
|
/// to tell the router to go forward.
|
||||||
|
can_go_forward: bool,
|
||||||
|
|
||||||
|
/// The current prefix.
|
||||||
|
prefix: Option<String>,
|
||||||
|
|
||||||
|
history: Box<dyn HistoryProvider<R>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A collection of router data that manages all routing functionality.
|
||||||
|
pub struct RouterContext<R>
|
||||||
|
where
|
||||||
|
R: Routable,
|
||||||
|
{
|
||||||
|
state: Arc<RwLock<MutableRouterState<R>>>,
|
||||||
|
|
||||||
|
subscribers: Arc<RwLock<HashSet<ScopeId>>>,
|
||||||
|
subscriber_update: Arc<dyn Fn(ScopeId)>,
|
||||||
|
routing_callback: Option<RoutingCallback<R>>,
|
||||||
|
|
||||||
|
failure_external_navigation: fn(Scope) -> Element,
|
||||||
|
failure_named_navigation: fn(Scope) -> Element,
|
||||||
|
failure_redirection_limit: fn(Scope) -> Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> Clone for RouterContext<R> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
state: self.state.clone(),
|
||||||
|
subscribers: self.subscribers.clone(),
|
||||||
|
subscriber_update: self.subscriber_update.clone(),
|
||||||
|
routing_callback: self.routing_callback.clone(),
|
||||||
|
failure_external_navigation: self.failure_external_navigation,
|
||||||
|
failure_named_navigation: self.failure_named_navigation,
|
||||||
|
failure_redirection_limit: self.failure_redirection_limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> RouterContext<R>
|
||||||
|
where
|
||||||
|
R: Routable,
|
||||||
|
{
|
||||||
|
pub(crate) fn new(cfg: RouterConfiguration<R>, mark_dirty: Arc<dyn Fn(ScopeId)>) -> Self
|
||||||
|
where
|
||||||
|
R: Clone,
|
||||||
|
{
|
||||||
|
let state = Arc::new(RwLock::new(MutableRouterState {
|
||||||
|
can_go_back: false,
|
||||||
|
can_go_forward: false,
|
||||||
|
prefix: Default::default(),
|
||||||
|
history: cfg.history,
|
||||||
|
}));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
subscribers: Arc::new(RwLock::new(HashSet::new())),
|
||||||
|
subscriber_update: mark_dirty,
|
||||||
|
|
||||||
|
routing_callback: cfg.on_update,
|
||||||
|
|
||||||
|
failure_external_navigation: cfg.failure_external_navigation,
|
||||||
|
failure_named_navigation: cfg.failure_named_navigation,
|
||||||
|
failure_redirection_limit: cfg.failure_redirection_limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether there is a previous page to navigate back to.
|
||||||
|
#[must_use]
|
||||||
|
pub fn can_go_back(&self) -> bool {
|
||||||
|
self.state.read().unwrap().can_go_back
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether there is a future page to navigate forward to.
|
||||||
|
#[must_use]
|
||||||
|
pub fn can_go_forward(&self) -> bool {
|
||||||
|
self.state.read().unwrap().can_go_forward
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Go back to the previous location.
|
||||||
|
///
|
||||||
|
/// Will fail silently if there is no previous location to go to.
|
||||||
|
pub fn go_back(&self) {
|
||||||
|
self.state.write().unwrap().history.go_back();
|
||||||
|
self.update_subscribers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Go back to the next location.
|
||||||
|
///
|
||||||
|
/// Will fail silently if there is no next location to go to.
|
||||||
|
pub fn go_forward(&self) {
|
||||||
|
self.state.write().unwrap().history.go_forward();
|
||||||
|
self.update_subscribers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a new location.
|
||||||
|
///
|
||||||
|
/// The previous location will be available to go back to.
|
||||||
|
pub fn push(&self, target: NavigationTarget<R>) -> Option<NavigationFailure<R>> {
|
||||||
|
let mut state = self.state_mut();
|
||||||
|
match target {
|
||||||
|
NavigationTarget::Internal(p) => state.history.push(p),
|
||||||
|
NavigationTarget::External(e) => return self.external(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_subscribers();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the current location.
|
||||||
|
///
|
||||||
|
/// The previous location will **not** be available to go back to.
|
||||||
|
pub fn replace(&self, target: NavigationTarget<R>) -> Option<NavigationFailure<R>> {
|
||||||
|
let mut state = self.state_mut();
|
||||||
|
match target {
|
||||||
|
NavigationTarget::Internal(p) => state.history.replace(p),
|
||||||
|
NavigationTarget::External(e) => return self.external(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_subscribers();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The route that is currently active.
|
||||||
|
pub fn current(&self) -> R
|
||||||
|
where
|
||||||
|
R: Clone,
|
||||||
|
{
|
||||||
|
self.state.read().unwrap().history.current_route().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The prefix that is currently active.
|
||||||
|
pub fn prefix(&self) -> Option<String> {
|
||||||
|
self.state.read().unwrap().prefix.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn external(&self, external: String) -> Option<NavigationFailure<R>> {
|
||||||
|
let mut state = self.state_mut();
|
||||||
|
match state.history.external(external.clone()) {
|
||||||
|
true => None,
|
||||||
|
false => Some(NavigationFailure::External(external)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_mut(&self) -> RwLockWriteGuard<MutableRouterState<R>> {
|
||||||
|
self.state.write().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manually subscribe to the current route
|
||||||
|
pub fn subscribe(&self, id: ScopeId) {
|
||||||
|
self.subscribers.write().unwrap().insert(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manually unsubscribe from the current route
|
||||||
|
pub fn unsubscribe(&self, id: ScopeId) {
|
||||||
|
self.subscribers.write().unwrap().remove(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_subscribers(&self) {
|
||||||
|
for &id in self.subscribers.read().unwrap().iter() {
|
||||||
|
(self.subscriber_update)(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[cfg(test)]
|
||||||
|
// mod tests {
|
||||||
|
// //! The tests for [`RouterContext`] test various functions that are not exposed as public.
|
||||||
|
// //! However, several of those have an observable effect on the behavior of exposed functions.
|
||||||
|
// //!
|
||||||
|
// //! The alternative would be to send messages via the services channel and calling one of the
|
||||||
|
// //! `run` functions. However, for readability and clarity, it was chosen to directly call the
|
||||||
|
// //! private functions.
|
||||||
|
|
||||||
|
// use std::sync::Mutex;
|
||||||
|
|
||||||
|
// use crate::{
|
||||||
|
// history::MemoryHistory,
|
||||||
|
// routes::{ParameterRoute, Route, RouteContent},
|
||||||
|
// };
|
||||||
|
|
||||||
|
// use super::*;
|
||||||
|
|
||||||
|
// fn test_segment() -> Segment<&'static str> {
|
||||||
|
// Segment::content(RouteContent::Content(ContentAtom("index")))
|
||||||
|
// .fixed(
|
||||||
|
// "fixed",
|
||||||
|
// Route::content(RouteContent::Content(ContentAtom("fixed"))).name::<bool>(),
|
||||||
|
// )
|
||||||
|
// .fixed(
|
||||||
|
// "redirect",
|
||||||
|
// Route::content(RouteContent::Redirect(NavigationTarget::Internal(
|
||||||
|
// String::from("fixed"),
|
||||||
|
// ))),
|
||||||
|
// )
|
||||||
|
// .fixed(
|
||||||
|
// "redirection-loop",
|
||||||
|
// Route::content(RouteContent::Redirect(NavigationTarget::Internal(
|
||||||
|
// String::from("/redirection-loop"),
|
||||||
|
// ))),
|
||||||
|
// )
|
||||||
|
// .fixed(
|
||||||
|
// "%F0%9F%8E%BA",
|
||||||
|
// Route::content(RouteContent::Content(ContentAtom("🎺"))),
|
||||||
|
// )
|
||||||
|
// .catch_all(ParameterRoute::empty::<bool>())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn new_provides_update_to_history() {
|
||||||
|
// struct TestHistory {}
|
||||||
|
|
||||||
|
// impl HistoryProvider for TestHistory {
|
||||||
|
// fn current_path(&self) -> String {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn current_query(&self) -> Option<String> {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn go_back(&mut self) {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn go_forward(&mut self) {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn push(&mut self, _path: String) {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn replace(&mut self, _path: String) {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
|
||||||
|
// callback();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::new(TestHistory {}),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// assert!(matches!(
|
||||||
|
// s.receiver.try_next().unwrap().unwrap(),
|
||||||
|
// RouterMessage::Update
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn update_routing() {
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::new(MemoryHistory::with_initial_path("/fixed?test=value").unwrap()),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// assert_eq!(s.names, s.state.try_read().unwrap().name_map);
|
||||||
|
// s.init();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||||
|
// assert_eq!(state.names, {
|
||||||
|
// let mut r = HashSet::new();
|
||||||
|
// r.insert(Name::of::<bool>());
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert!(state.parameters.is_empty());
|
||||||
|
// assert_eq!(state.path, String::from("/fixed"));
|
||||||
|
// assert_eq!(state.query, Some(String::from("test=value")));
|
||||||
|
// assert!(!state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// assert_eq!(s.names, state.name_map);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn update_routing_root_index() {
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::<MemoryHistory>::default(),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.init();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("index")]);
|
||||||
|
// assert_eq!(state.names, {
|
||||||
|
// let mut r = HashSet::new();
|
||||||
|
// r.insert(Name::of::<RootIndex>());
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert!(state.parameters.is_empty());
|
||||||
|
// assert_eq!(state.path, String::from("/"));
|
||||||
|
// assert!(state.query.is_none());
|
||||||
|
// assert!(state.prefix.is_none());
|
||||||
|
// assert!(!state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn update_routing_redirect() {
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::new(MemoryHistory::with_initial_path("/redirect").unwrap()),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.init();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||||
|
// assert_eq!(state.names, {
|
||||||
|
// let mut r = HashSet::new();
|
||||||
|
// r.insert(Name::of::<bool>());
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert!(state.parameters.is_empty());
|
||||||
|
// assert_eq!(state.path, String::from("/fixed"));
|
||||||
|
// assert!(!state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// #[should_panic = "reached redirect limit of 25"]
|
||||||
|
// #[cfg(debug_assertions)]
|
||||||
|
// fn update_routing_redirect_debug() {
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.init();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// #[cfg(not(debug_assertions))]
|
||||||
|
// fn update_routing_redirect_release() {
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.init();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("redirect limit")]);
|
||||||
|
// assert_eq!(state.names, {
|
||||||
|
// let mut r = HashSet::new();
|
||||||
|
// r.insert(Name::of::<FailureRedirectionLimit>());
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert!(state.parameters.is_empty());
|
||||||
|
// assert_eq!(state.path, String::from("/redirection-loop"));
|
||||||
|
// assert_eq!(state.can_go_back, false);
|
||||||
|
// assert_eq!(state.can_go_forward, false);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn update_subscribers() {
|
||||||
|
// let ids = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
// let ids2 = Arc::clone(&ids);
|
||||||
|
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// Segment::empty(),
|
||||||
|
// Box::<MemoryHistory>::default(),
|
||||||
|
// Arc::new(move |id| {
|
||||||
|
// ids2.lock().unwrap().push(id);
|
||||||
|
// }),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let id0 = Arc::new(0);
|
||||||
|
// s.subscribe(Arc::clone(&id0));
|
||||||
|
|
||||||
|
// let id1 = Arc::new(1);
|
||||||
|
// s.subscribe(Arc::clone(&id1));
|
||||||
|
|
||||||
|
// let id1 = Arc::try_unwrap(id1).unwrap();
|
||||||
|
// s.update_subscribers();
|
||||||
|
|
||||||
|
// assert_eq!(s.subscribers.len(), 1);
|
||||||
|
// assert_eq!(s.subscribers[0].upgrade().unwrap(), id0);
|
||||||
|
// assert_eq!(*ids.lock().unwrap(), vec![*id0, id1, *id0]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn push_internal() {
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::<MemoryHistory>::default(),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.push(NavigationTarget::Internal(String::from("/fixed")));
|
||||||
|
// s.init();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||||
|
// assert_eq!(state.names, {
|
||||||
|
// let mut r = HashSet::new();
|
||||||
|
// r.insert(Name::of::<bool>());
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert!(state.parameters.is_empty());
|
||||||
|
// assert_eq!(state.path, String::from("/fixed"));
|
||||||
|
// assert!(state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn push_named() {
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::<MemoryHistory>::default(),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.push(NavigationTarget::named::<bool>());
|
||||||
|
// s.init();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||||
|
// assert_eq!(state.names, {
|
||||||
|
// let mut r = HashSet::new();
|
||||||
|
// r.insert(Name::of::<bool>());
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert!(state.parameters.is_empty());
|
||||||
|
// assert_eq!(state.path, String::from("/fixed"));
|
||||||
|
// assert!(state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn push_external() {
|
||||||
|
// let (mut s, tx, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.init();
|
||||||
|
// tx.unbounded_send(RouterMessage::Push(NavigationTarget::External(
|
||||||
|
// String::from("https://dioxuslabs.com/"),
|
||||||
|
// )))
|
||||||
|
// .unwrap();
|
||||||
|
// s.run_current();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("external target")]);
|
||||||
|
// assert_eq!(state.names, {
|
||||||
|
// let mut r = HashSet::new();
|
||||||
|
// r.insert(Name::of::<FailureExternalNavigation>());
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert_eq!(state.parameters, {
|
||||||
|
// let mut r = HashMap::new();
|
||||||
|
// r.insert(
|
||||||
|
// Name::of::<FailureExternalNavigation>(),
|
||||||
|
// String::from("https://dioxuslabs.com/"),
|
||||||
|
// );
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert_eq!(state.path, String::from("/fixed"));
|
||||||
|
// assert!(!state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn replace_named() {
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::<MemoryHistory>::default(),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.replace(NavigationTarget::named::<bool>());
|
||||||
|
// s.init();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||||
|
// assert_eq!(state.names, {
|
||||||
|
// let mut r = HashSet::new();
|
||||||
|
// r.insert(Name::of::<bool>());
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert!(state.parameters.is_empty());
|
||||||
|
// assert_eq!(state.path, String::from("/fixed"));
|
||||||
|
// assert!(!state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn replace_internal() {
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::<MemoryHistory>::default(),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.replace(NavigationTarget::Internal(String::from("/fixed")));
|
||||||
|
// s.init();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||||
|
// assert_eq!(state.names, {
|
||||||
|
// let mut r = HashSet::new();
|
||||||
|
// r.insert(Name::of::<bool>());
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert!(state.parameters.is_empty());
|
||||||
|
// assert_eq!(state.path, String::from("/fixed"));
|
||||||
|
// assert!(!state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn replace_external() {
|
||||||
|
// let (mut s, tx, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.init();
|
||||||
|
// tx.unbounded_send(RouterMessage::Replace(NavigationTarget::External(
|
||||||
|
// String::from("https://dioxuslabs.com/"),
|
||||||
|
// )))
|
||||||
|
// .unwrap();
|
||||||
|
// s.run_current();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("external target")]);
|
||||||
|
// assert_eq!(state.names, {
|
||||||
|
// let mut r = HashSet::new();
|
||||||
|
// r.insert(Name::of::<FailureExternalNavigation>());
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert_eq!(state.parameters, {
|
||||||
|
// let mut r = HashMap::new();
|
||||||
|
// r.insert(
|
||||||
|
// Name::of::<FailureExternalNavigation>(),
|
||||||
|
// String::from("https://dioxuslabs.com/"),
|
||||||
|
// );
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert_eq!(state.path, String::from("/fixed"));
|
||||||
|
// assert!(!state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn subscribe() {
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// Segment::empty(),
|
||||||
|
// Box::<MemoryHistory>::default(),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let id = Arc::new(0);
|
||||||
|
// s.subscribe(Arc::clone(&id));
|
||||||
|
|
||||||
|
// assert_eq!(s.subscribers.len(), 1);
|
||||||
|
// assert_eq!(s.subscribers[0].upgrade().unwrap(), id);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn routing_callback() {
|
||||||
|
// let paths = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
// let paths2 = Arc::clone(&paths);
|
||||||
|
|
||||||
|
// let (mut s, c, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// Some(Arc::new(move |state| {
|
||||||
|
// paths2.lock().unwrap().push(state.path.clone());
|
||||||
|
// Some("/%F0%9F%8E%BA".into())
|
||||||
|
// })),
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// assert!(paths.lock().unwrap().is_empty());
|
||||||
|
|
||||||
|
// s.init();
|
||||||
|
// assert_eq!(*paths.lock().unwrap(), vec![String::from("/fixed")]);
|
||||||
|
|
||||||
|
// c.unbounded_send(RouterMessage::Update).unwrap();
|
||||||
|
// s.run_current();
|
||||||
|
// assert_eq!(
|
||||||
|
// *paths.lock().unwrap(),
|
||||||
|
// vec![String::from("/fixed"), String::from("/%F0%9F%8E%BA")]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("🎺")])
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn url_decoding_do() {
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::new(MemoryHistory::with_initial_path("/%F0%9F%A5%B3").unwrap()),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.init();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert!(state.content.is_empty());
|
||||||
|
// assert!(state.names.is_empty());
|
||||||
|
// assert_eq!(state.parameters, {
|
||||||
|
// let mut r = HashMap::new();
|
||||||
|
// r.insert(Name::of::<bool>(), String::from("🥳"));
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert_eq!(state.path, String::from("/%F0%9F%A5%B3"));
|
||||||
|
// assert!(!state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn url_decoding_do_not() {
|
||||||
|
// let (mut s, c, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::new(MemoryHistory::with_initial_path("/%F0%9F%8E%BA").unwrap()),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.init();
|
||||||
|
// c.unbounded_send(RouterMessage::Update).unwrap();
|
||||||
|
// s.run_current();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("🎺")]);
|
||||||
|
// assert!(state.names.is_empty());
|
||||||
|
// assert!(state.parameters.is_empty());
|
||||||
|
// assert_eq!(state.path, String::from("/%F0%9F%8E%BA"));
|
||||||
|
// assert!(!state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn prefix() {
|
||||||
|
// struct TestHistory {}
|
||||||
|
|
||||||
|
// impl HistoryProvider for TestHistory {
|
||||||
|
// fn current_path(&self) -> String {
|
||||||
|
// String::from("/")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn current_query(&self) -> Option<String> {
|
||||||
|
// None
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn current_prefix(&self) -> Option<String> {
|
||||||
|
// Some(String::from("/prefix"))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn can_go_back(&self) -> bool {
|
||||||
|
// false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn can_go_forward(&self) -> bool {
|
||||||
|
// false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn go_back(&mut self) {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn go_forward(&mut self) {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn push(&mut self, _path: String) {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn replace(&mut self, _path: String) {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
|
||||||
|
// callback();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||||
|
// test_segment(),
|
||||||
|
// Box::new(TestHistory {}),
|
||||||
|
// Arc::new(|_| {}),
|
||||||
|
// None,
|
||||||
|
// ContentAtom("external target"),
|
||||||
|
// ContentAtom("named target"),
|
||||||
|
// ContentAtom("redirect limit"),
|
||||||
|
// );
|
||||||
|
// s.init();
|
||||||
|
|
||||||
|
// let state = s.state.try_read().unwrap();
|
||||||
|
// assert_eq!(state.content, vec![ContentAtom("index")]);
|
||||||
|
// assert_eq!(state.names, {
|
||||||
|
// let mut r = HashSet::new();
|
||||||
|
// r.insert(Name::of::<RootIndex>());
|
||||||
|
// r
|
||||||
|
// });
|
||||||
|
// assert!(state.parameters.is_empty());
|
||||||
|
// assert_eq!(state.path, String::from("/"));
|
||||||
|
// assert!(state.query.is_none());
|
||||||
|
// assert_eq!(state.prefix, Some(String::from("/prefix")));
|
||||||
|
// assert!(!state.can_go_back);
|
||||||
|
// assert!(!state.can_go_forward);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -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());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -21,6 +21,8 @@ mod web_hash;
|
|||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
pub use web_hash::*;
|
pub use web_hash::*;
|
||||||
|
|
||||||
|
use crate::routable::Routable;
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
#[cfg(feature = "web")]
|
||||||
pub(crate) mod web_scroll;
|
pub(crate) mod web_scroll;
|
||||||
|
|
||||||
@@ -32,7 +34,7 @@ pub(crate) mod web_scroll;
|
|||||||
/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
|
/// 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
|
/// The described behaviors are designed to mimic a web browser, which most users should already
|
||||||
/// know. Deviations might confuse them.
|
/// know. Deviations might confuse them.
|
||||||
pub trait HistoryProvider {
|
pub trait HistoryProvider<R: Routable> {
|
||||||
/// Get the path of the current URL.
|
/// Get the path of the current URL.
|
||||||
///
|
///
|
||||||
/// **Must start** with `/`. **Must _not_ contain** the prefix.
|
/// **Must start** with `/`. **Must _not_ contain** the prefix.
|
||||||
@@ -46,20 +48,8 @@ pub trait HistoryProvider {
|
|||||||
/// assert_eq!(history.current_path(), "/path");
|
/// assert_eq!(history.current_path(), "/path");
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn current_path(&self) -> String;
|
fn current_route(&self) -> &R;
|
||||||
/// Get the query string of the current URL.
|
|
||||||
///
|
|
||||||
/// **Must _not_** start with `?`.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
|
||||||
/// let mut history = MemoryHistory::default();
|
|
||||||
/// assert_eq!(history.current_query(), None);
|
|
||||||
///
|
|
||||||
/// history.push(String::from("?some=value"));
|
|
||||||
/// assert_eq!(history.current_query(), Some("some=value".to_string()));
|
|
||||||
/// ```
|
|
||||||
fn current_query(&self) -> Option<String>;
|
|
||||||
/// Get the current path prefix of the URL.
|
/// Get the current path prefix of the URL.
|
||||||
///
|
///
|
||||||
/// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a
|
/// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a
|
||||||
@@ -87,6 +77,7 @@ pub trait HistoryProvider {
|
|||||||
fn can_go_back(&self) -> bool {
|
fn can_go_back(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Go back to a previous page.
|
/// Go back to a previous page.
|
||||||
///
|
///
|
||||||
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
|
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
|
||||||
@@ -127,6 +118,7 @@ pub trait HistoryProvider {
|
|||||||
fn can_go_forward(&self) -> bool {
|
fn can_go_forward(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Go forward to a future page.
|
/// Go forward to a future page.
|
||||||
///
|
///
|
||||||
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
|
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
|
||||||
@@ -162,7 +154,8 @@ pub trait HistoryProvider {
|
|||||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||||
/// assert!(history.can_go_back());
|
/// assert!(history.can_go_back());
|
||||||
/// ```
|
/// ```
|
||||||
fn push(&mut self, path: String);
|
fn push(&mut self, route: R);
|
||||||
|
|
||||||
/// Replace the current page with another one.
|
/// Replace the current page with another one.
|
||||||
///
|
///
|
||||||
/// This should merge the current URL with the `path` parameter (which may also include a query
|
/// This should merge the current URL with the `path` parameter (which may also include a query
|
||||||
@@ -178,7 +171,7 @@ pub trait HistoryProvider {
|
|||||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||||
/// assert!(!history.can_go_back());
|
/// assert!(!history.can_go_back());
|
||||||
/// ```
|
/// ```
|
||||||
fn replace(&mut self, path: String);
|
fn replace(&mut self, path: R);
|
||||||
|
|
||||||
/// Navigate to an external URL.
|
/// Navigate to an external URL.
|
||||||
///
|
///
|
||||||
@@ -23,7 +23,7 @@ use super::{
|
|||||||
/// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added.
|
/// 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
|
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
|
||||||
pub struct WebHistory {
|
pub struct WebHistory<R: Serialize + DeserializeOwned> {
|
||||||
do_scroll_restoration: bool,
|
do_scroll_restoration: bool,
|
||||||
history: History,
|
history: History,
|
||||||
listener_navigation: Option<EventListener>,
|
listener_navigation: Option<EventListener>,
|
||||||
@@ -72,7 +72,7 @@ impl WebHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryProvider for WebHistory {
|
impl HistoryProvider for WebHistory {
|
||||||
fn current_path(&self) -> String {
|
fn current_route(&self) -> String {
|
||||||
let path = self
|
let path = self
|
||||||
.window
|
.window
|
||||||
.location()
|
.location()
|
||||||
+2
-2
@@ -21,7 +21,7 @@ const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
|
|||||||
/// was not, because no browser supports WebAssembly without the [History API].
|
/// was not, because no browser supports WebAssembly without the [History API].
|
||||||
///
|
///
|
||||||
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
|
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
|
||||||
pub struct WebHashHistory {
|
pub struct WebHashHistory<R: Serialize + DeserializeOwned> {
|
||||||
do_scroll_restoration: bool,
|
do_scroll_restoration: bool,
|
||||||
history: History,
|
history: History,
|
||||||
listener_navigation: Option<EventListener>,
|
listener_navigation: Option<EventListener>,
|
||||||
@@ -121,7 +121,7 @@ impl WebHashHistory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryProvider for WebHashHistory {
|
impl HistoryProvider for WebHashHistory {
|
||||||
fn current_path(&self) -> String {
|
fn current_route(&self) -> String {
|
||||||
self.url()
|
self.url()
|
||||||
.map(|url| url.path().to_string())
|
.map(|url| url.path().to_string())
|
||||||
.unwrap_or(String::from("/"))
|
.unwrap_or(String::from("/"))
|
||||||
File renamed without changes.
@@ -1,71 +0,0 @@
|
|||||||
use dioxus::prelude::{ScopeId, ScopeState};
|
|
||||||
use dioxus_router_core::Navigator;
|
|
||||||
|
|
||||||
use crate::utils::use_router_internal::use_router_internal;
|
|
||||||
|
|
||||||
/// A hook that allows for programmatic navigation.
|
|
||||||
///
|
|
||||||
/// # Return values
|
|
||||||
/// - [`RouterError::NotInsideRouter`], when the calling component is not nested within another
|
|
||||||
/// component calling the [`use_router`] hook.
|
|
||||||
/// - Otherwise [`Ok`].
|
|
||||||
///
|
|
||||||
/// [`use_router`]: crate::hooks::use_router
|
|
||||||
///
|
|
||||||
/// # Panic
|
|
||||||
/// - When the calling component is not nested within another component calling the [`use_router`]
|
|
||||||
/// hook, but only in debug builds.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus::prelude::*;
|
|
||||||
/// # use dioxus_router::prelude::*;
|
|
||||||
/// fn App(cx: Scope) -> Element {
|
|
||||||
/// let (state, _) = use_router(
|
|
||||||
/// &cx,
|
|
||||||
/// &|| RouterConfiguration {
|
|
||||||
/// synchronous: true, // asynchronicity not needed for doc test
|
|
||||||
/// ..Default::default()
|
|
||||||
/// },
|
|
||||||
/// &|| Segment::content(comp(Redirect)).fixed("content", comp(Content))
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// render! {
|
|
||||||
/// h1 { "App" }
|
|
||||||
/// Outlet { }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// fn Redirect(cx: Scope) -> Element {
|
|
||||||
/// let nav = use_navigate(&cx)?;
|
|
||||||
/// nav.push("/content");
|
|
||||||
/// render! { () }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// fn Content(cx: Scope) -> Element {
|
|
||||||
/// render! {
|
|
||||||
/// p { "Content" }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// #
|
|
||||||
/// # let mut vdom = VirtualDom::new(App);
|
|
||||||
/// #
|
|
||||||
/// # // first render with Redirect component
|
|
||||||
/// # let _ = vdom.rebuild();
|
|
||||||
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1>");
|
|
||||||
/// #
|
|
||||||
/// # // second render with Content component
|
|
||||||
/// # let _ = vdom.rebuild();
|
|
||||||
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Content</p>");
|
|
||||||
/// ```
|
|
||||||
pub fn use_navigate(cx: &ScopeState) -> Option<Navigator<ScopeId>> {
|
|
||||||
match use_router_internal(cx) {
|
|
||||||
Some(r) => Some(r.sender.clone().into()),
|
|
||||||
None => {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
panic!("`use_navigate` must have access to a parent router");
|
|
||||||
#[allow(unreachable_code)]
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
use async_lock::RwLockReadGuard;
|
use dioxus::prelude::ScopeState;
|
||||||
use dioxus::{core::Component, prelude::ScopeState};
|
|
||||||
use dioxus_router_core::RouterState;
|
|
||||||
|
|
||||||
use crate::utils::use_router_internal::use_router_internal;
|
use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
|
||||||
|
|
||||||
/// A hook that provides access to information about the current routing location.
|
/// A hook that provides access to information about the current routing location.
|
||||||
///
|
///
|
||||||
@@ -56,13 +54,9 @@ use crate::utils::use_router_internal::use_router_internal;
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// [`use_router`]: crate::hooks::use_router
|
/// [`use_router`]: crate::hooks::use_router
|
||||||
pub fn use_route(cx: &ScopeState) -> Option<RwLockReadGuard<RouterState<Component>>> {
|
pub fn use_route<R: Routable + Clone>(cx: &ScopeState) -> Option<R> {
|
||||||
match use_router_internal(cx) {
|
match use_router_internal(cx) {
|
||||||
Some(r) => loop {
|
Some(r) => Some(r.current()),
|
||||||
if let Some(s) = r.state.try_read() {
|
|
||||||
break Some(s);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
None => {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
panic!("`use_route` must have access to a parent router");
|
panic!("`use_route` must have access to a parent router");
|
||||||
|
|||||||
@@ -1,200 +1,12 @@
|
|||||||
use async_lock::RwLockReadGuard;
|
use dioxus::prelude::ScopeState;
|
||||||
use dioxus::{core::Component, prelude::*};
|
|
||||||
use dioxus_router_core::{
|
|
||||||
history::{HistoryProvider, MemoryHistory},
|
|
||||||
routes::{ContentAtom, Segment},
|
|
||||||
Navigator, RouterService, RouterState, RoutingCallback,
|
|
||||||
};
|
|
||||||
use log::error;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
contexts::router::RouterContext,
|
prelude::RouterContext, routable::Routable, utils::use_router_internal::use_router_internal,
|
||||||
prelude::{
|
|
||||||
comp,
|
|
||||||
default_errors::{
|
|
||||||
FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The basic building block required for all other router components and hooks.
|
/// A hook that provides access to information about the router
|
||||||
///
|
pub fn use_router<R: Routable + Clone>(cx: &ScopeState) -> &RouterContext<R> {
|
||||||
/// This manages a [`dioxus_router_core::RouterService`], which in turn is required for basically
|
use_router_internal(cx)
|
||||||
/// all router functionality. All other components and hooks provided by [`dioxus_router`](crate)
|
.as_ref()
|
||||||
/// will only work as/in components nested within a component calling [`use_router`].
|
.expect("use_route must have access to a router")
|
||||||
///
|
|
||||||
/// Components calling [`use_router`] should not be nested within each other.
|
|
||||||
///
|
|
||||||
/// # Return values
|
|
||||||
/// This hook returns the current router state and a navigator. For more information about the
|
|
||||||
/// state, see the [`use_route`](crate::hooks::use_route) hook. For more information about the
|
|
||||||
/// [`Navigator`], see its own documentation and the [`use_navigate`](crate::hooks::use_navigate)
|
|
||||||
/// hook.
|
|
||||||
///
|
|
||||||
/// # Panic
|
|
||||||
/// - When used within a component, that is nested inside another component calling [`use_router`],
|
|
||||||
/// but only in debug builds.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// # use dioxus::prelude::*;
|
|
||||||
/// # use dioxus_router::prelude::*;
|
|
||||||
/// fn App(cx: Scope) -> Element {
|
|
||||||
/// let (_, _) = use_router(
|
|
||||||
/// &cx,
|
|
||||||
/// &|| RouterConfiguration {
|
|
||||||
/// synchronous: true, // asynchronicity not needed for doc test
|
|
||||||
/// ..Default::default()
|
|
||||||
/// },
|
|
||||||
/// &|| Segment::content(comp(Content))
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// render! {
|
|
||||||
/// h1 { "App" }
|
|
||||||
/// Outlet { }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// fn Content(cx: Scope) -> Element {
|
|
||||||
/// render! {
|
|
||||||
/// p { "Some content" }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// # let mut vdom = VirtualDom::new(App);
|
|
||||||
/// # let _ = vdom.rebuild();
|
|
||||||
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Some content</p>");
|
|
||||||
/// ```
|
|
||||||
pub fn use_router<'a>(
|
|
||||||
cx: &'a ScopeState,
|
|
||||||
cfg: &dyn Fn() -> RouterConfiguration,
|
|
||||||
content: &dyn Fn() -> Segment<Component>,
|
|
||||||
) -> (
|
|
||||||
RwLockReadGuard<'a, RouterState<Component>>,
|
|
||||||
Navigator<ScopeId>,
|
|
||||||
) {
|
|
||||||
let (service, state, sender) = cx.use_hook(|| {
|
|
||||||
#[allow(unreachable_code, unused_variables)]
|
|
||||||
if let Some(outer) = cx.consume_context::<RouterContext>() {
|
|
||||||
let msg = "components using `use_router` should not be nested within each other";
|
|
||||||
error!("{msg}, inner will be inactive and transparent");
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
panic!("{}", msg);
|
|
||||||
return (None, outer.state, outer.sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
let cfg = cfg();
|
|
||||||
let content = content();
|
|
||||||
|
|
||||||
let (mut service, sender, state) = RouterService::new(
|
|
||||||
content,
|
|
||||||
cfg.history,
|
|
||||||
cx.schedule_update_any(),
|
|
||||||
cfg.on_update,
|
|
||||||
cfg.failure_external_navigation,
|
|
||||||
cfg.failure_named_navigation,
|
|
||||||
cfg.failure_redirection_limit,
|
|
||||||
);
|
|
||||||
|
|
||||||
cx.provide_context(RouterContext {
|
|
||||||
state: state.clone(),
|
|
||||||
sender: sender.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
(
|
|
||||||
if cfg.synchronous {
|
|
||||||
service.init();
|
|
||||||
Some(service)
|
|
||||||
} else {
|
|
||||||
cx.spawn(async move { service.run().await });
|
|
||||||
None
|
|
||||||
},
|
|
||||||
state,
|
|
||||||
sender,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(service) = service {
|
|
||||||
service.run_current();
|
|
||||||
}
|
|
||||||
|
|
||||||
(
|
|
||||||
loop {
|
|
||||||
if let Some(state) = state.try_read() {
|
|
||||||
break state;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sender.clone().into(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Global configuration options for the router.
|
|
||||||
///
|
|
||||||
/// This implements [`Default`], so you can use it like this:
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use dioxus_router::prelude::RouterConfiguration;
|
|
||||||
/// let cfg = RouterConfiguration {
|
|
||||||
/// synchronous: false,
|
|
||||||
/// ..Default::default()
|
|
||||||
/// };
|
|
||||||
/// ```
|
|
||||||
pub struct RouterConfiguration {
|
|
||||||
/// A component to render when an external navigation fails.
|
|
||||||
///
|
|
||||||
/// Defaults to a router-internal component called `FailureExternalNavigation`. It is not part
|
|
||||||
/// of the public API. Do not confuse it with
|
|
||||||
/// [`dioxus_router_core::prelude::FailureExternalNavigation`].
|
|
||||||
pub failure_external_navigation: ContentAtom<Component>,
|
|
||||||
/// A component to render when a named navigation fails.
|
|
||||||
///
|
|
||||||
/// Defaults to a router-internal component called `FailureNamedNavigation`. It is not part of
|
|
||||||
/// the public API. Do not confuse it with
|
|
||||||
/// [`dioxus_router_core::prelude::FailureNamedNavigation`].
|
|
||||||
pub failure_named_navigation: ContentAtom<Component>,
|
|
||||||
/// A component to render when the redirect limit is reached.
|
|
||||||
///
|
|
||||||
/// Defaults to a router-internal component called `FailureRedirectionLimit`. It is not part of
|
|
||||||
/// the public API. Do not confuse it with
|
|
||||||
/// [`dioxus_router_core::prelude::FailureRedirectionLimit`].
|
|
||||||
pub failure_redirection_limit: ContentAtom<Component>,
|
|
||||||
/// The [`HistoryProvider`] the router should use.
|
|
||||||
///
|
|
||||||
/// Defaults to a default [`MemoryHistory`].
|
|
||||||
pub history: Box<dyn HistoryProvider>,
|
|
||||||
/// A function to be called whenever the routing is updated.
|
|
||||||
///
|
|
||||||
/// The callback is invoked after the routing is updated, but before components and hooks are
|
|
||||||
/// updated.
|
|
||||||
///
|
|
||||||
/// If the callback returns a [`NavigationTarget`] the router will replace the current location
|
|
||||||
/// with it. If no navigation failure was triggered, the router will then updated dependent
|
|
||||||
/// components and hooks.
|
|
||||||
///
|
|
||||||
/// The callback is called no more than once per rerouting. It will not be called if a
|
|
||||||
/// navigation failure occurs.
|
|
||||||
///
|
|
||||||
/// Defaults to [`None`].
|
|
||||||
///
|
|
||||||
/// [`NavigationTarget`]: dioxus_router_core::navigation::NavigationTarget
|
|
||||||
pub on_update: Option<RoutingCallback<Component>>,
|
|
||||||
/// Whether the router should run in synchronous mode.
|
|
||||||
///
|
|
||||||
/// If [`true`], the router will only update its state whenever the component using the
|
|
||||||
/// [`use_router`] hook rerenders. If [`false`], an asynchronous task is launched and the router
|
|
||||||
/// will update whenever it receives new input.
|
|
||||||
///
|
|
||||||
/// Defaults to [`false`].
|
|
||||||
pub synchronous: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RouterConfiguration {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
failure_external_navigation: comp(FailureExternalNavigation),
|
|
||||||
failure_named_navigation: comp(FailureNamedNavigation),
|
|
||||||
failure_redirection_limit: comp(FailureRedirectionLimit),
|
|
||||||
history: Box::<MemoryHistory>::default(),
|
|
||||||
on_update: None,
|
|
||||||
synchronous: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+14
-14
@@ -1,6 +1,10 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
// cannot use forbid, because props derive macro generates #[allow(missing_docs)]
|
// cannot use forbid, because props derive macro generates #[allow(missing_docs)]
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
pub mod navigation;
|
||||||
|
pub mod routable;
|
||||||
|
|
||||||
/// Components interacting with the router.
|
/// Components interacting with the router.
|
||||||
pub mod components {
|
pub mod components {
|
||||||
@@ -14,19 +18,23 @@ pub mod components {
|
|||||||
|
|
||||||
mod outlet;
|
mod outlet;
|
||||||
pub use outlet::*;
|
pub use outlet::*;
|
||||||
|
|
||||||
|
mod router;
|
||||||
|
pub use router::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
mod contexts {
|
mod contexts {
|
||||||
|
pub(crate) mod outlet;
|
||||||
pub(crate) mod router;
|
pub(crate) mod router;
|
||||||
|
pub use router::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use dioxus_router_core::history;
|
mod router_cfg;
|
||||||
|
|
||||||
|
pub mod history;
|
||||||
|
|
||||||
/// Hooks for interacting with the router in components.
|
/// Hooks for interacting with the router in components.
|
||||||
pub mod hooks {
|
pub mod hooks {
|
||||||
mod use_navigate;
|
|
||||||
pub use use_navigate::*;
|
|
||||||
|
|
||||||
mod use_router;
|
mod use_router;
|
||||||
pub use use_router::*;
|
pub use use_router::*;
|
||||||
|
|
||||||
@@ -36,18 +44,10 @@ pub mod hooks {
|
|||||||
|
|
||||||
/// A collection of useful items most applications might need.
|
/// A collection of useful items most applications might need.
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use dioxus_router_core::prelude::*;
|
|
||||||
|
|
||||||
pub use crate::components::*;
|
pub use crate::components::*;
|
||||||
|
pub use crate::contexts::*;
|
||||||
pub use crate::hooks::*;
|
pub use crate::hooks::*;
|
||||||
|
pub use dioxus_router_macro::routable;
|
||||||
/// Wrap a [`Component`](dioxus::core::Component) inside a [`ContentAtom`].
|
|
||||||
///
|
|
||||||
/// This is purely a convenience function.
|
|
||||||
#[must_use]
|
|
||||||
pub fn comp(component: dioxus::core::Component) -> ContentAtom<dioxus::core::Component> {
|
|
||||||
ContentAtom(component)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mod utils {
|
mod utils {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
//! Types pertaining to navigation.
|
||||||
|
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use url::{ParseError, Url};
|
||||||
|
|
||||||
|
use crate::routable::Routable;
|
||||||
|
|
||||||
|
/// A target for the router to navigate to.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum NavigationTarget<R: Routable> {
|
||||||
|
/// An internal path that the router can navigate to by itself.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use dioxus_router_core::navigation::NavigationTarget;
|
||||||
|
/// let explicit = NavigationTarget::Internal(String::from("/internal"));
|
||||||
|
/// let implicit: NavigationTarget = "/internal".into();
|
||||||
|
/// assert_eq!(explicit, implicit);
|
||||||
|
/// ```
|
||||||
|
Internal(R),
|
||||||
|
/// An external target that the router doesn't control.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use dioxus_router_core::navigation::NavigationTarget;
|
||||||
|
/// let explicit = NavigationTarget::External(String::from("https://dioxuslabs.com/"));
|
||||||
|
/// let implicit: NavigationTarget = "https://dioxuslabs.com/".into();
|
||||||
|
/// assert_eq!(explicit, implicit);
|
||||||
|
/// ```
|
||||||
|
External(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> From<R> for NavigationTarget<R> {
|
||||||
|
fn from(value: R) -> Self {
|
||||||
|
Self::Internal(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> Display for NavigationTarget<R> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
NavigationTarget::Internal(r) => write!(f, "{}", r),
|
||||||
|
NavigationTarget::External(s) => write!(f, "{}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that can occur when parsing a [`NavigationTarget`].
|
||||||
|
pub enum NavigationTargetParseError<R: Routable> {
|
||||||
|
/// A URL that is not valid.
|
||||||
|
InvalidUrl(ParseError),
|
||||||
|
/// An internal URL that is not valid.
|
||||||
|
InvalidInternalURL(<R as FromStr>::Err),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> FromStr for NavigationTarget<R> {
|
||||||
|
type Err = NavigationTargetParseError<R>;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match Url::parse(s) {
|
||||||
|
Ok(_) => Ok(Self::External(s.to_string())),
|
||||||
|
Err(ParseError::RelativeUrlWithoutBase) => {
|
||||||
|
Ok(Self::Internal(R::from_str(s).map_err(|e| {
|
||||||
|
NavigationTargetParseError::InvalidInternalURL(e)
|
||||||
|
})?))
|
||||||
|
}
|
||||||
|
Err(e) => Err(NavigationTargetParseError::InvalidUrl(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
|
//! # Routable
|
||||||
|
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
use crate::history::HistoryProvider;
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use std::{cell::RefCell, rc::Rc, str::FromStr, sync::Arc};
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// An error that occurs when parsing a route
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct RouteParseError<E: std::fmt::Display> {
|
pub struct RouteParseError<E: std::fmt::Display> {
|
||||||
|
/// The attempted routes that failed to match
|
||||||
pub attempted_routes: Vec<E>,
|
pub attempted_routes: Vec<E>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,39 +22,9 @@ impl<E: std::fmt::Display> std::fmt::Display for RouteParseError<E> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
/// Something that can be created from a query string
|
||||||
pub struct Router {
|
|
||||||
subscribers: Rc<RefCell<Vec<ScopeId>>>,
|
|
||||||
update_any: Arc<dyn Fn(ScopeId)>,
|
|
||||||
history: Rc<dyn HistoryProvider>,
|
|
||||||
route: Rc<RefCell<Option<Rc<dyn RouteRenderable>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Router {
|
|
||||||
fn set_route<R: Routable + 'static>(&self, route: R)
|
|
||||||
where
|
|
||||||
R::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
*self.route.borrow_mut() = Some(Rc::new(route));
|
|
||||||
for subscriber in self.subscribers.borrow().iter() {
|
|
||||||
(self.update_any)(*subscriber);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn use_router(cx: &ScopeState) -> &Router {
|
|
||||||
use_context(cx).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn use_route(cx: &ScopeState) -> Rc<dyn RouteRenderable> {
|
|
||||||
let router = use_router(cx);
|
|
||||||
cx.use_hook(|| {
|
|
||||||
router.subscribers.borrow_mut().push(cx.scope_id());
|
|
||||||
});
|
|
||||||
router.route.borrow().clone().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait FromQuery {
|
pub trait FromQuery {
|
||||||
|
/// Create an instance of `Self` from a query string
|
||||||
fn from_query(query: &str) -> Self;
|
fn from_query(query: &str) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,9 +34,12 @@ impl<T: for<'a> From<&'a str>> FromQuery for T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Something that can be created from a route segment
|
||||||
pub trait FromRouteSegment: Sized {
|
pub trait FromRouteSegment: Sized {
|
||||||
|
/// The error that can occur when parsing a route segment
|
||||||
type Err;
|
type Err;
|
||||||
|
|
||||||
|
/// Create an instance of `Self` from a route segment
|
||||||
fn from_route_segment(route: &str) -> Result<Self, Self::Err>;
|
fn from_route_segment(route: &str) -> Result<Self, Self::Err>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +54,9 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Something that can be converted to route segments
|
||||||
pub trait ToRouteSegments {
|
pub trait ToRouteSegments {
|
||||||
|
/// Display the route segments
|
||||||
fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
|
fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,9 +73,12 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Something that can be created from route segments
|
||||||
pub trait FromRouteSegments: Sized {
|
pub trait FromRouteSegments: Sized {
|
||||||
|
/// The error that can occur when parsing route segments
|
||||||
type Err;
|
type Err;
|
||||||
|
|
||||||
|
/// Create an instance of `Self` from route segments
|
||||||
fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err>;
|
fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,14 +93,9 @@ impl<I: std::iter::FromIterator<String>> FromRouteSegments for I {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Props, PartialEq)]
|
/// Something that can be routed to
|
||||||
pub struct RouterProps {
|
pub trait Routable: std::fmt::Display + std::str::FromStr + 'static {
|
||||||
pub current_route: String,
|
/// Render the route at the given level
|
||||||
}
|
|
||||||
pub trait Routable: std::fmt::Display + std::str::FromStr + 'static
|
|
||||||
where
|
|
||||||
<Self as FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
|
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,63 +124,3 @@ where
|
|||||||
self.render(cx, level)
|
self.render(cx, level)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct OutletContext {
|
|
||||||
current_level: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn use_outlet_context(cx: &ScopeState) -> &OutletContext {
|
|
||||||
let outlet_context = use_context(cx).unwrap();
|
|
||||||
outlet_context
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutletContext {
|
|
||||||
fn render(cx: &ScopeState) -> Element<'_> {
|
|
||||||
let outlet = use_outlet_context(cx);
|
|
||||||
let current_level = outlet.current_level;
|
|
||||||
cx.provide_context({
|
|
||||||
OutletContext {
|
|
||||||
current_level: current_level + 1,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
use_route(cx).render(cx, current_level)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn Outlet(cx: Scope) -> Element {
|
|
||||||
OutletContext::render(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn Router<R: Routable, H: HistoryProvider + Default + 'static>(
|
|
||||||
cx: Scope<RouterProps>,
|
|
||||||
) -> Element
|
|
||||||
where
|
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
let current_route = R::from_str(&cx.props.current_route);
|
|
||||||
let router = use_context_provider(cx, || Router {
|
|
||||||
subscribers: Rc::default(),
|
|
||||||
update_any: cx.schedule_update_any(),
|
|
||||||
history: Rc::<H>::default(),
|
|
||||||
route: Rc::new(RefCell::new(None)),
|
|
||||||
});
|
|
||||||
|
|
||||||
use_context_provider(cx, || OutletContext { current_level: 1 });
|
|
||||||
|
|
||||||
match current_route {
|
|
||||||
Ok(current_route) => {
|
|
||||||
router.set_route(current_route);
|
|
||||||
|
|
||||||
router.route.borrow().as_ref().unwrap().render(cx, 0)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
render! {
|
|
||||||
pre {
|
|
||||||
"{err}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use crate::contexts::router::RoutingCallback;
|
||||||
|
use crate::history::{HistoryProvider, MemoryHistory};
|
||||||
|
use crate::routable::Routable;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use crate::prelude::default_errors::{
|
||||||
|
FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Global configuration options for the router.
|
||||||
|
///
|
||||||
|
/// This implements [`Default`], so you can use it like this:
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use dioxus_router::prelude::RouterConfiguration;
|
||||||
|
/// let cfg = RouterConfiguration {
|
||||||
|
/// synchronous: false,
|
||||||
|
/// ..Default::default()
|
||||||
|
/// };
|
||||||
|
/// ```
|
||||||
|
pub struct RouterConfiguration<R: Routable> {
|
||||||
|
/// A component to render when an external navigation fails.
|
||||||
|
///
|
||||||
|
/// Defaults to a router-internal component called `FailureExternalNavigation`. It is not part
|
||||||
|
/// of the public API. Do not confuse it with
|
||||||
|
/// [`dioxus_router_core::prelude::FailureExternalNavigation`].
|
||||||
|
pub failure_external_navigation: fn(Scope) -> Element,
|
||||||
|
/// A component to render when a named navigation fails.
|
||||||
|
///
|
||||||
|
/// Defaults to a router-internal component called `FailureNamedNavigation`. It is not part of
|
||||||
|
/// the public API. Do not confuse it with
|
||||||
|
/// [`dioxus_router_core::prelude::FailureNamedNavigation`].
|
||||||
|
pub failure_named_navigation: fn(Scope) -> Element,
|
||||||
|
/// A component to render when the redirect limit is reached.
|
||||||
|
///
|
||||||
|
/// Defaults to a router-internal component called `FailureRedirectionLimit`. It is not part of
|
||||||
|
/// the public API. Do not confuse it with
|
||||||
|
/// [`dioxus_router_core::prelude::FailureRedirectionLimit`].
|
||||||
|
pub failure_redirection_limit: fn(Scope) -> Element,
|
||||||
|
/// The [`HistoryProvider`] the router should use.
|
||||||
|
///
|
||||||
|
/// Defaults to a default [`MemoryHistory`].
|
||||||
|
pub history: Box<dyn HistoryProvider<R>>,
|
||||||
|
/// A function to be called whenever the routing is updated.
|
||||||
|
///
|
||||||
|
/// The callback is invoked after the routing is updated, but before components and hooks are
|
||||||
|
/// updated.
|
||||||
|
///
|
||||||
|
/// If the callback returns a [`NavigationTarget`] the router will replace the current location
|
||||||
|
/// with it. If no navigation failure was triggered, the router will then updated dependent
|
||||||
|
/// components and hooks.
|
||||||
|
///
|
||||||
|
/// The callback is called no more than once per rerouting. It will not be called if a
|
||||||
|
/// navigation failure occurs.
|
||||||
|
///
|
||||||
|
/// Defaults to [`None`].
|
||||||
|
///
|
||||||
|
/// [`NavigationTarget`]: dioxus_router_core::navigation::NavigationTarget
|
||||||
|
pub on_update: Option<RoutingCallback<R>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable + Clone> Default for RouterConfiguration<R> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
failure_external_navigation: FailureExternalNavigation::<R>,
|
||||||
|
failure_named_navigation: FailureNamedNavigation::<R>,
|
||||||
|
failure_redirection_limit: FailureRedirectionLimit::<R>,
|
||||||
|
history: Box::<MemoryHistory<R>>::default(),
|
||||||
|
on_update: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -1,9 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use dioxus::prelude::{ScopeId, ScopeState};
|
||||||
|
|
||||||
use dioxus::prelude::ScopeState;
|
use crate::{contexts::router::RouterContext, routable::Routable};
|
||||||
use dioxus_router_core::RouterMessage;
|
|
||||||
|
|
||||||
use crate::contexts::router::RouterContext;
|
|
||||||
|
|
||||||
/// A private hook to subscribe to the router.
|
/// A private hook to subscribe to the router.
|
||||||
///
|
///
|
||||||
@@ -15,16 +12,25 @@ use crate::contexts::router::RouterContext;
|
|||||||
/// - Otherwise [`Some`].
|
/// - Otherwise [`Some`].
|
||||||
///
|
///
|
||||||
/// [`use_router`]: crate::hooks::use_router
|
/// [`use_router`]: crate::hooks::use_router
|
||||||
pub(crate) fn use_router_internal(cx: &ScopeState) -> &mut Option<RouterContext> {
|
pub(crate) fn use_router_internal<R: Routable>(cx: &ScopeState) -> &Option<RouterContext<R>> {
|
||||||
let id = cx.use_hook(|| Arc::new(cx.scope_id()));
|
let inner = cx.use_hook(|| {
|
||||||
|
let router = cx.consume_context::<RouterContext<R>>()?;
|
||||||
|
|
||||||
cx.use_hook(|| {
|
let id = cx.scope_id();
|
||||||
let router = cx.consume_context::<RouterContext>()?;
|
router.subscribe(id);
|
||||||
|
|
||||||
let _ = router
|
Some(Subscription { router, id })
|
||||||
.sender
|
});
|
||||||
.unbounded_send(RouterMessage::Subscribe(id.clone()));
|
cx.use_hook(|| inner.as_ref().map(|s| s.router.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
Some(router)
|
struct Subscription<R: Routable> {
|
||||||
})
|
router: RouterContext<R>,
|
||||||
|
id: ScopeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> Drop for Subscription<R> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.router.unsubscribe(self.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user