Intigrate macro with router hooks and components
This commit is contained in:
1 parent
ef6551a6cd
commit
e4b8fbfafe
37 files changed
+1442
-3009
No files matched your search
@@ -1,17 +1,12 @@
|
||||
use crate::{components::Link, hooks::use_route};
|
||||
use crate::{components::Link, hooks::use_route, navigation::NavigationTarget, routable::Routable};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router_core::prelude::{named, FailureExternalNavigation as FENName, RootIndex};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn FailureExternalNavigation(cx: Scope) -> Element {
|
||||
let state = use_route(cx).expect(
|
||||
pub fn FailureExternalNavigation<R: Routable + Clone>(cx: Scope) -> Element {
|
||||
let href = use_route::<R>(cx).expect(
|
||||
"`FailureExternalNavigation` can only be mounted by the router itself, \
|
||||
since it is not exposed",
|
||||
);
|
||||
let href = state.parameter::<FENName>().expect(
|
||||
"`FailureExternalNavigation` cannot be mounted without receiving its parameter, \
|
||||
since it is not exposed",
|
||||
);
|
||||
|
||||
render! {
|
||||
h1 { "External Navigation Failure!" }
|
||||
@@ -28,7 +23,7 @@ pub fn FailureExternalNavigation(cx: Scope) -> Element {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn FailureNamedNavigation(cx: Scope) -> Element {
|
||||
pub fn FailureNamedNavigation<R: Routable + Clone>(cx: Scope) -> Element {
|
||||
render! {
|
||||
h1 { "Named Navigation Failure!" }
|
||||
p {
|
||||
@@ -40,15 +35,15 @@ pub fn FailureNamedNavigation(cx: Scope) -> Element {
|
||||
"We are sorry for the inconvenience. The link below may help to fix the problem, but "
|
||||
"there is no guarantee."
|
||||
}
|
||||
Link {
|
||||
target: named::<RootIndex>(),
|
||||
Link::<R> {
|
||||
target: NavigationTarget::External("https://google.com".into()),
|
||||
"Click here to try to fix the failure."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn FailureRedirectionLimit(cx: Scope) -> Element {
|
||||
pub fn FailureRedirectionLimit<R: Routable + Clone>(cx: Scope) -> Element {
|
||||
render! {
|
||||
h1 { "Redirection Limit Failure!" }
|
||||
p {
|
||||
@@ -60,8 +55,8 @@ pub fn FailureRedirectionLimit(cx: Scope) -> Element {
|
||||
"We are sorry for the inconvenience. The link below may help to fix the problem, but "
|
||||
"there is no guarantee."
|
||||
}
|
||||
Link {
|
||||
target: named::<RootIndex>(),
|
||||
Link::<R> {
|
||||
target: NavigationTarget::External("https://google.com".into()),
|
||||
"Click here to try to fix the failure."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router_core::RouterMessage;
|
||||
use log::error;
|
||||
|
||||
use crate::utils::use_router_internal::use_router_internal;
|
||||
use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
|
||||
|
||||
/// The properties for a [`GoBackButton`] or a [`GoForwardButton`].
|
||||
#[derive(Debug, Props)]
|
||||
@@ -52,11 +51,11 @@ pub struct HistoryButtonProps<'a> {
|
||||
/// # );
|
||||
/// ```
|
||||
#[allow(non_snake_case)]
|
||||
pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||
pub fn GoBackButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||
let HistoryButtonProps { children } = cx.props;
|
||||
|
||||
// hook up to router
|
||||
let router = match use_router_internal(cx) {
|
||||
let router = match use_router_internal::<R>(cx) {
|
||||
Some(r) => r,
|
||||
#[allow(unreachable_code)]
|
||||
None => {
|
||||
@@ -67,20 +66,14 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let state = loop {
|
||||
if let Some(state) = router.state.try_read() {
|
||||
break state;
|
||||
}
|
||||
};
|
||||
let sender = router.sender.clone();
|
||||
|
||||
let disabled = !state.can_go_back;
|
||||
let disabled = !router.can_go_back();
|
||||
|
||||
render! {
|
||||
button {
|
||||
disabled: "{disabled}",
|
||||
prevent_default: "onclick",
|
||||
onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoBack); },
|
||||
onclick: move |_| router.go_back(),
|
||||
children
|
||||
}
|
||||
}
|
||||
@@ -127,11 +120,11 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||
/// # );
|
||||
/// ```
|
||||
#[allow(non_snake_case)]
|
||||
pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||
pub fn GoForwardButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||
let HistoryButtonProps { children } = cx.props;
|
||||
|
||||
// hook up to router
|
||||
let router = match use_router_internal(cx) {
|
||||
let router = match use_router_internal::<R>(cx) {
|
||||
Some(r) => r,
|
||||
#[allow(unreachable_code)]
|
||||
None => {
|
||||
@@ -142,20 +135,14 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let state = loop {
|
||||
if let Some(state) = router.state.try_read() {
|
||||
break state;
|
||||
}
|
||||
};
|
||||
let sender = router.sender.clone();
|
||||
|
||||
let disabled = !state.can_go_back;
|
||||
let disabled = !router.can_go_back();
|
||||
|
||||
render! {
|
||||
button {
|
||||
disabled: "{disabled}",
|
||||
prevent_default: "onclick",
|
||||
onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoForward); },
|
||||
onclick: move |_| router.go_forward(),
|
||||
children
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_router_core::{navigation::NavigationTarget, RouterMessage};
|
||||
use log::error;
|
||||
|
||||
use crate::navigation::NavigationTarget;
|
||||
use crate::routable::Routable;
|
||||
use crate::utils::use_router_internal::use_router_internal;
|
||||
|
||||
/// The properties for a [`Link`].
|
||||
#[derive(Props)]
|
||||
pub struct LinkProps<'a> {
|
||||
pub struct LinkProps<'a, R: Routable> {
|
||||
/// A class to apply to the generate HTML anchor tag if the `target` route is active.
|
||||
pub active_class: Option<&'a str>,
|
||||
/// The children to render within the generated HTML anchor tag.
|
||||
@@ -18,11 +19,6 @@ pub struct LinkProps<'a> {
|
||||
/// If `active_class` is [`Some`] and the `target` route is active, `active_class` will be
|
||||
/// appended at the end of `class`.
|
||||
pub class: Option<&'a str>,
|
||||
/// Require the __exact__ `target` to be active, for the [`Link`] to be active.
|
||||
///
|
||||
/// See [`RouterState::is_at`](dioxus_router_core::RouterState::is_at) for more details.
|
||||
#[props(default)]
|
||||
pub exact: bool,
|
||||
/// The id attribute for the generated HTML anchor tag.
|
||||
pub id: Option<&'a str>,
|
||||
/// When [`true`], the `target` route will be opened in a new tab.
|
||||
@@ -46,22 +42,21 @@ pub struct LinkProps<'a> {
|
||||
pub rel: Option<&'a str>,
|
||||
/// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
|
||||
#[props(into)]
|
||||
pub target: NavigationTarget,
|
||||
pub target: NavigationTarget<R>,
|
||||
}
|
||||
|
||||
impl Debug for LinkProps<'_> {
|
||||
impl<R: Routable> Debug for LinkProps<'_, R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LinkProps")
|
||||
.field("active_class", &self.active_class)
|
||||
.field("children", &self.children)
|
||||
.field("class", &self.class)
|
||||
.field("exact", &self.exact)
|
||||
.field("id", &self.id)
|
||||
.field("new_tab", &self.new_tab)
|
||||
.field("onclick", &self.onclick.as_ref().map(|_| "onclick is set"))
|
||||
.field("onclick_only", &self.onclick_only)
|
||||
.field("rel", &self.rel)
|
||||
.field("target", &self.target)
|
||||
.field("target", &self.target.to_string())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -130,12 +125,11 @@ impl Debug for LinkProps<'_> {
|
||||
/// # );
|
||||
/// ```
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
||||
pub fn Link<'a, R: Routable + Clone>(cx: Scope<'a, LinkProps<'a, R>>) -> Element {
|
||||
let LinkProps {
|
||||
active_class,
|
||||
children,
|
||||
class,
|
||||
exact,
|
||||
id,
|
||||
new_tab,
|
||||
onclick,
|
||||
@@ -145,7 +139,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
||||
} = cx.props;
|
||||
|
||||
// hook up to router
|
||||
let router = match use_router_internal(cx) {
|
||||
let router = match use_router_internal::<R>(cx) {
|
||||
Some(r) => r,
|
||||
#[allow(unreachable_code)]
|
||||
None => {
|
||||
@@ -156,19 +150,19 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let state = loop {
|
||||
if let Some(state) = router.state.try_read() {
|
||||
break state;
|
||||
}
|
||||
};
|
||||
let sender = router.sender.clone();
|
||||
|
||||
let href = state.href(target);
|
||||
let current_route = router.current();
|
||||
let href = current_route.to_string();
|
||||
let ac = active_class
|
||||
.and_then(|active_class| {
|
||||
state
|
||||
.is_at(target, *exact)
|
||||
.then(|| format!(" {active_class}"))
|
||||
.and_then(|active_class| match target {
|
||||
NavigationTarget::Internal(target) => {
|
||||
if href == target.to_string() {
|
||||
Some(format!(" {active_class}"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -186,7 +180,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
|
||||
let do_default = onclick.is_none() || !onclick_only;
|
||||
let action = move |event| {
|
||||
if do_default && is_router_nav {
|
||||
let _ = sender.unbounded_send(RouterMessage::Push(target.clone()));
|
||||
router.push(target.clone());
|
||||
}
|
||||
|
||||
if let Some(handler) = onclick {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
use crate::{contexts::outlet::OutletContext, routable::Routable};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// An outlet for the current content.
|
||||
///
|
||||
/// Only works as descendant of a component calling [`use_router`], otherwise it will be inactive.
|
||||
///
|
||||
/// The [`Outlet`] is aware of how many [`Outlet`]s it is nested within. It will render the content
|
||||
/// of the active route that is __exactly as deep__.
|
||||
///
|
||||
/// [`use_router`]: crate::hooks::use_router
|
||||
///
|
||||
/// # Panic
|
||||
/// - When the [`Outlet`] is not nested within another component calling the [`use_router`] hook,
|
||||
/// but only in debug builds.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_router::prelude::*;
|
||||
/// fn App(cx: Scope) -> Element {
|
||||
/// use_router(
|
||||
/// &cx,
|
||||
/// &|| RouterConfiguration {
|
||||
/// synchronous: true, // asynchronicity not needed for doc test
|
||||
/// ..Default::default()
|
||||
/// },
|
||||
/// &|| Segment::content(comp(Content))
|
||||
/// );
|
||||
///
|
||||
/// render! {
|
||||
/// h1 { "App" }
|
||||
/// Outlet { } // The content component will be rendered here
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn Content(cx: Scope) -> Element {
|
||||
/// render! {
|
||||
/// p { "Content" }
|
||||
/// }
|
||||
/// }
|
||||
/// #
|
||||
/// # let mut vdom = VirtualDom::new(App);
|
||||
/// # let _ = vdom.rebuild();
|
||||
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Content</p>");
|
||||
/// ```
|
||||
pub fn Outlet<R: Routable + Clone>(cx: Scope) -> Element {
|
||||
OutletContext::render::<R>(cx)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use dioxus::prelude::*;
|
||||
use log::error;
|
||||
use std::{cell::RefCell, str::FromStr};
|
||||
|
||||
use crate::{
|
||||
history::HistoryProvider,
|
||||
prelude::{outlet::OutletContext, RouterContext},
|
||||
routable::Routable,
|
||||
router_cfg::RouterConfiguration,
|
||||
};
|
||||
|
||||
/// The props for [`Router`].
|
||||
#[derive(Props)]
|
||||
pub struct RouterProps<R: Routable> {
|
||||
#[props(into)]
|
||||
initial_url: Option<String>,
|
||||
#[props(default, into)]
|
||||
config: RefCell<Option<RouterConfiguration<R>>>,
|
||||
}
|
||||
|
||||
impl<R: Routable> PartialEq for RouterProps<R> {
|
||||
fn eq(&self, _: &Self) -> bool {
|
||||
// prevent the router from re-rendering when the initial url or config changes
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A component that renders the current route.
|
||||
pub fn Router<R: Routable + Clone, H: HistoryProvider<R> + Default + 'static>(
|
||||
cx: Scope<RouterProps<R>>,
|
||||
) -> Element
|
||||
where
|
||||
<R as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
let router = use_context_provider(cx, || {
|
||||
#[allow(unreachable_code, unused_variables)]
|
||||
if let Some(outer) = cx.consume_context::<RouterContext<R>>() {
|
||||
let msg = "Router components should not be nested within each other";
|
||||
error!("{msg}, inner will be inactive and transparent");
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("{}", msg);
|
||||
}
|
||||
let router = RouterContext::new(
|
||||
cx.props.config.take().unwrap_or_default(),
|
||||
cx.schedule_update_any(),
|
||||
);
|
||||
if let Some(initial) = cx.props.initial_url.as_ref() {
|
||||
router.replace(
|
||||
initial
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("failed to parse initial url")),
|
||||
);
|
||||
}
|
||||
router
|
||||
});
|
||||
|
||||
use_context_provider(cx, || OutletContext { current_level: 1 });
|
||||
|
||||
router.current().render(cx, 0)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::{hooks::use_route, routable::Routable};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct OutletContext {
|
||||
pub current_level: usize,
|
||||
}
|
||||
|
||||
pub(crate) fn use_outlet_context(cx: &ScopeState) -> &OutletContext {
|
||||
let outlet_context = use_context(cx).unwrap();
|
||||
outlet_context
|
||||
}
|
||||
|
||||
impl OutletContext {
|
||||
pub(crate) fn render<R: Routable + Clone>(cx: &ScopeState) -> Element<'_> {
|
||||
let outlet = use_outlet_context(cx);
|
||||
let current_level = outlet.current_level;
|
||||
cx.provide_context({
|
||||
OutletContext {
|
||||
current_level: current_level + 1,
|
||||
}
|
||||
});
|
||||
|
||||
use_route::<R>(cx)
|
||||
.expect("Outlet must be inside of a router")
|
||||
.render(cx, current_level)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,809 @@
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
sync::{Arc, RwLock, RwLockWriteGuard},
|
||||
};
|
||||
|
||||
use async_lock::RwLock;
|
||||
use dioxus::{core::Component, prelude::ScopeId};
|
||||
use dioxus_router_core::{RouterMessage, RouterState};
|
||||
use futures_channel::mpsc::UnboundedSender;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RouterContext {
|
||||
pub(crate) state: Arc<RwLock<RouterState<Component>>>,
|
||||
pub(crate) sender: UnboundedSender<RouterMessage<ScopeId>>,
|
||||
use crate::{
|
||||
history::HistoryProvider, navigation::NavigationTarget, routable::Routable,
|
||||
router_cfg::RouterConfiguration,
|
||||
};
|
||||
|
||||
/// An error that can occur when navigating.
|
||||
pub enum NavigationFailure<R: Routable> {
|
||||
/// The router failed to navigate to an external URL.
|
||||
External(String),
|
||||
/// The router failed to navigate to an internal URL.
|
||||
Internal(<R as std::str::FromStr>::Err),
|
||||
}
|
||||
|
||||
/// A function the router will call after every routing update.
|
||||
pub type RoutingCallback<R> = Arc<dyn Fn(RouterContext<R>) -> Option<NavigationTarget<R>>>;
|
||||
|
||||
struct MutableRouterState<R>
|
||||
where
|
||||
R: Routable,
|
||||
{
|
||||
/// Whether there is a previous page to navigate back to.
|
||||
///
|
||||
/// Even if this is [`true`], there might not be a previous page. However, it is nonetheless
|
||||
/// safe to tell the router to go back.
|
||||
can_go_back: bool,
|
||||
/// Whether there is a future page to navigate forward to.
|
||||
///
|
||||
/// Even if this is [`true`], there might not be a future page. However, it is nonetheless safe
|
||||
/// to tell the router to go forward.
|
||||
can_go_forward: bool,
|
||||
|
||||
/// The current prefix.
|
||||
prefix: Option<String>,
|
||||
|
||||
history: Box<dyn HistoryProvider<R>>,
|
||||
}
|
||||
|
||||
/// A collection of router data that manages all routing functionality.
|
||||
pub struct RouterContext<R>
|
||||
where
|
||||
R: Routable,
|
||||
{
|
||||
state: Arc<RwLock<MutableRouterState<R>>>,
|
||||
|
||||
subscribers: Arc<RwLock<HashSet<ScopeId>>>,
|
||||
subscriber_update: Arc<dyn Fn(ScopeId)>,
|
||||
routing_callback: Option<RoutingCallback<R>>,
|
||||
|
||||
failure_external_navigation: fn(Scope) -> Element,
|
||||
failure_named_navigation: fn(Scope) -> Element,
|
||||
failure_redirection_limit: fn(Scope) -> Element,
|
||||
}
|
||||
|
||||
impl<R: Routable> Clone for RouterContext<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
state: self.state.clone(),
|
||||
subscribers: self.subscribers.clone(),
|
||||
subscriber_update: self.subscriber_update.clone(),
|
||||
routing_callback: self.routing_callback.clone(),
|
||||
failure_external_navigation: self.failure_external_navigation,
|
||||
failure_named_navigation: self.failure_named_navigation,
|
||||
failure_redirection_limit: self.failure_redirection_limit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> RouterContext<R>
|
||||
where
|
||||
R: Routable,
|
||||
{
|
||||
pub(crate) fn new(cfg: RouterConfiguration<R>, mark_dirty: Arc<dyn Fn(ScopeId)>) -> Self
|
||||
where
|
||||
R: Clone,
|
||||
{
|
||||
let state = Arc::new(RwLock::new(MutableRouterState {
|
||||
can_go_back: false,
|
||||
can_go_forward: false,
|
||||
prefix: Default::default(),
|
||||
history: cfg.history,
|
||||
}));
|
||||
|
||||
Self {
|
||||
state,
|
||||
subscribers: Arc::new(RwLock::new(HashSet::new())),
|
||||
subscriber_update: mark_dirty,
|
||||
|
||||
routing_callback: cfg.on_update,
|
||||
|
||||
failure_external_navigation: cfg.failure_external_navigation,
|
||||
failure_named_navigation: cfg.failure_named_navigation,
|
||||
failure_redirection_limit: cfg.failure_redirection_limit,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether there is a previous page to navigate back to.
|
||||
#[must_use]
|
||||
pub fn can_go_back(&self) -> bool {
|
||||
self.state.read().unwrap().can_go_back
|
||||
}
|
||||
|
||||
/// Check whether there is a future page to navigate forward to.
|
||||
#[must_use]
|
||||
pub fn can_go_forward(&self) -> bool {
|
||||
self.state.read().unwrap().can_go_forward
|
||||
}
|
||||
|
||||
/// Go back to the previous location.
|
||||
///
|
||||
/// Will fail silently if there is no previous location to go to.
|
||||
pub fn go_back(&self) {
|
||||
self.state.write().unwrap().history.go_back();
|
||||
self.update_subscribers();
|
||||
}
|
||||
|
||||
/// Go back to the next location.
|
||||
///
|
||||
/// Will fail silently if there is no next location to go to.
|
||||
pub fn go_forward(&self) {
|
||||
self.state.write().unwrap().history.go_forward();
|
||||
self.update_subscribers();
|
||||
}
|
||||
|
||||
/// Push a new location.
|
||||
///
|
||||
/// The previous location will be available to go back to.
|
||||
pub fn push(&self, target: NavigationTarget<R>) -> Option<NavigationFailure<R>> {
|
||||
let mut state = self.state_mut();
|
||||
match target {
|
||||
NavigationTarget::Internal(p) => state.history.push(p),
|
||||
NavigationTarget::External(e) => return self.external(e),
|
||||
}
|
||||
|
||||
self.update_subscribers();
|
||||
None
|
||||
}
|
||||
|
||||
/// Replace the current location.
|
||||
///
|
||||
/// The previous location will **not** be available to go back to.
|
||||
pub fn replace(&self, target: NavigationTarget<R>) -> Option<NavigationFailure<R>> {
|
||||
let mut state = self.state_mut();
|
||||
match target {
|
||||
NavigationTarget::Internal(p) => state.history.replace(p),
|
||||
NavigationTarget::External(e) => return self.external(e),
|
||||
}
|
||||
|
||||
self.update_subscribers();
|
||||
None
|
||||
}
|
||||
|
||||
/// The route that is currently active.
|
||||
pub fn current(&self) -> R
|
||||
where
|
||||
R: Clone,
|
||||
{
|
||||
self.state.read().unwrap().history.current_route().clone()
|
||||
}
|
||||
|
||||
/// The prefix that is currently active.
|
||||
pub fn prefix(&self) -> Option<String> {
|
||||
self.state.read().unwrap().prefix.clone()
|
||||
}
|
||||
|
||||
fn external(&self, external: String) -> Option<NavigationFailure<R>> {
|
||||
let mut state = self.state_mut();
|
||||
match state.history.external(external.clone()) {
|
||||
true => None,
|
||||
false => Some(NavigationFailure::External(external)),
|
||||
}
|
||||
}
|
||||
|
||||
fn state_mut(&self) -> RwLockWriteGuard<MutableRouterState<R>> {
|
||||
self.state.write().unwrap()
|
||||
}
|
||||
|
||||
/// Manually subscribe to the current route
|
||||
pub fn subscribe(&self, id: ScopeId) {
|
||||
self.subscribers.write().unwrap().insert(id);
|
||||
}
|
||||
|
||||
/// Manually unsubscribe from the current route
|
||||
pub fn unsubscribe(&self, id: ScopeId) {
|
||||
self.subscribers.write().unwrap().remove(&id);
|
||||
}
|
||||
|
||||
fn update_subscribers(&self) {
|
||||
for &id in self.subscribers.read().unwrap().iter() {
|
||||
(self.subscriber_update)(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// //! The tests for [`RouterContext`] test various functions that are not exposed as public.
|
||||
// //! However, several of those have an observable effect on the behavior of exposed functions.
|
||||
// //!
|
||||
// //! The alternative would be to send messages via the services channel and calling one of the
|
||||
// //! `run` functions. However, for readability and clarity, it was chosen to directly call the
|
||||
// //! private functions.
|
||||
|
||||
// use std::sync::Mutex;
|
||||
|
||||
// use crate::{
|
||||
// history::MemoryHistory,
|
||||
// routes::{ParameterRoute, Route, RouteContent},
|
||||
// };
|
||||
|
||||
// use super::*;
|
||||
|
||||
// fn test_segment() -> Segment<&'static str> {
|
||||
// Segment::content(RouteContent::Content(ContentAtom("index")))
|
||||
// .fixed(
|
||||
// "fixed",
|
||||
// Route::content(RouteContent::Content(ContentAtom("fixed"))).name::<bool>(),
|
||||
// )
|
||||
// .fixed(
|
||||
// "redirect",
|
||||
// Route::content(RouteContent::Redirect(NavigationTarget::Internal(
|
||||
// String::from("fixed"),
|
||||
// ))),
|
||||
// )
|
||||
// .fixed(
|
||||
// "redirection-loop",
|
||||
// Route::content(RouteContent::Redirect(NavigationTarget::Internal(
|
||||
// String::from("/redirection-loop"),
|
||||
// ))),
|
||||
// )
|
||||
// .fixed(
|
||||
// "%F0%9F%8E%BA",
|
||||
// Route::content(RouteContent::Content(ContentAtom("🎺"))),
|
||||
// )
|
||||
// .catch_all(ParameterRoute::empty::<bool>())
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn new_provides_update_to_history() {
|
||||
// struct TestHistory {}
|
||||
|
||||
// impl HistoryProvider for TestHistory {
|
||||
// fn current_path(&self) -> String {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn current_query(&self) -> Option<String> {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn go_back(&mut self) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn go_forward(&mut self) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn push(&mut self, _path: String) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn replace(&mut self, _path: String) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
|
||||
// callback();
|
||||
// }
|
||||
// }
|
||||
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::new(TestHistory {}),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
|
||||
// assert!(matches!(
|
||||
// s.receiver.try_next().unwrap().unwrap(),
|
||||
// RouterMessage::Update
|
||||
// ));
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn update_routing() {
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::new(MemoryHistory::with_initial_path("/fixed?test=value").unwrap()),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// assert_eq!(s.names, s.state.try_read().unwrap().name_map);
|
||||
// s.init();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||
// assert_eq!(state.names, {
|
||||
// let mut r = HashSet::new();
|
||||
// r.insert(Name::of::<bool>());
|
||||
// r
|
||||
// });
|
||||
// assert!(state.parameters.is_empty());
|
||||
// assert_eq!(state.path, String::from("/fixed"));
|
||||
// assert_eq!(state.query, Some(String::from("test=value")));
|
||||
// assert!(!state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// assert_eq!(s.names, state.name_map);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn update_routing_root_index() {
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::<MemoryHistory>::default(),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.init();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("index")]);
|
||||
// assert_eq!(state.names, {
|
||||
// let mut r = HashSet::new();
|
||||
// r.insert(Name::of::<RootIndex>());
|
||||
// r
|
||||
// });
|
||||
// assert!(state.parameters.is_empty());
|
||||
// assert_eq!(state.path, String::from("/"));
|
||||
// assert!(state.query.is_none());
|
||||
// assert!(state.prefix.is_none());
|
||||
// assert!(!state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn update_routing_redirect() {
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::new(MemoryHistory::with_initial_path("/redirect").unwrap()),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.init();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||
// assert_eq!(state.names, {
|
||||
// let mut r = HashSet::new();
|
||||
// r.insert(Name::of::<bool>());
|
||||
// r
|
||||
// });
|
||||
// assert!(state.parameters.is_empty());
|
||||
// assert_eq!(state.path, String::from("/fixed"));
|
||||
// assert!(!state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// #[should_panic = "reached redirect limit of 25"]
|
||||
// #[cfg(debug_assertions)]
|
||||
// fn update_routing_redirect_debug() {
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.init();
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// #[cfg(not(debug_assertions))]
|
||||
// fn update_routing_redirect_release() {
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.init();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("redirect limit")]);
|
||||
// assert_eq!(state.names, {
|
||||
// let mut r = HashSet::new();
|
||||
// r.insert(Name::of::<FailureRedirectionLimit>());
|
||||
// r
|
||||
// });
|
||||
// assert!(state.parameters.is_empty());
|
||||
// assert_eq!(state.path, String::from("/redirection-loop"));
|
||||
// assert_eq!(state.can_go_back, false);
|
||||
// assert_eq!(state.can_go_forward, false);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn update_subscribers() {
|
||||
// let ids = Arc::new(Mutex::new(Vec::new()));
|
||||
// let ids2 = Arc::clone(&ids);
|
||||
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// Segment::empty(),
|
||||
// Box::<MemoryHistory>::default(),
|
||||
// Arc::new(move |id| {
|
||||
// ids2.lock().unwrap().push(id);
|
||||
// }),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
|
||||
// let id0 = Arc::new(0);
|
||||
// s.subscribe(Arc::clone(&id0));
|
||||
|
||||
// let id1 = Arc::new(1);
|
||||
// s.subscribe(Arc::clone(&id1));
|
||||
|
||||
// let id1 = Arc::try_unwrap(id1).unwrap();
|
||||
// s.update_subscribers();
|
||||
|
||||
// assert_eq!(s.subscribers.len(), 1);
|
||||
// assert_eq!(s.subscribers[0].upgrade().unwrap(), id0);
|
||||
// assert_eq!(*ids.lock().unwrap(), vec![*id0, id1, *id0]);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn push_internal() {
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::<MemoryHistory>::default(),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.push(NavigationTarget::Internal(String::from("/fixed")));
|
||||
// s.init();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||
// assert_eq!(state.names, {
|
||||
// let mut r = HashSet::new();
|
||||
// r.insert(Name::of::<bool>());
|
||||
// r
|
||||
// });
|
||||
// assert!(state.parameters.is_empty());
|
||||
// assert_eq!(state.path, String::from("/fixed"));
|
||||
// assert!(state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn push_named() {
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::<MemoryHistory>::default(),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.push(NavigationTarget::named::<bool>());
|
||||
// s.init();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||
// assert_eq!(state.names, {
|
||||
// let mut r = HashSet::new();
|
||||
// r.insert(Name::of::<bool>());
|
||||
// r
|
||||
// });
|
||||
// assert!(state.parameters.is_empty());
|
||||
// assert_eq!(state.path, String::from("/fixed"));
|
||||
// assert!(state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn push_external() {
|
||||
// let (mut s, tx, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.init();
|
||||
// tx.unbounded_send(RouterMessage::Push(NavigationTarget::External(
|
||||
// String::from("https://dioxuslabs.com/"),
|
||||
// )))
|
||||
// .unwrap();
|
||||
// s.run_current();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("external target")]);
|
||||
// assert_eq!(state.names, {
|
||||
// let mut r = HashSet::new();
|
||||
// r.insert(Name::of::<FailureExternalNavigation>());
|
||||
// r
|
||||
// });
|
||||
// assert_eq!(state.parameters, {
|
||||
// let mut r = HashMap::new();
|
||||
// r.insert(
|
||||
// Name::of::<FailureExternalNavigation>(),
|
||||
// String::from("https://dioxuslabs.com/"),
|
||||
// );
|
||||
// r
|
||||
// });
|
||||
// assert_eq!(state.path, String::from("/fixed"));
|
||||
// assert!(!state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn replace_named() {
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::<MemoryHistory>::default(),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.replace(NavigationTarget::named::<bool>());
|
||||
// s.init();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||
// assert_eq!(state.names, {
|
||||
// let mut r = HashSet::new();
|
||||
// r.insert(Name::of::<bool>());
|
||||
// r
|
||||
// });
|
||||
// assert!(state.parameters.is_empty());
|
||||
// assert_eq!(state.path, String::from("/fixed"));
|
||||
// assert!(!state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn replace_internal() {
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::<MemoryHistory>::default(),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.replace(NavigationTarget::Internal(String::from("/fixed")));
|
||||
// s.init();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("fixed")]);
|
||||
// assert_eq!(state.names, {
|
||||
// let mut r = HashSet::new();
|
||||
// r.insert(Name::of::<bool>());
|
||||
// r
|
||||
// });
|
||||
// assert!(state.parameters.is_empty());
|
||||
// assert_eq!(state.path, String::from("/fixed"));
|
||||
// assert!(!state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn replace_external() {
|
||||
// let (mut s, tx, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.init();
|
||||
// tx.unbounded_send(RouterMessage::Replace(NavigationTarget::External(
|
||||
// String::from("https://dioxuslabs.com/"),
|
||||
// )))
|
||||
// .unwrap();
|
||||
// s.run_current();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("external target")]);
|
||||
// assert_eq!(state.names, {
|
||||
// let mut r = HashSet::new();
|
||||
// r.insert(Name::of::<FailureExternalNavigation>());
|
||||
// r
|
||||
// });
|
||||
// assert_eq!(state.parameters, {
|
||||
// let mut r = HashMap::new();
|
||||
// r.insert(
|
||||
// Name::of::<FailureExternalNavigation>(),
|
||||
// String::from("https://dioxuslabs.com/"),
|
||||
// );
|
||||
// r
|
||||
// });
|
||||
// assert_eq!(state.path, String::from("/fixed"));
|
||||
// assert!(!state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn subscribe() {
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// Segment::empty(),
|
||||
// Box::<MemoryHistory>::default(),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
|
||||
// let id = Arc::new(0);
|
||||
// s.subscribe(Arc::clone(&id));
|
||||
|
||||
// assert_eq!(s.subscribers.len(), 1);
|
||||
// assert_eq!(s.subscribers[0].upgrade().unwrap(), id);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn routing_callback() {
|
||||
// let paths = Arc::new(Mutex::new(Vec::new()));
|
||||
// let paths2 = Arc::clone(&paths);
|
||||
|
||||
// let (mut s, c, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
|
||||
// Arc::new(|_| {}),
|
||||
// Some(Arc::new(move |state| {
|
||||
// paths2.lock().unwrap().push(state.path.clone());
|
||||
// Some("/%F0%9F%8E%BA".into())
|
||||
// })),
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
|
||||
// assert!(paths.lock().unwrap().is_empty());
|
||||
|
||||
// s.init();
|
||||
// assert_eq!(*paths.lock().unwrap(), vec![String::from("/fixed")]);
|
||||
|
||||
// c.unbounded_send(RouterMessage::Update).unwrap();
|
||||
// s.run_current();
|
||||
// assert_eq!(
|
||||
// *paths.lock().unwrap(),
|
||||
// vec![String::from("/fixed"), String::from("/%F0%9F%8E%BA")]
|
||||
// );
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("🎺")])
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn url_decoding_do() {
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::new(MemoryHistory::with_initial_path("/%F0%9F%A5%B3").unwrap()),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.init();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert!(state.content.is_empty());
|
||||
// assert!(state.names.is_empty());
|
||||
// assert_eq!(state.parameters, {
|
||||
// let mut r = HashMap::new();
|
||||
// r.insert(Name::of::<bool>(), String::from("🥳"));
|
||||
// r
|
||||
// });
|
||||
// assert_eq!(state.path, String::from("/%F0%9F%A5%B3"));
|
||||
// assert!(!state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn url_decoding_do_not() {
|
||||
// let (mut s, c, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::new(MemoryHistory::with_initial_path("/%F0%9F%8E%BA").unwrap()),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.init();
|
||||
// c.unbounded_send(RouterMessage::Update).unwrap();
|
||||
// s.run_current();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("🎺")]);
|
||||
// assert!(state.names.is_empty());
|
||||
// assert!(state.parameters.is_empty());
|
||||
// assert_eq!(state.path, String::from("/%F0%9F%8E%BA"));
|
||||
// assert!(!state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn prefix() {
|
||||
// struct TestHistory {}
|
||||
|
||||
// impl HistoryProvider for TestHistory {
|
||||
// fn current_path(&self) -> String {
|
||||
// String::from("/")
|
||||
// }
|
||||
|
||||
// fn current_query(&self) -> Option<String> {
|
||||
// None
|
||||
// }
|
||||
|
||||
// fn current_prefix(&self) -> Option<String> {
|
||||
// Some(String::from("/prefix"))
|
||||
// }
|
||||
|
||||
// fn can_go_back(&self) -> bool {
|
||||
// false
|
||||
// }
|
||||
|
||||
// fn can_go_forward(&self) -> bool {
|
||||
// false
|
||||
// }
|
||||
|
||||
// fn go_back(&mut self) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn go_forward(&mut self) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn push(&mut self, _path: String) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn replace(&mut self, _path: String) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
// fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
|
||||
// callback();
|
||||
// }
|
||||
// }
|
||||
|
||||
// let (mut s, _, _) = RouterContext::<_, u8>::new(
|
||||
// test_segment(),
|
||||
// Box::new(TestHistory {}),
|
||||
// Arc::new(|_| {}),
|
||||
// None,
|
||||
// ContentAtom("external target"),
|
||||
// ContentAtom("named target"),
|
||||
// ContentAtom("redirect limit"),
|
||||
// );
|
||||
// s.init();
|
||||
|
||||
// let state = s.state.try_read().unwrap();
|
||||
// assert_eq!(state.content, vec![ContentAtom("index")]);
|
||||
// assert_eq!(state.names, {
|
||||
// let mut r = HashSet::new();
|
||||
// r.insert(Name::of::<RootIndex>());
|
||||
// r
|
||||
// });
|
||||
// assert!(state.parameters.is_empty());
|
||||
// assert_eq!(state.path, String::from("/"));
|
||||
// assert!(state.query.is_none());
|
||||
// assert_eq!(state.prefix, Some(String::from("/prefix")));
|
||||
// assert!(!state.can_go_back);
|
||||
// assert!(!state.can_go_forward);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,230 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::routable::Routable;
|
||||
|
||||
use super::HistoryProvider;
|
||||
|
||||
/// A [`HistoryProvider`] that stores all navigation information in memory.
|
||||
pub struct MemoryHistory<R: Routable> {
|
||||
current: Option<R>,
|
||||
history: Vec<R>,
|
||||
future: Vec<R>,
|
||||
}
|
||||
|
||||
impl<R: Routable> MemoryHistory<R> {
|
||||
/// Create a [`MemoryHistory`] starting at `path`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
|
||||
/// let mut history = MemoryHistory::with_initial_path("/some/path").unwrap();
|
||||
/// assert_eq!(history.current_path(), "/some/path");
|
||||
/// assert_eq!(history.can_go_back(), false);
|
||||
/// ```
|
||||
pub fn with_initial_path(path: impl Into<String>) -> Result<Self, <R as FromStr>::Err> {
|
||||
let path = path.into();
|
||||
|
||||
Ok(Self {
|
||||
current: Some(R::from_str(&path)?),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Routable> Default for MemoryHistory<R> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
current: None,
|
||||
history: Vec::new(),
|
||||
future: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
|
||||
fn current_route(&self) -> &R {
|
||||
self.current.as_ref().expect("current route is not set")
|
||||
}
|
||||
|
||||
fn can_go_back(&self) -> bool {
|
||||
!self.history.is_empty()
|
||||
}
|
||||
|
||||
fn go_back(&mut self) {
|
||||
if let Some(last) = self.history.pop() {
|
||||
let new = self.current.replace(last);
|
||||
if let Some(new) = new {
|
||||
self.future.push(new);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn can_go_forward(&self) -> bool {
|
||||
!self.future.is_empty()
|
||||
}
|
||||
|
||||
fn go_forward(&mut self) {
|
||||
if let Some(next) = self.future.pop() {
|
||||
let old = self.current.replace(next);
|
||||
if let Some(old) = old {
|
||||
self.history.push(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, new: R) {
|
||||
if let Some(current) = self.current.take() {
|
||||
self.history.push(current);
|
||||
}
|
||||
self.current = Some(new);
|
||||
self.future.clear();
|
||||
}
|
||||
|
||||
fn replace(&mut self, path: R) {
|
||||
self.current = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
|
||||
// #[test]
|
||||
// fn default() {
|
||||
// let mem = MemoryHistory::default();
|
||||
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
// assert_eq!(mem.history, Vec::<String>::new());
|
||||
// assert_eq!(mem.future, Vec::<String>::new());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn with_initial_path() {
|
||||
// let mem = MemoryHistory::with_initial_path("something").unwrap();
|
||||
// assert_eq!(
|
||||
// mem.current,
|
||||
// Url::parse(&format!("{INITIAL_URL}something")).unwrap()
|
||||
// );
|
||||
// assert_eq!(mem.history, Vec::<String>::new());
|
||||
// assert_eq!(mem.future, Vec::<String>::new());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn with_initial_path_with_leading_slash() {
|
||||
// let mem = MemoryHistory::with_initial_path("/something").unwrap();
|
||||
// assert_eq!(
|
||||
// mem.current,
|
||||
// Url::parse(&format!("{INITIAL_URL}something")).unwrap()
|
||||
// );
|
||||
// assert_eq!(mem.history, Vec::<String>::new());
|
||||
// assert_eq!(mem.future, Vec::<String>::new());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn can_go_back() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// assert!(!mem.can_go_back());
|
||||
|
||||
// mem.push(String::from("/test"));
|
||||
// assert!(mem.can_go_back());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn go_back() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("/test"));
|
||||
// mem.go_back();
|
||||
|
||||
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
// assert!(mem.history.is_empty());
|
||||
// assert_eq!(mem.future, vec![format!("{INITIAL_URL}test")]);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn can_go_forward() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// assert!(!mem.can_go_forward());
|
||||
|
||||
// mem.push(String::from("/test"));
|
||||
// mem.go_back();
|
||||
|
||||
// assert!(mem.can_go_forward());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn go_forward() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("/test"));
|
||||
// mem.go_back();
|
||||
// mem.go_forward();
|
||||
|
||||
// assert_eq!(
|
||||
// mem.current,
|
||||
// Url::parse(&format!("{INITIAL_URL}test")).unwrap()
|
||||
// );
|
||||
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
|
||||
// assert!(mem.future.is_empty());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn push() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("/test"));
|
||||
|
||||
// assert_eq!(
|
||||
// mem.current,
|
||||
// Url::parse(&format!("{INITIAL_URL}test")).unwrap()
|
||||
// );
|
||||
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
|
||||
// assert!(mem.future.is_empty());
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
|
||||
// #[cfg(debug_assertions)]
|
||||
// fn push_debug() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("//test"));
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// #[cfg(not(debug_assertions))]
|
||||
// fn push_release() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("//test"));
|
||||
|
||||
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
// assert!(mem.history.is_empty())
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn replace() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.push(String::from("/test"));
|
||||
// mem.push(String::from("/other"));
|
||||
// mem.go_back();
|
||||
// mem.replace(String::from("/replace"));
|
||||
|
||||
// assert_eq!(
|
||||
// mem.current,
|
||||
// Url::parse(&format!("{INITIAL_URL}replace")).unwrap()
|
||||
// );
|
||||
// assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
|
||||
// assert_eq!(mem.future, vec![format!("{INITIAL_URL}other")]);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
|
||||
// #[cfg(debug_assertions)]
|
||||
// fn replace_debug() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.replace(String::from("//test"));
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// #[cfg(not(debug_assertions))]
|
||||
// fn replace_release() {
|
||||
// let mut mem = MemoryHistory::default();
|
||||
// mem.replace(String::from("//test"));
|
||||
|
||||
// assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
|
||||
// }
|
||||
// }
|
||||
@@ -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>) {}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
use dioxus::prelude::{ScopeId, ScopeState};
|
||||
use dioxus_router_core::Navigator;
|
||||
|
||||
use crate::utils::use_router_internal::use_router_internal;
|
||||
|
||||
/// A hook that allows for programmatic navigation.
|
||||
///
|
||||
/// # Return values
|
||||
/// - [`RouterError::NotInsideRouter`], when the calling component is not nested within another
|
||||
/// component calling the [`use_router`] hook.
|
||||
/// - Otherwise [`Ok`].
|
||||
///
|
||||
/// [`use_router`]: crate::hooks::use_router
|
||||
///
|
||||
/// # Panic
|
||||
/// - When the calling component is not nested within another component calling the [`use_router`]
|
||||
/// hook, but only in debug builds.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_router::prelude::*;
|
||||
/// fn App(cx: Scope) -> Element {
|
||||
/// let (state, _) = use_router(
|
||||
/// &cx,
|
||||
/// &|| RouterConfiguration {
|
||||
/// synchronous: true, // asynchronicity not needed for doc test
|
||||
/// ..Default::default()
|
||||
/// },
|
||||
/// &|| Segment::content(comp(Redirect)).fixed("content", comp(Content))
|
||||
/// );
|
||||
///
|
||||
/// render! {
|
||||
/// h1 { "App" }
|
||||
/// Outlet { }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn Redirect(cx: Scope) -> Element {
|
||||
/// let nav = use_navigate(&cx)?;
|
||||
/// nav.push("/content");
|
||||
/// render! { () }
|
||||
/// }
|
||||
///
|
||||
/// fn Content(cx: Scope) -> Element {
|
||||
/// render! {
|
||||
/// p { "Content" }
|
||||
/// }
|
||||
/// }
|
||||
/// #
|
||||
/// # let mut vdom = VirtualDom::new(App);
|
||||
/// #
|
||||
/// # // first render with Redirect component
|
||||
/// # let _ = vdom.rebuild();
|
||||
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1>");
|
||||
/// #
|
||||
/// # // second render with Content component
|
||||
/// # let _ = vdom.rebuild();
|
||||
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Content</p>");
|
||||
/// ```
|
||||
pub fn use_navigate(cx: &ScopeState) -> Option<Navigator<ScopeId>> {
|
||||
match use_router_internal(cx) {
|
||||
Some(r) => Some(r.sender.clone().into()),
|
||||
None => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("`use_navigate` must have access to a parent router");
|
||||
#[allow(unreachable_code)]
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
use async_lock::RwLockReadGuard;
|
||||
use dioxus::{core::Component, prelude::ScopeState};
|
||||
use dioxus_router_core::RouterState;
|
||||
use dioxus::prelude::ScopeState;
|
||||
|
||||
use crate::utils::use_router_internal::use_router_internal;
|
||||
use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
|
||||
|
||||
/// A hook that provides access to information about the current routing location.
|
||||
///
|
||||
@@ -56,13 +54,9 @@ use crate::utils::use_router_internal::use_router_internal;
|
||||
/// ```
|
||||
///
|
||||
/// [`use_router`]: crate::hooks::use_router
|
||||
pub fn use_route(cx: &ScopeState) -> Option<RwLockReadGuard<RouterState<Component>>> {
|
||||
pub fn use_route<R: Routable + Clone>(cx: &ScopeState) -> Option<R> {
|
||||
match use_router_internal(cx) {
|
||||
Some(r) => loop {
|
||||
if let Some(s) = r.state.try_read() {
|
||||
break Some(s);
|
||||
}
|
||||
},
|
||||
Some(r) => Some(r.current()),
|
||||
None => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("`use_route` must have access to a parent router");
|
||||
|
||||
@@ -1,200 +1,12 @@
|
||||
use async_lock::RwLockReadGuard;
|
||||
use dioxus::{core::Component, prelude::*};
|
||||
use dioxus_router_core::{
|
||||
history::{HistoryProvider, MemoryHistory},
|
||||
routes::{ContentAtom, Segment},
|
||||
Navigator, RouterService, RouterState, RoutingCallback,
|
||||
};
|
||||
use log::error;
|
||||
use dioxus::prelude::ScopeState;
|
||||
|
||||
use crate::{
|
||||
contexts::router::RouterContext,
|
||||
prelude::{
|
||||
comp,
|
||||
default_errors::{
|
||||
FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit,
|
||||
},
|
||||
},
|
||||
prelude::RouterContext, routable::Routable, utils::use_router_internal::use_router_internal,
|
||||
};
|
||||
|
||||
/// The basic building block required for all other router components and hooks.
|
||||
///
|
||||
/// This manages a [`dioxus_router_core::RouterService`], which in turn is required for basically
|
||||
/// all router functionality. All other components and hooks provided by [`dioxus_router`](crate)
|
||||
/// will only work as/in components nested within a component calling [`use_router`].
|
||||
///
|
||||
/// Components calling [`use_router`] should not be nested within each other.
|
||||
///
|
||||
/// # Return values
|
||||
/// This hook returns the current router state and a navigator. For more information about the
|
||||
/// state, see the [`use_route`](crate::hooks::use_route) hook. For more information about the
|
||||
/// [`Navigator`], see its own documentation and the [`use_navigate`](crate::hooks::use_navigate)
|
||||
/// hook.
|
||||
///
|
||||
/// # Panic
|
||||
/// - When used within a component, that is nested inside another component calling [`use_router`],
|
||||
/// but only in debug builds.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// # use dioxus::prelude::*;
|
||||
/// # use dioxus_router::prelude::*;
|
||||
/// fn App(cx: Scope) -> Element {
|
||||
/// let (_, _) = use_router(
|
||||
/// &cx,
|
||||
/// &|| RouterConfiguration {
|
||||
/// synchronous: true, // asynchronicity not needed for doc test
|
||||
/// ..Default::default()
|
||||
/// },
|
||||
/// &|| Segment::content(comp(Content))
|
||||
/// );
|
||||
///
|
||||
/// render! {
|
||||
/// h1 { "App" }
|
||||
/// Outlet { }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn Content(cx: Scope) -> Element {
|
||||
/// render! {
|
||||
/// p { "Some content" }
|
||||
/// }
|
||||
/// }
|
||||
/// # let mut vdom = VirtualDom::new(App);
|
||||
/// # let _ = vdom.rebuild();
|
||||
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Some content</p>");
|
||||
/// ```
|
||||
pub fn use_router<'a>(
|
||||
cx: &'a ScopeState,
|
||||
cfg: &dyn Fn() -> RouterConfiguration,
|
||||
content: &dyn Fn() -> Segment<Component>,
|
||||
) -> (
|
||||
RwLockReadGuard<'a, RouterState<Component>>,
|
||||
Navigator<ScopeId>,
|
||||
) {
|
||||
let (service, state, sender) = cx.use_hook(|| {
|
||||
#[allow(unreachable_code, unused_variables)]
|
||||
if let Some(outer) = cx.consume_context::<RouterContext>() {
|
||||
let msg = "components using `use_router` should not be nested within each other";
|
||||
error!("{msg}, inner will be inactive and transparent");
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("{}", msg);
|
||||
return (None, outer.state, outer.sender);
|
||||
}
|
||||
|
||||
let cfg = cfg();
|
||||
let content = content();
|
||||
|
||||
let (mut service, sender, state) = RouterService::new(
|
||||
content,
|
||||
cfg.history,
|
||||
cx.schedule_update_any(),
|
||||
cfg.on_update,
|
||||
cfg.failure_external_navigation,
|
||||
cfg.failure_named_navigation,
|
||||
cfg.failure_redirection_limit,
|
||||
);
|
||||
|
||||
cx.provide_context(RouterContext {
|
||||
state: state.clone(),
|
||||
sender: sender.clone(),
|
||||
});
|
||||
|
||||
(
|
||||
if cfg.synchronous {
|
||||
service.init();
|
||||
Some(service)
|
||||
} else {
|
||||
cx.spawn(async move { service.run().await });
|
||||
None
|
||||
},
|
||||
state,
|
||||
sender,
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(service) = service {
|
||||
service.run_current();
|
||||
}
|
||||
|
||||
(
|
||||
loop {
|
||||
if let Some(state) = state.try_read() {
|
||||
break state;
|
||||
}
|
||||
},
|
||||
sender.clone().into(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Global configuration options for the router.
|
||||
///
|
||||
/// This implements [`Default`], so you can use it like this:
|
||||
/// ```rust,no_run
|
||||
/// # use dioxus_router::prelude::RouterConfiguration;
|
||||
/// let cfg = RouterConfiguration {
|
||||
/// synchronous: false,
|
||||
/// ..Default::default()
|
||||
/// };
|
||||
/// ```
|
||||
pub struct RouterConfiguration {
|
||||
/// A component to render when an external navigation fails.
|
||||
///
|
||||
/// Defaults to a router-internal component called `FailureExternalNavigation`. It is not part
|
||||
/// of the public API. Do not confuse it with
|
||||
/// [`dioxus_router_core::prelude::FailureExternalNavigation`].
|
||||
pub failure_external_navigation: ContentAtom<Component>,
|
||||
/// A component to render when a named navigation fails.
|
||||
///
|
||||
/// Defaults to a router-internal component called `FailureNamedNavigation`. It is not part of
|
||||
/// the public API. Do not confuse it with
|
||||
/// [`dioxus_router_core::prelude::FailureNamedNavigation`].
|
||||
pub failure_named_navigation: ContentAtom<Component>,
|
||||
/// A component to render when the redirect limit is reached.
|
||||
///
|
||||
/// Defaults to a router-internal component called `FailureRedirectionLimit`. It is not part of
|
||||
/// the public API. Do not confuse it with
|
||||
/// [`dioxus_router_core::prelude::FailureRedirectionLimit`].
|
||||
pub failure_redirection_limit: ContentAtom<Component>,
|
||||
/// The [`HistoryProvider`] the router should use.
|
||||
///
|
||||
/// Defaults to a default [`MemoryHistory`].
|
||||
pub history: Box<dyn HistoryProvider>,
|
||||
/// A function to be called whenever the routing is updated.
|
||||
///
|
||||
/// The callback is invoked after the routing is updated, but before components and hooks are
|
||||
/// updated.
|
||||
///
|
||||
/// If the callback returns a [`NavigationTarget`] the router will replace the current location
|
||||
/// with it. If no navigation failure was triggered, the router will then updated dependent
|
||||
/// components and hooks.
|
||||
///
|
||||
/// The callback is called no more than once per rerouting. It will not be called if a
|
||||
/// navigation failure occurs.
|
||||
///
|
||||
/// Defaults to [`None`].
|
||||
///
|
||||
/// [`NavigationTarget`]: dioxus_router_core::navigation::NavigationTarget
|
||||
pub on_update: Option<RoutingCallback<Component>>,
|
||||
/// Whether the router should run in synchronous mode.
|
||||
///
|
||||
/// If [`true`], the router will only update its state whenever the component using the
|
||||
/// [`use_router`] hook rerenders. If [`false`], an asynchronous task is launched and the router
|
||||
/// will update whenever it receives new input.
|
||||
///
|
||||
/// Defaults to [`false`].
|
||||
pub synchronous: bool,
|
||||
}
|
||||
|
||||
impl Default for RouterConfiguration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
failure_external_navigation: comp(FailureExternalNavigation),
|
||||
failure_named_navigation: comp(FailureNamedNavigation),
|
||||
failure_redirection_limit: comp(FailureRedirectionLimit),
|
||||
history: Box::<MemoryHistory>::default(),
|
||||
on_update: None,
|
||||
synchronous: false,
|
||||
}
|
||||
}
|
||||
/// A hook that provides access to information about the router
|
||||
pub fn use_router<R: Routable + Clone>(cx: &ScopeState) -> &RouterContext<R> {
|
||||
use_router_internal(cx)
|
||||
.as_ref()
|
||||
.expect("use_route must have access to a router")
|
||||
}
|
||||
+14
-14
@@ -1,6 +1,10 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
// cannot use forbid, because props derive macro generates #[allow(missing_docs)]
|
||||
#![deny(missing_docs)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod navigation;
|
||||
pub mod routable;
|
||||
|
||||
/// Components interacting with the router.
|
||||
pub mod components {
|
||||
@@ -14,19 +18,23 @@ pub mod components {
|
||||
|
||||
mod outlet;
|
||||
pub use outlet::*;
|
||||
|
||||
mod router;
|
||||
pub use router::*;
|
||||
}
|
||||
|
||||
mod contexts {
|
||||
pub(crate) mod outlet;
|
||||
pub(crate) mod router;
|
||||
pub use router::*;
|
||||
}
|
||||
|
||||
pub use dioxus_router_core::history;
|
||||
mod router_cfg;
|
||||
|
||||
pub mod history;
|
||||
|
||||
/// Hooks for interacting with the router in components.
|
||||
pub mod hooks {
|
||||
mod use_navigate;
|
||||
pub use use_navigate::*;
|
||||
|
||||
mod use_router;
|
||||
pub use use_router::*;
|
||||
|
||||
@@ -36,18 +44,10 @@ pub mod hooks {
|
||||
|
||||
/// A collection of useful items most applications might need.
|
||||
pub mod prelude {
|
||||
pub use dioxus_router_core::prelude::*;
|
||||
|
||||
pub use crate::components::*;
|
||||
pub use crate::contexts::*;
|
||||
pub use crate::hooks::*;
|
||||
|
||||
/// Wrap a [`Component`](dioxus::core::Component) inside a [`ContentAtom`].
|
||||
///
|
||||
/// This is purely a convenience function.
|
||||
#[must_use]
|
||||
pub fn comp(component: dioxus::core::Component) -> ContentAtom<dioxus::core::Component> {
|
||||
ContentAtom(component)
|
||||
}
|
||||
pub use dioxus_router_macro::routable;
|
||||
}
|
||||
|
||||
mod utils {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//! Types pertaining to navigation.
|
||||
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use url::{ParseError, Url};
|
||||
|
||||
use crate::routable::Routable;
|
||||
|
||||
/// A target for the router to navigate to.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum NavigationTarget<R: Routable> {
|
||||
/// An internal path that the router can navigate to by itself.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
||||
/// let explicit = NavigationTarget::Internal(String::from("/internal"));
|
||||
/// let implicit: NavigationTarget = "/internal".into();
|
||||
/// assert_eq!(explicit, implicit);
|
||||
/// ```
|
||||
Internal(R),
|
||||
/// An external target that the router doesn't control.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use dioxus_router_core::navigation::NavigationTarget;
|
||||
/// let explicit = NavigationTarget::External(String::from("https://dioxuslabs.com/"));
|
||||
/// let implicit: NavigationTarget = "https://dioxuslabs.com/".into();
|
||||
/// assert_eq!(explicit, implicit);
|
||||
/// ```
|
||||
External(String),
|
||||
}
|
||||
|
||||
impl<R: Routable> From<R> for NavigationTarget<R> {
|
||||
fn from(value: R) -> Self {
|
||||
Self::Internal(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Routable> Display for NavigationTarget<R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NavigationTarget::Internal(r) => write!(f, "{}", r),
|
||||
NavigationTarget::External(s) => write!(f, "{}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that can occur when parsing a [`NavigationTarget`].
|
||||
pub enum NavigationTargetParseError<R: Routable> {
|
||||
/// A URL that is not valid.
|
||||
InvalidUrl(ParseError),
|
||||
/// An internal URL that is not valid.
|
||||
InvalidInternalURL(<R as FromStr>::Err),
|
||||
}
|
||||
|
||||
impl<R: Routable> FromStr for NavigationTarget<R> {
|
||||
type Err = NavigationTargetParseError<R>;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match Url::parse(s) {
|
||||
Ok(_) => Ok(Self::External(s.to_string())),
|
||||
Err(ParseError::RelativeUrlWithoutBase) => {
|
||||
Ok(Self::Internal(R::from_str(s).map_err(|e| {
|
||||
NavigationTargetParseError::InvalidInternalURL(e)
|
||||
})?))
|
||||
}
|
||||
Err(e) => Err(NavigationTargetParseError::InvalidUrl(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, ¤t, 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, ¤t, 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, ¤t, 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, ¤t, 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, ¤t, 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, ¤t, 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(), ¶meters, "", &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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user