From 0cc0cba482439d2f0056c1b93ed014b8d78b47d7 Mon Sep 17 00:00:00 2001 From: Adrian Wannenmacher Date: Tue, 13 Dec 2022 00:09:32 +0100 Subject: [PATCH] add navigator and use_navigate hook --- packages/router-core/src/lib.rs | 3 + packages/router-core/src/navigator.rs | 97 +++++++++++++++++++++++ packages/router-core/src/service.rs | 27 +++++++ packages/router/src/hooks/use_navigate.rs | 20 +++++ packages/router/src/hooks/use_router.rs | 9 ++- packages/router/src/lib.rs | 3 + 6 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 packages/router-core/src/navigator.rs create mode 100644 packages/router/src/hooks/use_navigate.rs diff --git a/packages/router-core/src/lib.rs b/packages/router-core/src/lib.rs index 454aa1e4f..e9104e51d 100644 --- a/packages/router-core/src/lib.rs +++ b/packages/router-core/src/lib.rs @@ -8,6 +8,9 @@ pub use name::*; pub mod navigation; +mod navigator; +pub use navigator::*; + mod outlet; pub use outlet::*; diff --git a/packages/router-core/src/navigator.rs b/packages/router-core/src/navigator.rs new file mode 100644 index 000000000..0d6e94e94 --- /dev/null +++ b/packages/router-core/src/navigator.rs @@ -0,0 +1,97 @@ +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 { + sender: UnboundedSender>, +} + +impl Navigator { + /// 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) { + 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) { + let _ = self + .sender + .unbounded_send(RouterMessage::Replace(target.into())); + } +} + +impl From>> for Navigator { + fn from(sender: UnboundedSender>) -> Self { + Self { sender } + } +} + +#[cfg(test)] +mod tests { + use futures_channel::mpsc::{unbounded, UnboundedReceiver}; + + use super::*; + + fn prepare() -> (Navigator<()>, UnboundedReceiver>) { + 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))); + } +} diff --git a/packages/router-core/src/service.rs b/packages/router-core/src/service.rs index 765ff47bd..c73193c67 100644 --- a/packages/router-core/src/service.rs +++ b/packages/router-core/src/service.rs @@ -1,5 +1,6 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, + fmt::Debug, sync::{Arc, Weak}, }; @@ -36,6 +37,32 @@ pub enum RouterMessage { GoForward, } +impl Debug for RouterMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Subscribe(arg0) => f.debug_tuple("Subscribe").field(arg0).finish(), + Self::Push(arg0) => f.debug_tuple("Push").field(arg0).finish(), + Self::Replace(arg0) => f.debug_tuple("Replace").field(arg0).finish(), + Self::Update => write!(f, "Update"), + Self::GoBack => write!(f, "GoBack"), + Self::GoForward => write!(f, "GoForward"), + } + } +} + +impl PartialEq for RouterMessage { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Subscribe(l0), Self::Subscribe(r0)) => l0 == r0, + (Self::Push(l0), Self::Push(r0)) => l0 == r0, + (Self::Replace(l0), Self::Replace(r0)) => l0 == r0, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + +impl Eq for RouterMessage {} + enum NavigationFailure { External(String), Named(Name), diff --git a/packages/router/src/hooks/use_navigate.rs b/packages/router/src/hooks/use_navigate.rs new file mode 100644 index 000000000..20e5f617c --- /dev/null +++ b/packages/router/src/hooks/use_navigate.rs @@ -0,0 +1,20 @@ +use dioxus::prelude::{ScopeId, ScopeState}; +use dioxus_router_core::Navigator; +use log::error; + +use crate::utils::use_router_internal::use_router_internal; + +#[must_use] +pub fn use_navigate(cx: &ScopeState) -> Option> { + match use_router_internal(cx) { + Some(r) => Some(r.sender.clone().into()), + None => { + let msg = "`use_navigate` must have access to a parent router"; + error!("{msg}, will be inactive"); + #[cfg(debug_assertions)] + panic!("{}", msg); + #[cfg(not(debug_assertions))] + None + } + } +} diff --git a/packages/router/src/hooks/use_router.rs b/packages/router/src/hooks/use_router.rs index c65ad19b2..9575380cb 100644 --- a/packages/router/src/hooks/use_router.rs +++ b/packages/router/src/hooks/use_router.rs @@ -3,7 +3,7 @@ use dioxus::{core::Component, prelude::*}; use dioxus_router_core::{ history::{HistoryProvider, MemoryHistory}, routes::{ContentAtom, Segment}, - RouterService, RouterState, RoutingCallback, + Navigator, RouterService, RouterState, RoutingCallback, }; use crate::{ @@ -20,7 +20,10 @@ pub fn use_router<'a>( cx: &'a ScopeState, cfg: &dyn Fn() -> RouterConfiguration, content: &dyn Fn() -> Segment, -) -> (RwLockReadGuard<'a, RouterState>, ()) { +) -> ( + RwLockReadGuard<'a, RouterState>, + Navigator, +) { let (service, state, sender) = cx.use_hook(|| { let cfg = cfg(); let content = content(); @@ -62,7 +65,7 @@ pub fn use_router<'a>( break state; } }, - (), + sender.clone().into(), ) } diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index 35c0d2356..0eab933a1 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -13,6 +13,9 @@ mod contexts { } pub mod hooks { + mod use_navigate; + pub use use_navigate::*; + mod use_router; pub use use_router::*;