Move the history provider into the context (#3048)

* move history providers into a separate crate

* start moving route providers into the renderers

* clean up intoroutable

* remove into routable

* fix router tests

* Provide history providers in each renderer

* implement nested routers

* move the lens out of the history crate

* re-export dioxus history trait in the prelude

* also re-export the history function

* fix history doctests

* some light cleanups

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2024-10-31 12:44:04 -07:00
1 parent e5a1a62644
commit 281087469a
48 files changed
+875 -1419

No files matched your search

+261
View File
@@ -0,0 +1,261 @@
use dioxus_core::prelude::{provide_context, provide_root_context};
use std::{rc::Rc, sync::Arc};
mod memory;
pub use memory::*;
/// Get the history provider for the current platform if the platform doesn't implement a history functionality.
pub fn history() -> Rc<dyn History> {
match dioxus_core::prelude::try_consume_context::<Rc<dyn History>>() {
Some(history) => history,
None => {
tracing::error!("Unable to find a history provider in the renderer. Make sure your renderer supports the Router. Falling back to the in-memory history provider.");
provide_root_context(Rc::new(MemoryHistory::default()))
}
}
}
/// Provide a history context to the current component.
pub fn provide_history_context(history: Rc<dyn History>) {
provide_context(history);
}
pub trait History {
/// Get the path of the current URL.
///
/// **Must start** with `/`. **Must _not_ contain** the prefix.
///
/// ```rust
/// # use dioxus::prelude::*;
/// # #[component]
/// # fn Index() -> Element { VNode::empty() }
/// # #[component]
/// # fn OtherPage() -> Element { VNode::empty() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = dioxus::history::MemoryHistory::default();
/// assert_eq!(history.current_route(), "/");
///
/// history.push(Route::OtherPage {}.to_string());
/// assert_eq!(history.current_route(), "/some-other-page");
/// ```
#[must_use]
fn current_route(&self) -> String;
/// 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::prelude::*;
/// # #[component]
/// # fn Index() -> Element { VNode::empty() }
/// # fn Other() -> Element { VNode::empty() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/other")]
/// Other {},
/// }
/// let mut history = dioxus::history::MemoryHistory::default();
/// assert_eq!(history.can_go_back(), false);
///
/// history.push(Route::Other {}.to_string());
/// 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::prelude::*;
/// # #[component]
/// # fn Index() -> Element { VNode::empty() }
/// # #[component]
/// # fn OtherPage() -> Element { VNode::empty() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = dioxus::history::MemoryHistory::default();
/// assert_eq!(history.current_route(), "/");
///
/// history.go_back();
/// assert_eq!(history.current_route(), "/");
///
/// history.push(Route::OtherPage {}.to_string());
/// assert_eq!(history.current_route(), "/some-other-page");
///
/// history.go_back();
/// assert_eq!(history.current_route(), "/");
/// ```
fn go_back(&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::prelude::*;
/// # #[component]
/// # fn Index() -> Element { VNode::empty() }
/// # #[component]
/// # fn OtherPage() -> Element { VNode::empty() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = dioxus::history::MemoryHistory::default();
/// assert_eq!(history.can_go_forward(), false);
///
/// history.push(Route::OtherPage {}.to_string());
/// 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::prelude::*;
/// # #[component]
/// # fn Index() -> Element { VNode::empty() }
/// # #[component]
/// # fn OtherPage() -> Element { VNode::empty() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = dioxus::history::MemoryHistory::default();
/// history.push(Route::OtherPage {}.to_string());
/// assert_eq!(history.current_route(), Route::OtherPage {}.to_string());
///
/// history.go_back();
/// assert_eq!(history.current_route(), Route::Index {}.to_string());
///
/// history.go_forward();
/// assert_eq!(history.current_route(), Route::OtherPage {}.to_string());
/// ```
fn go_forward(&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::prelude::*;
/// # #[component]
/// # fn Index() -> Element { VNode::empty() }
/// # #[component]
/// # fn OtherPage() -> Element { VNode::empty() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = dioxus::history::MemoryHistory::default();
/// assert_eq!(history.current_route(), Route::Index {}.to_string());
///
/// history.push(Route::OtherPage {}.to_string());
/// assert_eq!(history.current_route(), Route::OtherPage {}.to_string());
/// assert!(history.can_go_back());
/// ```
fn push(&self, route: String);
/// 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::prelude::*;
/// # #[component]
/// # fn Index() -> Element { VNode::empty() }
/// # #[component]
/// # fn OtherPage() -> Element { VNode::empty() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = dioxus::history::MemoryHistory::default();
/// assert_eq!(history.current_route(), Route::Index {}.to_string());
///
/// history.replace(Route::OtherPage {}.to_string());
/// assert_eq!(history.current_route(), Route::OtherPage {}.to_string());
/// assert!(!history.can_go_back());
/// ```
fn replace(&self, path: String);
/// 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(&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(&self, callback: Arc<dyn Fn() + Send + Sync>) {}
/// Whether the router should include the legacy prevent default attribute instead of the new
/// prevent default method. This should only be used by liveview.
fn include_prevent_default(&self) -> bool {
false
}
}
+100
View File
@@ -0,0 +1,100 @@
use std::cell::RefCell;
use crate::History;
struct MemoryHistoryState {
current: String,
history: Vec<String>,
future: Vec<String>,
}
/// A [`History`] provider that stores all navigation information in memory.
pub struct MemoryHistory {
state: RefCell<MemoryHistoryState>,
}
impl Default for MemoryHistory {
fn default() -> Self {
Self::with_initial_path("/")
}
}
impl MemoryHistory {
/// Create a [`MemoryHistory`] starting at `path`.
///
/// ```rust
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[component]
/// # fn Index() -> Element { VNode::empty() }
/// # #[component]
/// # fn OtherPage() -> Element { VNode::empty() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
///
/// let mut history = dioxus_history::MemoryHistory::with_initial_path(Route::Index {});
/// assert_eq!(history.current_route(), Route::Index {}.to_string());
/// assert_eq!(history.can_go_back(), false);
/// ```
pub fn with_initial_path(path: impl ToString) -> Self {
Self {
state: MemoryHistoryState{
current: path.to_string().parse().unwrap_or_else(|err| {
panic!("index route does not exist:\n{err}\n use MemoryHistory::with_initial_path to set a custom path")
}),
history: Vec::new(),
future: Vec::new(),}.into()
}
}
}
impl History for MemoryHistory {
fn current_route(&self) -> String {
self.state.borrow().current.clone()
}
fn can_go_back(&self) -> bool {
!self.state.borrow().history.is_empty()
}
fn go_back(&self) {
let mut write = self.state.borrow_mut();
if let Some(last) = write.history.pop() {
let old = std::mem::replace(&mut write.current, last);
write.future.push(old);
}
}
fn can_go_forward(&self) -> bool {
!self.state.borrow().future.is_empty()
}
fn go_forward(&self) {
let mut write = self.state.borrow_mut();
if let Some(next) = write.future.pop() {
let old = std::mem::replace(&mut write.current, next);
write.history.push(old);
}
}
fn push(&self, new: String) {
let mut write = self.state.borrow_mut();
// don't push the same route twice
if write.current == new {
return;
}
let old = std::mem::replace(&mut write.current, new);
write.history.push(old);
write.future.clear();
}
fn replace(&self, path: String) {
let mut write = self.state.borrow_mut();
write.current = path;
}
}