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,17 +1,12 @@
use crate::{components::Link, hooks::use_route};
use crate::{components::Link, hooks::use_route, navigation::NavigationTarget, routable::Routable};
use dioxus::prelude::*;
use dioxus_router_core::prelude::{named, FailureExternalNavigation as FENName, RootIndex};
#[allow(non_snake_case)]
pub fn FailureExternalNavigation(cx: Scope) -> Element {
let state = use_route(cx).expect(
pub fn FailureExternalNavigation<R: Routable + Clone>(cx: Scope) -> Element {
let href = use_route::<R>(cx).expect(
"`FailureExternalNavigation` can only be mounted by the router itself, \
since it is not exposed",
);
let href = state.parameter::<FENName>().expect(
"`FailureExternalNavigation` cannot be mounted without receiving its parameter, \
since it is not exposed",
);
render! {
h1 { "External Navigation Failure!" }
@@ -28,7 +23,7 @@ pub fn FailureExternalNavigation(cx: Scope) -> Element {
}
#[allow(non_snake_case)]
pub fn FailureNamedNavigation(cx: Scope) -> Element {
pub fn FailureNamedNavigation<R: Routable + Clone>(cx: Scope) -> Element {
render! {
h1 { "Named Navigation Failure!" }
p {
@@ -40,15 +35,15 @@ pub fn FailureNamedNavigation(cx: Scope) -> Element {
"We are sorry for the inconvenience. The link below may help to fix the problem, but "
"there is no guarantee."
}
Link {
target: named::<RootIndex>(),
Link::<R> {
target: NavigationTarget::External("https://google.com".into()),
"Click here to try to fix the failure."
}
}
}
#[allow(non_snake_case)]
pub fn FailureRedirectionLimit(cx: Scope) -> Element {
pub fn FailureRedirectionLimit<R: Routable + Clone>(cx: Scope) -> Element {
render! {
h1 { "Redirection Limit Failure!" }
p {
@@ -60,8 +55,8 @@ pub fn FailureRedirectionLimit(cx: Scope) -> Element {
"We are sorry for the inconvenience. The link below may help to fix the problem, but "
"there is no guarantee."
}
Link {
target: named::<RootIndex>(),
Link::<R> {
target: NavigationTarget::External("https://google.com".into()),
"Click here to try to fix the failure."
}
}
@@ -1,8 +1,7 @@
use dioxus::prelude::*;
use dioxus_router_core::RouterMessage;
use log::error;
use crate::utils::use_router_internal::use_router_internal;
use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
/// The properties for a [`GoBackButton`] or a [`GoForwardButton`].
#[derive(Debug, Props)]
@@ -52,11 +51,11 @@ pub struct HistoryButtonProps<'a> {
/// # );
/// ```
#[allow(non_snake_case)]
pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
pub fn GoBackButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
let HistoryButtonProps { children } = cx.props;
// hook up to router
let router = match use_router_internal(cx) {
let router = match use_router_internal::<R>(cx) {
Some(r) => r,
#[allow(unreachable_code)]
None => {
@@ -67,20 +66,14 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
return None;
}
};
let state = loop {
if let Some(state) = router.state.try_read() {
break state;
}
};
let sender = router.sender.clone();
let disabled = !state.can_go_back;
let disabled = !router.can_go_back();
render! {
button {
disabled: "{disabled}",
prevent_default: "onclick",
onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoBack); },
onclick: move |_| router.go_back(),
children
}
}
@@ -127,11 +120,11 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
/// # );
/// ```
#[allow(non_snake_case)]
pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
pub fn GoForwardButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
let HistoryButtonProps { children } = cx.props;
// hook up to router
let router = match use_router_internal(cx) {
let router = match use_router_internal::<R>(cx) {
Some(r) => r,
#[allow(unreachable_code)]
None => {
@@ -142,20 +135,14 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
return None;
}
};
let state = loop {
if let Some(state) = router.state.try_read() {
break state;
}
};
let sender = router.sender.clone();
let disabled = !state.can_go_back;
let disabled = !router.can_go_back();
render! {
button {
disabled: "{disabled}",
prevent_default: "onclick",
onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoForward); },
onclick: move |_| router.go_forward(),
children
}
}
+20 -26
View File
@@ -1,14 +1,15 @@
use std::fmt::Debug;
use dioxus::prelude::*;
use dioxus_router_core::{navigation::NavigationTarget, RouterMessage};
use log::error;
use crate::navigation::NavigationTarget;
use crate::routable::Routable;
use crate::utils::use_router_internal::use_router_internal;
/// The properties for a [`Link`].
#[derive(Props)]
pub struct LinkProps<'a> {
pub struct LinkProps<'a, R: Routable> {
/// A class to apply to the generate HTML anchor tag if the `target` route is active.
pub active_class: Option<&'a str>,
/// The children to render within the generated HTML anchor tag.
@@ -18,11 +19,6 @@ pub struct LinkProps<'a> {
/// If `active_class` is [`Some`] and the `target` route is active, `active_class` will be
/// appended at the end of `class`.
pub class: Option<&'a str>,
/// Require the __exact__ `target` to be active, for the [`Link`] to be active.
///
/// See [`RouterState::is_at`](dioxus_router_core::RouterState::is_at) for more details.
#[props(default)]
pub exact: bool,
/// The id attribute for the generated HTML anchor tag.
pub id: Option<&'a str>,
/// When [`true`], the `target` route will be opened in a new tab.
@@ -46,22 +42,21 @@ pub struct LinkProps<'a> {
pub rel: Option<&'a str>,
/// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
#[props(into)]
pub target: NavigationTarget,
pub target: NavigationTarget<R>,
}
impl Debug for LinkProps<'_> {
impl<R: Routable> Debug for LinkProps<'_, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LinkProps")
.field("active_class", &self.active_class)
.field("children", &self.children)
.field("class", &self.class)
.field("exact", &self.exact)
.field("id", &self.id)
.field("new_tab", &self.new_tab)
.field("onclick", &self.onclick.as_ref().map(|_| "onclick is set"))
.field("onclick_only", &self.onclick_only)
.field("rel", &self.rel)
.field("target", &self.target)
.field("target", &self.target.to_string())
.finish()
}
}
@@ -130,12 +125,11 @@ impl Debug for LinkProps<'_> {
/// # );
/// ```
#[allow(non_snake_case)]
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
pub fn Link<'a, R: Routable + Clone>(cx: Scope<'a, LinkProps<'a, R>>) -> Element {
let LinkProps {
active_class,
children,
class,
exact,
id,
new_tab,
onclick,
@@ -145,7 +139,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
} = cx.props;
// hook up to router
let router = match use_router_internal(cx) {
let router = match use_router_internal::<R>(cx) {
Some(r) => r,
#[allow(unreachable_code)]
None => {
@@ -156,19 +150,19 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
return None;
}
};
let state = loop {
if let Some(state) = router.state.try_read() {
break state;
}
};
let sender = router.sender.clone();
let href = state.href(target);
let current_route = router.current();
let href = current_route.to_string();
let ac = active_class
.and_then(|active_class| {
state
.is_at(target, *exact)
.then(|| format!(" {active_class}"))
.and_then(|active_class| match target {
NavigationTarget::Internal(target) => {
if href == target.to_string() {
Some(format!(" {active_class}"))
} else {
None
}
}
_ => None,
})
.unwrap_or_default();
@@ -186,7 +180,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
let do_default = onclick.is_none() || !onclick_only;
let action = move |event| {
if do_default && is_router_nav {
let _ = sender.unbounded_send(RouterMessage::Push(target.clone()));
router.push(target.clone());
}
if let Some(handler) = onclick {
+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::{core::Component, prelude::ScopeId};
use dioxus_router_core::{RouterMessage, RouterState};
use futures_channel::mpsc::UnboundedSender;
use dioxus::prelude::*;
#[derive(Clone)]
pub(crate) struct RouterContext {
pub(crate) state: Arc<RwLock<RouterState<Component>>>,
pub(crate) sender: UnboundedSender<RouterMessage<ScopeId>>,
use crate::{
history::HistoryProvider, navigation::NavigationTarget, routable::Routable,
router_cfg::RouterConfiguration,
};
/// An error that can occur when navigating.
pub enum NavigationFailure<R: Routable> {
/// The router failed to navigate to an external URL.
External(String),
/// The router failed to navigate to an internal URL.
Internal(<R as std::str::FromStr>::Err),
}
/// A function the router will call after every routing update.
pub type RoutingCallback<R> = Arc<dyn Fn(RouterContext<R>) -> Option<NavigationTarget<R>>>;
struct MutableRouterState<R>
where
R: Routable,
{
/// Whether there is a previous page to navigate back to.
///
/// Even if this is [`true`], there might not be a previous page. However, it is nonetheless
/// safe to tell the router to go back.
can_go_back: bool,
/// Whether there is a future page to navigate forward to.
///
/// Even if this is [`true`], there might not be a future page. However, it is nonetheless safe
/// to tell the router to go forward.
can_go_forward: bool,
/// The current prefix.
prefix: Option<String>,
history: Box<dyn HistoryProvider<R>>,
}
/// A collection of router data that manages all routing functionality.
pub struct RouterContext<R>
where
R: Routable,
{
state: Arc<RwLock<MutableRouterState<R>>>,
subscribers: Arc<RwLock<HashSet<ScopeId>>>,
subscriber_update: Arc<dyn Fn(ScopeId)>,
routing_callback: Option<RoutingCallback<R>>,
failure_external_navigation: fn(Scope) -> Element,
failure_named_navigation: fn(Scope) -> Element,
failure_redirection_limit: fn(Scope) -> Element,
}
impl<R: Routable> Clone for RouterContext<R> {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
subscribers: self.subscribers.clone(),
subscriber_update: self.subscriber_update.clone(),
routing_callback: self.routing_callback.clone(),
failure_external_navigation: self.failure_external_navigation,
failure_named_navigation: self.failure_named_navigation,
failure_redirection_limit: self.failure_redirection_limit,
}
}
}
impl<R> RouterContext<R>
where
R: Routable,
{
pub(crate) fn new(cfg: RouterConfiguration<R>, mark_dirty: Arc<dyn Fn(ScopeId)>) -> Self
where
R: Clone,
{
let state = Arc::new(RwLock::new(MutableRouterState {
can_go_back: false,
can_go_forward: false,
prefix: Default::default(),
history: cfg.history,
}));
Self {
state,
subscribers: Arc::new(RwLock::new(HashSet::new())),
subscriber_update: mark_dirty,
routing_callback: cfg.on_update,
failure_external_navigation: cfg.failure_external_navigation,
failure_named_navigation: cfg.failure_named_navigation,
failure_redirection_limit: cfg.failure_redirection_limit,
}
}
/// Check whether there is a previous page to navigate back to.
#[must_use]
pub fn can_go_back(&self) -> bool {
self.state.read().unwrap().can_go_back
}
/// Check whether there is a future page to navigate forward to.
#[must_use]
pub fn can_go_forward(&self) -> bool {
self.state.read().unwrap().can_go_forward
}
/// Go back to the previous location.
///
/// Will fail silently if there is no previous location to go to.
pub fn go_back(&self) {
self.state.write().unwrap().history.go_back();
self.update_subscribers();
}
/// Go back to the next location.
///
/// Will fail silently if there is no next location to go to.
pub fn go_forward(&self) {
self.state.write().unwrap().history.go_forward();
self.update_subscribers();
}
/// Push a new location.
///
/// The previous location will be available to go back to.
pub fn push(&self, target: NavigationTarget<R>) -> Option<NavigationFailure<R>> {
let mut state = self.state_mut();
match target {
NavigationTarget::Internal(p) => state.history.push(p),
NavigationTarget::External(e) => return self.external(e),
}
self.update_subscribers();
None
}
/// Replace the current location.
///
/// The previous location will **not** be available to go back to.
pub fn replace(&self, target: NavigationTarget<R>) -> Option<NavigationFailure<R>> {
let mut state = self.state_mut();
match target {
NavigationTarget::Internal(p) => state.history.replace(p),
NavigationTarget::External(e) => return self.external(e),
}
self.update_subscribers();
None
}
/// The route that is currently active.
pub fn current(&self) -> R
where
R: Clone,
{
self.state.read().unwrap().history.current_route().clone()
}
/// The prefix that is currently active.
pub fn prefix(&self) -> Option<String> {
self.state.read().unwrap().prefix.clone()
}
fn external(&self, external: String) -> Option<NavigationFailure<R>> {
let mut state = self.state_mut();
match state.history.external(external.clone()) {
true => None,
false => Some(NavigationFailure::External(external)),
}
}
fn state_mut(&self) -> RwLockWriteGuard<MutableRouterState<R>> {
self.state.write().unwrap()
}
/// Manually subscribe to the current route
pub fn subscribe(&self, id: ScopeId) {
self.subscribers.write().unwrap().insert(id);
}
/// Manually unsubscribe from the current route
pub fn unsubscribe(&self, id: ScopeId) {
self.subscribers.write().unwrap().remove(&id);
}
fn update_subscribers(&self) {
for &id in self.subscribers.read().unwrap().iter() {
(self.subscriber_update)(id);
}
}
}
// #[cfg(test)]
// mod tests {
// //! The tests for [`RouterContext`] test various functions that are not exposed as public.
// //! However, several of those have an observable effect on the behavior of exposed functions.
// //!
// //! The alternative would be to send messages via the services channel and calling one of the
// //! `run` functions. However, for readability and clarity, it was chosen to directly call the
// //! private functions.
// use std::sync::Mutex;
// use crate::{
// history::MemoryHistory,
// routes::{ParameterRoute, Route, RouteContent},
// };
// use super::*;
// fn test_segment() -> Segment<&'static str> {
// Segment::content(RouteContent::Content(ContentAtom("index")))
// .fixed(
// "fixed",
// Route::content(RouteContent::Content(ContentAtom("fixed"))).name::<bool>(),
// )
// .fixed(
// "redirect",
// Route::content(RouteContent::Redirect(NavigationTarget::Internal(
// String::from("fixed"),
// ))),
// )
// .fixed(
// "redirection-loop",
// Route::content(RouteContent::Redirect(NavigationTarget::Internal(
// String::from("/redirection-loop"),
// ))),
// )
// .fixed(
// "%F0%9F%8E%BA",
// Route::content(RouteContent::Content(ContentAtom("🎺"))),
// )
// .catch_all(ParameterRoute::empty::<bool>())
// }
// #[test]
// fn new_provides_update_to_history() {
// struct TestHistory {}
// impl HistoryProvider for TestHistory {
// fn current_path(&self) -> String {
// todo!()
// }
// fn current_query(&self) -> Option<String> {
// todo!()
// }
// fn go_back(&mut self) {
// todo!()
// }
// fn go_forward(&mut self) {
// todo!()
// }
// fn push(&mut self, _path: String) {
// todo!()
// }
// fn replace(&mut self, _path: String) {
// todo!()
// }
// fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
// callback();
// }
// }
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(TestHistory {}),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// assert!(matches!(
// s.receiver.try_next().unwrap().unwrap(),
// RouterMessage::Update
// ));
// }
// #[test]
// fn update_routing() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed?test=value").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// assert_eq!(s.names, s.state.try_read().unwrap().name_map);
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert_eq!(state.query, Some(String::from("test=value")));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// assert_eq!(s.names, state.name_map);
// }
// #[test]
// fn update_routing_root_index() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("index")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<RootIndex>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/"));
// assert!(state.query.is_none());
// assert!(state.prefix.is_none());
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn update_routing_redirect() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/redirect").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// #[should_panic = "reached redirect limit of 25"]
// #[cfg(debug_assertions)]
// fn update_routing_redirect_debug() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// }
// #[test]
// #[cfg(not(debug_assertions))]
// fn update_routing_redirect_release() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("redirect limit")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<FailureRedirectionLimit>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/redirection-loop"));
// assert_eq!(state.can_go_back, false);
// assert_eq!(state.can_go_forward, false);
// }
// #[test]
// fn update_subscribers() {
// let ids = Arc::new(Mutex::new(Vec::new()));
// let ids2 = Arc::clone(&ids);
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// Segment::empty(),
// Box::<MemoryHistory>::default(),
// Arc::new(move |id| {
// ids2.lock().unwrap().push(id);
// }),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// let id0 = Arc::new(0);
// s.subscribe(Arc::clone(&id0));
// let id1 = Arc::new(1);
// s.subscribe(Arc::clone(&id1));
// let id1 = Arc::try_unwrap(id1).unwrap();
// s.update_subscribers();
// assert_eq!(s.subscribers.len(), 1);
// assert_eq!(s.subscribers[0].upgrade().unwrap(), id0);
// assert_eq!(*ids.lock().unwrap(), vec![*id0, id1, *id0]);
// }
// #[test]
// fn push_internal() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.push(NavigationTarget::Internal(String::from("/fixed")));
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn push_named() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.push(NavigationTarget::named::<bool>());
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn push_external() {
// let (mut s, tx, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// tx.unbounded_send(RouterMessage::Push(NavigationTarget::External(
// String::from("https://dioxuslabs.com/"),
// )))
// .unwrap();
// s.run_current();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("external target")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<FailureExternalNavigation>());
// r
// });
// assert_eq!(state.parameters, {
// let mut r = HashMap::new();
// r.insert(
// Name::of::<FailureExternalNavigation>(),
// String::from("https://dioxuslabs.com/"),
// );
// r
// });
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn replace_named() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.replace(NavigationTarget::named::<bool>());
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn replace_internal() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.replace(NavigationTarget::Internal(String::from("/fixed")));
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<bool>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn replace_external() {
// let (mut s, tx, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// tx.unbounded_send(RouterMessage::Replace(NavigationTarget::External(
// String::from("https://dioxuslabs.com/"),
// )))
// .unwrap();
// s.run_current();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("external target")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<FailureExternalNavigation>());
// r
// });
// assert_eq!(state.parameters, {
// let mut r = HashMap::new();
// r.insert(
// Name::of::<FailureExternalNavigation>(),
// String::from("https://dioxuslabs.com/"),
// );
// r
// });
// assert_eq!(state.path, String::from("/fixed"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn subscribe() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// Segment::empty(),
// Box::<MemoryHistory>::default(),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// let id = Arc::new(0);
// s.subscribe(Arc::clone(&id));
// assert_eq!(s.subscribers.len(), 1);
// assert_eq!(s.subscribers[0].upgrade().unwrap(), id);
// }
// #[test]
// fn routing_callback() {
// let paths = Arc::new(Mutex::new(Vec::new()));
// let paths2 = Arc::clone(&paths);
// let (mut s, c, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
// Arc::new(|_| {}),
// Some(Arc::new(move |state| {
// paths2.lock().unwrap().push(state.path.clone());
// Some("/%F0%9F%8E%BA".into())
// })),
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// assert!(paths.lock().unwrap().is_empty());
// s.init();
// assert_eq!(*paths.lock().unwrap(), vec![String::from("/fixed")]);
// c.unbounded_send(RouterMessage::Update).unwrap();
// s.run_current();
// assert_eq!(
// *paths.lock().unwrap(),
// vec![String::from("/fixed"), String::from("/%F0%9F%8E%BA")]
// );
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("🎺")])
// }
// #[test]
// fn url_decoding_do() {
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/%F0%9F%A5%B3").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert!(state.content.is_empty());
// assert!(state.names.is_empty());
// assert_eq!(state.parameters, {
// let mut r = HashMap::new();
// r.insert(Name::of::<bool>(), String::from("🥳"));
// r
// });
// assert_eq!(state.path, String::from("/%F0%9F%A5%B3"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn url_decoding_do_not() {
// let (mut s, c, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(MemoryHistory::with_initial_path("/%F0%9F%8E%BA").unwrap()),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// c.unbounded_send(RouterMessage::Update).unwrap();
// s.run_current();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("🎺")]);
// assert!(state.names.is_empty());
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/%F0%9F%8E%BA"));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// #[test]
// fn prefix() {
// struct TestHistory {}
// impl HistoryProvider for TestHistory {
// fn current_path(&self) -> String {
// String::from("/")
// }
// fn current_query(&self) -> Option<String> {
// None
// }
// fn current_prefix(&self) -> Option<String> {
// Some(String::from("/prefix"))
// }
// fn can_go_back(&self) -> bool {
// false
// }
// fn can_go_forward(&self) -> bool {
// false
// }
// fn go_back(&mut self) {
// todo!()
// }
// fn go_forward(&mut self) {
// todo!()
// }
// fn push(&mut self, _path: String) {
// todo!()
// }
// fn replace(&mut self, _path: String) {
// todo!()
// }
// fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
// callback();
// }
// }
// let (mut s, _, _) = RouterContext::<_, u8>::new(
// test_segment(),
// Box::new(TestHistory {}),
// Arc::new(|_| {}),
// None,
// ContentAtom("external target"),
// ContentAtom("named target"),
// ContentAtom("redirect limit"),
// );
// s.init();
// let state = s.state.try_read().unwrap();
// assert_eq!(state.content, vec![ContentAtom("index")]);
// assert_eq!(state.names, {
// let mut r = HashSet::new();
// r.insert(Name::of::<RootIndex>());
// r
// });
// assert!(state.parameters.is_empty());
// assert_eq!(state.path, String::from("/"));
// assert!(state.query.is_none());
// assert_eq!(state.prefix, Some(String::from("/prefix")));
// assert!(!state.can_go_back);
// assert!(!state.can_go_forward);
// }
// }
+230
View File
@@ -0,0 +1,230 @@
use std::str::FromStr;
use crate::routable::Routable;
use super::HistoryProvider;
/// A [`HistoryProvider`] that stores all navigation information in memory.
pub struct MemoryHistory<R: Routable> {
current: Option<R>,
history: Vec<R>,
future: Vec<R>,
}
impl<R: Routable> MemoryHistory<R> {
/// Create a [`MemoryHistory`] starting at `path`.
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::with_initial_path("/some/path").unwrap();
/// assert_eq!(history.current_path(), "/some/path");
/// assert_eq!(history.can_go_back(), false);
/// ```
pub fn with_initial_path(path: impl Into<String>) -> Result<Self, <R as FromStr>::Err> {
let path = path.into();
Ok(Self {
current: Some(R::from_str(&path)?),
..Default::default()
})
}
}
impl<R: Routable> Default for MemoryHistory<R> {
fn default() -> Self {
Self {
current: None,
history: Vec::new(),
future: Vec::new(),
}
}
}
impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
fn current_route(&self) -> &R {
self.current.as_ref().expect("current route is not set")
}
fn can_go_back(&self) -> bool {
!self.history.is_empty()
}
fn go_back(&mut self) {
if let Some(last) = self.history.pop() {
let new = self.current.replace(last);
if let Some(new) = new {
self.future.push(new);
}
}
}
fn can_go_forward(&self) -> bool {
!self.future.is_empty()
}
fn go_forward(&mut self) {
if let Some(next) = self.future.pop() {
let old = self.current.replace(next);
if let Some(old) = old {
self.history.push(old);
}
}
}
fn push(&mut self, new: R) {
if let Some(current) = self.current.take() {
self.history.push(current);
}
self.current = Some(new);
self.future.clear();
}
fn replace(&mut self, path: R) {
self.current = Some(path);
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// #[test]
// fn default() {
// let mem = MemoryHistory::default();
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// assert_eq!(mem.history, Vec::<String>::new());
// assert_eq!(mem.future, Vec::<String>::new());
// }
// #[test]
// fn with_initial_path() {
// let mem = MemoryHistory::with_initial_path("something").unwrap();
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}something")).unwrap()
// );
// assert_eq!(mem.history, Vec::<String>::new());
// assert_eq!(mem.future, Vec::<String>::new());
// }
// #[test]
// fn with_initial_path_with_leading_slash() {
// let mem = MemoryHistory::with_initial_path("/something").unwrap();
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}something")).unwrap()
// );
// assert_eq!(mem.history, Vec::<String>::new());
// assert_eq!(mem.future, Vec::<String>::new());
// }
// #[test]
// fn can_go_back() {
// let mut mem = MemoryHistory::default();
// assert!(!mem.can_go_back());
// mem.push(String::from("/test"));
// assert!(mem.can_go_back());
// }
// #[test]
// fn go_back() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// mem.go_back();
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// assert!(mem.history.is_empty());
// assert_eq!(mem.future, vec![format!("{INITIAL_URL}test")]);
// }
// #[test]
// fn can_go_forward() {
// let mut mem = MemoryHistory::default();
// assert!(!mem.can_go_forward());
// mem.push(String::from("/test"));
// mem.go_back();
// assert!(mem.can_go_forward());
// }
// #[test]
// fn go_forward() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// mem.go_back();
// mem.go_forward();
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}test")).unwrap()
// );
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
// assert!(mem.future.is_empty());
// }
// #[test]
// fn push() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}test")).unwrap()
// );
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
// assert!(mem.future.is_empty());
// }
// #[test]
// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
// #[cfg(debug_assertions)]
// fn push_debug() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("//test"));
// }
// #[test]
// #[cfg(not(debug_assertions))]
// fn push_release() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("//test"));
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// assert!(mem.history.is_empty())
// }
// #[test]
// fn replace() {
// let mut mem = MemoryHistory::default();
// mem.push(String::from("/test"));
// mem.push(String::from("/other"));
// mem.go_back();
// mem.replace(String::from("/replace"));
// assert_eq!(
// mem.current,
// Url::parse(&format!("{INITIAL_URL}replace")).unwrap()
// );
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
// assert_eq!(mem.future, vec![format!("{INITIAL_URL}other")]);
// }
// #[test]
// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
// #[cfg(debug_assertions)]
// fn replace_debug() {
// let mut mem = MemoryHistory::default();
// mem.replace(String::from("//test"));
// }
// #[test]
// #[cfg(not(debug_assertions))]
// fn replace_release() {
// let mut mem = MemoryHistory::default();
// mem.replace(String::from("//test"));
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
// }
// }
+193
View File
@@ -0,0 +1,193 @@
//! History Integration
//!
//! dioxus-router-core relies on so-called [`HistoryProvider`]s to store the current URL, and possibly a
//! history (i.e. a browsers back button) and future (i.e. a browsers forward button).
//!
//! To integrate dioxus-router-core with a any type of history, all you have to do is implement the
//! [`HistoryProvider`] trait. dioxus-router-core also comes with some (for now one) default implementations.
use std::sync::Arc;
mod memory;
pub use memory::*;
#[cfg(feature = "web")]
mod web;
#[cfg(feature = "web")]
pub use web::*;
#[cfg(feature = "web")]
mod web_hash;
#[cfg(feature = "web")]
pub use web_hash::*;
use crate::routable::Routable;
#[cfg(feature = "web")]
pub(crate) mod web_scroll;
/// An integration with some kind of navigation history.
///
/// Depending on your use case, your implementation may deviate from the described procedure. This
/// is fine, as long as both `current_path` and `current_query` match the described format.
///
/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
/// The described behaviors are designed to mimic a web browser, which most users should already
/// know. Deviations might confuse them.
pub trait HistoryProvider<R: Routable> {
/// Get the path of the current URL.
///
/// **Must start** with `/`. **Must _not_ contain** the prefix.
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.current_path(), "/");
///
/// history.push(String::from("/path"));
/// assert_eq!(history.current_path(), "/path");
/// ```
#[must_use]
fn current_route(&self) -> &R;
/// Get the current path prefix of the URL.
///
/// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a
/// dioxus-router-core-routed application is not running on `/`. The [`HistoryProvider`] is responsible
/// for removing the prefix from the dioxus-router-core-internal path, and also for adding it back in
/// during navigation. This functions value is only used for creating `href`s (e.g. for SSR or
/// display (but not navigation) in a web app).
fn current_prefix(&self) -> Option<String> {
None
}
/// Check whether there is a previous page to navigate back to.
///
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.can_go_back(), false);
///
/// history.push(String::from("/some-other-page"));
/// assert_eq!(history.can_go_back(), true);
/// ```
#[must_use]
fn can_go_back(&self) -> bool {
true
}
/// Go back to a previous page.
///
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
/// might be called, even if `can_go_back` returns [`false`].
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.current_path(), "/");
///
/// history.go_back();
/// assert_eq!(history.current_path(), "/");
///
/// history.push(String::from("/some-other-page"));
/// assert_eq!(history.current_path(), "/some-other-page");
///
/// history.go_back();
/// assert_eq!(history.current_path(), "/");
/// ```
fn go_back(&mut self);
/// Check whether there is a future page to navigate forward to.
///
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.can_go_forward(), false);
///
/// history.push(String::from("/some-other-page"));
/// assert_eq!(history.can_go_forward(), false);
///
/// history.go_back();
/// assert_eq!(history.can_go_forward(), true);
/// ```
#[must_use]
fn can_go_forward(&self) -> bool {
true
}
/// Go forward to a future page.
///
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
/// might be called, even if `can_go_forward` returns [`false`].
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// history.push(String::from("/some-other-page"));
/// assert_eq!(history.current_path(), "/some-other-page");
///
/// history.go_back();
/// assert_eq!(history.current_path(), "/");
///
/// history.go_forward();
/// assert_eq!(history.current_path(), "/some-other-page");
/// ```
fn go_forward(&mut self);
/// Go to another page.
///
/// This should do three things:
/// 1. Merge the current URL with the `path` parameter (which may also include a query part).
/// 2. Remove the previous URL to the navigation history.
/// 3. Clear the navigation future.
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.current_path(), "/");
///
/// history.push(String::from("/some-other-page"));
/// assert_eq!(history.current_path(), "/some-other-page");
/// assert!(history.can_go_back());
/// ```
fn push(&mut self, route: R);
/// Replace the current page with another one.
///
/// This should merge the current URL with the `path` parameter (which may also include a query
/// part). In contrast to the `push` function, the navigation history and future should stay
/// untouched.
///
/// ```rust
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
/// let mut history = MemoryHistory::default();
/// assert_eq!(history.current_path(), "/");
///
/// history.replace(String::from("/some-other-page"));
/// assert_eq!(history.current_path(), "/some-other-page");
/// assert!(!history.can_go_back());
/// ```
fn replace(&mut self, path: R);
/// Navigate to an external URL.
///
/// This should navigate to an external URL, which isn't controlled by the router. If a
/// [`HistoryProvider`] cannot do that, it should return [`false`], otherwise [`true`].
///
/// Returning [`false`] will cause the router to handle the external navigation failure.
#[allow(unused_variables)]
fn external(&mut self, url: String) -> bool {
false
}
/// Provide the [`HistoryProvider`] with an update callback.
///
/// Some [`HistoryProvider`]s may receive URL updates from outside the router. When such
/// updates are received, they should call `callback`, which will cause the router to update.
#[allow(unused_variables)]
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {}
}
+192
View File
@@ -0,0 +1,192 @@
use std::sync::{Arc, Mutex};
use gloo::{events::EventListener, render::AnimationFrame};
use log::error;
use web_sys::{window, History, ScrollRestoration, Window};
use super::{
web_scroll::{top_left, update_history, update_scroll},
HistoryProvider,
};
/// A [`HistoryProvider`] that integrates with a browser via the [History API].
///
/// # Prefix
/// This [`HistoryProvider`] supports a prefix, which can be used for web apps that aren't located
/// at the root of their domain.
///
/// Application developers are responsible for ensuring that right after the prefix comes a `/`. If
/// that is not the case, this [`HistoryProvider`] will replace the first character after the prefix
/// with one.
///
/// Application developers are responsible for not rendering the router if the prefix is not present
/// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added.
///
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
pub struct WebHistory<R: Serialize + DeserializeOwned> {
do_scroll_restoration: bool,
history: History,
listener_navigation: Option<EventListener>,
#[allow(dead_code)]
listener_scroll: Option<EventListener>,
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
prefix: Option<String>,
window: Window,
}
impl WebHistory {
/// Create a new [`WebHistory`].
///
/// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
/// state. It'll also set the browsers scroll restoration to `manual`.
pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
let window = window().expect("access to `window`");
let history = window.history().expect("`window` has access to `history`");
let listener_scroll = match do_scroll_restoration {
true => {
history
.set_scroll_restoration(ScrollRestoration::Manual)
.expect("`history` can set scroll restoration");
let w = window.clone();
let h = history.clone();
let document = w.document().expect("`window` has access to `document`");
Some(EventListener::new(&document, "scroll", move |_| {
update_history(&w, &h);
}))
}
false => None,
};
Self {
do_scroll_restoration,
history,
listener_navigation: None,
listener_scroll,
listener_animation_frame: Default::default(),
prefix: prefix,
window,
}
}
}
impl HistoryProvider for WebHistory {
fn current_route(&self) -> String {
let path = self
.window
.location()
.pathname()
.unwrap_or_else(|_| String::from("/"));
match &self.prefix {
None => path.to_string(),
Some(prefix) => path
.starts_with(prefix)
.then(|| path.split_at(prefix.len()).1)
.unwrap_or("/")
.to_string(),
}
}
fn current_query(&self) -> Option<String> {
self.window
.location()
.search()
.ok()
.map(|mut q| {
if q.starts_with('?') {
q.remove(0);
}
q
})
.and_then(|q| q.is_empty().then_some(q))
}
fn current_prefix(&self) -> Option<String> {
self.prefix.clone()
}
fn go_back(&mut self) {
if let Err(e) = self.history.back() {
error!("failed to go back: {e:?}")
}
}
fn go_forward(&mut self) {
if let Err(e) = self.history.forward() {
error!("failed to go forward: {e:?}")
}
}
fn push(&mut self, path: String) {
let path = match &self.prefix {
None => path,
Some(prefix) => format!("{prefix}{path}"),
};
let state = match self.do_scroll_restoration {
true => top_left(),
false => self.history.state().unwrap_or_default(),
};
let nav = self.history.push_state_with_url(&state, "", Some(&path));
match nav {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => error!("failed to push state: {e:?}"),
}
}
fn replace(&mut self, path: String) {
let path = match &self.prefix {
None => path,
Some(prefix) => format!("{prefix}{path}"),
};
let state = match self.do_scroll_restoration {
true => top_left(),
false => self.history.state().unwrap_or_default(),
};
let nav = self.history.replace_state_with_url(&state, "", Some(&path));
match nav {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => error!("failed to replace state: {e:?}"),
}
}
fn external(&mut self, url: String) -> bool {
match self.window.location().set_href(&url) {
Ok(_) => true,
Err(e) => {
error!("failed to navigate to external url (`{url}): {e:?}");
false
}
}
}
fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
let w = self.window.clone();
let h = self.history.clone();
let s = self.listener_animation_frame.clone();
let d = self.do_scroll_restoration;
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
(*callback)();
if d {
let mut s = s.lock().expect("unpoisoned scroll mutex");
*s = Some(update_scroll(&w, &h));
}
}));
}
}
+220
View File
@@ -0,0 +1,220 @@
use std::sync::{Arc, Mutex};
use gloo::{events::EventListener, render::AnimationFrame, utils::window};
use log::error;
use url::Url;
use web_sys::{History, ScrollRestoration, Window};
use super::{
web_scroll::{top_left, update_history, update_scroll},
HistoryProvider,
};
const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
/// A [`HistoryProvider`] that integrates with a browser via the [History API]. It uses the URLs
/// hash instead of its path.
///
/// Early web applications used the hash to store the current path because there was no other way
/// for them to interact with the history without triggering a browser navigation, as the
/// [History API] did not yet exist. While this implementation could have been written that way, it
/// was not, because no browser supports WebAssembly without the [History API].
///
/// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
pub struct WebHashHistory<R: Serialize + DeserializeOwned> {
do_scroll_restoration: bool,
history: History,
listener_navigation: Option<EventListener>,
#[allow(dead_code)]
listener_scroll: Option<EventListener>,
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
window: Window,
}
impl WebHashHistory {
/// Create a new [`WebHashHistory`].
///
/// If `do_scroll_restoration` is [`true`], [`WebHashHistory`] will take control of the history
/// state. It'll also set the browsers scroll restoration to `manual`.
pub fn new(do_scroll_restoration: bool) -> Self {
let window = window();
let history = window.history().expect("`window` has access to `history`");
history
.set_scroll_restoration(ScrollRestoration::Manual)
.expect("`history` can set scroll restoration");
let listener_scroll = match do_scroll_restoration {
true => {
history
.set_scroll_restoration(ScrollRestoration::Manual)
.expect("`history` can set scroll restoration");
let w = window.clone();
let h = history.clone();
let document = w.document().expect("`window` has access to `document`");
Some(EventListener::new(&document, "scroll", move |_| {
update_history(&w, &h);
}))
}
false => None,
};
Self {
do_scroll_restoration,
history,
listener_navigation: None,
listener_scroll,
listener_animation_frame: Default::default(),
window,
}
}
}
impl WebHashHistory {
fn join_url_to_hash(&self, path: String) -> Option<String> {
if path.starts_with("//") {
error!("cannot navigate to paths starting with `//`, got `{path}`");
return None;
}
let url = match self.url() {
Some(c) => match c.join(&path) {
Ok(new) => new,
Err(e) => {
error!("failed to join location with target: {e}");
return None;
}
},
None => {
error!("current location unknown");
return None;
}
};
Some(format!(
"#{path}{query}",
path = url.path(),
query = url.query().map(|q| format!("?{q}")).unwrap_or_default()
))
}
fn url(&self) -> Option<Url> {
let mut path = self.window.location().hash().ok()?;
if path.starts_with('#') {
path.remove(0);
}
if path.starts_with('/') {
path.remove(0);
}
match Url::parse(&format!("{INITIAL_URL}/{path}")) {
Ok(url) => Some(url),
Err(e) => {
error!("failed to parse hash path: {e}");
None
}
}
}
}
impl HistoryProvider for WebHashHistory {
fn current_route(&self) -> String {
self.url()
.map(|url| url.path().to_string())
.unwrap_or(String::from("/"))
}
fn current_query(&self) -> Option<String> {
self.url().and_then(|url| url.query().map(String::from))
}
fn current_prefix(&self) -> Option<String> {
Some(String::from("#"))
}
fn go_back(&mut self) {
if let Err(e) = self.history.back() {
error!("failed to go back: {e:?}")
}
}
fn go_forward(&mut self) {
if let Err(e) = self.history.forward() {
error!("failed to go forward: {e:?}")
}
}
fn push(&mut self, path: String) {
let hash = match self.join_url_to_hash(path) {
Some(hash) => hash,
None => return,
};
let state = match self.do_scroll_restoration {
true => top_left(),
false => self.history.state().unwrap_or_default(),
};
let nav = self.history.push_state_with_url(&state, "", Some(&hash));
match nav {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => error!("failed to push state: {e:?}"),
}
}
fn replace(&mut self, path: String) {
let hash = match self.join_url_to_hash(path) {
Some(hash) => hash,
None => return,
};
let state = match self.do_scroll_restoration {
true => top_left(),
false => self.history.state().unwrap_or_default(),
};
let nav = self.history.replace_state_with_url(&state, "", Some(&hash));
match nav {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => error!("failed to replace state: {e:?}"),
}
}
fn external(&mut self, url: String) -> bool {
match self.window.location().set_href(&url) {
Ok(_) => true,
Err(e) => {
error!("failed to navigate to external url (`{url}): {e:?}");
false
}
}
}
fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
let w = self.window.clone();
let h = self.history.clone();
let s = self.listener_animation_frame.clone();
let d = self.do_scroll_restoration;
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
(*callback)();
if d {
let mut s = s.lock().expect("unpoisoned scroll mutex");
*s = Some(update_scroll(&w, &h));
}
}));
}
}
+37
View File
@@ -0,0 +1,37 @@
use gloo::render::{request_animation_frame, AnimationFrame};
use log::error;
use serde::{Deserialize, Serialize};
use wasm_bindgen::JsValue;
use web_sys::{History, Window};
#[derive(Debug, Default, Deserialize, Serialize)]
pub(crate) struct ScrollPosition {
x: f64,
y: f64,
}
pub(crate) fn top_left() -> JsValue {
serde_wasm_bindgen::to_value(&ScrollPosition::default()).unwrap()
}
pub(crate) fn update_history(window: &Window, history: &History) {
let position = serde_wasm_bindgen::to_value(&ScrollPosition {
x: window.scroll_x().unwrap_or_default(),
y: window.scroll_y().unwrap_or_default(),
})
.unwrap();
if let Err(e) = history.replace_state(&position, "") {
error!("failed to update scroll position: {e:?}");
}
}
pub(crate) fn update_scroll(window: &Window, history: &History) -> AnimationFrame {
let ScrollPosition { x, y } = history
.state()
.map(|state| serde_wasm_bindgen::from_value(state).unwrap_or_default())
.unwrap_or_default();
let w = window.clone();
request_animation_frame(move |_| w.scroll_to_with_x_and_y(x, y))
}
-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::{core::Component, prelude::ScopeState};
use dioxus_router_core::RouterState;
use dioxus::prelude::ScopeState;
use crate::utils::use_router_internal::use_router_internal;
use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
/// A hook that provides access to information about the current routing location.
///
@@ -56,13 +54,9 @@ use crate::utils::use_router_internal::use_router_internal;
/// ```
///
/// [`use_router`]: crate::hooks::use_router
pub fn use_route(cx: &ScopeState) -> Option<RwLockReadGuard<RouterState<Component>>> {
pub fn use_route<R: Routable + Clone>(cx: &ScopeState) -> Option<R> {
match use_router_internal(cx) {
Some(r) => loop {
if let Some(s) = r.state.try_read() {
break Some(s);
}
},
Some(r) => Some(r.current()),
None => {
#[cfg(debug_assertions)]
panic!("`use_route` must have access to a parent router");
+7 -195
View File
@@ -1,200 +1,12 @@
use async_lock::RwLockReadGuard;
use dioxus::{core::Component, prelude::*};
use dioxus_router_core::{
history::{HistoryProvider, MemoryHistory},
routes::{ContentAtom, Segment},
Navigator, RouterService, RouterState, RoutingCallback,
};
use log::error;
use dioxus::prelude::ScopeState;
use crate::{
contexts::router::RouterContext,
prelude::{
comp,
default_errors::{
FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit,
},
},
prelude::RouterContext, routable::Routable, utils::use_router_internal::use_router_internal,
};
/// The basic building block required for all other router components and hooks.
///
/// This manages a [`dioxus_router_core::RouterService`], which in turn is required for basically
/// all router functionality. All other components and hooks provided by [`dioxus_router`](crate)
/// will only work as/in components nested within a component calling [`use_router`].
///
/// Components calling [`use_router`] should not be nested within each other.
///
/// # Return values
/// This hook returns the current router state and a navigator. For more information about the
/// state, see the [`use_route`](crate::hooks::use_route) hook. For more information about the
/// [`Navigator`], see its own documentation and the [`use_navigate`](crate::hooks::use_navigate)
/// hook.
///
/// # Panic
/// - When used within a component, that is nested inside another component calling [`use_router`],
/// but only in debug builds.
///
/// # Example
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_router::prelude::*;
/// fn App(cx: Scope) -> Element {
/// let (_, _) = use_router(
/// &cx,
/// &|| RouterConfiguration {
/// synchronous: true, // asynchronicity not needed for doc test
/// ..Default::default()
/// },
/// &|| Segment::content(comp(Content))
/// );
///
/// render! {
/// h1 { "App" }
/// Outlet { }
/// }
/// }
///
/// fn Content(cx: Scope) -> Element {
/// render! {
/// p { "Some content" }
/// }
/// }
/// # let mut vdom = VirtualDom::new(App);
/// # let _ = vdom.rebuild();
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Some content</p>");
/// ```
pub fn use_router<'a>(
cx: &'a ScopeState,
cfg: &dyn Fn() -> RouterConfiguration,
content: &dyn Fn() -> Segment<Component>,
) -> (
RwLockReadGuard<'a, RouterState<Component>>,
Navigator<ScopeId>,
) {
let (service, state, sender) = cx.use_hook(|| {
#[allow(unreachable_code, unused_variables)]
if let Some(outer) = cx.consume_context::<RouterContext>() {
let msg = "components using `use_router` should not be nested within each other";
error!("{msg}, inner will be inactive and transparent");
#[cfg(debug_assertions)]
panic!("{}", msg);
return (None, outer.state, outer.sender);
}
let cfg = cfg();
let content = content();
let (mut service, sender, state) = RouterService::new(
content,
cfg.history,
cx.schedule_update_any(),
cfg.on_update,
cfg.failure_external_navigation,
cfg.failure_named_navigation,
cfg.failure_redirection_limit,
);
cx.provide_context(RouterContext {
state: state.clone(),
sender: sender.clone(),
});
(
if cfg.synchronous {
service.init();
Some(service)
} else {
cx.spawn(async move { service.run().await });
None
},
state,
sender,
)
});
if let Some(service) = service {
service.run_current();
}
(
loop {
if let Some(state) = state.try_read() {
break state;
}
},
sender.clone().into(),
)
}
/// Global configuration options for the router.
///
/// This implements [`Default`], so you can use it like this:
/// ```rust,no_run
/// # use dioxus_router::prelude::RouterConfiguration;
/// let cfg = RouterConfiguration {
/// synchronous: false,
/// ..Default::default()
/// };
/// ```
pub struct RouterConfiguration {
/// A component to render when an external navigation fails.
///
/// Defaults to a router-internal component called `FailureExternalNavigation`. It is not part
/// of the public API. Do not confuse it with
/// [`dioxus_router_core::prelude::FailureExternalNavigation`].
pub failure_external_navigation: ContentAtom<Component>,
/// A component to render when a named navigation fails.
///
/// Defaults to a router-internal component called `FailureNamedNavigation`. It is not part of
/// the public API. Do not confuse it with
/// [`dioxus_router_core::prelude::FailureNamedNavigation`].
pub failure_named_navigation: ContentAtom<Component>,
/// A component to render when the redirect limit is reached.
///
/// Defaults to a router-internal component called `FailureRedirectionLimit`. It is not part of
/// the public API. Do not confuse it with
/// [`dioxus_router_core::prelude::FailureRedirectionLimit`].
pub failure_redirection_limit: ContentAtom<Component>,
/// The [`HistoryProvider`] the router should use.
///
/// Defaults to a default [`MemoryHistory`].
pub history: Box<dyn HistoryProvider>,
/// A function to be called whenever the routing is updated.
///
/// The callback is invoked after the routing is updated, but before components and hooks are
/// updated.
///
/// If the callback returns a [`NavigationTarget`] the router will replace the current location
/// with it. If no navigation failure was triggered, the router will then updated dependent
/// components and hooks.
///
/// The callback is called no more than once per rerouting. It will not be called if a
/// navigation failure occurs.
///
/// Defaults to [`None`].
///
/// [`NavigationTarget`]: dioxus_router_core::navigation::NavigationTarget
pub on_update: Option<RoutingCallback<Component>>,
/// Whether the router should run in synchronous mode.
///
/// If [`true`], the router will only update its state whenever the component using the
/// [`use_router`] hook rerenders. If [`false`], an asynchronous task is launched and the router
/// will update whenever it receives new input.
///
/// Defaults to [`false`].
pub synchronous: bool,
}
impl Default for RouterConfiguration {
fn default() -> Self {
Self {
failure_external_navigation: comp(FailureExternalNavigation),
failure_named_navigation: comp(FailureNamedNavigation),
failure_redirection_limit: comp(FailureRedirectionLimit),
history: Box::<MemoryHistory>::default(),
on_update: None,
synchronous: false,
}
}
/// A hook that provides access to information about the router
pub fn use_router<R: Routable + Clone>(cx: &ScopeState) -> &RouterContext<R> {
use_router_internal(cx)
.as_ref()
.expect("use_route must have access to a router")
}
+14 -14
View File
@@ -1,6 +1,10 @@
#![doc = include_str!("../README.md")]
// cannot use forbid, because props derive macro generates #[allow(missing_docs)]
#![deny(missing_docs)]
#![allow(non_snake_case)]
pub mod navigation;
pub mod routable;
/// Components interacting with the router.
pub mod components {
@@ -14,19 +18,23 @@ pub mod components {
mod outlet;
pub use outlet::*;
mod router;
pub use router::*;
}
mod contexts {
pub(crate) mod outlet;
pub(crate) mod router;
pub use router::*;
}
pub use dioxus_router_core::history;
mod router_cfg;
pub mod history;
/// Hooks for interacting with the router in components.
pub mod hooks {
mod use_navigate;
pub use use_navigate::*;
mod use_router;
pub use use_router::*;
@@ -36,18 +44,10 @@ pub mod hooks {
/// A collection of useful items most applications might need.
pub mod prelude {
pub use dioxus_router_core::prelude::*;
pub use crate::components::*;
pub use crate::contexts::*;
pub use crate::hooks::*;
/// Wrap a [`Component`](dioxus::core::Component) inside a [`ContentAtom`].
///
/// This is purely a convenience function.
#[must_use]
pub fn comp(component: dioxus::core::Component) -> ContentAtom<dioxus::core::Component> {
ContentAtom(component)
}
pub use dioxus_router_macro::routable;
}
mod utils {
+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)),
}
}
}
+126
View File
@@ -0,0 +1,126 @@
//! # Routable
#![allow(non_snake_case)]
use dioxus::prelude::*;
use std::str::FromStr;
/// An error that occurs when parsing a route
#[derive(Debug, PartialEq)]
pub struct RouteParseError<E: std::fmt::Display> {
/// The attempted routes that failed to match
pub attempted_routes: Vec<E>,
}
impl<E: std::fmt::Display> std::fmt::Display for RouteParseError<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Route did not match:\nAttempted Matches:\n")?;
for (i, route) in self.attempted_routes.iter().enumerate() {
writeln!(f, "{}) {route}", i + 1)?;
}
Ok(())
}
}
/// Something that can be created from a query string
pub trait FromQuery {
/// Create an instance of `Self` from a query string
fn from_query(query: &str) -> Self;
}
impl<T: for<'a> From<&'a str>> FromQuery for T {
fn from_query(query: &str) -> Self {
T::from(query)
}
}
/// Something that can be created from a route segment
pub trait FromRouteSegment: Sized {
/// The error that can occur when parsing a route segment
type Err;
/// Create an instance of `Self` from a route segment
fn from_route_segment(route: &str) -> Result<Self, Self::Err>;
}
impl<T: FromStr> FromRouteSegment for T
where
<T as FromStr>::Err: std::fmt::Display,
{
type Err = <T as FromStr>::Err;
fn from_route_segment(route: &str) -> Result<Self, Self::Err> {
T::from_str(route)
}
}
/// Something that can be converted to route segments
pub trait ToRouteSegments {
/// Display the route segments
fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
}
impl<I, T: std::fmt::Display> ToRouteSegments for I
where
I: IntoIterator<Item = T>,
{
fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for segment in self {
write!(f, "/")?;
write!(f, "{}", segment)?;
}
Ok(())
}
}
/// Something that can be created from route segments
pub trait FromRouteSegments: Sized {
/// The error that can occur when parsing route segments
type Err;
/// Create an instance of `Self` from route segments
fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err>;
}
impl<I: std::iter::FromIterator<String>> FromRouteSegments for I {
type Err = <String as FromRouteSegment>::Err;
fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err> {
segments
.iter()
.map(|s| String::from_route_segment(s))
.collect()
}
}
/// Something that can be routed to
pub trait Routable: std::fmt::Display + std::str::FromStr + 'static {
/// Render the route at the given level
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
}
trait RoutableFactory {
type Err: std::fmt::Display;
type Routable: Routable + FromStr<Err = Self::Err>;
}
impl<R: Routable + FromStr> RoutableFactory for R
where
<R as FromStr>::Err: std::fmt::Display,
{
type Err = <R as FromStr>::Err;
type Routable = R;
}
trait RouteRenderable: std::fmt::Display + 'static {
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
}
impl<R: Routable> RouteRenderable for R
where
<R as FromStr>::Err: std::fmt::Display,
{
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> {
self.render(cx, level)
}
}
+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,
}
}
}
+145
View File
@@ -0,0 +1,145 @@
use std::collections::BTreeMap;
use urlencoding::encode;
pub fn gen_sitemap<T: Clone>(seg: &Segment<T>, current: &str, map: &mut Vec<String>) {
for (p, r) in &seg.fixed {
let current = format!("{current}/{p}");
map.push(current.clone());
if let Some(n) = &r.nested {
gen_sitemap(n, &current, map);
}
}
for (_, r) in &seg.matching {
let current = format!("{current}/\\{}", r.key);
map.push(current.clone());
if let Some(n) = &r.nested {
gen_sitemap(n, &current, map);
}
}
if let Some(r) = &seg.catch_all {
let current = format!("{current}/\\{}", r.key);
map.push(current.clone());
if let Some(n) = &r.nested {
gen_sitemap(n, &current, map)
}
}
}
pub fn gen_parameter_sitemap<T: Clone>(
seg: &Segment<T>,
parameters: &BTreeMap<Name, Vec<String>>,
current: &str,
map: &mut Vec<String>,
) {
for (p, r) in &seg.fixed {
let current = format!("{current}/{p}");
map.push(current.clone());
if let Some(n) = &r.nested {
gen_parameter_sitemap(n, parameters, &current, map);
}
}
for (m, r) in &seg.matching {
if let Some(rp) = parameters.get(&r.key) {
for p in rp {
if m.matches(p) {
let current = format!("{current}/{}", encode(p).into_owned());
map.push(current.clone());
if let Some(n) = &r.nested {
gen_parameter_sitemap(n, parameters, &current, map);
}
}
}
}
}
if let Some(r) = &seg.catch_all {
if let Some(rp) = parameters.get(&r.key) {
for p in rp {
let current = format!("{current}/{}", encode(p).into_owned());
map.push(current.clone());
if let Some(n) = &r.nested {
gen_parameter_sitemap(n, parameters, &current, map);
}
}
}
}
}
#[cfg(test)]
mod tests {
use crate::routes::{ParameterRoute, Route};
use super::*;
fn test_segment() -> Segment<&'static str> {
Segment::empty()
.fixed(
"fixed",
Route::empty().nested(Segment::empty().fixed("nested", Route::empty())),
)
.matching(
String::from("m1"),
ParameterRoute::empty::<u8>().nested(
Segment::empty().matching(String::from("n2"), ParameterRoute::empty::<u16>()),
),
)
.matching(String::from("no match"), ParameterRoute::empty::<u32>())
.matching(String::from("no parameter"), ParameterRoute::empty::<u64>())
.catch_all(
ParameterRoute::empty::<u32>()
.nested(Segment::empty().catch_all(ParameterRoute::empty::<u16>())),
)
}
#[test]
fn sitemap() {
let mut result = Vec::new();
result.push(String::from("/"));
gen_sitemap(&test_segment(), "", &mut result);
assert_eq!(
result,
vec![
"/",
"/fixed",
"/fixed/nested",
"/\\u8",
"/\\u8/\\u16",
"/\\u32",
"/\\u64",
"/\\u32",
"/\\u32/\\u16"
]
);
}
#[test]
fn sitemap_with_parameters() {
let mut parameters = BTreeMap::new();
parameters.insert(Name::of::<u8>(), vec!["m1".to_string(), "m2".to_string()]);
parameters.insert(Name::of::<u16>(), vec!["n1".to_string(), "n2".to_string()]);
parameters.insert(Name::of::<u32>(), vec!["catch all".to_string()]);
let mut result = Vec::new();
result.push(String::from("/"));
gen_parameter_sitemap(&test_segment(), &parameters, "", &mut result);
assert_eq!(
result,
vec![
"/",
"/fixed",
"/fixed/nested",
"/m1",
"/m1/n2",
"/catch%20all",
"/catch%20all/n1",
"/catch%20all/n2"
]
);
}
}
@@ -1,9 +1,6 @@
use std::sync::Arc;
use dioxus::prelude::{ScopeId, ScopeState};
use dioxus::prelude::ScopeState;
use dioxus_router_core::RouterMessage;
use crate::contexts::router::RouterContext;
use crate::{contexts::router::RouterContext, routable::Routable};
/// A private hook to subscribe to the router.
///
@@ -15,16 +12,25 @@ use crate::contexts::router::RouterContext;
/// - Otherwise [`Some`].
///
/// [`use_router`]: crate::hooks::use_router
pub(crate) fn use_router_internal(cx: &ScopeState) -> &mut Option<RouterContext> {
let id = cx.use_hook(|| Arc::new(cx.scope_id()));
pub(crate) fn use_router_internal<R: Routable>(cx: &ScopeState) -> &Option<RouterContext<R>> {
let inner = cx.use_hook(|| {
let router = cx.consume_context::<RouterContext<R>>()?;
cx.use_hook(|| {
let router = cx.consume_context::<RouterContext>()?;
let id = cx.scope_id();
router.subscribe(id);
let _ = router
.sender
.unbounded_send(RouterMessage::Subscribe(id.clone()));
Some(Subscription { router, id })
});
cx.use_hook(|| inner.as_ref().map(|s| s.router.clone()))
}
Some(router)
})
struct Subscription<R: Routable> {
router: RouterContext<R>,
id: ScopeId,
}
impl<R: Routable> Drop for Subscription<R> {
fn drop(&mut self) {
self.router.unsubscribe(self.id);
}
}