Intigrate macro with router hooks and components

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

No files matched your search

-1
View File
@@ -3,7 +3,6 @@ members = [
"packages/dioxus", "packages/dioxus",
"packages/core", "packages/core",
"packages/core-macro", "packages/core-macro",
"packages/router-core",
"packages/router-macro", "packages/router-macro",
"packages/router", "packages/router",
"packages/html", "packages/html",
-29
View File
@@ -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
View File
@@ -1 +0,0 @@
# Dioxus Router Core
-256
View File
@@ -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());
}
}
-49
View File
@@ -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;
}
-448
View File
@@ -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())])
)
}
}
-97
View File
@@ -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
-288
View File
@@ -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()
}
}
}
-245
View File
@@ -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() {}
+3 -3
View File
@@ -190,7 +190,7 @@ impl RouteEnum {
} }
impl std::str::FromStr for #name { impl std::str::FromStr for #name {
type Err = RouteParseError<#error_name>; type Err = dioxus_router::routable::RouteParseError<#error_name>;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let route = s.strip_prefix('/').unwrap_or(s); let route = s.strip_prefix('/').unwrap_or(s);
@@ -200,7 +200,7 @@ impl RouteEnum {
#(#tokens)* #(#tokens)*
Err(RouteParseError { Err(dioxus_router::routable::RouteParseError {
attempted_routes: errors, attempted_routes: errors,
}) })
} }
@@ -289,7 +289,7 @@ impl RouteEnum {
let index_iter = 0..layers.len(); let index_iter = 0..layers.len();
quote! { quote! {
impl Routable for #name where Self: Clone { impl dioxus_router::routable::Routable for #name where Self: Clone {
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> { fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> {
let myself = self.clone(); let myself = self.clone();
match level { match level {
+1 -1
View File
@@ -90,7 +90,7 @@ impl Nest {
} }
RouteSegment::Dynamic(ident, ty) => { RouteSegment::Dynamic(ident, ty) => {
let missing_error = segment.missing_error_name().unwrap(); let missing_error = segment.missing_error_name().unwrap();
error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegment>::Err) }); error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) });
display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
error_variants.push(quote! { #missing_error }); error_variants.push(quote! { #missing_error });
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? }); display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
+1 -1
View File
@@ -14,7 +14,7 @@ impl QuerySegment {
let ident = &self.ident; let ident = &self.ident;
let ty = &self.ty; let ty = &self.ty;
quote! { quote! {
let #ident = <#ty as dioxus_router_core::router::FromQuery>::from_query(query); let #ident = <#ty as dioxus_router::routable::FromQuery>::from_query(query);
} }
} }
+2 -2
View File
@@ -203,13 +203,13 @@ impl Route {
} }
RouteSegment::Dynamic(ident, ty) => { RouteSegment::Dynamic(ident, ty) => {
let missing_error = segment.missing_error_name().unwrap(); let missing_error = segment.missing_error_name().unwrap();
error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegment>::Err) }); error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) });
display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
error_variants.push(quote! { #missing_error }); error_variants.push(quote! { #missing_error });
display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? }); display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
} }
RouteSegment::CatchAll(ident, ty) => { RouteSegment::CatchAll(ident, ty) => {
error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegments>::Err) }); error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegments>::Err) });
display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? }); display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
} }
} }
+2 -2
View File
@@ -80,7 +80,7 @@ impl RouteSegment {
{ {
let mut segments = segments.clone(); let mut segments = segments.clone();
let parsed = if let Some(segment) = segments.next() { let parsed = if let Some(segment) = segments.next() {
<#ty as dioxus_router_core::router::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))) <#ty as dioxus_router::routable::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
} else { } else {
Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#missing_error_name)) Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#missing_error_name))
}; };
@@ -101,7 +101,7 @@ impl RouteSegment {
let parsed = { let parsed = {
let mut segments = segments.clone(); let mut segments = segments.clone();
let segments: Vec<_> = segments.collect(); let segments: Vec<_> = segments.collect();
<#ty as dioxus_router_core::router::FromRouteSegments>::from_route_segments(&segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err))) <#ty as dioxus_router::routable::FromRouteSegments>::from_route_segments(&segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
}; };
match parsed { match parsed {
Ok(#name) => { Ok(#name) => {
+4 -7
View File
@@ -12,19 +12,16 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies] [dependencies]
anyhow = "1.0.66" anyhow = "1.0.66"
async-lock = "2.7.0"
dioxus = { path="../dioxus" } dioxus = { path="../dioxus" }
dioxus-router-core = { path = "../router-core"} dioxus-router-macro = { path="../router-macro" }
futures-channel = "0.3.25"
futures-util = "0.3.25"
log = "0.4.17" log = "0.4.17"
serde = { version = "1.0.163", features = ["derive"] }
thiserror = "1.0.37" thiserror = "1.0.37"
url = "2.3.1"
[features] [features]
regex = ["dioxus-router-core/regex"]
serde = ["dioxus-router-core/serde"]
wasm_test = [] wasm_test = []
web = ["dioxus-router-core/web"] web = []
[dev-dependencies] [dev-dependencies]
dioxus = { path = "../dioxus" } dioxus = { path = "../dioxus" }
@@ -1,17 +1,12 @@
use crate::{components::Link, hooks::use_route}; use crate::{components::Link, hooks::use_route, navigation::NavigationTarget, routable::Routable};
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_router_core::prelude::{named, FailureExternalNavigation as FENName, RootIndex};
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn FailureExternalNavigation(cx: Scope) -> Element { pub fn FailureExternalNavigation<R: Routable + Clone>(cx: Scope) -> Element {
let state = use_route(cx).expect( let href = use_route::<R>(cx).expect(
"`FailureExternalNavigation` can only be mounted by the router itself, \ "`FailureExternalNavigation` can only be mounted by the router itself, \
since it is not exposed", since it is not exposed",
); );
let href = state.parameter::<FENName>().expect(
"`FailureExternalNavigation` cannot be mounted without receiving its parameter, \
since it is not exposed",
);
render! { render! {
h1 { "External Navigation Failure!" } h1 { "External Navigation Failure!" }
@@ -28,7 +23,7 @@ pub fn FailureExternalNavigation(cx: Scope) -> Element {
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn FailureNamedNavigation(cx: Scope) -> Element { pub fn FailureNamedNavigation<R: Routable + Clone>(cx: Scope) -> Element {
render! { render! {
h1 { "Named Navigation Failure!" } h1 { "Named Navigation Failure!" }
p { p {
@@ -40,15 +35,15 @@ pub fn FailureNamedNavigation(cx: Scope) -> Element {
"We are sorry for the inconvenience. The link below may help to fix the problem, but " "We are sorry for the inconvenience. The link below may help to fix the problem, but "
"there is no guarantee." "there is no guarantee."
} }
Link { Link::<R> {
target: named::<RootIndex>(), target: NavigationTarget::External("https://google.com".into()),
"Click here to try to fix the failure." "Click here to try to fix the failure."
} }
} }
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn FailureRedirectionLimit(cx: Scope) -> Element { pub fn FailureRedirectionLimit<R: Routable + Clone>(cx: Scope) -> Element {
render! { render! {
h1 { "Redirection Limit Failure!" } h1 { "Redirection Limit Failure!" }
p { p {
@@ -60,8 +55,8 @@ pub fn FailureRedirectionLimit(cx: Scope) -> Element {
"We are sorry for the inconvenience. The link below may help to fix the problem, but " "We are sorry for the inconvenience. The link below may help to fix the problem, but "
"there is no guarantee." "there is no guarantee."
} }
Link { Link::<R> {
target: named::<RootIndex>(), target: NavigationTarget::External("https://google.com".into()),
"Click here to try to fix the failure." "Click here to try to fix the failure."
} }
} }
@@ -1,8 +1,7 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_router_core::RouterMessage;
use log::error; use log::error;
use crate::utils::use_router_internal::use_router_internal; use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
/// The properties for a [`GoBackButton`] or a [`GoForwardButton`]. /// The properties for a [`GoBackButton`] or a [`GoForwardButton`].
#[derive(Debug, Props)] #[derive(Debug, Props)]
@@ -52,11 +51,11 @@ pub struct HistoryButtonProps<'a> {
/// # ); /// # );
/// ``` /// ```
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { pub fn GoBackButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
let HistoryButtonProps { children } = cx.props; let HistoryButtonProps { children } = cx.props;
// hook up to router // hook up to router
let router = match use_router_internal(cx) { let router = match use_router_internal::<R>(cx) {
Some(r) => r, Some(r) => r,
#[allow(unreachable_code)] #[allow(unreachable_code)]
None => { None => {
@@ -67,20 +66,14 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
return None; return None;
} }
}; };
let state = loop {
if let Some(state) = router.state.try_read() {
break state;
}
};
let sender = router.sender.clone();
let disabled = !state.can_go_back; let disabled = !router.can_go_back();
render! { render! {
button { button {
disabled: "{disabled}", disabled: "{disabled}",
prevent_default: "onclick", prevent_default: "onclick",
onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoBack); }, onclick: move |_| router.go_back(),
children children
} }
} }
@@ -127,11 +120,11 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
/// # ); /// # );
/// ``` /// ```
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element { pub fn GoForwardButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
let HistoryButtonProps { children } = cx.props; let HistoryButtonProps { children } = cx.props;
// hook up to router // hook up to router
let router = match use_router_internal(cx) { let router = match use_router_internal::<R>(cx) {
Some(r) => r, Some(r) => r,
#[allow(unreachable_code)] #[allow(unreachable_code)]
None => { None => {
@@ -142,20 +135,14 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
return None; return None;
} }
}; };
let state = loop {
if let Some(state) = router.state.try_read() {
break state;
}
};
let sender = router.sender.clone();
let disabled = !state.can_go_back; let disabled = !router.can_go_back();
render! { render! {
button { button {
disabled: "{disabled}", disabled: "{disabled}",
prevent_default: "onclick", prevent_default: "onclick",
onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoForward); }, onclick: move |_| router.go_forward(),
children children
} }
} }
+20 -26
View File
@@ -1,14 +1,15 @@
use std::fmt::Debug; use std::fmt::Debug;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_router_core::{navigation::NavigationTarget, RouterMessage};
use log::error; use log::error;
use crate::navigation::NavigationTarget;
use crate::routable::Routable;
use crate::utils::use_router_internal::use_router_internal; use crate::utils::use_router_internal::use_router_internal;
/// The properties for a [`Link`]. /// The properties for a [`Link`].
#[derive(Props)] #[derive(Props)]
pub struct LinkProps<'a> { pub struct LinkProps<'a, R: Routable> {
/// A class to apply to the generate HTML anchor tag if the `target` route is active. /// A class to apply to the generate HTML anchor tag if the `target` route is active.
pub active_class: Option<&'a str>, pub active_class: Option<&'a str>,
/// The children to render within the generated HTML anchor tag. /// The children to render within the generated HTML anchor tag.
@@ -18,11 +19,6 @@ pub struct LinkProps<'a> {
/// If `active_class` is [`Some`] and the `target` route is active, `active_class` will be /// If `active_class` is [`Some`] and the `target` route is active, `active_class` will be
/// appended at the end of `class`. /// appended at the end of `class`.
pub class: Option<&'a str>, pub class: Option<&'a str>,
/// Require the __exact__ `target` to be active, for the [`Link`] to be active.
///
/// See [`RouterState::is_at`](dioxus_router_core::RouterState::is_at) for more details.
#[props(default)]
pub exact: bool,
/// The id attribute for the generated HTML anchor tag. /// The id attribute for the generated HTML anchor tag.
pub id: Option<&'a str>, pub id: Option<&'a str>,
/// When [`true`], the `target` route will be opened in a new tab. /// When [`true`], the `target` route will be opened in a new tab.
@@ -46,22 +42,21 @@ pub struct LinkProps<'a> {
pub rel: Option<&'a str>, pub rel: Option<&'a str>,
/// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag. /// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
#[props(into)] #[props(into)]
pub target: NavigationTarget, pub target: NavigationTarget<R>,
} }
impl Debug for LinkProps<'_> { impl<R: Routable> Debug for LinkProps<'_, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LinkProps") f.debug_struct("LinkProps")
.field("active_class", &self.active_class) .field("active_class", &self.active_class)
.field("children", &self.children) .field("children", &self.children)
.field("class", &self.class) .field("class", &self.class)
.field("exact", &self.exact)
.field("id", &self.id) .field("id", &self.id)
.field("new_tab", &self.new_tab) .field("new_tab", &self.new_tab)
.field("onclick", &self.onclick.as_ref().map(|_| "onclick is set")) .field("onclick", &self.onclick.as_ref().map(|_| "onclick is set"))
.field("onclick_only", &self.onclick_only) .field("onclick_only", &self.onclick_only)
.field("rel", &self.rel) .field("rel", &self.rel)
.field("target", &self.target) .field("target", &self.target.to_string())
.finish() .finish()
} }
} }
@@ -130,12 +125,11 @@ impl Debug for LinkProps<'_> {
/// # ); /// # );
/// ``` /// ```
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element { pub fn Link<'a, R: Routable + Clone>(cx: Scope<'a, LinkProps<'a, R>>) -> Element {
let LinkProps { let LinkProps {
active_class, active_class,
children, children,
class, class,
exact,
id, id,
new_tab, new_tab,
onclick, onclick,
@@ -145,7 +139,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
} = cx.props; } = cx.props;
// hook up to router // hook up to router
let router = match use_router_internal(cx) { let router = match use_router_internal::<R>(cx) {
Some(r) => r, Some(r) => r,
#[allow(unreachable_code)] #[allow(unreachable_code)]
None => { None => {
@@ -156,19 +150,19 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
return None; return None;
} }
}; };
let state = loop {
if let Some(state) = router.state.try_read() {
break state;
}
};
let sender = router.sender.clone();
let href = state.href(target); let current_route = router.current();
let href = current_route.to_string();
let ac = active_class let ac = active_class
.and_then(|active_class| { .and_then(|active_class| match target {
state NavigationTarget::Internal(target) => {
.is_at(target, *exact) if href == target.to_string() {
.then(|| format!(" {active_class}")) Some(format!(" {active_class}"))
} else {
None
}
}
_ => None,
}) })
.unwrap_or_default(); .unwrap_or_default();
@@ -186,7 +180,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
let do_default = onclick.is_none() || !onclick_only; let do_default = onclick.is_none() || !onclick_only;
let action = move |event| { let action = move |event| {
if do_default && is_router_nav { if do_default && is_router_nav {
let _ = sender.unbounded_send(RouterMessage::Push(target.clone())); router.push(target.clone());
} }
if let Some(handler) = onclick { if let Some(handler) = onclick {
+49
View File
@@ -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)
}
+60
View File
@@ -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)
}
+29
View File
@@ -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)
}
}
+806 -9
View File
@@ -1,12 +1,809 @@
use std::sync::Arc; use std::{
collections::HashSet,
sync::{Arc, RwLock, RwLockWriteGuard},
};
use async_lock::RwLock; use dioxus::prelude::*;
use dioxus::{core::Component, prelude::ScopeId};
use dioxus_router_core::{RouterMessage, RouterState};
use futures_channel::mpsc::UnboundedSender;
#[derive(Clone)] use crate::{
pub(crate) struct RouterContext { history::HistoryProvider, navigation::NavigationTarget, routable::Routable,
pub(crate) state: Arc<RwLock<RouterState<Component>>>, router_cfg::RouterConfiguration,
pub(crate) sender: UnboundedSender<RouterMessage<ScopeId>>, };
/// An error that can occur when navigating.
pub enum NavigationFailure<R: Routable> {
/// The router failed to navigate to an external URL.
External(String),
/// The router failed to navigate to an internal URL.
Internal(<R as std::str::FromStr>::Err),
} }
/// A function the router will call after every routing update.
pub type RoutingCallback<R> = Arc<dyn Fn(RouterContext<R>) -> Option<NavigationTarget<R>>>;
struct MutableRouterState<R>
where
R: Routable,
{
/// Whether there is a previous page to navigate back to.
///
/// Even if this is [`true`], there might not be a previous page. However, it is nonetheless
/// safe to tell the router to go back.
can_go_back: bool,
/// Whether there is a future page to navigate forward to.
///
/// Even if this is [`true`], there might not be a future page. However, it is nonetheless safe
/// to tell the router to go forward.
can_go_forward: bool,
/// The current prefix.
prefix: Option<String>,
history: Box<dyn HistoryProvider<R>>,
}
/// A collection of router data that manages all routing functionality.
pub struct RouterContext<R>
where
R: Routable,
{
state: Arc<RwLock<MutableRouterState<R>>>,
subscribers: Arc<RwLock<HashSet<ScopeId>>>,
subscriber_update: Arc<dyn Fn(ScopeId)>,
routing_callback: Option<RoutingCallback<R>>,
failure_external_navigation: fn(Scope) -> Element,
failure_named_navigation: fn(Scope) -> Element,
failure_redirection_limit: fn(Scope) -> Element,
}
impl<R: Routable> Clone for RouterContext<R> {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
subscribers: self.subscribers.clone(),
subscriber_update: self.subscriber_update.clone(),
routing_callback: self.routing_callback.clone(),
failure_external_navigation: self.failure_external_navigation,
failure_named_navigation: self.failure_named_navigation,
failure_redirection_limit: self.failure_redirection_limit,
}
}
}
impl<R> RouterContext<R>
where
R: Routable,
{
pub(crate) fn new(cfg: RouterConfiguration<R>, mark_dirty: Arc<dyn Fn(ScopeId)>) -> Self
where
R: Clone,
{
let state = Arc::new(RwLock::new(MutableRouterState {
can_go_back: false,
can_go_forward: false,
prefix: Default::default(),
history: cfg.history,
}));
Self {
state,
subscribers: Arc::new(RwLock::new(HashSet::new())),
subscriber_update: mark_dirty,
routing_callback: cfg.on_update,
failure_external_navigation: cfg.failure_external_navigation,
failure_named_navigation: cfg.failure_named_navigation,
failure_redirection_limit: cfg.failure_redirection_limit,
}
}
/// Check whether there is a previous page to navigate back to.
#[must_use]
pub fn can_go_back(&self) -> bool {
self.state.read().unwrap().can_go_back
}
/// Check whether there is a future page to navigate forward to.
#[must_use]
pub fn can_go_forward(&self) -> bool {
self.state.read().unwrap().can_go_forward
}
/// Go back to the previous location.
///
/// Will fail silently if there is no previous location to go to.
pub fn go_back(&self) {
self.state.write().unwrap().history.go_back();
self.update_subscribers();
}
/// Go back to the next location.
///
/// Will fail silently if there is no next location to go to.
pub fn go_forward(&self) {
self.state.write().unwrap().history.go_forward();
self.update_subscribers();
}
/// Push a new location.
///
/// The previous location will be available to go back to.
pub fn push(&self, target: NavigationTarget<R>) -> Option<NavigationFailure<R>> {
let mut state = self.state_mut();
match target {
NavigationTarget::Internal(p) => state.history.push(p),
NavigationTarget::External(e) => return self.external(e),
}
self.update_subscribers();
None
}
/// Replace the current location.
///
/// The previous location will **not** be available to go back to.
pub fn replace(&self, target: NavigationTarget<R>) -> Option<NavigationFailure<R>> {
let mut state = self.state_mut();
match target {
NavigationTarget::Internal(p) => state.history.replace(p),
NavigationTarget::External(e) => return self.external(e),
}
self.update_subscribers();
None
}
/// The route that is currently active.
pub fn current(&self) -> R
where
R: Clone,
{
self.state.read().unwrap().history.current_route().clone()
}
/// The prefix that is currently active.
pub fn prefix(&self) -> Option<String> {
self.state.read().unwrap().prefix.clone()
}
fn external(&self, external: String) -> Option<NavigationFailure<R>> {
let mut state = self.state_mut();
match state.history.external(external.clone()) {
true => None,
false => Some(NavigationFailure::External(external)),
}
}
fn state_mut(&self) -> RwLockWriteGuard<MutableRouterState<R>> {
self.state.write().unwrap()
}
/// Manually subscribe to the current route
pub fn subscribe(&self, id: ScopeId) {
self.subscribers.write().unwrap().insert(id);
}
/// Manually unsubscribe from the current route
pub fn unsubscribe(&self, id: ScopeId) {
self.subscribers.write().unwrap().remove(&id);
}
fn update_subscribers(&self) {
for &id in self.subscribers.read().unwrap().iter() {
(self.subscriber_update)(id);
}
}
}
// #[cfg(test)]
// mod tests {
// //! The tests for [`RouterContext`] test various functions that are not exposed as public.
// //! However, several of those have an observable effect on the behavior of exposed functions.
// //!
// //! The alternative would be to send messages via the services channel and calling one of the
// //! `run` functions. However, for readability and clarity, it was chosen to directly call the
// //! private functions.
// use std::sync::Mutex;
// use crate::{
// history::MemoryHistory,
// routes::{ParameterRoute, Route, RouteContent},
// };
// use super::*;
// fn test_segment() -> Segment<&'static str> {
// Segment::content(RouteContent::Content(ContentAtom("index")))
// .fixed(
// "fixed",
// Route::content(RouteContent::Content(ContentAtom("fixed"))).name::<bool>(),
// )
// .fixed(
// "redirect",
// Route::content(RouteContent::Redirect(NavigationTarget::Internal(
// String::from("fixed"),
// ))),
// )
// .fixed(
// "redirection-loop",
// Route::content(RouteContent::Redirect(NavigationTarget::Internal(
// String::from("/redirection-loop"),
// ))),
// )
// .fixed(
// "%F0%9F%8E%BA",
// Route::content(RouteContent::Content(ContentAtom("🎺"))),
// )
// .catch_all(ParameterRoute::empty::<bool>())
// }
// #[test]
// fn new_provides_update_to_history() {
// struct TestHistory {}
// impl HistoryProvider for TestHistory {
// fn current_path(&self) -> String {
// todo!()
// }
// fn current_query(&self) -> Option<String> {
// todo!()
// }
// fn go_back(&mut self) {
// todo!()
// }
// fn go_forward(&mut self) {
// todo!()
// }
// fn push(&mut self, _path: String) {
// todo!()
// }
// fn replace(&mut self, _path: String) {
// todo!()
// }
// fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
// callback();
// }
// }
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(TestHistory {}),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// assert!(matches!(
// s.receiver.try_next().unwrap().unwrap(),
// RouterMessage::Update
// ));
// }
// #[test]
// fn update_routing() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed?test=value").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// assert_eq!(s.names, s.state.try_read().unwrap().name_map);
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert_eq!(state.query, Some(String::from("test=value")));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// assert_eq!(s.names, state.name_map);
// }
// #[test]
// fn update_routing_root_index() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("index")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<RootIndex>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/"));
// assert!(state.query.is_none());
// assert!(state.prefix.is_none());
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn update_routing_redirect() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/redirect").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// #[should_panic = "reached redirect limit of 25"]
// #[cfg(debug_assertions)]
// fn update_routing_redirect_debug() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// }
// #[test]
// #[cfg(not(debug_assertions))]
// fn update_routing_redirect_release() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("redirect limit")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<FailureRedirectionLimit>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/redirection-loop"));
// assert_eq!(state.can_go_back, false);
// assert_eq!(state.can_go_forward, false);
// }
// #[test]
// fn update_subscribers() {
// let ids = Arc::new(Mutex::new(Vec::new()));
// let ids2 = Arc::clone(&ids);
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// Segment::empty(),
// Box::<MemoryHistory>::default(),
// Arc::new(move |id| {
// ids2.lock().unwrap().push(id);
// }),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// let id0 = Arc::new(0);
// s.subscribe(Arc::clone(&id0));
// let id1 = Arc::new(1);
// s.subscribe(Arc::clone(&id1));
// let id1 = Arc::try_unwrap(id1).unwrap();
// s.update_subscribers();
// assert_eq!(s.subscribers.len(), 1);
// assert_eq!(s.subscribers[0].upgrade().unwrap(), id0);
// assert_eq!(*ids.lock().unwrap(), vec![*id0, id1, *id0]);
// }
// #[test]
// fn push_internal() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.push(NavigationTarget::Internal(String::from("/fixed")));
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn push_named() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.push(NavigationTarget::named::<bool>());
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn push_external() {
// let (mut s, tx, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// tx.unbounded_send(RouterMessage::Push(NavigationTarget::External(
// String::from("https://dioxuslabs.com/"),
// )))
// .unwrap();
// s.run_current();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("external target")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<FailureExternalNavigation>());
// r
// });
// assert_eq!(state.parameters, {
// let mut r = HashMap::new();
// r.insert(
// Name::of::<FailureExternalNavigation>(),
// String::from("https://dioxuslabs.com/"),
// );
// r
// });
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn replace_named() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.replace(NavigationTarget::named::<bool>());
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn replace_internal() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.replace(NavigationTarget::Internal(String::from("/fixed")));
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn replace_external() {
// let (mut s, tx, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// tx.unbounded_send(RouterMessage::Replace(NavigationTarget::External(
// String::from("https://dioxuslabs.com/"),
// )))
// .unwrap();
// s.run_current();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("external target")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<FailureExternalNavigation>());
// r
// });
// assert_eq!(state.parameters, {
// let mut r = HashMap::new();
// r.insert(
// Name::of::<FailureExternalNavigation>(),
// String::from("https://dioxuslabs.com/"),
// );
// r
// });
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn subscribe() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// Segment::empty(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// let id = Arc::new(0);
// s.subscribe(Arc::clone(&id));
// assert_eq!(s.subscribers.len(), 1);
// assert_eq!(s.subscribers[0].upgrade().unwrap(), id);
// }
// #[test]
// fn routing_callback() {
// let paths = Arc::new(Mutex::new(Vec::new()));
// let paths2 = Arc::clone(&paths);
// let (mut s, c, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
// Arc::new(|_| {}),
// Some(Arc::new(move |state| {
// paths2.lock().unwrap().push(state.path.clone());
// Some("/%F0%9F%8E%BA".into())
// })),
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// assert!(paths.lock().unwrap().is_empty());
// s.init();
// assert_eq!(*paths.lock().unwrap(), vec![String::from("/fixed")]);
// c.unbounded_send(RouterMessage::Update).unwrap();
// s.run_current();
// assert_eq!(
// *paths.lock().unwrap(),
// vec![String::from("/fixed"), String::from("/%F0%9F%8E%BA")]
// );
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("🎺")])
// }
// #[test]
// fn url_decoding_do() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/%F0%9F%A5%B3").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert!(state.content.is_empty());
// assert!(state.names.is_empty());
// assert_eq!(state.parameters, {
// let mut r = HashMap::new();
// r.insert(Name::of::<bool>(), String::from("🥳"));
// r
// });
// assert_eq!(state.path, String::from("/%F0%9F%A5%B3"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn url_decoding_do_not() {
// let (mut s, c, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/%F0%9F%8E%BA").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// c.unbounded_send(RouterMessage::Update).unwrap();
// s.run_current();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("🎺")]);
// assert!(state.names.is_empty());
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/%F0%9F%8E%BA"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn prefix() {
// struct TestHistory {}
// impl HistoryProvider for TestHistory {
// fn current_path(&self) -> String {
// String::from("/")
// }
// fn current_query(&self) -> Option<String> {
// None
// }
// fn current_prefix(&self) -> Option<String> {
// Some(String::from("/prefix"))
// }
// fn can_go_back(&self) -> bool {
// false
// }
// fn can_go_forward(&self) -> bool {
// false
// }
// fn go_back(&mut self) {
// todo!()
// }
// fn go_forward(&mut self) {
// todo!()
// }
// fn push(&mut self, _path: String) {
// todo!()
// }
// fn replace(&mut self, _path: String) {
// todo!()
// }
// fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
// callback();
// }
// }
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(TestHistory {}),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("index")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<RootIndex>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/"));
// assert!(state.query.is_none());
// assert_eq!(state.prefix, Some(String::from("/prefix")));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// }
+230
View File
@@ -0,0 +1,230 @@
use std::str::FromStr;
use crate::routable::Routable;
use super::HistoryProvider;
/// A [`HistoryProvider`] that stores all navigation information in memory.
pub struct MemoryHistory<R: Routable> {
current: Option<R>,
history: Vec<R>,
future: Vec<R>,
}
impl<R: Routable> MemoryHistory<R> {
/// Create a [`MemoryHistory`] starting at `path`.
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::with_initial_path("/some/path").unwrap();
/// assert_eq!(history.current_path(), "/some/path");
/// assert_eq!(history.can_go_back(), false);
/// ```
pub fn with_initial_path(path: impl Into<String>) -> Result<Self, <R as FromStr>::Err> {
let path = path.into();
Ok(Self {
current: Some(R::from_str(&path)?),
..Default::default()
})
}
}
impl<R: Routable> Default for MemoryHistory<R> {
fn default() -> Self {
Self {
current: None,
history: Vec::new(),
future: Vec::new(),
}
}
}
impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
fn current_route(&self) -> &R {
self.current.as_ref().expect("current route is not set")
}
fn can_go_back(&self) -> bool {
!self.history.is_empty()
}
fn go_back(&mut self) {
if let Some(last) = self.history.pop() {
let new = self.current.replace(last);
if let Some(new) = new {
self.future.push(new);
}
}
}
fn can_go_forward(&self) -> bool {
!self.future.is_empty()
}
fn go_forward(&mut self) {
if let Some(next) = self.future.pop() {
let old = self.current.replace(next);
if let Some(old) = old {
self.history.push(old);
}
}
}
fn push(&mut self, new: R) {
if let Some(current) = self.current.take() {
self.history.push(current);
}
self.current = Some(new);
self.future.clear();
}
fn replace(&mut self, path: R) {
self.current = Some(path);
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// #[test]
// fn default() {
// let mem = MemoryHistory::default();
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// assert_eq!(mem.history, Vec::<String>::new());
// assert_eq!(mem.future, Vec::<String>::new());
// }
// #[test]
// fn with_initial_path() {
// let mem = MemoryHistory::with_initial_path("something").unwrap();
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}something")).unwrap()
// );
// assert_eq!(mem.history, Vec::<String>::new());
// assert_eq!(mem.future, Vec::<String>::new());
// }
// #[test]
// fn with_initial_path_with_leading_slash() {
// let mem = MemoryHistory::with_initial_path("/something").unwrap();
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}something")).unwrap()
// );
// assert_eq!(mem.history, Vec::<String>::new());
// assert_eq!(mem.future, Vec::<String>::new());
// }
// #[test]
// fn can_go_back() {
// let mut mem = MemoryHistory::default();
// assert!(!mem.can_go_back());
// mem.push(String::from("/test"));
// assert!(mem.can_go_back());
// }
// #[test]
// fn go_back() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// mem.go_back();
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// assert!(mem.history.is_empty());
// assert_eq!(mem.future, vec![format!("{INITIAL_URL}test")]);
// }
// #[test]
// fn can_go_forward() {
// let mut mem = MemoryHistory::default();
// assert!(!mem.can_go_forward());
// mem.push(String::from("/test"));
// mem.go_back();
// assert!(mem.can_go_forward());
// }
// #[test]
// fn go_forward() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// mem.go_back();
// mem.go_forward();
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}test")).unwrap()
// );
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
// assert!(mem.future.is_empty());
// }
// #[test]
// fn push() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}test")).unwrap()
// );
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
// assert!(mem.future.is_empty());
// }
// #[test]
// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
// #[cfg(debug_assertions)]
// fn push_debug() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("//test"));
// }
// #[test]
// #[cfg(not(debug_assertions))]
// fn push_release() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("//test"));
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// assert!(mem.history.is_empty())
// }
// #[test]
// fn replace() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// mem.push(String::from("/other"));
// mem.go_back();
// mem.replace(String::from("/replace"));
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}replace")).unwrap()
// );
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
// assert_eq!(mem.future, vec![format!("{INITIAL_URL}other")]);
// }
// #[test]
// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
// #[cfg(debug_assertions)]
// fn replace_debug() {
// let mut mem = MemoryHistory::default();
// mem.replace(String::from("//test"));
// }
// #[test]
// #[cfg(not(debug_assertions))]
// fn replace_release() {
// let mut mem = MemoryHistory::default();
// mem.replace(String::from("//test"));
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// }
// }
@@ -21,6 +21,8 @@ mod web_hash;
#[cfg(feature = "web")] #[cfg(feature = "web")]
pub use web_hash::*; pub use web_hash::*;
use crate::routable::Routable;
#[cfg(feature = "web")] #[cfg(feature = "web")]
pub(crate) mod web_scroll; pub(crate) mod web_scroll;
@@ -32,7 +34,7 @@ pub(crate) mod web_scroll;
/// However, you should document all deviations. Also, make sure the navigation is user-friendly. /// However, you should document all deviations. Also, make sure the navigation is user-friendly.
/// The described behaviors are designed to mimic a web browser, which most users should already /// The described behaviors are designed to mimic a web browser, which most users should already
/// know. Deviations might confuse them. /// know. Deviations might confuse them.
pub trait HistoryProvider { pub trait HistoryProvider<R: Routable> {
/// Get the path of the current URL. /// Get the path of the current URL.
/// ///
/// **Must start** with `/`. **Must _not_ contain** the prefix. /// **Must start** with `/`. **Must _not_ contain** the prefix.
@@ -46,20 +48,8 @@ pub trait HistoryProvider {
/// assert_eq!(history.current_path(), "/path"); /// assert_eq!(history.current_path(), "/path");
/// ``` /// ```
#[must_use] #[must_use]
fn current_path(&self) -> String; fn current_route(&self) -> &R;
/// Get the query string of the current URL.
///
/// **Must _not_** start with `?`.
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.current_query(), None);
///
/// history.push(String::from("?some=value"));
/// assert_eq!(history.current_query(), Some("some=value".to_string()));
/// ```
fn current_query(&self) -> Option<String>;
/// Get the current path prefix of the URL. /// Get the current path prefix of the URL.
/// ///
/// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a /// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a
@@ -87,6 +77,7 @@ pub trait HistoryProvider {
fn can_go_back(&self) -> bool { fn can_go_back(&self) -> bool {
true true
} }
/// Go back to a previous page. /// Go back to a previous page.
/// ///
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method /// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
@@ -127,6 +118,7 @@ pub trait HistoryProvider {
fn can_go_forward(&self) -> bool { fn can_go_forward(&self) -> bool {
true true
} }
/// Go forward to a future page. /// Go forward to a future page.
/// ///
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method /// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
@@ -162,7 +154,8 @@ pub trait HistoryProvider {
/// assert_eq!(history.current_path(), "/some-other-page"); /// assert_eq!(history.current_path(), "/some-other-page");
/// assert!(history.can_go_back()); /// assert!(history.can_go_back());
/// ``` /// ```
fn push(&mut self, path: String); fn push(&mut self, route: R);
/// Replace the current page with another one. /// Replace the current page with another one.
/// ///
/// This should merge the current URL with the `path` parameter (which may also include a query /// This should merge the current URL with the `path` parameter (which may also include a query
@@ -178,7 +171,7 @@ pub trait HistoryProvider {
/// assert_eq!(history.current_path(), "/some-other-page"); /// assert_eq!(history.current_path(), "/some-other-page");
/// assert!(!history.can_go_back()); /// assert!(!history.can_go_back());
/// ``` /// ```
fn replace(&mut self, path: String); fn replace(&mut self, path: R);
/// Navigate to an external URL. /// Navigate to an external URL.
/// ///
@@ -23,7 +23,7 @@ use super::{
/// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added. /// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added.
/// ///
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API /// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
pub struct WebHistory { pub struct WebHistory<R: Serialize + DeserializeOwned> {
do_scroll_restoration: bool, do_scroll_restoration: bool,
history: History, history: History,
listener_navigation: Option<EventListener>, listener_navigation: Option<EventListener>,
@@ -72,7 +72,7 @@ impl WebHistory {
} }
impl HistoryProvider for WebHistory { impl HistoryProvider for WebHistory {
fn current_path(&self) -> String { fn current_route(&self) -> String {
let path = self let path = self
.window .window
.location() .location()
@@ -21,7 +21,7 @@ const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
/// was not, because no browser supports WebAssembly without the [History API]. /// was not, because no browser supports WebAssembly without the [History API].
/// ///
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API /// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
pub struct WebHashHistory { pub struct WebHashHistory<R: Serialize + DeserializeOwned> {
do_scroll_restoration: bool, do_scroll_restoration: bool,
history: History, history: History,
listener_navigation: Option<EventListener>, listener_navigation: Option<EventListener>,
@@ -121,7 +121,7 @@ impl WebHashHistory {
} }
impl HistoryProvider for WebHashHistory { impl HistoryProvider for WebHashHistory {
fn current_path(&self) -> String { fn current_route(&self) -> String {
self.url() self.url()
.map(|url| url.path().to_string()) .map(|url| url.path().to_string())
.unwrap_or(String::from("/")) .unwrap_or(String::from("/"))
-71
View File
@@ -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
}
}
}
+4 -10
View File
@@ -1,8 +1,6 @@
use async_lock::RwLockReadGuard; use dioxus::prelude::ScopeState;
use dioxus::{core::Component, prelude::ScopeState};
use dioxus_router_core::RouterState;
use crate::utils::use_router_internal::use_router_internal; use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
/// A hook that provides access to information about the current routing location. /// A hook that provides access to information about the current routing location.
/// ///
@@ -56,13 +54,9 @@ use crate::utils::use_router_internal::use_router_internal;
/// ``` /// ```
/// ///
/// [`use_router`]: crate::hooks::use_router /// [`use_router`]: crate::hooks::use_router
pub fn use_route(cx: &ScopeState) -> Option<RwLockReadGuard<RouterState<Component>>> { pub fn use_route<R: Routable + Clone>(cx: &ScopeState) -> Option<R> {
match use_router_internal(cx) { match use_router_internal(cx) {
Some(r) => loop { Some(r) => Some(r.current()),
if let Some(s) = r.state.try_read() {
break Some(s);
}
},
None => { None => {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
panic!("`use_route` must have access to a parent router"); panic!("`use_route` must have access to a parent router");
+7 -195
View File
@@ -1,200 +1,12 @@
use async_lock::RwLockReadGuard; use dioxus::prelude::ScopeState;
use dioxus::{core::Component, prelude::*};
use dioxus_router_core::{
history::{HistoryProvider, MemoryHistory},
routes::{ContentAtom, Segment},
Navigator, RouterService, RouterState, RoutingCallback,
};
use log::error;
use crate::{ use crate::{
contexts::router::RouterContext, prelude::RouterContext, routable::Routable, utils::use_router_internal::use_router_internal,
prelude::{
comp,
default_errors::{
FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit,
},
},
}; };
/// The basic building block required for all other router components and hooks. /// A hook that provides access to information about the router
/// pub fn use_router<R: Routable + Clone>(cx: &ScopeState) -> &RouterContext<R> {
/// This manages a [`dioxus_router_core::RouterService`], which in turn is required for basically use_router_internal(cx)
/// all router functionality. All other components and hooks provided by [`dioxus_router`](crate) .as_ref()
/// will only work as/in components nested within a component calling [`use_router`]. .expect("use_route must have access to a router")
///
/// Components calling [`use_router`] should not be nested within each other.
///
/// # Return values
/// This hook returns the current router state and a navigator. For more information about the
/// state, see the [`use_route`](crate::hooks::use_route) hook. For more information about the
/// [`Navigator`], see its own documentation and the [`use_navigate`](crate::hooks::use_navigate)
/// hook.
///
/// # Panic
/// - When used within a component, that is nested inside another component calling [`use_router`],
/// but only in debug builds.
///
/// # Example
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_router::prelude::*;
/// fn App(cx: Scope) -> Element {
/// let (_, _) = use_router(
/// &cx,
/// &|| RouterConfiguration {
/// synchronous: true, // asynchronicity not needed for doc test
/// ..Default::default()
/// },
/// &|| Segment::content(comp(Content))
/// );
///
/// render! {
/// h1 { "App" }
/// Outlet { }
/// }
/// }
///
/// fn Content(cx: Scope) -> Element {
/// render! {
/// p { "Some content" }
/// }
/// }
/// # let mut vdom = VirtualDom::new(App);
/// # let _ = vdom.rebuild();
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Some content</p>");
/// ```
pub fn use_router<'a>(
cx: &'a ScopeState,
cfg: &dyn Fn() -> RouterConfiguration,
content: &dyn Fn() -> Segment<Component>,
) -> (
RwLockReadGuard<'a, RouterState<Component>>,
Navigator<ScopeId>,
) {
let (service, state, sender) = cx.use_hook(|| {
#[allow(unreachable_code, unused_variables)]
if let Some(outer) = cx.consume_context::<RouterContext>() {
let msg = "components using `use_router` should not be nested within each other";
error!("{msg}, inner will be inactive and transparent");
#[cfg(debug_assertions)]
panic!("{}", msg);
return (None, outer.state, outer.sender);
}
let cfg = cfg();
let content = content();
let (mut service, sender, state) = RouterService::new(
content,
cfg.history,
cx.schedule_update_any(),
cfg.on_update,
cfg.failure_external_navigation,
cfg.failure_named_navigation,
cfg.failure_redirection_limit,
);
cx.provide_context(RouterContext {
state: state.clone(),
sender: sender.clone(),
});
(
if cfg.synchronous {
service.init();
Some(service)
} else {
cx.spawn(async move { service.run().await });
None
},
state,
sender,
)
});
if let Some(service) = service {
service.run_current();
}
(
loop {
if let Some(state) = state.try_read() {
break state;
}
},
sender.clone().into(),
)
}
/// Global configuration options for the router.
///
/// This implements [`Default`], so you can use it like this:
/// ```rust,no_run
/// # use dioxus_router::prelude::RouterConfiguration;
/// let cfg = RouterConfiguration {
/// synchronous: false,
/// ..Default::default()
/// };
/// ```
pub struct RouterConfiguration {
/// A component to render when an external navigation fails.
///
/// Defaults to a router-internal component called `FailureExternalNavigation`. It is not part
/// of the public API. Do not confuse it with
/// [`dioxus_router_core::prelude::FailureExternalNavigation`].
pub failure_external_navigation: ContentAtom<Component>,
/// A component to render when a named navigation fails.
///
/// Defaults to a router-internal component called `FailureNamedNavigation`. It is not part of
/// the public API. Do not confuse it with
/// [`dioxus_router_core::prelude::FailureNamedNavigation`].
pub failure_named_navigation: ContentAtom<Component>,
/// A component to render when the redirect limit is reached.
///
/// Defaults to a router-internal component called `FailureRedirectionLimit`. It is not part of
/// the public API. Do not confuse it with
/// [`dioxus_router_core::prelude::FailureRedirectionLimit`].
pub failure_redirection_limit: ContentAtom<Component>,
/// The [`HistoryProvider`] the router should use.
///
/// Defaults to a default [`MemoryHistory`].
pub history: Box<dyn HistoryProvider>,
/// A function to be called whenever the routing is updated.
///
/// The callback is invoked after the routing is updated, but before components and hooks are
/// updated.
///
/// If the callback returns a [`NavigationTarget`] the router will replace the current location
/// with it. If no navigation failure was triggered, the router will then updated dependent
/// components and hooks.
///
/// The callback is called no more than once per rerouting. It will not be called if a
/// navigation failure occurs.
///
/// Defaults to [`None`].
///
/// [`NavigationTarget`]: dioxus_router_core::navigation::NavigationTarget
pub on_update: Option<RoutingCallback<Component>>,
/// Whether the router should run in synchronous mode.
///
/// If [`true`], the router will only update its state whenever the component using the
/// [`use_router`] hook rerenders. If [`false`], an asynchronous task is launched and the router
/// will update whenever it receives new input.
///
/// Defaults to [`false`].
pub synchronous: bool,
}
impl Default for RouterConfiguration {
fn default() -> Self {
Self {
failure_external_navigation: comp(FailureExternalNavigation),
failure_named_navigation: comp(FailureNamedNavigation),
failure_redirection_limit: comp(FailureRedirectionLimit),
history: Box::<MemoryHistory>::default(),
on_update: None,
synchronous: false,
}
}
} }
+14 -14
View File
@@ -1,6 +1,10 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
// cannot use forbid, because props derive macro generates #[allow(missing_docs)] // cannot use forbid, because props derive macro generates #[allow(missing_docs)]
#![deny(missing_docs)] #![deny(missing_docs)]
#![allow(non_snake_case)]
pub mod navigation;
pub mod routable;
/// Components interacting with the router. /// Components interacting with the router.
pub mod components { pub mod components {
@@ -14,19 +18,23 @@ pub mod components {
mod outlet; mod outlet;
pub use outlet::*; pub use outlet::*;
mod router;
pub use router::*;
} }
mod contexts { mod contexts {
pub(crate) mod outlet;
pub(crate) mod router; pub(crate) mod router;
pub use router::*;
} }
pub use dioxus_router_core::history; mod router_cfg;
pub mod history;
/// Hooks for interacting with the router in components. /// Hooks for interacting with the router in components.
pub mod hooks { pub mod hooks {
mod use_navigate;
pub use use_navigate::*;
mod use_router; mod use_router;
pub use use_router::*; pub use use_router::*;
@@ -36,18 +44,10 @@ pub mod hooks {
/// A collection of useful items most applications might need. /// A collection of useful items most applications might need.
pub mod prelude { pub mod prelude {
pub use dioxus_router_core::prelude::*;
pub use crate::components::*; pub use crate::components::*;
pub use crate::contexts::*;
pub use crate::hooks::*; pub use crate::hooks::*;
pub use dioxus_router_macro::routable;
/// Wrap a [`Component`](dioxus::core::Component) inside a [`ContentAtom`].
///
/// This is purely a convenience function.
#[must_use]
pub fn comp(component: dioxus::core::Component) -> ContentAtom<dioxus::core::Component> {
ContentAtom(component)
}
} }
mod utils { mod utils {
+69
View File
@@ -0,0 +1,69 @@
//! Types pertaining to navigation.
use std::{fmt::Display, str::FromStr};
use url::{ParseError, Url};
use crate::routable::Routable;
/// A target for the router to navigate to.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NavigationTarget<R: Routable> {
/// An internal path that the router can navigate to by itself.
///
/// ```rust
/// # use dioxus_router_core::navigation::NavigationTarget;
/// let explicit = NavigationTarget::Internal(String::from("/internal"));
/// let implicit: NavigationTarget = "/internal".into();
/// assert_eq!(explicit, implicit);
/// ```
Internal(R),
/// An external target that the router doesn't control.
///
/// ```rust
/// # use dioxus_router_core::navigation::NavigationTarget;
/// let explicit = NavigationTarget::External(String::from("https://dioxuslabs.com/"));
/// let implicit: NavigationTarget = "https://dioxuslabs.com/".into();
/// assert_eq!(explicit, implicit);
/// ```
External(String),
}
impl<R: Routable> From<R> for NavigationTarget<R> {
fn from(value: R) -> Self {
Self::Internal(value)
}
}
impl<R: Routable> Display for NavigationTarget<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NavigationTarget::Internal(r) => write!(f, "{}", r),
NavigationTarget::External(s) => write!(f, "{}", s),
}
}
}
/// An error that can occur when parsing a [`NavigationTarget`].
pub enum NavigationTargetParseError<R: Routable> {
/// A URL that is not valid.
InvalidUrl(ParseError),
/// An internal URL that is not valid.
InvalidInternalURL(<R as FromStr>::Err),
}
impl<R: Routable> FromStr for NavigationTarget<R> {
type Err = NavigationTargetParseError<R>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match Url::parse(s) {
Ok(_) => Ok(Self::External(s.to_string())),
Err(ParseError::RelativeUrlWithoutBase) => {
Ok(Self::Internal(R::from_str(s).map_err(|e| {
NavigationTargetParseError::InvalidInternalURL(e)
})?))
}
Err(e) => Err(NavigationTargetParseError::InvalidUrl(e)),
}
}
}
@@ -1,11 +1,14 @@
//! # Routable
#![allow(non_snake_case)] #![allow(non_snake_case)]
use crate::history::HistoryProvider;
use dioxus::prelude::*; use dioxus::prelude::*;
use std::{cell::RefCell, rc::Rc, str::FromStr, sync::Arc}; use std::str::FromStr;
/// An error that occurs when parsing a route
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct RouteParseError<E: std::fmt::Display> { pub struct RouteParseError<E: std::fmt::Display> {
/// The attempted routes that failed to match
pub attempted_routes: Vec<E>, pub attempted_routes: Vec<E>,
} }
@@ -19,39 +22,9 @@ impl<E: std::fmt::Display> std::fmt::Display for RouteParseError<E> {
} }
} }
#[derive(Clone)] /// Something that can be created from a query string
pub struct Router {
subscribers: Rc<RefCell<Vec<ScopeId>>>,
update_any: Arc<dyn Fn(ScopeId)>,
history: Rc<dyn HistoryProvider>,
route: Rc<RefCell<Option<Rc<dyn RouteRenderable>>>>,
}
impl Router {
fn set_route<R: Routable + 'static>(&self, route: R)
where
R::Err: std::fmt::Display,
{
*self.route.borrow_mut() = Some(Rc::new(route));
for subscriber in self.subscribers.borrow().iter() {
(self.update_any)(*subscriber);
}
}
}
fn use_router(cx: &ScopeState) -> &Router {
use_context(cx).unwrap()
}
fn use_route(cx: &ScopeState) -> Rc<dyn RouteRenderable> {
let router = use_router(cx);
cx.use_hook(|| {
router.subscribers.borrow_mut().push(cx.scope_id());
});
router.route.borrow().clone().unwrap()
}
pub trait FromQuery { pub trait FromQuery {
/// Create an instance of `Self` from a query string
fn from_query(query: &str) -> Self; fn from_query(query: &str) -> Self;
} }
@@ -61,9 +34,12 @@ impl<T: for<'a> From<&'a str>> FromQuery for T {
} }
} }
/// Something that can be created from a route segment
pub trait FromRouteSegment: Sized { pub trait FromRouteSegment: Sized {
/// The error that can occur when parsing a route segment
type Err; type Err;
/// Create an instance of `Self` from a route segment
fn from_route_segment(route: &str) -> Result<Self, Self::Err>; fn from_route_segment(route: &str) -> Result<Self, Self::Err>;
} }
@@ -78,7 +54,9 @@ where
} }
} }
/// Something that can be converted to route segments
pub trait ToRouteSegments { pub trait ToRouteSegments {
/// Display the route segments
fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result; fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
} }
@@ -95,9 +73,12 @@ where
} }
} }
/// Something that can be created from route segments
pub trait FromRouteSegments: Sized { pub trait FromRouteSegments: Sized {
/// The error that can occur when parsing route segments
type Err; type Err;
/// Create an instance of `Self` from route segments
fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err>; fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err>;
} }
@@ -112,14 +93,9 @@ impl<I: std::iter::FromIterator<String>> FromRouteSegments for I {
} }
} }
#[derive(Props, PartialEq)] /// Something that can be routed to
pub struct RouterProps { pub trait Routable: std::fmt::Display + std::str::FromStr + 'static {
pub current_route: String, /// Render the route at the given level
}
pub trait Routable: std::fmt::Display + std::str::FromStr + 'static
where
<Self as FromStr>::Err: std::fmt::Display,
{
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>; fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
} }
@@ -148,63 +124,3 @@ where
self.render(cx, level) self.render(cx, level)
} }
} }
#[derive(Clone)]
struct OutletContext {
current_level: usize,
}
fn use_outlet_context(cx: &ScopeState) -> &OutletContext {
let outlet_context = use_context(cx).unwrap();
outlet_context
}
impl OutletContext {
fn render(cx: &ScopeState) -> Element<'_> {
let outlet = use_outlet_context(cx);
let current_level = outlet.current_level;
cx.provide_context({
OutletContext {
current_level: current_level + 1,
}
});
use_route(cx).render(cx, current_level)
}
}
pub fn Outlet(cx: Scope) -> Element {
OutletContext::render(cx)
}
pub fn Router<R: Routable, H: HistoryProvider + Default + 'static>(
cx: Scope<RouterProps>,
) -> Element
where
<R as FromStr>::Err: std::fmt::Display,
{
let current_route = R::from_str(&cx.props.current_route);
let router = use_context_provider(cx, || Router {
subscribers: Rc::default(),
update_any: cx.schedule_update_any(),
history: Rc::<H>::default(),
route: Rc::new(RefCell::new(None)),
});
use_context_provider(cx, || OutletContext { current_level: 1 });
match current_route {
Ok(current_route) => {
router.set_route(current_route);
router.route.borrow().as_ref().unwrap().render(cx, 0)
}
Err(err) => {
render! {
pre {
"{err}"
}
}
}
}
}
+71
View File
@@ -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,
}
}
}
@@ -1,9 +1,6 @@
use std::sync::Arc; use dioxus::prelude::{ScopeId, ScopeState};
use dioxus::prelude::ScopeState; use crate::{contexts::router::RouterContext, routable::Routable};
use dioxus_router_core::RouterMessage;
use crate::contexts::router::RouterContext;
/// A private hook to subscribe to the router. /// A private hook to subscribe to the router.
/// ///
@@ -15,16 +12,25 @@ use crate::contexts::router::RouterContext;
/// - Otherwise [`Some`]. /// - Otherwise [`Some`].
/// ///
/// [`use_router`]: crate::hooks::use_router /// [`use_router`]: crate::hooks::use_router
pub(crate) fn use_router_internal(cx: &ScopeState) -> &mut Option<RouterContext> { pub(crate) fn use_router_internal<R: Routable>(cx: &ScopeState) -> &Option<RouterContext<R>> {
let id = cx.use_hook(|| Arc::new(cx.scope_id())); let inner = cx.use_hook(|| {
let router = cx.consume_context::<RouterContext<R>>()?;
cx.use_hook(|| { let id = cx.scope_id();
let router = cx.consume_context::<RouterContext>()?; router.subscribe(id);
let _ = router Some(Subscription { router, id })
.sender });
.unbounded_send(RouterMessage::Subscribe(id.clone())); cx.use_hook(|| inner.as_ref().map(|s| s.router.clone()))
}
Some(router) struct Subscription<R: Routable> {
}) router: RouterContext<R>,
id: ScopeId,
}
impl<R: Routable> Drop for Subscription<R> {
fn drop(&mut self) {
self.router.unsubscribe(self.id);
}
} }