From 281087469ab19062f4751fedb461eb9ed046aad2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 31 Oct 2024 14:44:04 -0500 Subject: [PATCH] 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 --- Cargo.lock | 25 +- Cargo.toml | 27 +- examples/meta.rs | 25 +- packages/desktop/Cargo.toml | 1 + packages/desktop/src/webview.rs | 3 + packages/dioxus-lib/Cargo.toml | 3 +- packages/dioxus-lib/src/lib.rs | 7 + packages/dioxus/Cargo.toml | 11 +- packages/dioxus/src/lib.rs | 8 + packages/document/src/document.rs | 43 ++- packages/history/Cargo.toml | 11 + .../src/history/mod.rs => history/src/lib.rs} | 249 ++++-------------- packages/history/src/memory.rs | 100 +++++++ packages/liveview/Cargo.toml | 1 + .../liveview/src/{eval.rs => document.rs} | 45 ++-- .../liveview.rs => liveview/src/history.rs} | 155 ++++------- packages/liveview/src/lib.rs | 3 +- packages/liveview/src/pool.rs | 8 +- packages/liveview/src/query.rs | 1 - packages/router-macro/src/route.rs | 18 +- packages/router-macro/src/route_tree.rs | 2 +- packages/router/Cargo.toml | 23 +- packages/router/examples/simple_routes.rs | 204 -------------- .../router/src/components/child_router.rs | 68 +++++ .../router/src/components/history_provider.rs | 20 ++ packages/router/src/components/link.rs | 86 +----- packages/router/src/components/outlet.rs | 5 +- packages/router/src/components/router.rs | 5 +- packages/router/src/contexts/navigator.rs | 9 +- packages/router/src/contexts/router.rs | 232 +++++++--------- packages/router/src/history/memory.rs | 103 -------- packages/router/src/history/web_hash.rs | 211 --------------- packages/router/src/history/web_history.rs | 42 --- packages/router/src/history/web_scroll.rs | 22 -- packages/router/src/lib.rs | 14 +- packages/router/src/navigation.rs | 45 +++- packages/router/src/router_cfg.rs | 93 +------ .../router/src/utils/use_router_internal.rs | 14 +- packages/router/tests/via_ssr/link.rs | 94 ++++++- packages/router/tests/via_ssr/outlet.rs | 11 +- packages/router/tests/via_ssr/redirect.rs | 11 +- .../router/tests/via_ssr/without_index.rs | 11 +- packages/static-generation/Cargo.toml | 6 +- packages/web/Cargo.toml | 4 + packages/web/src/document.rs | 9 +- .../history/web.rs => web/src/history/mod.rs} | 176 +++++++------ packages/web/src/history/scroll.rs | 28 ++ packages/web/src/lib.rs | 2 + 48 files changed, 875 insertions(+), 1419 deletions(-) create mode 100644 packages/history/Cargo.toml rename packages/{router/src/history/mod.rs => history/src/lib.rs} (50%) create mode 100644 packages/history/src/memory.rs rename packages/liveview/src/{eval.rs => document.rs} (85%) rename packages/{router/src/history/liveview.rs => liveview/src/history.rs} (77%) delete mode 100644 packages/router/examples/simple_routes.rs create mode 100644 packages/router/src/components/child_router.rs create mode 100644 packages/router/src/components/history_provider.rs delete mode 100644 packages/router/src/history/memory.rs delete mode 100644 packages/router/src/history/web_hash.rs delete mode 100644 packages/router/src/history/web_history.rs delete mode 100644 packages/router/src/history/web_scroll.rs rename packages/{router/src/history/web.rs => web/src/history/mod.rs} (60%) create mode 100644 packages/web/src/history/scroll.rs diff --git a/Cargo.lock b/Cargo.lock index 3e03b19e2..6719c5802 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3169,6 +3169,7 @@ dependencies = [ "dioxus-devtools", "dioxus-document", "dioxus-fullstack", + "dioxus-history", "dioxus-hooks", "dioxus-html", "dioxus-liveview", @@ -3374,6 +3375,7 @@ dependencies = [ "dioxus-core", "dioxus-devtools", "dioxus-document", + "dioxus-history", "dioxus-hooks", "dioxus-html", "dioxus-interpreter-js", @@ -3530,6 +3532,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "dioxus-history" +version = "0.6.0-alpha.3" +dependencies = [ + "dioxus", + "dioxus-core", + "tracing", +] + [[package]] name = "dioxus-hooks" version = "0.6.0-alpha.3" @@ -3628,6 +3639,7 @@ dependencies = [ "dioxus-core", "dioxus-core-macro", "dioxus-document", + "dioxus-history", "dioxus-hooks", "dioxus-html", "dioxus-rsx", @@ -3644,6 +3656,7 @@ dependencies = [ "dioxus-core", "dioxus-devtools", "dioxus-document", + "dioxus-history", "dioxus-html", "dioxus-interpreter-js", "futures-channel", @@ -3733,26 +3746,18 @@ dependencies = [ "criterion", "dioxus", "dioxus-cli-config", - "dioxus-fullstack", + "dioxus-history", "dioxus-lib", - "dioxus-liveview", - "dioxus-router", "dioxus-router-macro", "dioxus-ssr", "gloo", - "gloo-utils 0.1.7", - "http 1.1.0", - "js-sys", "rustversion", "serde", - "serde_json", "tokio", "tracing", "url", "urlencoding", - "wasm-bindgen", "wasm-bindgen-test", - "web-sys", ] [[package]] @@ -3880,10 +3885,12 @@ dependencies = [ "ciborium", "console_error_panic_hook", "dioxus", + "dioxus-cli-config", "dioxus-core", "dioxus-core-types", "dioxus-devtools", "dioxus-document", + "dioxus-history", "dioxus-html", "dioxus-interpreter-js", "dioxus-signals", diff --git a/Cargo.toml b/Cargo.toml index 15ce27981..2520d9d3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ members = [ "packages/extension", "packages/fullstack", "packages/generational-box", + "packages/history", "packages/hooks", "packages/html-internal-macro", "packages/html", @@ -116,15 +117,17 @@ dioxus-core = { path = "packages/core", version = "0.6.0-alpha.3" } dioxus-core-types = { path = "packages/core-types", version = "0.6.0-alpha.3" } dioxus-core-macro = { path = "packages/core-macro", version = "0.6.0-alpha.3" } dioxus-config-macro = { path = "packages/config-macro", version = "0.6.0-alpha.3" } -dioxus-document = { path = "packages/document", version = "0.6.0-alpha.3" } dioxus-router = { path = "packages/router", version = "0.6.0-alpha.3" } dioxus-router-macro = { path = "packages/router-macro", version = "0.6.0-alpha.3" } +dioxus-document = { path = "packages/document", version = "0.6.0-alpha.3", default-features = false } +dioxus-history = { path = "packages/history", version = "0.6.0-alpha.3", default-features = false } dioxus-html = { path = "packages/html", version = "0.6.0-alpha.3", default-features = false } dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.6.0-alpha.3" } dioxus-hooks = { path = "packages/hooks", version = "0.6.0-alpha.3" } dioxus-web = { path = "packages/web", version = "0.6.0-alpha.3", default-features = false } +dioxus-isrg = { path = "packages/isrg", version = "0.6.0-alpha.3" } dioxus-ssr = { path = "packages/ssr", version = "0.6.0-alpha.3", default-features = false } -dioxus-desktop = { path = "packages/desktop", version = "0.6.0-alpha.3" } +dioxus-desktop = { path = "packages/desktop", version = "0.6.0-alpha.3", default-features = false } dioxus-mobile = { path = "packages/mobile", version = "0.6.0-alpha.3" } dioxus-interpreter-js = { path = "packages/interpreter", version = "0.6.0-alpha.3" } dioxus-liveview = { path = "packages/liveview", version = "0.6.0-alpha.3" } @@ -134,19 +137,17 @@ dioxus-rsx = { path = "packages/rsx", version = "0.6.0-alpha.3" } dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.6.0-alpha.3" } dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.6.0-alpha.3" } dioxus-signals = { path = "packages/signals", version = "0.6.0-alpha.3" } -dioxus-devtools = { path = "packages/devtools", version = "0.6.0-alpha.3" } -dioxus-devtools-types = { path = "packages/devtools-types", version = "0.6.0-alpha.3" } -dioxus-fullstack = { path = "packages/fullstack", version = "0.6.0-alpha.3", default-features = false } -dioxus-static-site-generation = { path = "packages/static-generation", version = "0.6.0-alpha.3" } -dioxus_server_macro = { path = "packages/server-macro", version = "0.6.0-alpha.3", default-features = false } -dioxus-isrg = { path = "packages/isrg", version = "0.6.0-alpha.3" } -lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.6.0-alpha.3" } dioxus-cli-config = { path = "packages/cli-config", version = "0.6.0-alpha.3" } generational-box = { path = "packages/generational-box", version = "0.6.0-alpha.3" } - -manganis = { path = "packages/manganis/manganis", version = "0.6.0-alpha.3" } -manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.0-alpha.3" } -manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.0-alpha.3" } +dioxus-devtools = { path = "packages/devtools", version = "0.6.0-alpha.3" } +dioxus-devtools-types = { path = "packages/devtools-types", version = "0.6.0-alpha.3" } +dioxus-fullstack = { path = "packages/fullstack", version = "0.6.0-alpha.1" } +dioxus-static-site-generation = { path = "packages/static-generation", version = "0.6.0-alpha.3" } +dioxus_server_macro = { path = "packages/server-macro", version = "0.6.0-alpha.3", default-features = false } +lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.6.0-alpha.3" } +manganis = { path = "packages/manganis/manganis", version = "0.6.0-alpha.3" } +manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.0-alpha.3" } +manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.0-alpha.3" } warnings = { version = "0.2.0" } diff --git a/examples/meta.rs b/examples/meta.rs index a01695ffb..9d8e1a4ae 100644 --- a/examples/meta.rs +++ b/examples/meta.rs @@ -11,10 +11,25 @@ fn app() -> Element { // You can use the Meta component to render a meta tag into the head of the page // Meta tags are useful to provide information about the page to search engines and social media sites // This example sets up meta tags for the open graph protocol for social media previews - document::Meta { property: "og:title", content: "My Site" } - document::Meta { property: "og:type", content: "website" } - document::Meta { property: "og:url", content: "https://www.example.com" } - document::Meta { property: "og:image", content: "https://example.com/image.jpg" } - document::Meta { name: "description", content: "My Site is a site" } + document::Meta { + property: "og:title", + content: "My Site", + } + document::Meta { + property: "og:type", + content: "website", + } + document::Meta { + property: "og:url", + content: "https://www.example.com", + } + document::Meta { + property: "og:image", + content: "https://example.com/image.jpg", + } + document::Meta { + name: "description", + content: "My Site is a site", + } } } diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index d97a492c2..200edfe23 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -54,6 +54,7 @@ urlencoding = "2.1.2" async-trait = "0.1.68" tao = { workspace = true, features = ["rwh_05"] } once_cell = { workspace = true } +dioxus-history.workspace = true [target.'cfg(unix)'.dependencies] diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index cefe9c553..b6bf3a01d 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -14,6 +14,7 @@ use crate::{ }; use dioxus_core::{Runtime, ScopeId, VirtualDom}; use dioxus_document::Document; +use dioxus_history::{History, MemoryHistory}; use dioxus_hooks::to_owned; use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData}; use futures_util::{pin_mut, FutureExt}; @@ -392,9 +393,11 @@ impl WebviewInstance { // Provide the desktop context to the virtual dom and edit handler edits.set_desktop_context(desktop_context.clone()); let provider: Rc = Rc::new(DesktopDocument::new(desktop_context.clone())); + let history_provider: Rc = Rc::new(MemoryHistory::default()); dom.in_runtime(|| { ScopeId::ROOT.provide_context(desktop_context.clone()); ScopeId::ROOT.provide_context(provider); + ScopeId::ROOT.provide_context(history_provider); }); WebviewInstance { diff --git a/packages/dioxus-lib/Cargo.toml b/packages/dioxus-lib/Cargo.toml index 5ae189e0e..50d0a6fa2 100644 --- a/packages/dioxus-lib/Cargo.toml +++ b/packages/dioxus-lib/Cargo.toml @@ -14,6 +14,7 @@ rust-version = "1.79.0" dioxus-core = { workspace = true } dioxus-html = { workspace = true, optional = true } dioxus-document = { workspace = true, optional = true } +dioxus-history = { workspace = true, optional = true } dioxus-core-macro = { workspace = true, optional = true } dioxus-config-macro = { workspace = true, optional = true } dioxus-hooks = { workspace = true, optional = true } @@ -27,7 +28,7 @@ dioxus = { workspace = true } default = ["macro", "html", "signals", "hooks"] signals = ["dep:dioxus-signals"] macro = ["dep:dioxus-core-macro", "dep:dioxus-rsx", "dep:dioxus-config-macro"] -html = ["dep:dioxus-html", "dep:dioxus-document"] +html = ["dep:dioxus-html", "dep:dioxus-document", "dep:dioxus-history"] hooks = ["dep:dioxus-hooks"] [package.metadata.docs.rs] diff --git a/packages/dioxus-lib/src/lib.rs b/packages/dioxus-lib/src/lib.rs index 5aabbdf6a..0e5cdc922 100644 --- a/packages/dioxus-lib/src/lib.rs +++ b/packages/dioxus-lib/src/lib.rs @@ -19,6 +19,9 @@ pub use dioxus_html as html; #[cfg(feature = "html")] pub use dioxus_document as document; +#[cfg(feature = "html")] +pub use dioxus_history as history; + #[cfg(feature = "macro")] pub use dioxus_rsx as rsx; @@ -26,6 +29,10 @@ pub use dioxus_rsx as rsx; pub use dioxus_core_macro as core_macro; pub mod prelude { + #[cfg(feature = "html")] + #[cfg_attr(docsrs, doc(cfg(feature = "html")))] + pub use dioxus_history::{history, History}; + #[cfg(feature = "hooks")] pub use crate::hooks::*; diff --git a/packages/dioxus/Cargo.toml b/packages/dioxus/Cargo.toml index 0025b6898..6ae7752c0 100644 --- a/packages/dioxus/Cargo.toml +++ b/packages/dioxus/Cargo.toml @@ -14,6 +14,7 @@ rust-version = "1.79.0" dioxus-core = { workspace = true } dioxus-html = { workspace = true, default-features = false, optional = true } dioxus-document = { workspace = true, optional = true } +dioxus-history = { workspace = true, optional = true } dioxus-core-macro = { workspace = true, optional = true } dioxus-config-macro = { workspace = true, optional = true } dioxus-hooks = { workspace = true, optional = true } @@ -45,18 +46,18 @@ devtools = ["dep:dioxus-devtools", "dioxus-web?/devtools", "dioxus-fullstack?/de mounted = ["dioxus-web?/mounted", "dioxus-html?/mounted"] file_engine = ["dioxus-web?/file_engine"] asset = ["dep:manganis"] -document = ["dioxus-web?/document", "dioxus-document"] +document = ["dioxus-web?/document", "dep:dioxus-document", "dep:dioxus-history"] launch = ["dep:dioxus-config-macro"] router = ["dep:dioxus-router"] # Platforms -fullstack = ["dep:dioxus-fullstack", "dioxus-config-macro/fullstack", "dep:serde", "dioxus-router?/fullstack"] +fullstack = ["dep:dioxus-fullstack", "dioxus-config-macro/fullstack", "dep:serde"] desktop = ["dep:dioxus-desktop", "dioxus-fullstack?/desktop", "dioxus-config-macro/desktop"] mobile = ["dep:dioxus-mobile", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"] -web = ["dep:dioxus-web", "dioxus-fullstack?/web", "dioxus-static-site-generation?/web", "dioxus-config-macro/web", "dioxus-router?/web"] -ssr = ["dep:dioxus-ssr", "dioxus-router?/ssr", "dioxus-config-macro/ssr"] -liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview", "dioxus-router?/liveview"] +web = ["dep:dioxus-web", "dioxus-fullstack?/web", "dioxus-static-site-generation?/web", "dioxus-config-macro/web"] +ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"] +liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"] static-generation = ["dep:dioxus-static-site-generation", "dioxus-config-macro/static-generation"] axum = ["server"] server = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "dioxus-static-site-generation?/server", "ssr", "dioxus-liveview?/axum", "dep:axum"] diff --git a/packages/dioxus/src/lib.rs b/packages/dioxus/src/lib.rs index 175a9467c..b25e4218e 100644 --- a/packages/dioxus/src/lib.rs +++ b/packages/dioxus/src/lib.rs @@ -54,6 +54,10 @@ pub mod events { #[cfg_attr(docsrs, doc(cfg(feature = "document")))] pub use dioxus_document as document; +#[cfg(feature = "document")] +#[cfg_attr(docsrs, doc(cfg(feature = "document")))] +pub use dioxus_history as history; + #[cfg(feature = "html")] #[cfg_attr(docsrs, doc(cfg(feature = "html")))] pub use dioxus_html as html; @@ -67,6 +71,10 @@ pub mod prelude { #[cfg_attr(docsrs, doc(cfg(feature = "document")))] pub use dioxus_document as document; + #[cfg(feature = "document")] + #[cfg_attr(docsrs, doc(cfg(feature = "document")))] + pub use dioxus_history::{history, History}; + #[cfg(feature = "launch")] #[cfg_attr(docsrs, doc(cfg(feature = "launch")))] pub use crate::launch::*; diff --git a/packages/document/src/document.rs b/packages/document/src/document.rs index 709e68638..822f14956 100644 --- a/packages/document/src/document.rs +++ b/packages/document/src/document.rs @@ -126,31 +126,26 @@ pub struct NoOpDocument; impl Document for NoOpDocument { fn eval(&self, _: String) -> Eval { let owner = generational_box::Owner::default(); - let boxed = owner.insert(Box::new(NoOpEvaluator {}) as Box); - Eval::new(boxed) - } -} + struct NoOpEvaluator; + impl Evaluator for NoOpEvaluator { + fn poll_join( + &mut self, + _: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Err(EvalError::Unsupported)) + } -/// An evaluator that does nothing -#[derive(Default)] -pub struct NoOpEvaluator; + fn poll_recv( + &mut self, + _: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Err(EvalError::Unsupported)) + } -impl Evaluator for NoOpEvaluator { - fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> { - Err(EvalError::Unsupported) - } - - fn poll_recv( - &mut self, - _context: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - std::task::Poll::Ready(Err(EvalError::Unsupported)) - } - - fn poll_join( - &mut self, - _context: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - std::task::Poll::Ready(Err(EvalError::Unsupported)) + fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> { + Err(EvalError::Unsupported) + } + } + Eval::new(owner.insert(Box::new(NoOpEvaluator))) } } diff --git a/packages/history/Cargo.toml b/packages/history/Cargo.toml new file mode 100644 index 000000000..5dab8b778 --- /dev/null +++ b/packages/history/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "dioxus-history" +edition = "2021" +version.workspace = true + +[dependencies] +dioxus-core = { workspace = true } +tracing.workspace = true + +[dev-dependencies] +dioxus = { workspace = true, features = ["router"] } diff --git a/packages/router/src/history/mod.rs b/packages/history/src/lib.rs similarity index 50% rename from packages/router/src/history/mod.rs rename to packages/history/src/lib.rs index 1b5d18612..8dcce62e7 100644 --- a/packages/router/src/history/mod.rs +++ b/packages/history/src/lib.rs @@ -1,57 +1,31 @@ -//! 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}; +use dioxus_core::prelude::{provide_context, provide_root_context}; +use std::{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; +/// Get the history provider for the current platform if the platform doesn't implement a history functionality. +pub fn history() -> Rc { + match dioxus_core::prelude::try_consume_context::>() { + 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())) + } + } +} -#[cfg(feature = "liveview")] -mod liveview; -#[cfg(feature = "liveview")] -pub use liveview::*; +/// Provide a history context to the current component. +pub fn provide_history_context(history: Rc) { + provide_context(history); +} -// #[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 { +pub trait History { /// 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() } @@ -64,14 +38,14 @@ pub trait HistoryProvider { /// #[route("/some-other-page")] /// OtherPage {}, /// } - /// let mut history = MemoryHistory::::default(); - /// assert_eq!(history.current_route().to_string(), "/"); + /// let mut history = dioxus::history::MemoryHistory::default(); + /// assert_eq!(history.current_route(), "/"); /// - /// history.push(Route::OtherPage {}); - /// assert_eq!(history.current_route().to_string(), "/some-other-page"); + /// history.push(Route::OtherPage {}.to_string()); + /// assert_eq!(history.current_route(), "/some-other-page"); /// ``` #[must_use] - fn current_route(&self) -> R; + fn current_route(&self) -> String; /// Get the current path prefix of the URL. /// @@ -89,7 +63,6 @@ pub trait HistoryProvider { /// If a [`HistoryProvider`] cannot know this, it should return [`true`]. /// /// ```rust - /// # use dioxus_router::prelude::*; /// # use dioxus::prelude::*; /// # #[component] /// # fn Index() -> Element { VNode::empty() } @@ -101,10 +74,10 @@ pub trait HistoryProvider { /// #[route("/other")] /// Other {}, /// } - /// let mut history = MemoryHistory::::default(); + /// let mut history = dioxus::history::MemoryHistory::default(); /// assert_eq!(history.can_go_back(), false); /// - /// history.push(Route::Other {}); + /// history.push(Route::Other {}.to_string()); /// assert_eq!(history.can_go_back(), true); /// ``` #[must_use] @@ -118,7 +91,6 @@ pub trait HistoryProvider { /// might be called, even if `can_go_back` returns [`false`]. /// /// ```rust - /// # use dioxus_router::prelude::*; /// # use dioxus::prelude::*; /// # #[component] /// # fn Index() -> Element { VNode::empty() } @@ -131,26 +103,25 @@ pub trait HistoryProvider { /// #[route("/some-other-page")] /// OtherPage {}, /// } - /// let mut history = MemoryHistory::::default(); - /// assert_eq!(history.current_route().to_string(), "/"); + /// let mut history = dioxus::history::MemoryHistory::default(); + /// assert_eq!(history.current_route(), "/"); /// /// history.go_back(); - /// assert_eq!(history.current_route().to_string(), "/"); + /// assert_eq!(history.current_route(), "/"); /// - /// history.push(Route::OtherPage {}); - /// assert_eq!(history.current_route().to_string(), "/some-other-page"); + /// history.push(Route::OtherPage {}.to_string()); + /// assert_eq!(history.current_route(), "/some-other-page"); /// /// history.go_back(); - /// assert_eq!(history.current_route().to_string(), "/"); + /// assert_eq!(history.current_route(), "/"); /// ``` - fn go_back(&mut self); + 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_router::prelude::*; /// # use dioxus::prelude::*; /// # #[component] /// # fn Index() -> Element { VNode::empty() } @@ -163,10 +134,10 @@ pub trait HistoryProvider { /// #[route("/some-other-page")] /// OtherPage {}, /// } - /// let mut history = MemoryHistory::::default(); + /// let mut history = dioxus::history::MemoryHistory::default(); /// assert_eq!(history.can_go_forward(), false); /// - /// history.push(Route::OtherPage {}); + /// history.push(Route::OtherPage {}.to_string()); /// assert_eq!(history.can_go_forward(), false); /// /// history.go_back(); @@ -183,7 +154,6 @@ pub trait HistoryProvider { /// might be called, even if `can_go_forward` returns [`false`]. /// /// ```rust - /// # use dioxus_router::prelude::*; /// # use dioxus::prelude::*; /// # #[component] /// # fn Index() -> Element { VNode::empty() } @@ -196,17 +166,17 @@ pub trait HistoryProvider { /// #[route("/some-other-page")] /// OtherPage {}, /// } - /// let mut history = MemoryHistory::::default(); - /// history.push(Route::OtherPage {}); - /// assert_eq!(history.current_route(), Route::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 {}); + /// assert_eq!(history.current_route(), Route::Index {}.to_string()); /// /// history.go_forward(); - /// assert_eq!(history.current_route(), Route::OtherPage {}); + /// assert_eq!(history.current_route(), Route::OtherPage {}.to_string()); /// ``` - fn go_forward(&mut self); + fn go_forward(&self); /// Go to another page. /// @@ -216,7 +186,6 @@ pub trait HistoryProvider { /// 3. Clear the navigation future. /// /// ```rust - /// # use dioxus_router::prelude::*; /// # use dioxus::prelude::*; /// # #[component] /// # fn Index() -> Element { VNode::empty() } @@ -229,14 +198,14 @@ pub trait HistoryProvider { /// #[route("/some-other-page")] /// OtherPage {}, /// } - /// let mut history = MemoryHistory::::default(); - /// assert_eq!(history.current_route(), Route::Index {}); + /// let mut history = dioxus::history::MemoryHistory::default(); + /// assert_eq!(history.current_route(), Route::Index {}.to_string()); /// - /// history.push(Route::OtherPage {}); - /// assert_eq!(history.current_route(), Route::OtherPage {}); + /// history.push(Route::OtherPage {}.to_string()); + /// assert_eq!(history.current_route(), Route::OtherPage {}.to_string()); /// assert!(history.can_go_back()); /// ``` - fn push(&mut self, route: R); + fn push(&self, route: String); /// Replace the current page with another one. /// @@ -245,7 +214,6 @@ pub trait HistoryProvider { /// untouched. /// /// ```rust - /// # use dioxus_router::prelude::*; /// # use dioxus::prelude::*; /// # #[component] /// # fn Index() -> Element { VNode::empty() } @@ -258,14 +226,14 @@ pub trait HistoryProvider { /// #[route("/some-other-page")] /// OtherPage {}, /// } - /// let mut history = MemoryHistory::::default(); - /// assert_eq!(history.current_route(), Route::Index {}); + /// let mut history = dioxus::history::MemoryHistory::default(); + /// assert_eq!(history.current_route(), Route::Index {}.to_string()); /// - /// history.replace(Route::OtherPage {}); - /// assert_eq!(history.current_route(), Route::OtherPage {}); + /// history.replace(Route::OtherPage {}.to_string()); + /// assert_eq!(history.current_route(), Route::OtherPage {}.to_string()); /// assert!(!history.can_go_back()); /// ``` - fn replace(&mut self, path: R); + fn replace(&self, path: String); /// Navigate to an external URL. /// @@ -274,7 +242,7 @@ pub trait HistoryProvider { /// /// Returning [`false`] will cause the router to handle the external navigation failure. #[allow(unused_variables)] - fn external(&mut self, url: String) -> bool { + fn external(&self, url: String) -> bool { false } @@ -283,120 +251,11 @@ pub trait HistoryProvider { /// 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) {} -} + fn updater(&self, callback: Arc) {} -pub(crate) trait AnyHistoryProvider { - fn parse_route(&self, route: &str) -> Result, String>; - - #[must_use] - fn current_route(&self) -> Rc; - - #[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); - - fn replace(&mut self, path: Rc); - - #[allow(unused_variables)] - fn external(&mut self, url: String) -> bool { + /// 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 } - - #[allow(unused_variables)] - fn updater(&mut self, callback: Arc) {} - - #[cfg(feature = "liveview")] - fn is_liveview(&self) -> bool; -} - -pub(crate) struct AnyHistoryProviderImplWrapper { - inner: H, - _marker: std::marker::PhantomData, -} - -impl AnyHistoryProviderImplWrapper { - pub fn new(inner: H) -> Self { - Self { - inner, - _marker: std::marker::PhantomData, - } - } -} - -impl Default for AnyHistoryProviderImplWrapper { - fn default() -> Self { - Self::new(H::default()) - } -} - -impl AnyHistoryProvider for AnyHistoryProviderImplWrapper -where - R: Routable, - ::Err: std::fmt::Display, - H: HistoryProvider, -{ - fn parse_route(&self, route: &str) -> Result, String> { - R::from_str(route) - .map_err(|err| err.to_string()) - .map(|route| Rc::new(route) as Rc) - } - - fn current_route(&self) -> Rc { - 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) { - self.inner - .push(route.downcast::().unwrap().as_ref().clone()) - } - - fn replace(&mut self, route: Rc) { - self.inner - .replace(route.downcast::().unwrap().as_ref().clone()) - } - - fn external(&mut self, url: String) -> bool { - self.inner.external(url) - } - - fn updater(&mut self, callback: Arc) { - self.inner.updater(callback) - } - - #[cfg(feature = "liveview")] - fn is_liveview(&self) -> bool { - use std::any::TypeId; - - TypeId::of::() == TypeId::of::>() - } } diff --git a/packages/history/src/memory.rs b/packages/history/src/memory.rs new file mode 100644 index 000000000..eadcc08b7 --- /dev/null +++ b/packages/history/src/memory.rs @@ -0,0 +1,100 @@ +use std::cell::RefCell; + +use crate::History; + +struct MemoryHistoryState { + current: String, + history: Vec, + future: Vec, +} + +/// A [`History`] provider that stores all navigation information in memory. +pub struct MemoryHistory { + state: RefCell, +} + +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; + } +} diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 95b1b4dd6..1cc226587 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -24,6 +24,7 @@ serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.91" dioxus-html = { workspace = true, features = ["serialize", "mounted"] } dioxus-document = { workspace = true } +dioxus-history = { workspace = true } rustc-hash = { workspace = true } dioxus-core = { workspace = true, features = ["serialize"] } dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } diff --git a/packages/liveview/src/eval.rs b/packages/liveview/src/document.rs similarity index 85% rename from packages/liveview/src/eval.rs rename to packages/liveview/src/document.rs index 4ba176501..ec7178165 100644 --- a/packages/liveview/src/eval.rs +++ b/packages/liveview/src/document.rs @@ -1,28 +1,12 @@ use dioxus_core::ScopeId; use dioxus_document::{Document, Eval, EvalError, Evaluator}; +use dioxus_history::History; use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; use std::rc::Rc; +use crate::history::LiveviewHistory; use crate::query::{Query, QueryEngine}; -/// Provides the LiveviewDocument through [`ScopeId::provide_context`]. -pub fn init_eval() { - let query = ScopeId::ROOT.consume_context::().unwrap(); - let provider: Rc = Rc::new(LiveviewDocument { query }); - ScopeId::ROOT.provide_context(provider); -} - -/// Reprints the liveview-target's provider of evaluators. -pub struct LiveviewDocument { - query: QueryEngine, -} - -impl Document for LiveviewDocument { - fn eval(&self, js: String) -> Eval { - Eval::new(LiveviewEvaluator::create(self.query.clone(), js)) - } -} - /// Represents a liveview-target's JavaScript evaluator. pub(crate) struct LiveviewEvaluator { query: Query, @@ -75,3 +59,28 @@ impl Evaluator for LiveviewEvaluator { .map_err(|e| EvalError::Communication(e.to_string())) } } + +/// Provides the LiveviewDocument through [`ScopeId::provide_context`]. +pub fn init_document() { + let query = ScopeId::ROOT.consume_context::().unwrap(); + let provider: Rc = Rc::new(LiveviewDocument { + query: query.clone(), + }); + ScopeId::ROOT.provide_context(provider); + let history = LiveviewHistory::new(Rc::new(move |script: &str| { + Eval::new(LiveviewEvaluator::create(query.clone(), script.to_string())) + })); + let history: Rc = Rc::new(history); + ScopeId::ROOT.provide_context(history); +} + +/// Reprints the liveview-target's provider of evaluators. +pub struct LiveviewDocument { + query: QueryEngine, +} + +impl Document for LiveviewDocument { + fn eval(&self, js: String) -> Eval { + Eval::new(LiveviewEvaluator::create(self.query.clone(), js)) + } +} diff --git a/packages/router/src/history/liveview.rs b/packages/liveview/src/history.rs similarity index 77% rename from packages/router/src/history/liveview.rs rename to packages/liveview/src/history.rs index 7504f7810..7009ed58b 100644 --- a/packages/router/src/history/liveview.rs +++ b/packages/liveview/src/history.rs @@ -1,27 +1,21 @@ -use super::HistoryProvider; -use crate::routable::Routable; -use dioxus_lib::document::Eval; -use dioxus_lib::prelude::*; +use dioxus_core::prelude::spawn; +use dioxus_document::Eval; +use dioxus_history::History; use serde::{Deserialize, Serialize}; +use std::rc::Rc; use std::sync::{Mutex, RwLock}; -use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc}; +use std::{collections::BTreeMap, sync::Arc}; /// A [`HistoryProvider`] that evaluates history through JS. -pub struct LiveviewHistory -where - ::Err: std::fmt::Display, -{ - action_tx: tokio::sync::mpsc::UnboundedSender>, - timeline: Arc>>, +pub(crate) struct LiveviewHistory { + action_tx: tokio::sync::mpsc::UnboundedSender, + timeline: Arc>, updater_callback: Arc>>, } -struct Timeline -where - ::Err: std::fmt::Display, -{ +struct Timeline { current_index: usize, - routes: BTreeMap, + routes: BTreeMap, } #[derive(Serialize, Deserialize, Debug)] @@ -30,28 +24,22 @@ struct State { } #[derive(Serialize, Deserialize, Debug)] -struct Session -where - ::Err: std::fmt::Display, -{ +struct Session { #[serde(with = "routes")] - routes: BTreeMap, + routes: BTreeMap, last_visited: usize, } -enum Action { +enum Action { GoBack, GoForward, - Push(R), - Replace(R), + Push(String), + Replace(String), External(String), } -impl Timeline -where - ::Err: std::fmt::Display, -{ - fn new(initial_path: R) -> Self { +impl Timeline { + fn new(initial_path: String) -> Self { Self { current_index: 0, routes: BTreeMap::from([(0, initial_path)]), @@ -60,9 +48,9 @@ where fn init( &mut self, - route: R, + route: String, state: Option, - session: Option>, + session: Option, depth: usize, ) -> State { if let Some(session) = session { @@ -88,7 +76,7 @@ where state } - fn update(&mut self, route: R, state: Option) -> State { + fn update(&mut self, route: String, state: Option) -> State { if let Some(state) = state { self.current_index = state.index; self.routes.insert(self.current_index, route); @@ -98,7 +86,7 @@ where } } - fn push(&mut self, route: R) -> State { + fn push(&mut self, route: String) -> State { // top of stack let index = self.current_index + 1; self.current_index = index; @@ -109,18 +97,18 @@ where } } - fn replace(&mut self, route: R) -> State { + fn replace(&mut self, route: String) -> State { self.routes.insert(self.current_index, route); State { index: self.current_index, } } - fn current_route(&self) -> &R { + fn current_route(&self) -> &str { &self.routes[&self.current_index] } - fn session(&self) -> Session { + fn session(&self) -> Session { Session { routes: self.routes.clone(), last_visited: self.current_index, @@ -128,30 +116,19 @@ where } } -impl Default for LiveviewHistory -where - ::Err: std::fmt::Display, -{ - fn default() -> Self { - Self::new() - } -} - -impl LiveviewHistory -where - ::Err: std::fmt::Display, -{ +impl LiveviewHistory { /// 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 { + pub(crate) fn new(eval: Rc Eval>) -> 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) }), + eval ) } @@ -161,22 +138,17 @@ where /// # 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::>(); + fn new_with_initial_path(initial_path: String, eval: Rc Eval>) -> Self { + let (action_tx, mut action_rx) = tokio::sync::mpsc::unbounded_channel::(); let timeline = Arc::new(Mutex::new(Timeline::new(initial_path))); let updater_callback: Arc>> = 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 Eval>; - // Listen to server actions spawn({ let timeline = timeline.clone(); - let create_eval = create_eval.clone(); + let create_eval = eval.clone(); async move { loop { let eval = action_rx.recv().await.expect("sender to exist"); @@ -236,7 +208,7 @@ where spawn({ let updater = updater_callback.clone(); let timeline = timeline.clone(); - let create_eval = create_eval.clone(); + let create_eval = eval.clone(); async move { let mut popstate_eval = { let init_eval = create_eval( @@ -249,16 +221,11 @@ where ]; "#, ).await.expect("serializable state"); - let (route, state, session, depth) = serde_json::from_value::<( - String, - Option, - Option>, - usize, - )>(init_eval) - .expect("serializable state"); - let Ok(route) = R::from_str(&route.to_string()) else { - return; - }; + let (route, state, session, depth) = + serde_json::from_value::<(String, Option, Option, usize)>( + init_eval, + ) + .expect("serializable state"); 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"); @@ -291,9 +258,6 @@ where }; let (route, state) = serde_json::from_value::<(String, Option)>(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"); @@ -322,34 +286,31 @@ where } } -impl HistoryProvider for LiveviewHistory -where - ::Err: std::fmt::Display, -{ - fn go_back(&mut self) { +impl History for LiveviewHistory { + fn go_back(&self) { let _ = self.action_tx.send(Action::GoBack); } - fn go_forward(&mut self) { + fn go_forward(&self) { let _ = self.action_tx.send(Action::GoForward); } - fn push(&mut self, route: R) { + fn push(&self, route: String) { let _ = self.action_tx.send(Action::Push(route)); } - fn replace(&mut self, route: R) { + fn replace(&self, route: String) { let _ = self.action_tx.send(Action::Replace(route)); } - fn external(&mut self, url: String) -> bool { + fn external(&self, url: String) -> bool { let _ = self.action_tx.send(Action::External(url)); true } - fn current_route(&self) -> R { + fn current_route(&self) -> String { let timeline = self.timeline.lock().expect("unpoisoned mutex"); - timeline.current_route().clone() + timeline.current_route().to_string() } fn can_go_back(&self) -> bool { @@ -377,23 +338,20 @@ where }) } - fn updater(&mut self, callback: Arc) { + fn updater(&self, callback: Arc) { 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(routes: &BTreeMap, serializer: S) -> Result + pub fn serialize(routes: &BTreeMap, serializer: S) -> Result where S: Serializer, - R: Routable, { let mut map = serializer.serialize_map(Some(routes.len()))?; for (index, route) in routes.iter() { @@ -402,22 +360,14 @@ mod routes { map.end() } - pub fn deserialize<'de, D, R>(deserializer: D) -> Result, D::Error> + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, - R: Routable, - ::Err: std::fmt::Display, { - struct BTreeMapVisitor { - marker: std::marker::PhantomData, - } + struct BTreeMapVisitor {} - impl<'de, R> Visitor<'de> for BTreeMapVisitor - where - R: Routable, - ::Err: std::fmt::Display, - { - type Value = BTreeMap; + impl<'de> Visitor<'de> for BTreeMapVisitor { + type Value = BTreeMap; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("a map with indices and routable values") @@ -430,15 +380,12 @@ mod routes { let mut routes = BTreeMap::new(); while let Some((index, route)) = map.next_entry::()? { let index = index.parse::().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, - }) + deserializer.deserialize_map(BTreeMapVisitor {}) } } diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index ebe0c0357..354a9629b 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -13,8 +13,9 @@ use dioxus_interpreter_js::NATIVE_JS; use futures_util::{SinkExt, StreamExt}; pub use pool::*; mod config; -mod eval; +mod document; mod events; +mod history; pub use config::*; #[cfg(feature = "axum")] pub mod launch; diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index dc5f7ad41..6e3302084 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -1,6 +1,6 @@ use crate::{ + document::init_document, element::LiveviewElement, - eval::init_eval, events::SerializedHtmlEventConverter, query::{QueryEngine, QueryResult}, LiveViewError, @@ -130,9 +130,9 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li // Create the a proxy for query engine let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel(); let query_engine = QueryEngine::new(query_tx); - vdom.in_runtime(|| { - ScopeId::ROOT.provide_context(query_engine.clone()); - init_eval(); + vdom.runtime().on_scope(ScopeId::ROOT, || { + provide_context(query_engine.clone()); + init_document(); }); // pin the futures so we can use select! diff --git a/packages/liveview/src/query.rs b/packages/liveview/src/query.rs index adebfad6e..7e2777930 100644 --- a/packages/liveview/src/query.rs +++ b/packages/liveview/src/query.rs @@ -43,7 +43,6 @@ let dioxus = { }"#; /// Tracks what query ids are currently active - pub(crate) struct SharedSlab { pub(crate) slab: Rc>>, } diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index bf8af74c4..60cf243ed 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -242,7 +242,23 @@ impl Route { quote! { #[allow(unused)] (#last_index.., Self::#name { #field_name, .. }) => { - #field_name.render(level - #last_index) + rsx! { + dioxus_router::components::child_router::ChildRouter { + route: #field_name, + // Try to parse the current route as a parent route, and then match it as a child route + parse_route_from_root_route: |__route| if let Ok(__route) = __route.parse() { + if let Self::#name { #field_name, .. } = __route { + Some(#field_name) + } else { + None + } + } else { + None + }, + // Try to parse the child route and turn it into a parent route + format_route_as_root_route: |#field_name| Self::#name { #field_name: #field_name }.to_string(), + } + } } } } diff --git a/packages/router-macro/src/route_tree.rs b/packages/router-macro/src/route_tree.rs index 8025bd338..166ce5b99 100644 --- a/packages/router-macro/src/route_tree.rs +++ b/packages/router-macro/src/route_tree.rs @@ -274,7 +274,7 @@ pub(crate) enum RouteTreeSegmentData<'a> { Redirect(&'a Redirect), } -impl<'a> RouteTreeSegmentData<'a> { +impl RouteTreeSegmentData<'_> { pub fn to_tokens( &self, nests: &[Nest], diff --git a/packages/router/Cargo.toml b/packages/router/Cargo.toml index 2cbe5d6cf..a1b5eb37c 100644 --- a/packages/router/Cargo.toml +++ b/packages/router/Cargo.toml @@ -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"] diff --git a/packages/router/examples/simple_routes.rs b/packages/router/examples/simple_routes.rs deleted file mode 100644 index 2083d4635..000000000 --- a/packages/router/examples/simple_routes.rs +++ /dev/null @@ -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#" - - - -
- {glue} - - "#, - 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:: {} - } -} - -#[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:: {} - } - } -} - -#[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(¤t_route_str.read()); - - let site_map = Route::SITE_MAP - .iter() - .flat_map(|seg| seg.flatten().into_iter()) - .collect::>(); - - 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 }, -} diff --git a/packages/router/src/components/child_router.rs b/packages/router/src/components/child_router.rs new file mode 100644 index 000000000..89eb3ca01 --- /dev/null +++ b/packages/router/src/components/child_router.rs @@ -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 { + format_route_as_root_route: fn(R) -> String, + parse_route_from_root_route: fn(&str) -> Option, +} + +impl ChildRouteMapping { + 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 { + (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() -> Option> { + try_consume_context() +} + +impl Clone for ChildRouteMapping { + 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 { + /// 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, + /// Take a child route and return a parent route + format_route_as_root_route: fn(R) -> String, +} + +impl PartialEq for ChildRouterProps { + 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(props: ChildRouterProps) -> 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) +} diff --git a/packages/router/src/components/history_provider.rs b/packages/router/src/components/history_provider.rs new file mode 100644 index 000000000..e0ed6b6f1 --- /dev/null +++ b/packages/router/src/components/history_provider.rs @@ -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>, + /// The children to render within the history provider. + children: Element, +) -> Element { + use_hook(|| { + provide_history_context(history(())); + }); + + children +} diff --git a/packages/router/src/components/link.rs b/packages/router/src/components/link.rs index 94ed58925..75b688c55 100644 --- a/packages/router/src/components/link.rs +++ b/packages/router/src/components/link.rs @@ -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), -} - -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 From for IntoRoutable { - fn from(value: R) -> Self { - IntoRoutable::Route(Rc::new(value) as Rc) - } -} - -impl From> for IntoRoutable { - fn from(value: NavigationTarget) -> Self { - match value { - NavigationTarget::Internal(route) => IntoRoutable::Route(Rc::new(route) as Rc), - NavigationTarget::External(url) => IntoRoutable::FromStr(url), - } - } -} - -impl From 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 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, @@ -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> = 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() }" ) }; diff --git a/packages/router/src/components/outlet.rs b/packages/router/src/components/outlet.rs index e7ffd9c9a..6431a9c4f 100644 --- a/packages/router/src/components/outlet.rs +++ b/packages/router/src/components/outlet.rs @@ -58,8 +58,9 @@ use dioxus_lib::prelude::*; /// # #[component] /// # fn App() -> Element { /// # rsx! { -/// # Router:: { -/// # 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, +/// # Router:: {} /// # } /// # } /// # } diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 1a8c066fa..79d2c6c33 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -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:: { current_level: 0, diff --git a/packages/router/src/contexts/navigator.rs b/packages/router/src/contexts/navigator.rs index 1538755c4..42a62a51f 100644 --- a/packages/router/src/contexts/navigator.rs +++ b/packages/router/src/contexts/navigator.rs @@ -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) -> Option { + pub fn push(&self, target: impl Into) -> Option { 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) -> Option { + pub fn replace( + &self, + target: impl Into, + ) -> Option { self.0.replace(target) } } diff --git a/packages/router/src/contexts/router.rs b/packages/router/src/contexts/router.rs index 26341b07e..0659267e8 100644 --- a/packages/router/src/contexts/router.rs +++ b/packages/router/src/contexts/router.rs @@ -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 = Arc) -> Option>>; -pub(crate) type AnyRoutingCallback = - Arc Option>>>; +pub(crate) type AnyRoutingCallback = Arc Option>; struct RouterContextInner { /// The current prefix. prefix: Option, - history: Box, - unresolved_error: Option, - subscribers: Arc>>, - subscriber_update: Arc, + subscribers: Arc>>, routing_callback: Option, 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 { - 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( - mut cfg: RouterConfig, - mark_dirty: Arc, - ) -> Self + pub(crate) fn new(cfg: RouterConfig) -> Self where ::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) - } + 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 Option>>> + }) as Arc Option> }), failure_external_navigation: cfg.failure_external_navigation, - any_route_to_string: |route| { - route - .downcast_ref::() - .unwrap_or_else(|| { - panic!( - "Route is not of the expected type: {}\n found typeid: {:?}\n expected typeid: {:?}", - std::any::type_name::(), - route.type_id(), - std::any::TypeId::of::() - ) - }) - .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, 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>, - ) -> Option { + pub(crate) fn push_any(&self, target: NavigationTarget) -> Option { { 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) -> Option { - let target = self.resolve_into_routable(target.into()); + pub fn push(&self, target: impl Into) -> Option { + 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) -> Option { - let target = self.resolve_into_routable(target.into()); - + pub fn replace( + &self, + target: impl Into, + ) -> Option { + 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(&self) -> R { - self.inner - .read() - .history - .current_route() - .downcast::() - .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> { - match into_routable { - IntoRoutable::FromStr(url) => { - let parsed_route: NavigationTarget> = 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::(); + 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 { 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 { - 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 { @@ -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 { @@ -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() diff --git a/packages/router/src/history/memory.rs b/packages/router/src/history/memory.rs deleted file mode 100644 index fd4b43dd5..000000000 --- a/packages/router/src/history/memory.rs +++ /dev/null @@ -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 { - current: R, - history: Vec, - future: Vec, -} - -impl MemoryHistory -where - ::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::::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 Default for MemoryHistory -where - ::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 HistoryProvider for MemoryHistory { - 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; - } -} diff --git a/packages/router/src/history/web_hash.rs b/packages/router/src/history/web_hash.rs deleted file mode 100644 index c23bca237..000000000 --- a/packages/router/src/history/web_hash.rs +++ /dev/null @@ -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 { - do_scroll_restoration: bool, - history: History, - listener_navigation: Option, - #[allow(dead_code)] - listener_scroll: Option, - listener_animation_frame: Arc>>, - window: Window, - phantom: std::marker::PhantomData, -} - -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, - phantom: Default::default(), - } - } -} - -impl WebHashHistory { - fn join_url_to_hash(&self, path: R) -> Option { - 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 { - 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) -> R { - self.url() - .map(|url| url.path().to_string()) - .unwrap_or(String::from("/")) - } - - fn current_prefix(&self) -> Option { - 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) { - 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)); - } - })); - } -} diff --git a/packages/router/src/history/web_history.rs b/packages/router/src/history/web_history.rs deleted file mode 100644 index df5219874..000000000 --- a/packages/router/src/history/web_history.rs +++ /dev/null @@ -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::().ok()?; - let x = state.get(0).as_f64()?; - let y = state.get(1).as_f64()?; - Some([x, y]) - }) -} diff --git a/packages/router/src/history/web_scroll.rs b/packages/router/src/history/web_scroll.rs deleted file mode 100644 index 9662d8e95..000000000 --- a/packages/router/src/history/web_scroll.rs +++ /dev/null @@ -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)) - } -} diff --git a/packages/router/src/lib.rs b/packages/router/src/lib.rs index 47cf12d91..4c47cc03c 100644 --- a/packages/router/src/lib.rs +++ b/packages/router/src/lib.rs @@ -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::*; diff --git a/packages/router/src/navigation.rs b/packages/router/src/navigation.rs index 3c8a8e28d..0c78c42b8 100644 --- a/packages/router/src/navigation.rs +++ b/packages/router/src/navigation.rs @@ -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 From 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 { +pub enum NavigationTarget { /// An internal path that the router can navigate to by itself. /// /// ```rust @@ -80,6 +92,35 @@ impl From for NavigationTarget { } } +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 for NavigationTarget { + fn from(value: String) -> Self { + let router = router(); + match router.internal_route(&value) { + true => NavigationTarget::Internal(value), + false => NavigationTarget::External(value), + } + } +} + +impl From> for NavigationTarget { + fn from(value: NavigationTarget) -> Self { + match value { + NavigationTarget::Internal(r) => r.into(), + NavigationTarget::External(s) => Self::External(s), + } + } +} + impl Display for NavigationTarget { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/packages/router/src/router_cfg.rs b/packages/router/src/router_cfg.rs index 9a1851539..675956f3a 100644 --- a/packages/router/src/router_cfg.rs +++ b/packages/router/src/router_cfg.rs @@ -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::::default()); +/// +/// fn ExternalNavigationFailure() -> Element { +/// rsx! { +/// "Failed to navigate to external URL" +/// } +/// } +/// +/// let cfg = RouterConfig::::default().failure_external_navigation(ExternalNavigationFailure); /// ``` pub struct RouterConfig { pub(crate) failure_external_navigation: fn() -> Element, - pub(crate) history: Option>, pub(crate) on_update: Option>, - pub(crate) initial_route: Option, } impl Default for RouterConfig { fn default() -> Self { Self { failure_external_navigation: FailureExternalNavigation, - history: None, on_update: None, - initial_route: None, } } } -impl RouterConfig -where - ::Err: std::fmt::Display, -{ - pub(crate) fn take_history(&mut self) -> Box { - 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 RouterConfig 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 + '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(initial_route: R) -> Box -where - ::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::::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::::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), - )) -} diff --git a/packages/router/src/utils/use_router_internal.rs b/packages/router/src/utils/use_router_internal.rs index 04853233c..e67e08deb 100644 --- a/packages/router/src/utils/use_router_internal.rs +++ b/packages/router/src/utils/use_router_internal.rs @@ -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 { - let router = try_consume_context::()?; - 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) } diff --git a/packages/router/tests/via_ssr/link.rs b/packages/router/tests/via_ssr/link.rs index 0cdfb92eb..efc8201f6 100644 --- a/packages/router/tests/via_ssr/link.rs +++ b/packages/router/tests/via_ssr/link.rs @@ -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() -> String +where + ::Err: std::fmt::Display, +{ + prepare_at::("/") +} + +fn prepare_at(at: impl ToString) -> String where ::Err: std::fmt::Display, { let mut vdom = VirtualDom::new_with_props( App, AppProps:: { + at: at.to_string(), phantom: std::marker::PhantomData, }, ); @@ -16,12 +26,14 @@ where #[derive(Props)] struct AppProps { + at: String, phantom: std::marker::PhantomData, } impl Clone for AppProps { fn clone(&self) -> Self { Self { + at: self.at.clone(), phantom: std::marker::PhantomData, } } @@ -34,14 +46,15 @@ where } #[allow(non_snake_case)] - fn App(_props: AppProps) -> Element + fn App(props: AppProps) -> Element where ::Err: std::fmt::Display, { rsx! { h1 { "App" } - Router:: { - config: |_| RouterConfig::default().history(MemoryHistory::default()) + HistoryProvider { + history: move |_| Rc::new(MemoryHistory::with_initial_path(props.at.clone())) as Rc, + Router:: {} } } } @@ -345,3 +358,76 @@ fn with_rel() { assert_eq!(prepare::(), 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::("/"), + "

