From ece8f0fb2269385a8a023e14ca8cd406357a227d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 31 May 2023 17:12:52 -0500 Subject: [PATCH] general cleanup --- packages/router/examples/simple_routes.rs | 4 + .../router/src/components/default_errors.rs | 62 +- packages/router/src/components/outlet.rs | 6 +- packages/router/src/components/router.rs | 4 +- packages/router/src/contexts/outlet.rs | 19 +- packages/router/src/contexts/router.rs | 718 ++---------------- packages/router/src/history/memory.rs | 1 - packages/router/src/history/mod.rs | 17 +- packages/router/src/hooks/use_route.rs | 5 +- packages/router/src/hooks/use_router.rs | 2 +- packages/router/src/lib.rs | 10 +- packages/router/src/navigation.rs | 41 +- packages/router/src/router_cfg.rs | 25 +- packages/router/tests/via_ssr/link.rs | 6 +- packages/router/tests/via_ssr/outlet.rs | 2 +- 15 files changed, 155 insertions(+), 767 deletions(-) diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs index 623096dc2..9b8821c25 100644 --- a/packages/router/examples/simple_routes.rs +++ b/packages/router/examples/simple_routes.rs @@ -101,6 +101,10 @@ fn Route3(cx: Scope, dynamic: String) -> Element { target: Route::Route2 { user_id: 8888 }, "hello world link" } + button { + onclick: move |_| { router.push(NavigationTarget::External("https://www.google.com".to_string())); }, + "google link" + } p { "Site Map" } pre { "{site_map:#?}" } p { "Dynamic link" } diff --git a/packages/router/src/components/default_errors.rs b/packages/router/src/components/default_errors.rs index 9264942c0..16a680803 100644 --- a/packages/router/src/components/default_errors.rs +++ b/packages/router/src/components/default_errors.rs @@ -1,15 +1,10 @@ -use crate::{ - components::GenericLink, hooks::use_generic_route, navigation::NavigationTarget, - routable::Routable, -}; +use crate::{hooks::use_generic_router, routable::Routable}; use dioxus::prelude::*; +/// The default component to render when an external navigation fails. #[allow(non_snake_case)] pub fn FailureExternalNavigation(cx: Scope) -> Element { - let href = use_generic_route::(cx).expect( - "`FailureExternalNavigation` can only be mounted by the router itself, \ - since it is not exposed", - ); + let router = use_generic_router::(cx); render! { h1 { "External Navigation Failure!" } @@ -18,53 +13,10 @@ pub fn FailureExternalNavigation(cx: Scope) -> Element { "operation has failed. Click the link below to complete the navigation manually." } a { - href: "{href}", - rel: "noopener noreferrer", - "Click here to fix the failure." - } - } -} - -#[allow(non_snake_case)] -pub fn FailureNamedNavigation(cx: Scope) -> Element { - render! { - h1 { "Named Navigation Failure!" } - p { - "The application has tried to navigate to an unknown name. This is a bug. Please " - "inform the developer, so they can fix it." - b { "Thank you!" } - } - p { - "We are sorry for the inconvenience. The link below may help to fix the problem, but " - "there is no guarantee." - } - GenericLink:: { - target: NavigationTarget::Internal(R::from_str("/").unwrap_or_else(|_| { - panic!("Failed to parse `/` as a Route") - })), - "Click here to try to fix the failure." - } - } -} - -#[allow(non_snake_case)] -pub fn FailureRedirectionLimit(cx: Scope) -> Element { - render! { - h1 { "Redirection Limit Failure!" } - p { - "The application seems to have entered into an endless redirection loop. This is a " - "bug. Please inform the developer, so they can fix it." - b { "Thank you!" } - } - p { - "We are sorry for the inconvenience. The link below may help to fix the problem, but " - "there is no guarantee." - } - GenericLink:: { - target: NavigationTarget::Internal(R::from_str("/").unwrap_or_else(|_| { - panic!("Failed to parse `/` as a Route") - })), - "Click here to try to fix the failure." + onclick: move |_| { + router.clear_error() + }, + "Click here to go back" } } } diff --git a/packages/router/src/components/outlet.rs b/packages/router/src/components/outlet.rs index d45c9fd82..842553587 100644 --- a/packages/router/src/components/outlet.rs +++ b/packages/router/src/components/outlet.rs @@ -1,15 +1,15 @@ -use crate::{contexts::outlet::OutletContext, routable::Routable}; +use crate::prelude::{outlet::OutletContext, *}; use dioxus::prelude::*; /// An outlet for the current content. /// /// Only works as descendant of a [`GenericRouter`] component, otherwise it will be inactive. /// -/// The [`Outlet`] is aware of how many [`Outlet`]s it is nested within. It will render the content +/// The [`GenericOutlet`] 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__. /// /// # Panic -/// - When the [`Outlet`] is not nested within another component calling the [`use_router`] hook, +/// - When the [`GenericOutlet`] is not nested a [`GenericRouter`] component, /// but only in debug builds. /// /// # Example diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 977df4b64..ba515abb9 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -9,7 +9,7 @@ use crate::{ router_cfg::RouterConfiguration, }; -/// The config for [`Router`]. +/// The config for [`GenericRouter`]. pub struct RouterCfg { config: RefCell>>, } @@ -33,7 +33,7 @@ impl From> for RouterCfg { } } -/// The props for [`Router`]. +/// The props for [`GenericRouter`]. #[derive(Props)] pub struct GenericRouterProps where diff --git a/packages/router/src/contexts/outlet.rs b/packages/router/src/contexts/outlet.rs index f5a78ab98..421f1640b 100644 --- a/packages/router/src/contexts/outlet.rs +++ b/packages/router/src/contexts/outlet.rs @@ -1,6 +1,6 @@ use dioxus::prelude::*; -use crate::{hooks::use_generic_route, routable::Routable}; +use crate::{routable::Routable, utils::use_router_internal::use_router_internal}; #[derive(Clone)] pub(crate) struct OutletContext { @@ -16,7 +16,10 @@ pub(crate) fn use_outlet_context(cx: &ScopeState) -> &OutletContext { } impl OutletContext { - pub(crate) fn render(cx: &ScopeState) -> Element<'_> { + pub(crate) fn render(cx: Scope) -> Element<'_> { + let router = use_router_internal::(cx) + .as_ref() + .expect("Outlet must be inside of a router"); let outlet = use_outlet_context(cx); let current_level = outlet.current_level; cx.provide_context({ @@ -25,8 +28,14 @@ impl OutletContext { } }); - use_generic_route::(cx) - .expect("Outlet must be inside of a router") - .render(cx, current_level) + if let Some(error) = router.render_error(cx) { + if current_level == 0 { + return Some(error); + } else { + return None; + } + } + + router.current().render(cx, current_level) } } diff --git a/packages/router/src/contexts/router.rs b/packages/router/src/contexts/router.rs index 832593f0d..abb85e712 100644 --- a/packages/router/src/contexts/router.rs +++ b/packages/router/src/contexts/router.rs @@ -11,12 +11,8 @@ use crate::{ }; /// An error that can occur when navigating. -pub enum NavigationFailure { - /// The router failed to navigate to an external URL. - External(String), - /// The router failed to navigate to an internal URL. - Internal(::Err), -} +#[derive(Debug, Clone)] +pub struct ExternalNavigationFailure(String); /// A function the router will call after every routing update. pub type RoutingCallback = Arc) -> Option>>; @@ -40,6 +36,8 @@ where prefix: Option, history: Box>, + + unresolved_error: Option, } /// A collection of router data that manages all routing functionality. @@ -54,8 +52,6 @@ where routing_callback: Option>, failure_external_navigation: fn(Scope) -> Element, - failure_named_navigation: fn(Scope) -> Element, - failure_redirection_limit: fn(Scope) -> Element, } impl Clone for GenericRouterContext { @@ -66,8 +62,6 @@ impl Clone for GenericRouterContext { 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, } } } @@ -88,6 +82,7 @@ where can_go_forward: false, prefix: Default::default(), history: cfg.history, + unresolved_error: None, })); let subscriber_update = mark_dirty.clone(); @@ -101,8 +96,6 @@ where 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, }; // set the updater @@ -134,46 +127,61 @@ where /// /// 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(); + { + self.state.write().unwrap().history.go_back(); + } + + self.change_route(); } /// 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(); + { + self.state.write().unwrap().history.go_forward(); + } + + self.change_route(); } /// Push a new location. /// /// The previous location will be available to go back to. - pub fn push(&self, target: impl Into>) -> Option> { + pub fn push( + &self, + target: impl Into>, + ) -> Option { let target = target.into(); - let mut state = self.state_mut(); match target { - NavigationTarget::Internal(p) => state.history.push(p), + NavigationTarget::Internal(p) => { + let mut state = self.state_mut(); + state.history.push(p) + } NavigationTarget::External(e) => return self.external(e), } - self.update_subscribers(); - None + self.change_route() } /// Replace the current location. /// /// The previous location will **not** be available to go back to. - pub fn replace(&self, target: impl Into>) -> Option> { + pub fn replace( + &self, + target: impl Into>, + ) -> Option { let target = target.into(); - let mut state = self.state_mut(); - match target { - NavigationTarget::Internal(p) => state.history.replace(p), - NavigationTarget::External(e) => return self.external(e), + + { + 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 + self.change_route() } /// The route that is currently active. @@ -189,11 +197,18 @@ where self.state.read().unwrap().prefix.clone() } - fn external(&self, external: String) -> Option> { + fn external(&self, external: String) -> Option { let mut state = self.state_mut(); match state.history.external(external.clone()) { true => None, - false => Some(NavigationFailure::External(external)), + false => { + let failure = ExternalNavigationFailure(external); + state.unresolved_error = Some(failure.clone()); + + self.update_subscribers(); + + Some(failure) + } } } @@ -216,614 +231,37 @@ where (self.subscriber_update)(id); } } + + /// Clear any unresolved errors + pub fn clear_error(&self) { + self.state.write().unwrap().unresolved_error = None; + + self.update_subscribers(); + } + + pub(crate) fn render_error<'a>(&self, cx: Scope<'a>) -> Element<'a> { + self.state + .read() + .unwrap() + .unresolved_error + .as_ref() + .and_then(|_| (self.failure_external_navigation)(cx)) + } + + fn change_route(&self) -> Option { + if let Some(callback) = &self.routing_callback { + let myself = self.clone(); + if let Some(new) = callback(myself) { + let mut state = self.state_mut(); + match new { + NavigationTarget::Internal(p) => state.history.replace(p), + NavigationTarget::External(e) => return self.external(e), + } + } + } + + self.update_subscribers(); + + None + } } - -// #[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::(), -// ) -// .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::()) -// } - -// #[test] -// fn new_provides_update_to_history() { -// struct TestHistory {} - -// impl HistoryProvider for TestHistory { -// fn current_route(&self) -> String { -// todo!() -// } - -// fn current_query(&self) -> Option { -// 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) { -// 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::()); -// 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::::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::()); -// 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::()); -// 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::()); -// 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::::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::::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::()); -// 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::::default(), -// Arc::new(|_| {}), -// None, -// ContentAtom("external target"), -// ContentAtom("named target"), -// ContentAtom("redirect limit"), -// ); -// s.push(NavigationTarget::named::()); -// 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::()); -// 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::()); -// r -// }); -// assert_eq!(state.parameters, { -// let mut r = HashMap::new(); -// r.insert( -// Name::of::(), -// 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::::default(), -// Arc::new(|_| {}), -// None, -// ContentAtom("external target"), -// ContentAtom("named target"), -// ContentAtom("redirect limit"), -// ); -// s.replace(NavigationTarget::named::()); -// 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::()); -// 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::::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::()); -// 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::()); -// r -// }); -// assert_eq!(state.parameters, { -// let mut r = HashMap::new(); -// r.insert( -// Name::of::(), -// 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::::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::(), 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_route(&self) -> String { -// String::from("/") -// } - -// fn current_query(&self) -> Option { -// None -// } - -// fn current_prefix(&self) -> Option { -// 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) { -// 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::()); -// 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); -// } -// } diff --git a/packages/router/src/history/memory.rs b/packages/router/src/history/memory.rs index 08949f8ea..0c535906a 100644 --- a/packages/router/src/history/memory.rs +++ b/packages/router/src/history/memory.rs @@ -18,7 +18,6 @@ where /// Create a [`MemoryHistory`] starting at `path`. /// /// ```rust - /// # use dioxus_router::history::{HistoryProvider, MemoryHistory}; /// # use dioxus_router::prelude::*; /// # use serde::{Deserialize, Serialize}; /// # use dioxus::prelude::*; diff --git a/packages/router/src/history/mod.rs b/packages/router/src/history/mod.rs index 0b04b4ed4..c427b1054 100644 --- a/packages/router/src/history/mod.rs +++ b/packages/router/src/history/mod.rs @@ -1,10 +1,14 @@ //! History Integration //! -//! dioxus-router-core relies on so-called [`HistoryProvider`]s to store the current URL, and possibly a +//! dioxus-router-core relies on [`HistoryProvider`]s to store the current Route, 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. +//! To integrate dioxus-router with a any type of history, all you have to do is implement the +//! [`HistoryProvider`] trait. +//! +//! dioxus-router contains two built in history providers: +//! 1) [`MemoryHistory`] for desktop/mobile/ssr platforms +//! 2) [`WebHistory`] for web platforms use std::sync::Arc; @@ -42,7 +46,6 @@ pub trait HistoryProvider { /// **Must start** with `/`. **Must _not_ contain** the prefix. /// /// ```rust - /// # use dioxus_router::history::{HistoryProvider, MemoryHistory}; /// # use dioxus_router::prelude::*; /// # use serde::{Deserialize, Serialize}; /// # use dioxus::prelude::*; @@ -82,7 +85,6 @@ pub trait HistoryProvider { /// If a [`HistoryProvider`] cannot know this, it should return [`true`]. /// /// ```rust - /// # use dioxus_router::history::{HistoryProvider, MemoryHistory}; /// # use dioxus_router::prelude::*; /// # use serde::{Deserialize, Serialize}; /// # use dioxus::prelude::*; @@ -110,7 +112,6 @@ pub trait HistoryProvider { /// might be called, even if `can_go_back` returns [`false`]. /// /// ```rust - /// # use dioxus_router::history::{HistoryProvider, MemoryHistory}; /// # use dioxus_router::prelude::*; /// # use serde::{Deserialize, Serialize}; /// # use dioxus::prelude::*; @@ -144,7 +145,6 @@ pub trait HistoryProvider { /// If a [`HistoryProvider`] cannot know this, it should return [`true`]. /// /// ```rust - /// # use dioxus_router::history::{HistoryProvider, MemoryHistory}; /// # use dioxus_router::prelude::*; /// # use serde::{Deserialize, Serialize}; /// # use dioxus::prelude::*; @@ -179,7 +179,6 @@ pub trait HistoryProvider { /// might be called, even if `can_go_forward` returns [`false`]. /// /// ```rust - /// # use dioxus_router::history::{HistoryProvider, MemoryHistory}; /// # use dioxus_router::prelude::*; /// # use serde::{Deserialize, Serialize}; /// # use dioxus::prelude::*; @@ -214,7 +213,6 @@ pub trait HistoryProvider { /// 3. Clear the navigation future. /// /// ```rust - /// # use dioxus_router::history::{HistoryProvider, MemoryHistory}; /// # use dioxus_router::prelude::*; /// # use serde::{Deserialize, Serialize}; /// # use dioxus::prelude::*; @@ -245,7 +243,6 @@ pub trait HistoryProvider { /// untouched. /// /// ```rust - /// # use dioxus_router::history::{HistoryProvider, MemoryHistory}; /// # use dioxus_router::prelude::*; /// # use serde::{Deserialize, Serialize}; /// # use dioxus::prelude::*; diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 6fbddcccc..28fbd03f1 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/packages/router/src/hooks/use_route.rs @@ -10,14 +10,13 @@ use crate::utils::use_router_internal::use_router_internal; /// - Otherwise the current route. /// /// # Panic -/// - When the calling component is not nested within another component calling the [`use_router`] -/// hook, but only in debug builds. +/// - When the calling component is not nested within a [`GenericRouter`] component durring a debug build. /// /// # Example /// ```rust /// # use dioxus::prelude::*; /// # use serde::{Deserialize, Serialize}; -/// # use dioxus_router::{history::*, prelude::*}; +/// # use dioxus_router::{prelude::*}; /// /// #[derive(Clone, Serialize, Deserialize, Routable)] /// enum Route { diff --git a/packages/router/src/hooks/use_router.rs b/packages/router/src/hooks/use_router.rs index 666426517..4b644e5ea 100644 --- a/packages/router/src/hooks/use_router.rs +++ b/packages/router/src/hooks/use_router.rs @@ -9,7 +9,7 @@ use crate::{ /// /// ```rust /// # use dioxus::prelude::*; -/// # use dioxus_router::{history::*, prelude::*}; +/// # use dioxus_router::prelude::*; /// # use serde::{Deserialize, Serialize}; /// #[derive(Clone, Serialize, Deserialize, Routable)] /// enum Route { diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index cf6a5f74a..fc15a0381 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -7,8 +7,9 @@ pub mod navigation; pub mod routable; /// Components interacting with the router. -pub mod components { - pub(crate) mod default_errors; +mod components { + mod default_errors; + pub use default_errors::*; mod history_buttons; pub use history_buttons::*; @@ -31,10 +32,10 @@ mod contexts { mod router_cfg; -pub mod history; +mod history; /// Hooks for interacting with the router in components. -pub mod hooks { +mod hooks { mod use_router; pub use use_router::*; @@ -48,6 +49,7 @@ pub mod prelude { pub use crate::contexts::*; pub use crate::history::*; pub use crate::hooks::*; + pub use crate::navigation::*; pub use crate::routable::*; pub use crate::router_cfg::RouterConfiguration; pub use dioxus_router_macro::Routable; diff --git a/packages/router/src/navigation.rs b/packages/router/src/navigation.rs index 069469eb4..f9f38cbac 100644 --- a/packages/router/src/navigation.rs +++ b/packages/router/src/navigation.rs @@ -1,13 +1,17 @@ //! Types pertaining to navigation. -use std::{fmt::Display, str::FromStr}; +use std::{ + convert::TryFrom, + fmt::{Debug, Display}, + str::FromStr, +}; use url::{ParseError, Url}; use crate::routable::Routable; /// A target for the router to navigate to. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum NavigationTarget { /// An internal path that the router can navigate to by itself. /// @@ -26,7 +30,7 @@ pub enum NavigationTarget { /// Index {}, /// } /// let explicit = NavigationTarget::Internal(Route::Index {}); - /// let implicit: NavigationTarget:: = "/".into(); + /// let implicit: NavigationTarget:: = "/".parse().unwrap(); /// assert_eq!(explicit, implicit); /// ``` Internal(R), @@ -47,25 +51,17 @@ pub enum NavigationTarget { /// Index {}, /// } /// let explicit = NavigationTarget::::External(String::from("https://dioxuslabs.com/")); - /// let implicit: NavigationTarget:: = "https://dioxuslabs.com/".into(); + /// let implicit: NavigationTarget:: = "https://dioxuslabs.com/".parse().unwrap(); /// assert_eq!(explicit, implicit); /// ``` External(String), } -impl From<&str> for NavigationTarget -where - ::Err: Display, -{ - fn from(value: &str) -> Self { - Self::from_str(value).unwrap_or_else(|err| match err { - NavigationTargetParseError::InvalidUrl(e) => { - panic!("Failed to parse `{}` as a URL: {}", value, e) - } - NavigationTargetParseError::InvalidInternalURL(e) => { - panic!("Failed to parse `{}` as a `Routable`: {}", value, e) - } - }) +impl TryFrom<&str> for NavigationTarget { + type Error = NavigationTargetParseError; + + fn try_from(value: &str) -> Result { + value.parse() } } @@ -92,6 +88,17 @@ pub enum NavigationTargetParseError { InvalidInternalURL(::Err), } +impl Debug for NavigationTargetParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NavigationTargetParseError::InvalidUrl(e) => write!(f, "Invalid URL: {}", e), + NavigationTargetParseError::InvalidInternalURL(_) => { + write!(f, "Invalid internal URL") + } + } + } +} + impl FromStr for NavigationTarget { type Err = NavigationTargetParseError; diff --git a/packages/router/src/router_cfg.rs b/packages/router/src/router_cfg.rs index 040978333..96792d031 100644 --- a/packages/router/src/router_cfg.rs +++ b/packages/router/src/router_cfg.rs @@ -1,13 +1,10 @@ use crate::contexts::router::RoutingCallback; use crate::history::HistoryProvider; -use crate::prelude::*; use crate::routable::Routable; use dioxus::prelude::*; use serde::{de::DeserializeOwned, Serialize}; -use crate::prelude::default_errors::{ - FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, -}; +use crate::prelude::*; /// Global configuration options for the router. /// @@ -33,22 +30,8 @@ use crate::prelude::default_errors::{ 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::prelude::FailureExternalNavigation`]. + /// Defaults to a router-internal component called [`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::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::prelude::FailureRedirectionLimit`]. - pub failure_redirection_limit: fn(Scope) -> Element, /// The [`HistoryProvider`] the router should use. /// /// Defaults to a default [`MemoryHistory`]. @@ -58,7 +41,7 @@ pub struct RouterConfiguration { /// The callback is invoked after the routing is updated, but before components and hooks are /// updated. /// - /// If the callback returns a [`dioxus_router::navigation::NavigationTarget`] the router will replace the current location + /// 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. /// @@ -76,8 +59,6 @@ where fn default() -> Self { Self { failure_external_navigation: FailureExternalNavigation::, - failure_named_navigation: FailureNamedNavigation::, - failure_redirection_limit: FailureRedirectionLimit::, history: Box::>::default(), on_update: None, } diff --git a/packages/router/tests/via_ssr/link.rs b/packages/router/tests/via_ssr/link.rs index d6f8527ea..7f15a9cc7 100644 --- a/packages/router/tests/via_ssr/link.rs +++ b/packages/router/tests/via_ssr/link.rs @@ -101,7 +101,7 @@ fn href_external() { fn Root(cx: Scope) -> Element { render! { Link { - target: "https://dioxuslabs.com/", + target: NavigationTarget::External("https://dioxuslabs.com/".into()), "Link" } } @@ -171,7 +171,7 @@ fn with_active_class_active() { fn Root(cx: Scope) -> Element { render! { Link { - target: "/", + target: Route::Root {}, active_class: "active_class", class: "test_class", "Link" @@ -322,7 +322,7 @@ fn with_new_tab_external() { fn Root(cx: Scope) -> Element { render! { Link { - target: "https://dioxuslabs.com/", + target: NavigationTarget::External("https://dioxuslabs.com/".into()), new_tab: true, "Link" } diff --git a/packages/router/tests/via_ssr/outlet.rs b/packages/router/tests/via_ssr/outlet.rs index 7eeb46299..dab2e60f4 100644 --- a/packages/router/tests/via_ssr/outlet.rs +++ b/packages/router/tests/via_ssr/outlet.rs @@ -1,7 +1,7 @@ #![allow(non_snake_case, unused)] use dioxus::prelude::*; -use dioxus_router::{history::MemoryHistory, prelude::*}; +use dioxus_router::prelude::*; use serde::{Deserialize, Serialize}; fn prepare(path: impl Into) -> VirtualDom {