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

+2 -21
View File
@@ -11,24 +11,11 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies]
dioxus-lib = { workspace = true }
dioxus-history = { workspace = true }
dioxus-router-macro = { workspace = true }
gloo = { version = "0.8.0", optional = true }
tracing = { workspace = true }
urlencoding = "2.1.3"
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1.0.91", optional = true }
url = "2.3.1"
wasm-bindgen = { workspace = true, optional = true }
web-sys = { version = "0.3.60", optional = true, features = [
"ScrollRestoration",
] }
js-sys = { version = "0.3.63", optional = true }
gloo-utils = { version = "0.1.6", optional = true }
dioxus-liveview = { workspace = true, optional = true }
dioxus-ssr = { workspace = true, optional = true }
http = { workspace = true, optional = true }
dioxus-fullstack = { workspace = true, optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
dioxus-cli-config = { workspace = true }
rustversion = "1.0.17"
@@ -36,18 +23,11 @@ rustversion = "1.0.17"
# dev-dependncey crates
[target.'cfg(target_family = "wasm")'.dev-dependencies]
console_error_panic_hook = "0.1.7"
dioxus-router = { workspace = true, features = ["web"] }
# dioxus-web = { workspace = true }
gloo = "0.8.0"
wasm-bindgen-test = "0.3.33"
[features]
default = []
ssr = []
liveview = ["dioxus-liveview", "dep:tokio", "dep:serde", "dep:serde_json"]
wasm_test = []
web = ["dep:gloo", "dep:web-sys", "dep:wasm-bindgen", "dep:gloo-utils", "dep:js-sys", "dioxus-router-macro/web"]
fullstack = ["dep:dioxus-fullstack"]
[dev-dependencies]
axum = { workspace = true, features = ["ws"] }
@@ -57,6 +37,7 @@ criterion = { workspace = true, features = ["async_tokio", "html_reports"] }
ciborium = { version = "0.2.1" }
base64 = { version = "0.21.0" }
serde = { version = "1", features = ["derive"] }
tokio = { workspace = true, features = ["full"] }
[package.metadata.docs.rs]
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
-204
View File
@@ -1,204 +0,0 @@
use dioxus::prelude::*;
use std::str::FromStr;
#[cfg(feature = "liveview")]
#[tokio::main]
async fn main() {
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
let listen_address: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
let view = dioxus_liveview::LiveViewPool::new();
let app = Router::new()
.fallback(get(move || async move {
Html(format!(
r#"
<!DOCTYPE html>
<html>
<head></head>
<body><div id="main"></div></body>
{glue}
</html>
"#,
glue = dioxus_liveview::interpreter_glue(&format!("ws://{listen_address}/ws"))
))
}))
.route(
"/ws",
get(move |ws: WebSocketUpgrade| async move {
ws.on_upgrade(move |socket| async move {
_ = view.launch(dioxus_liveview::axum_socket(socket), app).await;
})
}),
);
println!("Listening on http://{listen_address}");
let listener = tokio::net::TcpListener::bind(&listen_address)
.await
.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "liveview"))]
fn main() {
dioxus::launch(app)
}
fn app() -> Element {
rsx! {
Router::<Route> {}
}
}
#[component]
fn UserFrame(user_id: usize) -> Element {
rsx! {
pre { "UserFrame{{\n\tuser_id:{user_id}\n}}" }
div { background_color: "rgba(0,0,0,50%)",
"children:"
Outlet::<Route> {}
}
}
}
#[component]
fn Route1(user_id: usize, dynamic: usize, query: String) -> Element {
rsx! {
pre {
"Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\tquery:{query}\n}}"
}
Link {
to: Route::Route1 {
user_id,
dynamic,
query: String::new(),
},
"Route1 with extra+\".\""
}
p { "Footer" }
Link {
to: Route::Route3 {
dynamic: String::new(),
},
"Home"
}
}
}
#[component]
fn Route2(user_id: usize) -> Element {
rsx! {
pre { "Route2{{\n\tuser_id:{user_id}\n}}" }
{(0..user_id).map(|i| rsx! { p { "{i}" } })}
p { "Footer" }
Link {
to: Route::Route3 {
dynamic: String::new(),
},
"Home"
}
}
}
#[component]
fn Route3(dynamic: String) -> Element {
let mut current_route_str = use_signal(String::new);
let current_route = use_route();
let parsed = Route::from_str(&current_route_str.read());
let site_map = Route::SITE_MAP
.iter()
.flat_map(|seg| seg.flatten().into_iter())
.collect::<Vec<_>>();
let navigator = use_navigator();
rsx! {
input {
oninput: move |evt: FormEvent| {
*current_route_str.write() = evt.value();
},
value: "{current_route_str}"
}
"dynamic: {dynamic}"
Link { to: Route::Route2 { user_id: 8888 }, "hello world link" }
button {
disabled: !navigator.can_go_back(),
onclick: move |_| {
navigator.go_back();
},
"go back"
}
button {
disabled: !navigator.can_go_forward(),
onclick: move |_| {
navigator.go_forward();
},
"go forward"
}
button {
onclick: move |_| {
navigator.push("https://www.google.com");
},
"google link"
}
p { "Site Map" }
pre { "{site_map:#?}" }
p { "Dynamic link" }
match parsed {
Ok(route) => {
if route != current_route {
rsx! {
Link {
to: route.clone(),
"{route}"
}
}
}
else {
VNode::empty()
}
}
Err(err) => {
rsx! {
pre {
color: "red",
"Invalid route:\n{err}"
}
}
}
}
}
}
#[rustfmt::skip]
#[derive(Clone, Debug, PartialEq, Routable)]
enum Route {
#[nest("/test")]
// Nests with parameters have types taken from child routes
#[nest("/user/:user_id")]
// Everything inside the nest has the added parameter `user_id: usize`
// UserFrame is a layout component that will receive the `user_id: usize` parameter
#[layout(UserFrame)]
#[route("/:dynamic?:query")]
Route1 {
// The type is taken from the first instance of the dynamic parameter
user_id: usize,
dynamic: usize,
query: String,
},
#[route("/hello_world")]
// You can opt out of the layout by using the `!` prefix
#[layout(!UserFrame)]
Route2 { user_id: usize },
#[end_layout]
#[end_nest]
#[end_nest]
#[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})]
#[route("/:dynamic")]
Route3 { dynamic: String },
}
@@ -0,0 +1,68 @@
/// Components that allow the macro to add child routers. This component provides a context
/// to the child router that maps child routes to root routes and vice versa.
use dioxus_lib::prelude::*;
use crate::prelude::Routable;
/// Maps a child route into the root router and vice versa
// NOTE: Currently child routers only support simple static prefixes, but this
// API could be expanded to support dynamic prefixes as well
pub(crate) struct ChildRouteMapping<R> {
format_route_as_root_route: fn(R) -> String,
parse_route_from_root_route: fn(&str) -> Option<R>,
}
impl<R: Routable> ChildRouteMapping<R> {
pub(crate) fn format_route_as_root_route(&self, route: R) -> String {
(self.format_route_as_root_route)(route)
}
pub(crate) fn parse_route_from_root_route(&self, route: &str) -> Option<R> {
(self.parse_route_from_root_route)(route)
}
}
/// Get the formatter that handles adding and stripping the prefix from a child route
pub(crate) fn consume_child_route_mapping<R: Routable>() -> Option<ChildRouteMapping<R>> {
try_consume_context()
}
impl<R> Clone for ChildRouteMapping<R> {
fn clone(&self) -> Self {
Self {
format_route_as_root_route: self.format_route_as_root_route,
parse_route_from_root_route: self.parse_route_from_root_route,
}
}
}
/// Props for the [`ChildHistoryProvider`] component.
#[derive(Props, Clone)]
pub struct ChildRouterProps<R: Routable> {
/// The child route to render
route: R,
/// Take a parent route and return a child route or none if the route is not part of the child
parse_route_from_root_route: fn(&str) -> Option<R>,
/// Take a child route and return a parent route
format_route_as_root_route: fn(R) -> String,
}
impl<R: Routable> PartialEq for ChildRouterProps<R> {
fn eq(&self, _: &Self) -> bool {
false
}
}
/// A component that provides a [`History`] to a child router. The `#[child]` attribute on the router macro will insert this automatically.
#[component]
#[allow(missing_docs)]
pub fn ChildRouter<R: Routable>(props: ChildRouterProps<R>) -> Element {
use_hook(|| {
provide_context(ChildRouteMapping {
format_route_as_root_route: props.format_route_as_root_route,
parse_route_from_root_route: props.parse_route_from_root_route,
})
});
props.route.render(0)
}
@@ -0,0 +1,20 @@
use dioxus_history::{provide_history_context, History};
use dioxus_lib::prelude::*;
use std::rc::Rc;
/// A component that provides a [`History`] for all child [`Router`] components. Renderers generally provide a default history automatically.
#[component]
#[allow(missing_docs)]
pub fn HistoryProvider(
/// The history to provide to child components.
history: Callback<(), Rc<dyn History>>,
/// The children to render within the history provider.
children: Element,
) -> Element {
use_hook(|| {
provide_history_context(history(()));
});
children
}
+8 -78
View File
@@ -1,83 +1,14 @@
#![allow(clippy::type_complexity)]
use std::any::Any;
use std::fmt::Debug;
use std::rc::Rc;
use dioxus_lib::prelude::*;
use tracing::error;
use crate::navigation::NavigationTarget;
use crate::prelude::Routable;
use crate::utils::use_router_internal::use_router_internal;
use url::Url;
/// Something that can be converted into a [`NavigationTarget`].
#[derive(Clone)]
pub enum IntoRoutable {
/// A raw string target.
FromStr(String),
/// A internal target.
Route(Rc<dyn Any>),
}
impl PartialEq for IntoRoutable {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(IntoRoutable::FromStr(a), IntoRoutable::FromStr(b)) => a == b,
(IntoRoutable::Route(a), IntoRoutable::Route(b)) => Rc::ptr_eq(a, b),
_ => false,
}
}
}
impl<R: Routable> From<R> for IntoRoutable {
fn from(value: R) -> Self {
IntoRoutable::Route(Rc::new(value) as Rc<dyn Any>)
}
}
impl<R: Routable> From<NavigationTarget<R>> for IntoRoutable {
fn from(value: NavigationTarget<R>) -> Self {
match value {
NavigationTarget::Internal(route) => IntoRoutable::Route(Rc::new(route) as Rc<dyn Any>),
NavigationTarget::External(url) => IntoRoutable::FromStr(url),
}
}
}
impl From<String> for IntoRoutable {
fn from(value: String) -> Self {
IntoRoutable::FromStr(value)
}
}
impl From<&String> for IntoRoutable {
fn from(value: &String) -> Self {
IntoRoutable::FromStr(value.to_string())
}
}
impl From<&str> for IntoRoutable {
fn from(value: &str) -> Self {
IntoRoutable::FromStr(value.to_string())
}
}
impl From<Url> for IntoRoutable {
fn from(url: Url) -> Self {
IntoRoutable::FromStr(url.to_string())
}
}
impl From<&Url> for IntoRoutable {
fn from(url: &Url) -> Self {
IntoRoutable::FromStr(url.to_string())
}
}
/// The properties for a [`Link`].
#[derive(Props, Clone, PartialEq)]
pub struct LinkProps {
@@ -119,7 +50,7 @@ pub struct LinkProps {
/// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
#[props(into)]
pub to: IntoRoutable,
pub to: NavigationTarget,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
@@ -227,12 +158,11 @@ pub fn Link(props: LinkProps) -> Element {
}
};
let current_url = router.current_route_string();
let current_url = router.full_route_string();
let href = match &to {
IntoRoutable::FromStr(url) => url.to_string(),
IntoRoutable::Route(route) => router.any_route_to_string(&**route),
NavigationTarget::Internal(url) => url.clone(),
NavigationTarget::External(route) => route.clone(),
};
let parsed_route: NavigationTarget<Rc<dyn Any>> = router.resolve_into_routable(to.clone());
let mut class_ = String::new();
if let Some(c) = class {
@@ -257,7 +187,7 @@ pub fn Link(props: LinkProps) -> Element {
let tag_target = new_tab.then_some("_blank");
let is_external = matches!(parsed_route, NavigationTarget::External(_));
let is_external = matches!(to, NavigationTarget::External(_));
let is_router_nav = !is_external && !new_tab;
let rel = rel.or_else(|| is_external.then_some("noopener noreferrer".to_string()));
@@ -281,7 +211,7 @@ pub fn Link(props: LinkProps) -> Element {
event.prevent_default();
if do_default && is_router_nav {
router.push_any(router.resolve_into_routable(to.clone()));
router.push_any(to.clone());
}
if let Some(handler) = onclick {
@@ -301,8 +231,8 @@ pub fn Link(props: LinkProps) -> Element {
let liveview_prevent_default = {
// If the event is a click with the left mouse button and no modifiers, prevent the default action
// and navigate to the href with client side routing
router.is_liveview().then_some(
"if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) { event.preventDefault() }"
router.include_prevent_default().then_some(
"if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) { event.preventDefault() }"
)
};
+3 -2
View File
@@ -58,8 +58,9 @@ use dioxus_lib::prelude::*;
/// # #[component]
/// # fn App() -> Element {
/// # rsx! {
/// # Router::<Route> {
/// # config: || RouterConfig::default().history(MemoryHistory::with_initial_path(Route::Child {}))
/// # dioxus_router::components::HistoryProvider {
/// # history: move |_| std::rc::Rc::new(dioxus_history::MemoryHistory::with_initial_path(Route::Child {}.to_string())) as std::rc::Rc<dyn dioxus_history::History>,
/// # Router::<Route> {}
/// # }
/// # }
/// # }
+1 -4
View File
@@ -46,10 +46,7 @@ where
use crate::prelude::{outlet::OutletContext, RouterContext};
use_hook(|| {
provide_router_context(RouterContext::new(
props.config.call(()),
schedule_update_any(),
));
provide_router_context(RouterContext::new(props.config.call(())));
provide_context(OutletContext::<R> {
current_level: 0,
+6 -3
View File
@@ -1,4 +1,4 @@
use crate::prelude::{ExternalNavigationFailure, IntoRoutable, RouterContext};
use crate::prelude::{ExternalNavigationFailure, NavigationTarget, RouterContext};
/// Acquire the navigator without subscribing to updates.
///
@@ -48,14 +48,17 @@ impl Navigator {
/// Push a new location.
///
/// The previous location will be available to go back to.
pub fn push(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
pub fn push(&self, target: impl Into<NavigationTarget>) -> Option<ExternalNavigationFailure> {
self.0.push(target)
}
/// Replace the current location.
///
/// The previous location will **not** be available to go back to.
pub fn replace(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
pub fn replace(
&self,
target: impl Into<NavigationTarget>,
) -> Option<ExternalNavigationFailure> {
self.0.replace(target)
}
}
+88 -144
View File
@@ -1,17 +1,14 @@
use std::{
any::Any,
collections::HashSet,
rc::Rc,
sync::{Arc, RwLock},
sync::{Arc, Mutex},
};
use dioxus_history::history;
use dioxus_lib::prelude::*;
use crate::{
navigation::NavigationTarget,
prelude::{AnyHistoryProvider, IntoRoutable, SiteMapSegment},
routable::Routable,
router_cfg::RouterConfig,
components::child_router::consume_child_route_mapping, navigation::NavigationTarget,
prelude::SiteMapSegment, routable::Routable, router_cfg::RouterConfig,
};
/// This context is set in the root of the virtual dom if there is a router present.
@@ -47,38 +44,39 @@ pub struct ExternalNavigationFailure(pub String);
/// A function the router will call after every routing update.
pub(crate) type RoutingCallback<R> =
Arc<dyn Fn(GenericRouterContext<R>) -> Option<NavigationTarget<R>>>;
pub(crate) type AnyRoutingCallback =
Arc<dyn Fn(RouterContext) -> Option<NavigationTarget<Rc<dyn Any>>>>;
pub(crate) type AnyRoutingCallback = Arc<dyn Fn(RouterContext) -> Option<NavigationTarget>>;
struct RouterContextInner {
/// The current prefix.
prefix: Option<String>,
history: Box<dyn AnyHistoryProvider>,
unresolved_error: Option<ExternalNavigationFailure>,
subscribers: Arc<RwLock<HashSet<ScopeId>>>,
subscriber_update: Arc<dyn Fn(ScopeId)>,
subscribers: Arc<Mutex<HashSet<ReactiveContext>>>,
routing_callback: Option<AnyRoutingCallback>,
failure_external_navigation: fn() -> Element,
any_route_to_string: fn(&dyn Any) -> String,
internal_route: fn(&str) -> bool,
site_map: &'static [SiteMapSegment],
}
impl RouterContextInner {
fn update_subscribers(&self) {
let update = &self.subscriber_update;
for &id in self.subscribers.read().unwrap().iter() {
update(id);
for &id in self.subscribers.lock().unwrap().iter() {
id.mark_dirty();
}
}
fn subscribe_to_current_context(&self) {
if let Some(rc) = ReactiveContext::current() {
rc.subscribe(self.subscribers.clone());
}
}
fn external(&mut self, external: String) -> Option<ExternalNavigationFailure> {
match self.history.external(external.clone()) {
match history().external(external.clone()) {
true => None,
false => {
let failure = ExternalNavigationFailure(external);
@@ -99,23 +97,17 @@ pub struct RouterContext {
}
impl RouterContext {
pub(crate) fn new<R: Routable + 'static>(
mut cfg: RouterConfig<R>,
mark_dirty: Arc<dyn Fn(ScopeId) + Sync + Send>,
) -> Self
pub(crate) fn new<R: Routable + 'static>(cfg: RouterConfig<R>) -> Self
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
let subscriber_update = mark_dirty.clone();
let subscribers = Arc::new(RwLock::new(HashSet::new()));
let subscribers = Arc::new(Mutex::new(HashSet::new()));
let mapping = consume_child_route_mapping();
let mut myself = RouterContextInner {
let myself = RouterContextInner {
prefix: Default::default(),
history: cfg.take_history(),
unresolved_error: None,
subscribers: subscribers.clone(),
subscriber_update,
routing_callback: cfg.on_update.map(|update| {
Arc::new(move |ctx| {
let ctx = GenericRouterContext {
@@ -123,42 +115,30 @@ impl RouterContext {
_marker: std::marker::PhantomData,
};
update(ctx).map(|t| match t {
NavigationTarget::Internal(r) => {
NavigationTarget::Internal(Rc::new(r) as Rc<dyn Any>)
}
NavigationTarget::Internal(r) => match mapping.as_ref() {
Some(mapping) => {
NavigationTarget::Internal(mapping.format_route_as_root_route(r))
}
None => NavigationTarget::Internal(r.to_string()),
},
NavigationTarget::External(s) => NavigationTarget::External(s),
})
})
as Arc<dyn Fn(RouterContext) -> Option<NavigationTarget<Rc<dyn Any>>>>
}) as Arc<dyn Fn(RouterContext) -> Option<NavigationTarget>>
}),
failure_external_navigation: cfg.failure_external_navigation,
any_route_to_string: |route| {
route
.downcast_ref::<R>()
.unwrap_or_else(|| {
panic!(
"Route is not of the expected type: {}\n found typeid: {:?}\n expected typeid: {:?}",
std::any::type_name::<R>(),
route.type_id(),
std::any::TypeId::of::<R>()
)
})
.to_string()
},
internal_route: |route| R::from_str(route).is_ok(),
site_map: R::SITE_MAP,
};
// set the updater
{
myself.history.updater(Arc::new(move || {
for &id in subscribers.read().unwrap().iter() {
(mark_dirty)(id);
}
}));
}
history().updater(Arc::new(move || {
for &rc in subscribers.lock().unwrap().iter() {
rc.mark_dirty();
}
}));
Self {
inner: CopyValue::new_in_scope(myself, ScopeId::ROOT),
@@ -167,41 +147,27 @@ impl RouterContext {
/// Check if the router is running in a liveview context
/// We do some slightly weird things for liveview because of the network boundary
pub fn is_liveview(&self) -> bool {
#[cfg(feature = "liveview")]
{
self.inner.read().history.is_liveview()
}
#[cfg(not(feature = "liveview"))]
{
false
}
}
pub(crate) fn route_from_str(&self, route: &str) -> Result<Rc<dyn Any>, String> {
self.inner.read().history.parse_route(route)
pub(crate) fn include_prevent_default(&self) -> bool {
history().include_prevent_default()
}
/// Check whether there is a previous page to navigate back to.
#[must_use]
pub fn can_go_back(&self) -> bool {
self.inner.read().history.can_go_back()
history().can_go_back()
}
/// Check whether there is a future page to navigate forward to.
#[must_use]
pub fn can_go_forward(&self) -> bool {
self.inner.read().history.can_go_forward()
history().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.inner.write_unchecked().history.go_back();
}
history().go_back();
self.change_route();
}
@@ -209,21 +175,15 @@ impl RouterContext {
///
/// Will fail silently if there is no next location to go to.
pub fn go_forward(&self) {
{
self.inner.write_unchecked().history.go_forward();
}
history().go_forward();
self.change_route();
}
pub(crate) fn push_any(
&self,
target: NavigationTarget<Rc<dyn Any>>,
) -> Option<ExternalNavigationFailure> {
pub(crate) fn push_any(&self, target: NavigationTarget) -> Option<ExternalNavigationFailure> {
{
let mut write = self.inner.write_unchecked();
match target {
NavigationTarget::Internal(p) => write.history.push(p),
NavigationTarget::Internal(p) => history().push(p),
NavigationTarget::External(e) => return write.external(e),
}
}
@@ -234,12 +194,15 @@ impl RouterContext {
/// Push a new location.
///
/// The previous location will be available to go back to.
pub fn push(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
let target = self.resolve_into_routable(target.into());
pub fn push(&self, target: impl Into<NavigationTarget>) -> Option<ExternalNavigationFailure> {
let target = target.into();
{
let mut write = self.inner.write_unchecked();
match target {
NavigationTarget::Internal(p) => write.history.push(p),
NavigationTarget::Internal(p) => {
let history = history();
history.push(p)
}
NavigationTarget::External(e) => return write.external(e),
}
}
@@ -250,13 +213,18 @@ impl RouterContext {
/// Replace the current location.
///
/// The previous location will **not** be available to go back to.
pub fn replace(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
let target = self.resolve_into_routable(target.into());
pub fn replace(
&self,
target: impl Into<NavigationTarget>,
) -> Option<ExternalNavigationFailure> {
let target = target.into();
{
let mut state = self.inner.write_unchecked();
match target {
NavigationTarget::Internal(p) => state.history.replace(p),
NavigationTarget::Internal(p) => {
let history = history();
history.replace(p)
}
NavigationTarget::External(e) => return state.external(e),
}
}
@@ -266,56 +234,34 @@ impl RouterContext {
/// The route that is currently active.
pub fn current<R: Routable>(&self) -> R {
self.inner
.read()
.history
.current_route()
.downcast::<R>()
.unwrap()
.as_ref()
.clone()
}
/// The route that is currently active.
pub fn current_route_string(&self) -> String {
self.any_route_to_string(&*self.inner.read().history.current_route())
}
pub(crate) fn any_route_to_string(&self, route: &dyn Any) -> String {
(self.inner.read().any_route_to_string)(route)
}
pub(crate) fn resolve_into_routable(
&self,
into_routable: IntoRoutable,
) -> NavigationTarget<Rc<dyn Any>> {
match into_routable {
IntoRoutable::FromStr(url) => {
let parsed_route: NavigationTarget<Rc<dyn Any>> = match self.route_from_str(&url) {
Ok(route) => NavigationTarget::Internal(route),
Err(_) => NavigationTarget::External(url),
};
parsed_route
}
IntoRoutable::Route(route) => NavigationTarget::Internal(route),
let absolute_route = self.full_route_string();
// If this is a child route, map the absolute route to the child route before parsing
let mapping = consume_child_route_mapping::<R>();
match mapping.as_ref() {
Some(mapping) => mapping
.parse_route_from_root_route(&absolute_route)
.unwrap_or_else(|| {
panic!("route's display implementation must be parsable by FromStr")
}),
None => R::from_str(&absolute_route).unwrap_or_else(|_| {
panic!("route's display implementation must be parsable by FromStr")
}),
}
}
/// The full route that is currently active. If this is called from inside a child router, this will always return the parent's view of the route.
pub fn full_route_string(&self) -> String {
let inner = self.inner.read();
inner.subscribe_to_current_context();
let history = history();
history.current_route()
}
/// The prefix that is currently active.
pub fn prefix(&self) -> Option<String> {
self.inner.read().prefix.clone()
}
/// Manually subscribe to the current route
pub fn subscribe(&self, id: ScopeId) {
self.inner.read().subscribers.write().unwrap().insert(id);
}
/// Manually unsubscribe from the current route
pub fn unsubscribe(&self, id: ScopeId) {
self.inner.read().subscribers.write().unwrap().remove(&id);
}
/// Clear any unresolved errors
pub fn clear_error(&self) {
let mut write_inner = self.inner.write_unchecked();
@@ -330,11 +276,12 @@ impl RouterContext {
}
pub(crate) fn render_error(&self) -> Option<Element> {
let inner_read = self.inner.write_unchecked();
inner_read
let inner_write = self.inner.write_unchecked();
inner_write.subscribe_to_current_context();
inner_write
.unresolved_error
.as_ref()
.map(|_| (inner_read.failure_external_navigation)())
.map(|_| (inner_write.failure_external_navigation)())
}
fn change_route(&self) -> Option<ExternalNavigationFailure> {
@@ -346,7 +293,10 @@ impl RouterContext {
if let Some(new) = callback(myself) {
let mut self_write = self.inner.write_unchecked();
match new {
NavigationTarget::Internal(p) => self_write.history.replace(p),
NavigationTarget::Internal(p) => {
let history = history();
history.replace(p)
}
NavigationTarget::External(e) => return self_write.external(e),
}
}
@@ -356,6 +306,10 @@ impl RouterContext {
None
}
pub(crate) fn internal_route(&self, route: &str) -> bool {
(self.inner.read().internal_route)(route)
}
}
pub struct GenericRouterContext<R> {
@@ -426,16 +380,6 @@ where
self.inner.prefix()
}
/// Manually subscribe to the current route
pub fn subscribe(&self, id: ScopeId) {
self.inner.subscribe(id)
}
/// Manually unsubscribe from the current route
pub fn unsubscribe(&self, id: ScopeId) {
self.inner.unsubscribe(id)
}
/// Clear any unresolved errors
pub fn clear_error(&self) {
self.inner.clear_error()
-444
View File
@@ -1,444 +0,0 @@
use super::HistoryProvider;
use crate::routable::Routable;
use dioxus_lib::document::Eval;
use dioxus_lib::prelude::*;
use serde::{Deserialize, Serialize};
use std::sync::{Mutex, RwLock};
use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc};
/// A [`HistoryProvider`] that evaluates history through JS.
pub struct LiveviewHistory<R: Routable>
where
<R as FromStr>::Err: std::fmt::Display,
{
action_tx: tokio::sync::mpsc::UnboundedSender<Action<R>>,
timeline: Arc<Mutex<Timeline<R>>>,
updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>>,
}
struct Timeline<R: Routable>
where
<R as FromStr>::Err: std::fmt::Display,
{
current_index: usize,
routes: BTreeMap<usize, R>,
}
#[derive(Serialize, Deserialize, Debug)]
struct State {
index: usize,
}
#[derive(Serialize, Deserialize, Debug)]
struct Session<R: Routable>
where
<R as FromStr>::Err: std::fmt::Display,
{
#[serde(with = "routes")]
routes: BTreeMap<usize, R>,
last_visited: usize,
}
enum Action<R: Routable> {
GoBack,
GoForward,
Push(R),
Replace(R),
External(String),
}
impl<R: Routable> Timeline<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
fn new(initial_path: R) -> Self {
Self {
current_index: 0,
routes: BTreeMap::from([(0, initial_path)]),
}
}
fn init(
&mut self,
route: R,
state: Option<State>,
session: Option<Session<R>>,
depth: usize,
) -> State {
if let Some(session) = session {
self.routes = session.routes;
if state.is_none() {
// top of stack
let last_visited = session.last_visited;
self.routes.retain(|&lhs, _| lhs <= last_visited);
}
};
let state = match state {
Some(state) => {
self.current_index = state.index;
state
}
None => {
let index = depth - 1;
self.current_index = index;
State { index }
}
};
self.routes.insert(state.index, route);
state
}
fn update(&mut self, route: R, state: Option<State>) -> State {
if let Some(state) = state {
self.current_index = state.index;
self.routes.insert(self.current_index, route);
state
} else {
self.push(route)
}
}
fn push(&mut self, route: R) -> State {
// top of stack
let index = self.current_index + 1;
self.current_index = index;
self.routes.insert(index, route);
self.routes.retain(|&rhs, _| index >= rhs);
State {
index: self.current_index,
}
}
fn replace(&mut self, route: R) -> State {
self.routes.insert(self.current_index, route);
State {
index: self.current_index,
}
}
fn current_route(&self) -> &R {
&self.routes[&self.current_index]
}
fn session(&self) -> Session<R> {
Session {
routes: self.routes.clone(),
last_visited: self.current_index,
}
}
}
impl<R: Routable> Default for LiveviewHistory<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self::new()
}
}
impl<R: Routable> LiveviewHistory<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
/// Create a [`LiveviewHistory`] in the given scope.
/// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
///
/// # Panics
///
/// Panics if the function is not called in a dioxus runtime with a Liveview context.
pub fn new() -> Self {
Self::new_with_initial_path(
"/".parse().unwrap_or_else(|err| {
panic!("index route does not exist:\n{}\n use LiveviewHistory::new_with_initial_path to set a custom path", err)
}),
)
}
/// Create a [`LiveviewHistory`] in the given scope, starting at `initial_path`.
/// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
///
/// # Panics
///
/// Panics if the function is not called in a dioxus runtime with a Liveview context.
pub fn new_with_initial_path(initial_path: R) -> Self {
let (action_tx, mut action_rx) = tokio::sync::mpsc::unbounded_channel::<Action<R>>();
let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
Arc::new(RwLock::new(Arc::new(|| {})));
let eval_provider = dioxus_lib::document::document();
let create_eval = Rc::new(move |script: &str| eval_provider.eval(script.to_string()))
as Rc<dyn Fn(&str) -> Eval>;
// Listen to server actions
spawn({
let timeline = timeline.clone();
let create_eval = create_eval.clone();
async move {
loop {
let eval = action_rx.recv().await.expect("sender to exist");
let _ = match eval {
Action::GoBack => create_eval(
r#"
// this triggers a PopState event
history.back();
"#,
),
Action::GoForward => create_eval(
r#"
// this triggers a PopState event
history.forward();
"#,
),
Action::Push(route) => {
let mut timeline = timeline.lock().expect("unpoisoned mutex");
let state = timeline.push(route.clone());
let state = serde_json::to_string(&state).expect("serializable state");
let session = serde_json::to_string(&timeline.session())
.expect("serializable session");
create_eval(&format!(
r#"
// this does not trigger a PopState event
history.pushState({state}, "", "{route}");
sessionStorage.setItem("liveview", '{session}');
"#
))
}
Action::Replace(route) => {
let mut timeline = timeline.lock().expect("unpoisoned mutex");
let state = timeline.replace(route.clone());
let state = serde_json::to_string(&state).expect("serializable state");
let session = serde_json::to_string(&timeline.session())
.expect("serializable session");
create_eval(&format!(
r#"
// this does not trigger a PopState event
history.replaceState({state}, "", "{route}");
sessionStorage.setItem("liveview", '{session}');
"#
))
}
Action::External(url) => create_eval(&format!(
r#"
location.href = "{url}";
"#
)),
};
}
}
});
// Listen to browser actions
spawn({
let updater = updater_callback.clone();
let timeline = timeline.clone();
let create_eval = create_eval.clone();
async move {
let mut popstate_eval = {
let init_eval = create_eval(
r#"
return [
document.location.pathname + "?" + document.location.search + "\#" + document.location.hash,
history.state,
JSON.parse(sessionStorage.getItem("liveview")),
history.length,
];
"#,
).await.expect("serializable state");
let (route, state, session, depth) = serde_json::from_value::<(
String,
Option<State>,
Option<Session<R>>,
usize,
)>(init_eval)
.expect("serializable state");
let Ok(route) = R::from_str(&route.to_string()) else {
return;
};
let mut timeline = timeline.lock().expect("unpoisoned mutex");
let state = timeline.init(route.clone(), state, session, depth);
let state = serde_json::to_string(&state).expect("serializable state");
let session =
serde_json::to_string(&timeline.session()).expect("serializable session");
// Call the updater callback
(updater.read().unwrap())();
create_eval(&format!(
r#"
// this does not trigger a PopState event
history.replaceState({state}, "", "{route}");
sessionStorage.setItem("liveview", '{session}');
window.addEventListener("popstate", (event) => {{
dioxus.send([
document.location.pathname + "?" + document.location.search + "\#" + document.location.hash,
event.state,
]);
}});
"#
))
};
loop {
let event = match popstate_eval.recv().await {
Ok(event) => event,
Err(_) => continue,
};
let (route, state) = serde_json::from_value::<(String, Option<State>)>(event)
.expect("serializable state");
let Ok(route) = R::from_str(&route.to_string()) else {
return;
};
let mut timeline = timeline.lock().expect("unpoisoned mutex");
let state = timeline.update(route.clone(), state);
let state = serde_json::to_string(&state).expect("serializable state");
let session =
serde_json::to_string(&timeline.session()).expect("serializable session");
let _ = create_eval(&format!(
r#"
// this does not trigger a PopState event
history.replaceState({state}, "", "{route}");
sessionStorage.setItem("liveview", '{session}');
"#
));
// Call the updater callback
(updater.read().unwrap())();
}
}
});
Self {
action_tx,
timeline,
updater_callback,
}
}
}
impl<R: Routable> HistoryProvider<R> for LiveviewHistory<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
fn go_back(&mut self) {
let _ = self.action_tx.send(Action::GoBack);
}
fn go_forward(&mut self) {
let _ = self.action_tx.send(Action::GoForward);
}
fn push(&mut self, route: R) {
let _ = self.action_tx.send(Action::Push(route));
}
fn replace(&mut self, route: R) {
let _ = self.action_tx.send(Action::Replace(route));
}
fn external(&mut self, url: String) -> bool {
let _ = self.action_tx.send(Action::External(url));
true
}
fn current_route(&self) -> R {
let timeline = self.timeline.lock().expect("unpoisoned mutex");
timeline.current_route().clone()
}
fn can_go_back(&self) -> bool {
let timeline = self.timeline.lock().expect("unpoisoned mutex");
// Check if the one before is contiguous (i.e., not an external page)
let visited_indices: Vec<usize> = timeline.routes.keys().cloned().collect();
visited_indices
.iter()
.position(|&rhs| timeline.current_index == rhs)
.map_or(false, |index| {
index > 0 && visited_indices[index - 1] == timeline.current_index - 1
})
}
fn can_go_forward(&self) -> bool {
let timeline = self.timeline.lock().expect("unpoisoned mutex");
// Check if the one after is contiguous (i.e., not an external page)
let visited_indices: Vec<usize> = timeline.routes.keys().cloned().collect();
visited_indices
.iter()
.rposition(|&rhs| timeline.current_index == rhs)
.map_or(false, |index| {
index < visited_indices.len() - 1
&& visited_indices[index + 1] == timeline.current_index + 1
})
}
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
let mut updater_callback = self.updater_callback.write().unwrap();
*updater_callback = callback;
}
}
mod routes {
use crate::prelude::Routable;
use core::str::FromStr;
use serde::de::{MapAccess, Visitor};
use serde::{ser::SerializeMap, Deserializer, Serializer};
use std::collections::BTreeMap;
pub fn serialize<S, R>(routes: &BTreeMap<usize, R>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
R: Routable,
{
let mut map = serializer.serialize_map(Some(routes.len()))?;
for (index, route) in routes.iter() {
map.serialize_entry(&index.to_string(), &route.to_string())?;
}
map.end()
}
pub fn deserialize<'de, D, R>(deserializer: D) -> Result<BTreeMap<usize, R>, D::Error>
where
D: Deserializer<'de>,
R: Routable,
<R as FromStr>::Err: std::fmt::Display,
{
struct BTreeMapVisitor<R> {
marker: std::marker::PhantomData<R>,
}
impl<'de, R> Visitor<'de> for BTreeMapVisitor<R>
where
R: Routable,
<R as FromStr>::Err: std::fmt::Display,
{
type Value = BTreeMap<usize, R>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a map with indices and routable values")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut routes = BTreeMap::new();
while let Some((index, route)) = map.next_entry::<String, String>()? {
let index = index.parse::<usize>().map_err(serde::de::Error::custom)?;
let route = R::from_str(&route).map_err(serde::de::Error::custom)?;
routes.insert(index, route);
}
Ok(routes)
}
}
deserializer.deserialize_map(BTreeMapVisitor {
marker: std::marker::PhantomData,
})
}
}
-103
View File
@@ -1,103 +0,0 @@
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: R,
history: Vec<R>,
future: Vec<R>,
}
impl<R: Routable> MemoryHistory<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
/// 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 = MemoryHistory::<Route>::with_initial_path(Route::Index {});
/// assert_eq!(history.current_route(), Route::Index {});
/// assert_eq!(history.can_go_back(), false);
/// ```
pub fn with_initial_path(path: R) -> Self {
Self {
current: path,
history: Vec::new(),
future: Vec::new(),
}
}
}
impl<R: Routable> Default for MemoryHistory<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self {
current: "/".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(),
}
}
}
impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
fn current_route(&self) -> R {
self.current.clone()
}
fn can_go_back(&self) -> bool {
!self.history.is_empty()
}
fn go_back(&mut self) {
if let Some(last) = self.history.pop() {
let old = std::mem::replace(&mut self.current, last);
self.future.push(old);
}
}
fn can_go_forward(&self) -> bool {
!self.future.is_empty()
}
fn go_forward(&mut self) {
if let Some(next) = self.future.pop() {
let old = std::mem::replace(&mut self.current, next);
self.history.push(old);
}
}
fn push(&mut self, new: R) {
// don't push the same route twice
if self.current.to_string() == new.to_string() {
return;
}
let old = std::mem::replace(&mut self.current, new);
self.history.push(old);
self.future.clear();
}
fn replace(&mut self, path: R) {
self.current = path;
}
}
-402
View File
@@ -1,402 +0,0 @@
//! History Integration
//!
//! dioxus-router-core relies on [`HistoryProvider`]s to store the current Route, and possibly a
//! history (i.e. a browsers back button) and future (i.e. a browsers forward button).
//!
//! To integrate dioxus-router with a any type of history, all you have to do is implement the
//! [`HistoryProvider`] trait.
//!
//! dioxus-router contains two built in history providers:
//! 1) [`MemoryHistory`] for desktop/mobile/ssr platforms
//! 2) [`WebHistory`] for web platforms
use std::{any::Any, rc::Rc, sync::Arc};
mod memory;
pub use memory::*;
#[cfg(feature = "web")]
mod web;
#[cfg(feature = "web")]
pub use web::*;
#[cfg(feature = "web")]
pub(crate) mod web_history;
#[cfg(feature = "liveview")]
mod liveview;
#[cfg(feature = "liveview")]
pub use liveview::*;
// #[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_route` 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::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 = MemoryHistory::<Route>::default();
/// assert_eq!(history.current_route().to_string(), "/");
///
/// history.push(Route::OtherPage {});
/// assert_eq!(history.current_route().to_string(), "/some-other-page");
/// ```
#[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::prelude::*;
/// # 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 = MemoryHistory::<Route>::default();
/// assert_eq!(history.can_go_back(), false);
///
/// history.push(Route::Other {});
/// 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::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 = MemoryHistory::<Route>::default();
/// assert_eq!(history.current_route().to_string(), "/");
///
/// history.go_back();
/// assert_eq!(history.current_route().to_string(), "/");
///
/// history.push(Route::OtherPage {});
/// assert_eq!(history.current_route().to_string(), "/some-other-page");
///
/// history.go_back();
/// assert_eq!(history.current_route().to_string(), "/");
/// ```
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::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 = MemoryHistory::<Route>::default();
/// assert_eq!(history.can_go_forward(), false);
///
/// history.push(Route::OtherPage {});
/// 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::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 = MemoryHistory::<Route>::default();
/// history.push(Route::OtherPage {});
/// assert_eq!(history.current_route(), Route::OtherPage {});
///
/// history.go_back();
/// assert_eq!(history.current_route(), Route::Index {});
///
/// history.go_forward();
/// assert_eq!(history.current_route(), Route::OtherPage {});
/// ```
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::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 = MemoryHistory::<Route>::default();
/// assert_eq!(history.current_route(), Route::Index {});
///
/// history.push(Route::OtherPage {});
/// assert_eq!(history.current_route(), Route::OtherPage {});
/// 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::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 = MemoryHistory::<Route>::default();
/// assert_eq!(history.current_route(), Route::Index {});
///
/// history.replace(Route::OtherPage {});
/// assert_eq!(history.current_route(), Route::OtherPage {});
/// 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>) {}
}
pub(crate) trait AnyHistoryProvider {
fn parse_route(&self, route: &str) -> Result<Rc<dyn Any>, String>;
#[must_use]
fn current_route(&self) -> Rc<dyn Any>;
#[must_use]
fn can_go_back(&self) -> bool {
true
}
fn go_back(&mut self);
#[must_use]
fn can_go_forward(&self) -> bool {
true
}
fn go_forward(&mut self);
fn push(&mut self, route: Rc<dyn Any>);
fn replace(&mut self, path: Rc<dyn Any>);
#[allow(unused_variables)]
fn external(&mut self, url: String) -> bool {
false
}
#[allow(unused_variables)]
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {}
#[cfg(feature = "liveview")]
fn is_liveview(&self) -> bool;
}
pub(crate) struct AnyHistoryProviderImplWrapper<R, H> {
inner: H,
_marker: std::marker::PhantomData<R>,
}
impl<R, H> AnyHistoryProviderImplWrapper<R, H> {
pub fn new(inner: H) -> Self {
Self {
inner,
_marker: std::marker::PhantomData,
}
}
}
impl<R, H: Default> Default for AnyHistoryProviderImplWrapper<R, H> {
fn default() -> Self {
Self::new(H::default())
}
}
impl<R, H: 'static> AnyHistoryProvider for AnyHistoryProviderImplWrapper<R, H>
where
R: Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
H: HistoryProvider<R>,
{
fn parse_route(&self, route: &str) -> Result<Rc<dyn Any>, String> {
R::from_str(route)
.map_err(|err| err.to_string())
.map(|route| Rc::new(route) as Rc<dyn Any>)
}
fn current_route(&self) -> Rc<dyn Any> {
let route = self.inner.current_route();
Rc::new(route)
}
fn can_go_back(&self) -> bool {
self.inner.can_go_back()
}
fn go_back(&mut self) {
self.inner.go_back()
}
fn can_go_forward(&self) -> bool {
self.inner.can_go_forward()
}
fn go_forward(&mut self) {
self.inner.go_forward()
}
fn push(&mut self, route: Rc<dyn Any>) {
self.inner
.push(route.downcast::<R>().unwrap().as_ref().clone())
}
fn replace(&mut self, route: Rc<dyn Any>) {
self.inner
.replace(route.downcast::<R>().unwrap().as_ref().clone())
}
fn external(&mut self, url: String) -> bool {
self.inner.external(url)
}
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
self.inner.updater(callback)
}
#[cfg(feature = "liveview")]
fn is_liveview(&self) -> bool {
use std::any::TypeId;
TypeId::of::<H>() == TypeId::of::<LiveviewHistory<R>>()
}
}
-252
View File
@@ -1,252 +0,0 @@
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use gloo::{console::error, events::EventListener, render::AnimationFrame};
use wasm_bindgen::JsValue;
use web_sys::{window, History, ScrollRestoration, Window};
use crate::routable::Routable;
use super::{
web_history::{get_current, push_state_and_url, replace_state_with_url},
web_scroll::ScrollPosition,
HistoryProvider,
};
#[allow(dead_code)]
fn base_path() -> Option<PathBuf> {
tracing::trace!(
"Using base_path from the CLI: {:?}",
dioxus_cli_config::base_path()
);
dioxus_cli_config::base_path()
}
#[allow(clippy::extra_unused_type_parameters)]
fn update_scroll<R>(window: &Window, history: &History) {
let scroll = ScrollPosition::of_window(window);
if let Err(err) = replace_state_with_url(history, &[scroll.x, scroll.y], None) {
error!(err);
}
}
/// A [`HistoryProvider`] that integrates with a browser via the [History API](https://developer.mozilla.org/en-US/docs/Web/API/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.
pub struct WebHistory<R: Routable> {
do_scroll_restoration: bool,
history: History,
listener_navigation: Option<EventListener>,
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
prefix: Option<String>,
window: Window,
phantom: std::marker::PhantomData<R>,
}
impl<R: Routable> Default for WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self::new(None, true)
}
}
impl<R: Routable> WebHistory<R> {
/// 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
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
let myself = Self::new_inner(prefix, do_scroll_restoration);
let current_route = myself.current_route();
let current_route_str = current_route.to_string();
let prefix_str = myself.prefix.as_deref().unwrap_or("");
let current_url = format!("{prefix_str}{current_route_str}");
let state = myself.create_state(current_route);
let _ = replace_state_with_url(&myself.history, &state, Some(&current_url));
myself
}
fn new_inner(prefix: Option<String>, do_scroll_restoration: bool) -> Self
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
let window = window().expect("access to `window`");
let history = window.history().expect("`window` has access to `history`");
if do_scroll_restoration {
history
.set_scroll_restoration(ScrollRestoration::Manual)
.expect("`history` can set scroll restoration");
}
let prefix = prefix
// If there isn't a base path, try to grab one from the CLI
.or_else(|| base_path().map(|s| s.display().to_string()))
// Normalize the prefix to start and end with no slashes
.map(|prefix| prefix.trim_matches('/').to_string())
// If the prefix is empty, don't add it
.filter(|prefix| !prefix.is_empty())
// Otherwise, start with a slash
.map(|prefix| format!("/{prefix}"));
Self {
do_scroll_restoration,
history,
listener_navigation: None,
listener_animation_frame: Default::default(),
prefix,
window,
phantom: Default::default(),
}
}
fn scroll_pos(&self) -> ScrollPosition {
self.do_scroll_restoration
.then(|| ScrollPosition::of_window(&self.window))
.unwrap_or_default()
}
fn create_state(&self, _state: R) -> [f64; 2] {
let scroll = self.scroll_pos();
[scroll.x, scroll.y]
}
}
impl<R: Routable> WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn route_from_location(&self) -> R {
let location = self.window.location();
let path = location.pathname().unwrap_or_else(|_| "/".into())
+ &location.search().unwrap_or("".into())
+ &location.hash().unwrap_or("".into());
let mut path = match self.prefix {
None => &path,
Some(ref prefix) => path.strip_prefix(prefix).unwrap_or(prefix),
};
// If the path is empty, parse the root route instead
if path.is_empty() {
path = "/"
}
R::from_str(path).unwrap_or_else(|err| panic!("{}", err))
}
fn full_path(&self, state: &R) -> String {
match &self.prefix {
None => format!("{state}"),
Some(prefix) => format!("{prefix}{state}"),
}
}
fn handle_nav(&self, result: Result<(), JsValue>) {
match result {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => error!("failed to change state: ", e),
}
}
fn navigate_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
}
}
}
}
impl<R: Routable> HistoryProvider<R> for WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn current_route(&self) -> R {
self.route_from_location()
}
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, state: R) {
if state.to_string() == self.current_route().to_string() {
// don't push the same state twice
return;
}
let w = window().expect("access to `window`");
let h = w.history().expect("`window` has access to `history`");
// update the scroll position before pushing the new state
update_scroll::<R>(&w, &h);
let path = self.full_path(&state);
let state: [f64; 2] = self.create_state(state);
self.handle_nav(push_state_and_url(&self.history, &state, path));
}
fn replace(&mut self, state: R) {
let path = self.full_path(&state);
let state = self.create_state(state);
self.handle_nav(replace_state_with_url(&self.history, &state, Some(&path)));
}
fn external(&mut self, url: String) -> bool {
self.navigate_external(url)
}
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");
if let Some([x, y]) = get_current(&h) {
*s = Some(ScrollPosition { x, y }.scroll_to(w.clone()));
}
}
}));
}
}
-211
View File
@@ -1,211 +0,0 @@
use std::sync::{Arc, Mutex};
use gloo::{events::EventListener, render::AnimationFrame, utils::window};
use serde::{de::DeserializeOwned, Serialize};
use tracing::error;
use url::Url;
use web_sys::{History, ScrollRestoration, Window};
use crate::routable::Routable;
use super::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](https://developer.mozilla.org/en-US/docs/Web/API/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].
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,
phantom: std::marker::PhantomData<R>,
}
impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
/// 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,
phantom: Default::default(),
}
}
}
impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
fn join_url_to_hash(&self, path: R) -> Option<String> {
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<R: Serialize + DeserializeOwned + Routable> HistoryProvider<R> for WebHashHistory<R> {
fn current_route(&self) -> R {
self.url()
.map(|url| url.path().to_string())
.unwrap_or(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: R) {
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: R) {
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));
}
}));
}
}
@@ -1,42 +0,0 @@
use gloo::console::error;
use wasm_bindgen::JsValue;
use web_sys::History;
pub(crate) fn replace_state_with_url(
history: &History,
value: &[f64; 2],
url: Option<&str>,
) -> Result<(), JsValue> {
let position = js_sys::Array::new();
position.push(&JsValue::from(value[0]));
position.push(&JsValue::from(value[1]));
history.replace_state_with_url(&position, "", url)
}
pub(crate) fn push_state_and_url(
history: &History,
value: &[f64; 2],
url: String,
) -> Result<(), JsValue> {
let position = js_sys::Array::new();
position.push(&JsValue::from(value[0]));
position.push(&JsValue::from(value[1]));
history.push_state_with_url(&position, "", Some(&url))
}
pub(crate) fn get_current(history: &History) -> Option<[f64; 2]> {
use wasm_bindgen::JsCast;
let state = history.state();
if let Err(err) = &state {
error!(err);
}
state.ok().and_then(|state| {
let state = state.dyn_into::<js_sys::Array>().ok()?;
let x = state.get(0).as_f64()?;
let y = state.get(1).as_f64()?;
Some([x, y])
})
}
-22
View File
@@ -1,22 +0,0 @@
use gloo::render::{request_animation_frame, AnimationFrame};
use web_sys::Window;
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct ScrollPosition {
pub x: f64,
pub y: f64,
}
impl ScrollPosition {
pub(crate) fn of_window(window: &Window) -> Self {
Self {
x: window.scroll_x().unwrap_or_default(),
y: window.scroll_y().unwrap_or_default(),
}
}
pub(crate) fn scroll_to(&self, window: Window) -> AnimationFrame {
let Self { x, y } = *self;
request_animation_frame(move |_| window.scroll_to_with_x_and_y(x, y))
}
}
+10 -4
View File
@@ -24,6 +24,12 @@ pub mod components {
mod router;
pub use router::*;
mod history_provider;
pub use history_provider::*;
#[doc(hidden)]
pub mod child_router;
}
mod contexts {
@@ -37,8 +43,6 @@ mod contexts {
mod router_cfg;
mod history;
/// Hooks for interacting with the router in components.
pub mod hooks {
mod use_router;
@@ -55,9 +59,11 @@ pub use hooks::router;
/// A collection of useful items most applications might need.
pub mod prelude {
pub use crate::components::*;
pub use crate::components::{
GoBackButton, GoForwardButton, HistoryButtonProps, Link, LinkProps, Outlet, Router,
RouterProps,
};
pub use crate::contexts::*;
pub use crate::history::*;
pub use crate::hooks::*;
pub use crate::navigation::*;
pub use crate::routable::*;
+43 -2
View File
@@ -7,11 +7,23 @@ use std::{
use url::{ParseError, Url};
use crate::routable::Routable;
use crate::{components::child_router::consume_child_route_mapping, routable::Routable, router};
impl<R: Routable> From<R> for NavigationTarget {
fn from(value: R) -> Self {
// If this is a child route, map it to the root route first
let mapping = consume_child_route_mapping();
match mapping.as_ref() {
Some(mapping) => NavigationTarget::Internal(mapping.format_route_as_root_route(value)),
// Otherwise, just use the internal route
None => NavigationTarget::Internal(value.to_string()),
}
}
}
/// A target for the router to navigate to.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum NavigationTarget<R> {
pub enum NavigationTarget<R = String> {
/// An internal path that the router can navigate to by itself.
///
/// ```rust
@@ -80,6 +92,35 @@ impl<R: Routable> From<R> for NavigationTarget<R> {
}
}
impl From<&str> for NavigationTarget {
fn from(value: &str) -> Self {
let router = router();
match router.internal_route(value) {
true => NavigationTarget::Internal(value.to_string()),
false => NavigationTarget::External(value.to_string()),
}
}
}
impl From<String> for NavigationTarget {
fn from(value: String) -> Self {
let router = router();
match router.internal_route(&value) {
true => NavigationTarget::Internal(value),
false => NavigationTarget::External(value),
}
}
}
impl<R: Routable> From<NavigationTarget<R>> for NavigationTarget {
fn from(value: NavigationTarget<R>) -> Self {
match value {
NavigationTarget::Internal(r) => r.into(),
NavigationTarget::External(s) => Self::External(s),
}
}
}
impl<R: Routable> Display for NavigationTarget<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
+9 -84
View File
@@ -1,4 +1,4 @@
use crate::prelude::*;
use crate::{components::FailureExternalNavigation, prelude::*};
use dioxus_lib::prelude::*;
use std::sync::Arc;
@@ -17,42 +17,29 @@ use std::sync::Arc;
/// #[route("/")]
/// Index {},
/// }
/// let cfg = RouterConfig::default().history(MemoryHistory::<Route>::default());
///
/// fn ExternalNavigationFailure() -> Element {
/// rsx! {
/// "Failed to navigate to external URL"
/// }
/// }
///
/// let cfg = RouterConfig::<Route>::default().failure_external_navigation(ExternalNavigationFailure);
/// ```
pub struct RouterConfig<R> {
pub(crate) failure_external_navigation: fn() -> Element,
pub(crate) history: Option<Box<dyn AnyHistoryProvider>>,
pub(crate) on_update: Option<RoutingCallback<R>>,
pub(crate) initial_route: Option<R>,
}
impl<R> Default for RouterConfig<R> {
fn default() -> Self {
Self {
failure_external_navigation: FailureExternalNavigation,
history: None,
on_update: None,
initial_route: None,
}
}
}
impl<R: Routable + Clone> RouterConfig<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
pub(crate) fn take_history(&mut self) -> Box<dyn AnyHistoryProvider> {
self.history
.take()
.unwrap_or_else(|| {
let initial_route = self.initial_route.clone().unwrap_or_else(|| "/".parse().unwrap_or_else(|err|
panic!("index route does not exist:\n{}\n use MemoryHistory::with_initial_path or RouterConfig::initial_route to set a custom path", err)
));
default_history(initial_route)
})
}
}
impl<R> RouterConfig<R>
where
R: Routable,
@@ -81,24 +68,6 @@ where
}
}
/// The [`HistoryProvider`] the router should use.
///
/// Defaults to a different history provider depending on the target platform.
pub fn history(self, history: impl HistoryProvider<R> + 'static) -> Self {
Self {
history: Some(Box::new(AnyHistoryProviderImplWrapper::new(history))),
..self
}
}
/// The initial route the router should use if no history provider is set.
pub fn initial_route(self, route: R) -> Self {
Self {
initial_route: Some(route),
..self
}
}
/// A component to render when an external navigation fails.
///
/// Defaults to a router-internal component called [`FailureExternalNavigation`]
@@ -109,47 +78,3 @@ where
}
}
}
/// Get the default history provider for the current platform.
#[allow(unreachable_code, unused)]
fn default_history<R: Routable + Clone>(initial_route: R) -> Box<dyn AnyHistoryProvider>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
// If we're on the web and have wasm, use the web history provider
#[cfg(all(target_arch = "wasm32", feature = "web"))]
return Box::new(AnyHistoryProviderImplWrapper::new(
WebHistory::<R>::default(),
));
// If we're using fullstack and server side rendering, use the memory history provider
#[cfg(all(feature = "fullstack", feature = "ssr"))]
return Box::new(AnyHistoryProviderImplWrapper::new(
MemoryHistory::<R>::with_initial_path(
dioxus_fullstack::prelude::server_context()
.request_parts()
.uri
.to_string()
.parse()
.unwrap_or_else(|err| {
tracing::error!("Failed to parse uri: {}", err);
"/".parse().unwrap_or_else(|err| {
panic!("Failed to parse uri: {}", err);
})
}),
),
));
// If liveview is enabled, use the liveview history provider
#[cfg(feature = "liveview")]
return Box::new(AnyHistoryProviderImplWrapper::new(
LiveviewHistory::new_with_initial_path(initial_route),
));
// If none of the above, use the memory history provider, which is a decent enough fallback
// Eventually we want to integrate with the mobile history provider, and other platform providers
Box::new(AnyHistoryProviderImplWrapper::new(
MemoryHistory::with_initial_path(initial_route),
))
}
@@ -11,17 +11,5 @@ use crate::prelude::*;
/// - [`None`], when the current component isn't a descendant of a [`Router`] component.
/// - Otherwise [`Some`].
pub(crate) fn use_router_internal() -> Option<RouterContext> {
let router = try_consume_context::<RouterContext>()?;
let id = current_scope_id().expect("use_router_internal called outside of a component");
use_drop({
to_owned![router];
move || {
router.unsubscribe(id);
}
});
use_hook(move || {
router.subscribe(id);
Some(router)
})
use_hook(try_consume_context)
}
+90 -4
View File
@@ -1,13 +1,23 @@
use dioxus::prelude::*;
use std::str::FromStr;
use dioxus_history::{History, MemoryHistory};
use dioxus_router::components::HistoryProvider;
use std::{rc::Rc, str::FromStr};
fn prepare<R: Routable>() -> String
where
<R as FromStr>::Err: std::fmt::Display,
{
prepare_at::<R>("/")
}
fn prepare_at<R: Routable>(at: impl ToString) -> String
where
<R as FromStr>::Err: std::fmt::Display,
{
let mut vdom = VirtualDom::new_with_props(
App,
AppProps::<R> {
at: at.to_string(),
phantom: std::marker::PhantomData,
},
);
@@ -16,12 +26,14 @@ where
#[derive(Props)]
struct AppProps<R: Routable> {
at: String,
phantom: std::marker::PhantomData<R>,
}
impl<R: Routable> Clone for AppProps<R> {
fn clone(&self) -> Self {
Self {
at: self.at.clone(),
phantom: std::marker::PhantomData,
}
}
@@ -34,14 +46,15 @@ where
}
#[allow(non_snake_case)]
fn App<R: Routable>(_props: AppProps<R>) -> Element
fn App<R: Routable>(props: AppProps<R>) -> Element
where
<R as FromStr>::Err: std::fmt::Display,
{
rsx! {
h1 { "App" }
Router::<R> {
config: |_| RouterConfig::default().history(MemoryHistory::default())
HistoryProvider {
history: move |_| Rc::new(MemoryHistory::with_initial_path(props.at.clone())) as Rc<dyn History>,
Router::<R> {}
}
}
}
@@ -345,3 +358,76 @@ fn with_rel() {
assert_eq!(prepare::<Route>(), expected);
}
#[test]
fn with_child_route() {
#[derive(Routable, Clone, PartialEq, Debug)]
enum ChildRoute {
#[route("/")]
ChildRoot {},
#[route("/:not_static")]
NotStatic { not_static: String },
}
#[derive(Routable, Clone, PartialEq, Debug)]
enum Route {
#[route("/")]
Root {},
#[route("/test")]
Test {},
#[child("/child")]
Nested { child: ChildRoute },
}
#[component]
fn Test() -> Element {
unimplemented!()
}
#[component]
fn Root() -> Element {
rsx! {
Link {
to: Route::Test {},
"Parent Link"
}
Link {
to: Route::Nested { child: ChildRoute::NotStatic { not_static: "this-is-a-child-route".to_string() } },
"Child Link"
}
}
}
#[component]
fn ChildRoot() -> Element {
rsx! {
Link {
to: Route::Test {},
"Parent Link"
}
Link {
to: ChildRoute::NotStatic { not_static: "this-is-a-child-route".to_string() },
"Child Link 1"
}
Link {
to: Route::Nested { child: ChildRoute::NotStatic { not_static: "this-is-a-child-route".to_string() } },
"Child Link 2"
}
}
}
#[component]
fn NotStatic(not_static: String) -> Element {
unimplemented!()
}
assert_eq!(
prepare_at::<Route>("/"),
"<h1>App</h1><a href=\"/test\">Parent Link</a><a href=\"/child/this-is-a-child-route\">Child Link</a>"
);
assert_eq!(
prepare_at::<Route>("/child"),
"<h1>App</h1><a href=\"/test\">Parent Link</a><a href=\"/child/this-is-a-child-route\">Child Link 1</a><a href=\"/child/this-is-a-child-route\">Child Link 2</a>"
);
}
+7 -4
View File
@@ -1,6 +1,10 @@
#![allow(unused)]
use std::rc::Rc;
use dioxus::prelude::*;
use dioxus_history::{History, MemoryHistory};
use dioxus_router::components::HistoryProvider;
use dioxus_router::prelude::*;
fn prepare(path: impl Into<String>) -> VirtualDom {
@@ -38,10 +42,9 @@ fn prepare(path: impl Into<String>) -> VirtualDom {
fn App(path: Route) -> Element {
rsx! {
h1 { "App" }
Router::<Route> {
config: move |_| {
RouterConfig::default().history(MemoryHistory::with_initial_path(path.clone()))
}
HistoryProvider {
history: move |_| Rc::new(MemoryHistory::with_initial_path(path.clone())) as Rc<dyn History>,
Router::<Route> {}
}
}
}
+6 -5
View File
@@ -1,5 +1,7 @@
use dioxus::prelude::*;
use std::str::FromStr;
use dioxus_history::{History, MemoryHistory};
use dioxus_router::components::HistoryProvider;
use std::{rc::Rc, str::FromStr};
// Tests for regressions of <https://github.com/DioxusLabs/dioxus/issues/2549>
#[test]
@@ -33,10 +35,9 @@ fn Home(lang: String) -> Element {
#[component]
fn App(path: Route) -> Element {
rsx! {
Router::<Route> {
config: {
move |_| RouterConfig::default().history(MemoryHistory::with_initial_path(path.clone()))
}
HistoryProvider {
history: move |_| Rc::new(MemoryHistory::with_initial_path(path.clone())) as Rc<dyn History>,
Router::<Route> {}
}
}
}
@@ -1,4 +1,8 @@
use std::rc::Rc;
use dioxus::prelude::*;
use dioxus_history::{History, MemoryHistory};
use dioxus_router::components::HistoryProvider;
// Tests for regressions of <https://github.com/DioxusLabs/dioxus/issues/2468>
#[test]
@@ -32,10 +36,9 @@ fn Test() -> Element {
#[component]
fn App(path: Route) -> Element {
rsx! {
Router::<Route> {
config: {
move || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
}
HistoryProvider {
history: move |_| Rc::new(MemoryHistory::with_initial_path(path)) as Rc<dyn History>,
Router::<Route> {}
}
}
}