App

Parent LinkChild Link" + ); + + assert_eq!( + prepare_at::("/child"), + "

App

Parent LinkChild Link 1Child Link 2" + ); +} diff --git a/packages/router/tests/via_ssr/outlet.rs b/packages/router/tests/via_ssr/outlet.rs index f3ab2f658..57bb79f58 100644 --- a/packages/router/tests/via_ssr/outlet.rs +++ b/packages/router/tests/via_ssr/outlet.rs @@ -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) -> VirtualDom { @@ -38,10 +42,9 @@ fn prepare(path: impl Into) -> VirtualDom { fn App(path: Route) -> Element { rsx! { h1 { "App" } - Router:: { - config: move |_| { - RouterConfig::default().history(MemoryHistory::with_initial_path(path.clone())) - } + HistoryProvider { + history: move |_| Rc::new(MemoryHistory::with_initial_path(path.clone())) as Rc, + Router:: {} } } } diff --git a/packages/router/tests/via_ssr/redirect.rs b/packages/router/tests/via_ssr/redirect.rs index bd9c919ca..6ee4df566 100644 --- a/packages/router/tests/via_ssr/redirect.rs +++ b/packages/router/tests/via_ssr/redirect.rs @@ -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 #[test] @@ -33,10 +35,9 @@ fn Home(lang: String) -> Element { #[component] fn App(path: Route) -> Element { rsx! { - Router:: { - config: { - move |_| RouterConfig::default().history(MemoryHistory::with_initial_path(path.clone())) - } + HistoryProvider { + history: move |_| Rc::new(MemoryHistory::with_initial_path(path.clone())) as Rc, + Router:: {} } } } diff --git a/packages/router/tests/via_ssr/without_index.rs b/packages/router/tests/via_ssr/without_index.rs index 9ee85706b..62c9a5e97 100644 --- a/packages/router/tests/via_ssr/without_index.rs +++ b/packages/router/tests/via_ssr/without_index.rs @@ -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 #[test] @@ -32,10 +36,9 @@ fn Test() -> Element { #[component] fn App(path: Route) -> Element { rsx! { - Router:: { - config: { - move || RouterConfig::default().history(MemoryHistory::with_initial_path(path)) - } + HistoryProvider { + history: move |_| Rc::new(MemoryHistory::with_initial_path(path)) as Rc, + Router:: {} } } } diff --git a/packages/static-generation/Cargo.toml b/packages/static-generation/Cargo.toml index 3e5ac6e32..15f7a6f68 100644 --- a/packages/static-generation/Cargo.toml +++ b/packages/static-generation/Cargo.toml @@ -13,7 +13,7 @@ resolver = "2" [dependencies] dioxus-fullstack = { workspace = true } dioxus-lib.workspace = true -dioxus-router = { workspace = true, features = ["fullstack"]} +dioxus-router = { workspace = true } dioxus-ssr = { workspace = true, optional = true } dioxus-isrg = { workspace = true, optional = true } axum = { workspace = true, features = ["ws", "macros"], optional = true } @@ -32,8 +32,8 @@ criterion = { workspace = true } [features] default = [] -server = ["dioxus-fullstack/server", "dioxus-router/ssr", "dep:dioxus-ssr", "dep:tokio", "dep:http", "dep:axum", "dep:tower-http", "dep:dioxus-devtools", "dep:dioxus-cli-config", "dep:tower", "dep:dioxus-isrg"] -web = ["dioxus-fullstack/web", "dioxus-router/web", "dep:dioxus-web"] +server = ["dioxus-fullstack/server", "dep:dioxus-ssr", "dep:tokio", "dep:http", "dep:axum", "dep:tower-http", "dep:dioxus-devtools", "dep:dioxus-cli-config", "dep:tower", "dep:dioxus-isrg"] +web = ["dioxus-fullstack/web", "dep:dioxus-web"] [package.metadata.docs.rs] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index ec21e97d5..9aed05f93 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -12,7 +12,9 @@ keywords = ["dom", "ui", "gui", "react", "wasm"] [dependencies] dioxus-core = { workspace = true } dioxus-core-types = { workspace = true } +dioxus-cli-config = { workspace = true } dioxus-html = { workspace = true } +dioxus-history = { workspace = true } dioxus-document = { workspace = true } dioxus-devtools = { workspace = true } dioxus-signals = { workspace = true } @@ -55,6 +57,7 @@ features = [ "Document", "DragEvent", "FocusEvent", + "History", "HtmlElement", "HtmlFormElement", "HtmlInputElement", @@ -67,6 +70,7 @@ features = [ "PointerEvent", "ResizeObserverEntry", "ResizeObserverSize", + "ScrollRestoration", "Text", "Touch", "TouchEvent", diff --git a/packages/web/src/document.rs b/packages/web/src/document.rs index ff9d8b461..8d89a02f4 100644 --- a/packages/web/src/document.rs +++ b/packages/web/src/document.rs @@ -1,5 +1,6 @@ use dioxus_core::ScopeId; use dioxus_document::{Document, Eval, EvalError, Evaluator}; +use dioxus_history::History; use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; use js_sys::Function; use serde::Serialize; @@ -9,6 +10,8 @@ use std::pin::Pin; use std::{rc::Rc, str::FromStr}; use wasm_bindgen::prelude::*; +use crate::history::WebHistory; + #[wasm_bindgen::prelude::wasm_bindgen] pub struct JSOwner { _owner: Box, @@ -53,12 +56,16 @@ extern "C" { pub async fn rust_recv(this: &WeakDioxusChannel) -> wasm_bindgen::JsValue; } -/// Provides the WebEvalProvider through [`ScopeId::provide_context`]. +/// Provides the Document through [`ScopeId::provide_context`]. pub fn init_document() { let provider: Rc = Rc::new(WebDocument); if ScopeId::ROOT.has_context::>().is_none() { ScopeId::ROOT.provide_context(provider); } + let history_provider: Rc = Rc::new(WebHistory::default()); + if ScopeId::ROOT.has_context::>().is_none() { + ScopeId::ROOT.provide_context(history_provider); + } } /// The web-target's document provider. diff --git a/packages/router/src/history/web.rs b/packages/web/src/history/mod.rs similarity index 60% rename from packages/router/src/history/web.rs rename to packages/web/src/history/mod.rs index 443e7feab..6f8c51eb2 100644 --- a/packages/router/src/history/web.rs +++ b/packages/web/src/history/mod.rs @@ -1,35 +1,24 @@ -use std::{ - path::PathBuf, - sync::{Arc, Mutex}, -}; +use scroll::ScrollPosition; +use std::path::PathBuf; +use wasm_bindgen::JsCast; +use wasm_bindgen::{prelude::Closure, JsValue}; +use web_sys::{window, Window}; +use web_sys::{Event, History, ScrollRestoration}; -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, -}; +mod scroll; #[allow(dead_code)] fn base_path() -> Option { - tracing::trace!( - "Using base_path from the CLI: {:?}", - dioxus_cli_config::base_path() - ); - dioxus_cli_config::base_path() + let base_path = dioxus_cli_config::base_path(); + tracing::trace!("Using base_path from the CLI: {:?}", base_path); + base_path } #[allow(clippy::extra_unused_type_parameters)] -fn update_scroll(window: &Window, history: &History) { +fn update_scroll(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); + web_sys::console::error_1(&err); } } @@ -45,50 +34,38 @@ fn update_scroll(window: &Window, history: &History) { /// /// 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 { +pub struct WebHistory { do_scroll_restoration: bool, history: History, - listener_navigation: Option, - listener_animation_frame: Arc>>, prefix: Option, window: Window, - phantom: std::marker::PhantomData, } -impl Default for WebHistory -where - ::Err: std::fmt::Display, -{ +impl Default for WebHistory { fn default() -> Self { Self::new(None, true) } } -impl WebHistory { +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, do_scroll_restoration: bool) -> Self - where - ::Err: std::fmt::Display, - { + pub fn new(prefix: Option, do_scroll_restoration: bool) -> Self { let myself = Self::new_inner(prefix, do_scroll_restoration); - let current_route = myself.current_route(); + let current_route = dioxus_history::History::current_route(&myself); 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 state = myself.create_state(); let _ = replace_state_with_url(&myself.history, &state, Some(¤t_url)); myself } - fn new_inner(prefix: Option, do_scroll_restoration: bool) -> Self - where - ::Err: std::fmt::Display, - { + fn new_inner(prefix: Option, do_scroll_restoration: bool) -> Self { let window = window().expect("access to `window`"); let history = window.history().expect("`window` has access to `history`"); @@ -111,11 +88,8 @@ impl WebHistory { Self { do_scroll_restoration, history, - listener_navigation: None, - listener_animation_frame: Default::default(), prefix, window, - phantom: Default::default(), } } @@ -125,17 +99,14 @@ impl WebHistory { .unwrap_or_default() } - fn create_state(&self, _state: R) -> [f64; 2] { + fn create_state(&self) -> [f64; 2] { let scroll = self.scroll_pos(); [scroll.x, scroll.y] } } -impl WebHistory -where - ::Err: std::fmt::Display, -{ - fn route_from_location(&self) -> R { +impl WebHistory { + fn route_from_location(&self) -> String { let location = self.window.location(); let path = location.pathname().unwrap_or_else(|_| "/".into()) + &location.search().unwrap_or("".into()) @@ -148,12 +119,12 @@ where if path.is_empty() { path = "/" } - R::from_str(path).unwrap_or_else(|err| panic!("{}", err)) + path.to_string() } - fn full_path(&self, state: &R) -> String { + fn full_path(&self, state: &String) -> String { match &self.prefix { - None => format!("{state}"), + None => state.to_string(), Some(prefix) => format!("{prefix}{state}"), } } @@ -165,26 +136,30 @@ where self.window.scroll_to_with_x_and_y(0.0, 0.0) } } - Err(e) => error!("failed to change state: ", e), + Err(e) => { + web_sys::console::error_2(&JsValue::from_str("failed to change state: "), &e); + } } } - fn navigate_external(&mut self, url: String) -> bool { + fn navigate_external(&self, url: String) -> bool { match self.window.location().set_href(&url) { Ok(_) => true, Err(e) => { - error!("failed to navigate to external url (", url, "): ", e); + web_sys::console::error_4( + &JsValue::from_str("failed to navigate to external url ("), + &JsValue::from_str(&url), + &JsValue::from_str("): "), + &e, + ); false } } } } -impl HistoryProvider for WebHistory -where - ::Err: std::fmt::Display, -{ - fn current_route(&self) -> R { +impl dioxus_history::History for WebHistory { + fn current_route(&self) -> String { self.route_from_location() } @@ -192,20 +167,20 @@ where self.prefix.clone() } - fn go_back(&mut self) { + fn go_back(&self) { if let Err(e) = self.history.back() { - error!("failed to go back: ", e) + web_sys::console::error_2(&JsValue::from_str("failed to go back: "), &e); } } - fn go_forward(&mut self) { + fn go_forward(&self) { if let Err(e) = self.history.forward() { - error!("failed to go forward: ", e) + web_sys::console::error_2(&JsValue::from_str("failed to go forward: "), &e); } } - fn push(&mut self, state: R) { - if state.to_string() == self.current_route().to_string() { + fn push(&self, state: String) { + if state == self.current_route() { // don't push the same state twice return; } @@ -214,39 +189,82 @@ where let h = w.history().expect("`window` has access to `history`"); // update the scroll position before pushing the new state - update_scroll::(&w, &h); + update_scroll(&w, &h); let path = self.full_path(&state); - let state: [f64; 2] = self.create_state(state); + let state: [f64; 2] = self.create_state(); self.handle_nav(push_state_and_url(&self.history, &state, path)); } - fn replace(&mut self, state: R) { + fn replace(&self, state: String) { let path = self.full_path(&state); - let state = self.create_state(state); + let state = self.create_state(); self.handle_nav(replace_state_with_url(&self.history, &state, Some(&path))); } - fn external(&mut self, url: String) -> bool { + fn external(&self, url: String) -> bool { self.navigate_external(url) } - fn updater(&mut self, callback: std::sync::Arc) { + fn updater(&self, callback: std::sync::Arc) { 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 |_| { + let function = Closure::wrap(Box::new(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())); + ScrollPosition { x, y }.scroll_to(w.clone()) } } - })); + }) as Box); + self.window + .add_event_listener_with_callback( + "popstate", + &function.into_js_value().unchecked_into(), + ) + .unwrap(); } } + +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 { + web_sys::console::error_1(err); + } + state.ok().and_then(|state| { + let state = state.dyn_into::().ok()?; + let x = state.get(0).as_f64()?; + let y = state.get(1).as_f64()?; + Some([x, y]) + }) +} diff --git a/packages/web/src/history/scroll.rs b/packages/web/src/history/scroll.rs new file mode 100644 index 000000000..806696665 --- /dev/null +++ b/packages/web/src/history/scroll.rs @@ -0,0 +1,28 @@ +use wasm_bindgen::{prelude::Closure, JsCast}; +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) { + let Self { x, y } = *self; + let f = Closure::wrap( + Box::new(move || window.scroll_to_with_x_and_y(x, y)) as Box + ); + web_sys::window() + .expect("should be run in a context with a `Window` object (dioxus cannot be run from a web worker)") + .request_animation_frame(&f.into_js_value().unchecked_into()) + .expect("should register `requestAnimationFrame` OK"); + } +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 8875f90b5..f15d2462f 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -39,6 +39,8 @@ mod document; #[cfg(feature = "file_engine")] mod file_engine; #[cfg(feature = "document")] +mod history; +#[cfg(feature = "document")] pub use document::WebDocument; #[cfg(feature = "file_engine")] pub use file_engine::*;