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/core",
|
||||
"packages/core-macro",
|
||||
"packages/router-core",
|
||||
"packages/router-macro",
|
||||
"packages/router",
|
||||
"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 {
|
||||
type Err = RouteParseError<#error_name>;
|
||||
type Err = dioxus_router::routable::RouteParseError<#error_name>;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let route = s.strip_prefix('/').unwrap_or(s);
|
||||
@@ -200,7 +200,7 @@ impl RouteEnum {
|
||||
|
||||
#(#tokens)*
|
||||
|
||||
Err(RouteParseError {
|
||||
Err(dioxus_router::routable::RouteParseError {
|
||||
attempted_routes: errors,
|
||||
})
|
||||
}
|
||||
@@ -289,7 +289,7 @@ impl RouteEnum {
|
||||
let index_iter = 0..layers.len();
|
||||
|
||||
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> {
|
||||
let myself = self.clone();
|
||||
match level {
|
||||
|
||||
@@ -90,7 +90,7 @@ impl Nest {
|
||||
}
|
||||
RouteSegment::Dynamic(ident, ty) => {
|
||||
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)? });
|
||||
error_variants.push(quote! { #missing_error });
|
||||
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 ty = &self.ty;
|
||||
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) => {
|
||||
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)? });
|
||||
error_variants.push(quote! { #missing_error });
|
||||
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#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)? });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ impl RouteSegment {
|
||||
{
|
||||
let mut segments = segments.clone();
|
||||
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 {
|
||||
Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#missing_error_name))
|
||||
};
|
||||
@@ -101,7 +101,7 @@ impl RouteSegment {
|
||||
let parsed = {
|
||||
let mut segments = segments.clone();
|
||||
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 {
|
||||
Ok(#name) => {
|
||||
|
||||
@@ -12,19 +12,16 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
async-lock = "2.7.0"
|
||||
dioxus = { path="../dioxus" }
|
||||
dioxus-router-core = { path = "../router-core"}
|
||||
futures-channel = "0.3.25"
|
||||
futures-util = "0.3.25"
|
||||
dioxus-router-macro = { path="../router-macro" }
|
||||
log = "0.4.17"
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
thiserror = "1.0.37"
|
||||
url = "2.3.1"
|
||||
|
||||
[features]
|
||||
regex = ["dioxus-router-core/regex"]
|
||||
serde = ["dioxus-router-core/serde"]
|
||||
wasm_test = []
|
||||
web = ["dioxus-router-core/web"]
|
||||
web = []
|
||||
|
||||
[dev-dependencies]
|
||||
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_router_core::prelude::{named, FailureExternalNavigation as FENName, RootIndex};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn FailureExternalNavigation(cx: Scope) -> Element {
|
||||
let state = use_route(cx).expect(
|
||||
pub fn FailureExternalNavigation<R: Routable + Clone>(cx: Scope) -> Element {
|
||||
let href = use_route::<R>(cx).expect(
|
||||
"`FailureExternalNavigation` can only be mounted by the router itself, \
|
||||
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! {
|
||||
h1 { "External Navigation Failure!" }
|
||||
@@ -28,7 +23,7 @@ pub fn FailureExternalNavigation(cx: Scope) -> Element {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn FailureNamedNavigation(cx: Scope) -> Element {
|
||||
pub fn FailureNamedNavigation<R: Routable + Clone>(cx: Scope) -> Element {
|
||||
render! {
|
||||
h1 { "Named Navigation Failure!" }
|
||||
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 "
|
||||
"there is no guarantee."
|
||||
}
|
||||
Link {
|
||||
target: named::<RootIndex>(),
|
||||
Link::<R> {
|
||||
target: NavigationTarget::External("https://google.com".into()),
|
||||
"Click here to try to fix the failure."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn FailureRedirectionLimit(cx: Scope) -> Element {
|
||||
pub fn FailureRedirectionLimit<R: Routable + Clone>(cx: Scope) -> Element {
|
||||
render! {
|
||||
h1 { "Redirection Limit Failure!" }
|
||||
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 "
|
||||
"there is no guarantee."
|
||||
}
|
||||
Link {
|
||||
target: named::<RootIndex>(),
|
||||
Link::<R> {
|
||||
target: NavigationTarget::External("https://google.com".into()),
|
||||
"Click here to try to fix the failure."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router_core::RouterMessage;
|
||||
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`].
|
||||
#[derive(Debug, Props)]
|
||||
@@ -52,11 +51,11 @@ pub struct HistoryButtonProps<'a> {
|
||||
/// # );
|
||||
/// ```
|
||||
#[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;
|
||||
|
||||
// hook up to router
|
||||
let router = match use_router_internal(cx) {
|
||||
let router = match use_router_internal::<R>(cx) {
|
||||
Some(r) => r,
|
||||
#[allow(unreachable_code)]
|
||||
None => {
|
||||
@@ -67,20 +66,14 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||
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! {
|
||||
button {
|
||||
disabled: "{disabled}",
|
||||
prevent_default: "onclick",
|
||||
onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoBack); },
|
||||
onclick: move |_| router.go_back(),
|
||||
children
|
||||
}
|
||||
}
|
||||
@@ -127,11 +120,11 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||
/// # );
|
||||
/// ```
|
||||
#[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;
|
||||
|
||||
// hook up to router
|
||||
let router = match use_router_internal(cx) {
|
||||
let router = match use_router_internal::<R>(cx) {
|
||||
Some(r) => r,
|
||||
#[allow(unreachable_code)]
|
||||
None => {
|
||||
@@ -142,20 +135,14 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||
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! {
|
||||
button {
|
||||
disabled: "{disabled}",
|
||||
prevent_default: "onclick",
|
||||
onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoForward); },
|
||||
onclick: move |_| router.go_forward(),
|
||||
children
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router_core::{navigation::NavigationTarget, RouterMessage};
|
||||
use log::error;
|
||||
|
||||
use crate::navigation::NavigationTarget;
|
||||
use crate::routable::Routable;
|
||||
use crate::utils::use_router_internal::use_router_internal;
|
||||
|
||||
/// The properties for a [`Link`].
|
||||
#[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.
|
||||
pub active_class: Option<&'a str>,
|
||||
/// 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
|
||||
/// appended at the end of `class`.
|
||||
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.
|
||||
pub id: Option<&'a str>,
|
||||
/// 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>,
|
||||
/// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
|
||||
#[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 {
|
||||
f.debug_struct("LinkProps")
|
||||
.field("active_class", &self.active_class)
|
||||
.field("children", &self.children)
|
||||
.field("class", &self.class)
|
||||
.field("exact", &self.exact)
|
||||
.field("id", &self.id)
|
||||
.field("new_tab", &self.new_tab)
|
||||
.field("onclick", &self.onclick.as_ref().map(|_| "onclick is set"))
|
||||
.field("onclick_only", &self.onclick_only)
|
||||
.field("rel", &self.rel)
|
||||
.field("target", &self.target)
|
||||
.field("target", &self.target.to_string())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -130,12 +125,11 @@ impl Debug for LinkProps<'_> {
|
||||
/// # );
|
||||
/// ```
|
||||
#[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 {
|
||||
active_class,
|
||||
children,
|
||||
class,
|
||||
exact,
|
||||
id,
|
||||
new_tab,
|
||||
onclick,
|
||||
@@ -145,7 +139,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
||||
} = cx.props;
|
||||
|
||||
// hook up to router
|
||||
let router = match use_router_internal(cx) {
|
||||
let router = match use_router_internal::<R>(cx) {
|
||||
Some(r) => r,
|
||||
#[allow(unreachable_code)]
|
||||
None => {
|
||||
@@ -156,19 +150,19 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
||||
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
|
||||
.and_then(|active_class| {
|
||||
state
|
||||
.is_at(target, *exact)
|
||||
.then(|| format!(" {active_class}"))
|
||||
.and_then(|active_class| match target {
|
||||
NavigationTarget::Internal(target) => {
|
||||
if href == target.to_string() {
|
||||
Some(format!(" {active_class}"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.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 action = move |event| {
|
||||
if do_default && is_router_nav {
|
||||
let _ = sender.unbounded_send(RouterMessage::Push(target.clone()));
|
||||
router.push(target.clone());
|
||||
}
|
||||
|
||||
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::{core::Component, prelude::ScopeId};
|
||||
use dioxus_router_core::{RouterMessage, RouterState};
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RouterContext {
|
||||
pub(crate) state: Arc<RwLock<RouterState<Component>>>,
|
||||
pub(crate) sender: UnboundedSender<RouterMessage<ScopeId>>,
|
||||
use crate::{
|
||||
history::HistoryProvider, navigation::NavigationTarget, routable::Routable,
|
||||
router_cfg::RouterConfiguration,
|
||||
};
|
||||
|
||||
/// 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")]
|
||||
pub use web_hash::*;
|
||||
|
||||
use crate::routable::Routable;
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
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.
|
||||
/// The described behaviors are designed to mimic a web browser, which most users should already
|
||||
/// know. Deviations might confuse them.
|
||||
pub trait HistoryProvider {
|
||||
pub trait HistoryProvider<R: Routable> {
|
||||
/// Get the path of the current URL.
|
||||
///
|
||||
/// **Must start** with `/`. **Must _not_ contain** the prefix.
|
||||
@@ -46,20 +48,8 @@ pub trait HistoryProvider {
|
||||
/// assert_eq!(history.current_path(), "/path");
|
||||
/// ```
|
||||
#[must_use]
|
||||
fn current_path(&self) -> String;
|
||||
/// 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>;
|
||||
fn current_route(&self) -> &R;
|
||||
|
||||
/// Get the current path prefix of the URL.
|
||||
///
|
||||
/// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a
|
||||
@@ -87,6 +77,7 @@ pub trait HistoryProvider {
|
||||
fn can_go_back(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Go back to a previous page.
|
||||
///
|
||||
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
|
||||
@@ -127,6 +118,7 @@ pub trait HistoryProvider {
|
||||
fn can_go_forward(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Go forward to a future page.
|
||||
///
|
||||
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
|
||||
@@ -162,7 +154,8 @@ pub trait HistoryProvider {
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
/// assert!(history.can_go_back());
|
||||
/// ```
|
||||
fn push(&mut self, path: String);
|
||||
fn push(&mut self, route: R);
|
||||
|
||||
/// Replace the current page with another one.
|
||||
///
|
||||
/// This should merge the current URL with the `path` parameter (which may also include a query
|
||||
@@ -178,7 +171,7 @@ pub trait HistoryProvider {
|
||||
/// assert_eq!(history.current_path(), "/some-other-page");
|
||||
/// assert!(!history.can_go_back());
|
||||
/// ```
|
||||
fn replace(&mut self, path: String);
|
||||
fn replace(&mut self, path: R);
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// [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,
|
||||
history: History,
|
||||
listener_navigation: Option<EventListener>,
|
||||
@@ -72,7 +72,7 @@ impl WebHistory {
|
||||
}
|
||||
|
||||
impl HistoryProvider for WebHistory {
|
||||
fn current_path(&self) -> String {
|
||||
fn current_route(&self) -> String {
|
||||
let path = self
|
||||
.window
|
||||
.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].
|
||||
///
|
||||
/// [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,
|
||||
history: History,
|
||||
listener_navigation: Option<EventListener>,
|
||||
@@ -121,7 +121,7 @@ impl WebHashHistory {
|
||||
}
|
||||
|
||||
impl HistoryProvider for WebHashHistory {
|
||||
fn current_path(&self) -> String {
|
||||
fn current_route(&self) -> String {
|
||||
self.url()
|
||||
.map(|url| url.path().to_string())
|
||||
.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::{core::Component, prelude::ScopeState};
|
||||
use dioxus_router_core::RouterState;
|
||||
use dioxus::prelude::ScopeState;
|
||||
|
||||
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.
|
||||
///
|
||||
@@ -56,13 +54,9 @@ use crate::utils::use_router_internal::use_router_internal;
|
||||
/// ```
|
||||
///
|
||||
/// [`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) {
|
||||
Some(r) => loop {
|
||||
if let Some(s) = r.state.try_read() {
|
||||
break Some(s);
|
||||
}
|
||||
},
|
||||
Some(r) => Some(r.current()),
|
||||
None => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("`use_route` must have access to a parent router");
|
||||
|
||||
@@ -1,200 +1,12 @@
|
||||
use async_lock::RwLockReadGuard;
|
||||
use dioxus::{core::Component, prelude::*};
|
||||
use dioxus_router_core::{
|
||||
history::{HistoryProvider, MemoryHistory},
|
||||
routes::{ContentAtom, Segment},
|
||||
Navigator, RouterService, RouterState, RoutingCallback,
|
||||
};
|
||||
use log::error;
|
||||
use dioxus::prelude::ScopeState;
|
||||
|
||||
use crate::{
|
||||
contexts::router::RouterContext,
|
||||
prelude::{
|
||||
comp,
|
||||
default_errors::{
|
||||
FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit,
|
||||
},
|
||||
},
|
||||
prelude::RouterContext, routable::Routable, utils::use_router_internal::use_router_internal,
|
||||
};
|
||||
|
||||
/// The basic building block required for all other router components and hooks.
|
||||
///
|
||||
/// This manages a [`dioxus_router_core::RouterService`], which in turn is required for basically
|
||||
/// all router functionality. All other components and hooks provided by [`dioxus_router`](crate)
|
||||
/// will only work as/in components nested within a component calling [`use_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,
|
||||
}
|
||||
}
|
||||
/// A hook that provides access to information about the router
|
||||
pub fn use_router<R: Routable + Clone>(cx: &ScopeState) -> &RouterContext<R> {
|
||||
use_router_internal(cx)
|
||||
.as_ref()
|
||||
.expect("use_route must have access to a router")
|
||||
}
|
||||
+14
-14
@@ -1,6 +1,10 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
// cannot use forbid, because props derive macro generates #[allow(missing_docs)]
|
||||
#![deny(missing_docs)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod navigation;
|
||||
pub mod routable;
|
||||
|
||||
/// Components interacting with the router.
|
||||
pub mod components {
|
||||
@@ -14,19 +18,23 @@ pub mod components {
|
||||
|
||||
mod outlet;
|
||||
pub use outlet::*;
|
||||
|
||||
mod router;
|
||||
pub use router::*;
|
||||
}
|
||||
|
||||
mod contexts {
|
||||
pub(crate) mod outlet;
|
||||
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.
|
||||
pub mod hooks {
|
||||
mod use_navigate;
|
||||
pub use use_navigate::*;
|
||||
|
||||
mod use_router;
|
||||
pub use use_router::*;
|
||||
|
||||
@@ -36,18 +44,10 @@ pub mod hooks {
|
||||
|
||||
/// A collection of useful items most applications might need.
|
||||
pub mod prelude {
|
||||
pub use dioxus_router_core::prelude::*;
|
||||
|
||||
pub use crate::components::*;
|
||||
pub use crate::contexts::*;
|
||||
pub use crate::hooks::*;
|
||||
|
||||
/// 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)
|
||||
}
|
||||
pub use dioxus_router_macro::routable;
|
||||
}
|
||||
|
||||
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)]
|
||||
use crate::history::HistoryProvider;
|
||||
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)]
|
||||
pub struct RouteParseError<E: std::fmt::Display> {
|
||||
/// The attempted routes that failed to match
|
||||
pub attempted_routes: Vec<E>,
|
||||
}
|
||||
|
||||
@@ -19,39 +22,9 @@ impl<E: std::fmt::Display> std::fmt::Display for RouteParseError<E> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
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()
|
||||
}
|
||||
|
||||
/// Something that can be created from a query string
|
||||
pub trait FromQuery {
|
||||
/// Create an instance of `Self` from a query string
|
||||
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 {
|
||||
/// The error that can occur when parsing a route segment
|
||||
type Err;
|
||||
|
||||
/// Create an instance of `Self` from a route segment
|
||||
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 {
|
||||
/// Display the route segments
|
||||
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 {
|
||||
/// The error that can occur when parsing route segments
|
||||
type Err;
|
||||
|
||||
/// Create an instance of `Self` from route segments
|
||||
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)]
|
||||
pub struct RouterProps {
|
||||
pub current_route: String,
|
||||
}
|
||||
pub trait Routable: std::fmt::Display + std::str::FromStr + 'static
|
||||
where
|
||||
<Self as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
/// Something that can be routed to
|
||||
pub trait Routable: std::fmt::Display + std::str::FromStr + 'static {
|
||||
/// Render the route at the given level
|
||||
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
|
||||
}
|
||||
|
||||
@@ -148,63 +124,3 @@ where
|
||||
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 dioxus_router_core::RouterMessage;
|
||||
|
||||
use crate::contexts::router::RouterContext;
|
||||
use crate::{contexts::router::RouterContext, routable::Routable};
|
||||
|
||||
/// A private hook to subscribe to the router.
|
||||
///
|
||||
@@ -15,16 +12,25 @@ use crate::contexts::router::RouterContext;
|
||||
/// - Otherwise [`Some`].
|
||||
///
|
||||
/// [`use_router`]: crate::hooks::use_router
|
||||
pub(crate) fn use_router_internal(cx: &ScopeState) -> &mut Option<RouterContext> {
|
||||
let id = cx.use_hook(|| Arc::new(cx.scope_id()));
|
||||
pub(crate) fn use_router_internal<R: Routable>(cx: &ScopeState) -> &Option<RouterContext<R>> {
|
||||
let inner = cx.use_hook(|| {
|
||||
let router = cx.consume_context::<RouterContext<R>>()?;
|
||||
|
||||
cx.use_hook(|| {
|
||||
let router = cx.consume_context::<RouterContext>()?;
|
||||
let id = cx.scope_id();
|
||||
router.subscribe(id);
|
||||
|
||||
let _ = router
|
||||
.sender
|
||||
.unbounded_send(RouterMessage::Subscribe(id.clone()));
|
||||
Some(Subscription { router, id })
|
||||
});
|
||||
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