Move the history provider into the context (#3048)

* move history providers into a separate crate

* start moving route providers into the renderers

* clean up intoroutable

* remove into routable

* fix router tests

* Provide history providers in each renderer

* implement nested routers

* move the lens out of the history crate

* re-export dioxus history trait in the prelude

* also re-export the history function

* fix history doctests

* some light cleanups

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan Almloff 2024-10-31 14:44:04 -05:00 committed by GitHub
parent e5a1a62644
commit 281087469a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 877 additions and 1421 deletions

25
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -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",
}
}
}

View file

@ -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]

View file

@ -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<dyn Document> = Rc::new(DesktopDocument::new(desktop_context.clone()));
let history_provider: Rc<dyn History> = 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 {

View file

@ -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]

View file

@ -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::*;

View file

@ -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"]

View file

@ -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::*;

View file

@ -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<dyn Evaluator + 'static>);
Eval::new(boxed)
}
}
/// An evaluator that does nothing
#[derive(Default)]
pub struct NoOpEvaluator;
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<Result<serde_json::Value, EvalError>> {
std::task::Poll::Ready(Err(EvalError::Unsupported))
}
fn poll_join(
&mut self,
_context: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
std::task::Poll::Ready(Err(EvalError::Unsupported))
struct NoOpEvaluator;
impl Evaluator for NoOpEvaluator {
fn poll_join(
&mut self,
_: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
std::task::Poll::Ready(Err(EvalError::Unsupported))
}
fn poll_recv(
&mut self,
_: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
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)))
}
}

View file

@ -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"] }

View file

@ -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<dyn History> {
match dioxus_core::prelude::try_consume_context::<Rc<dyn History>>() {
Some(history) => history,
None => {
tracing::error!("Unable to find a history provider in the renderer. Make sure your renderer supports the Router. Falling back to the in-memory history provider.");
provide_root_context(Rc::new(MemoryHistory::default()))
}
}
}
#[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<dyn History>) {
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<R: Routable> {
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<R: Routable> {
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::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<R: Routable> {
/// 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<R: Routable> {
/// #[route("/other")]
/// Other {},
/// }
/// let mut history = MemoryHistory::<Route>::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<R: Routable> {
/// 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<R: Routable> {
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::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<R: Routable> {
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::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<R: Routable> {
/// 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<R: Routable> {
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::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<R: Routable> {
/// 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<R: Routable> {
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::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<R: Routable> {
/// untouched.
///
/// ```rust
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[component]
/// # fn Index() -> Element { VNode::empty() }
@ -258,14 +226,14 @@ pub trait HistoryProvider<R: Routable> {
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::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<R: Routable> {
///
/// 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<R: Routable> {
/// Some [`HistoryProvider`]s may receive URL updates from outside the router. When such
/// updates are received, they should call `callback`, which will cause the router to update.
#[allow(unused_variables)]
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {}
}
fn updater(&self, callback: Arc<dyn Fn() + Send + Sync>) {}
pub(crate) trait AnyHistoryProvider {
fn parse_route(&self, route: &str) -> Result<Rc<dyn Any>, String>;
#[must_use]
fn current_route(&self) -> Rc<dyn Any>;
#[must_use]
fn can_go_back(&self) -> bool {
true
}
fn go_back(&mut self);
#[must_use]
fn can_go_forward(&self) -> bool {
true
}
fn go_forward(&mut self);
fn push(&mut self, route: Rc<dyn Any>);
fn replace(&mut self, path: Rc<dyn Any>);
#[allow(unused_variables)]
fn external(&mut self, url: String) -> bool {
/// 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<dyn Fn() + Send + Sync>) {}
#[cfg(feature = "liveview")]
fn is_liveview(&self) -> bool;
}
pub(crate) struct AnyHistoryProviderImplWrapper<R, H> {
inner: H,
_marker: std::marker::PhantomData<R>,
}
impl<R, H> AnyHistoryProviderImplWrapper<R, H> {
pub fn new(inner: H) -> Self {
Self {
inner,
_marker: std::marker::PhantomData,
}
}
}
impl<R, H: Default> Default for AnyHistoryProviderImplWrapper<R, H> {
fn default() -> Self {
Self::new(H::default())
}
}
impl<R, H: 'static> AnyHistoryProvider for AnyHistoryProviderImplWrapper<R, H>
where
R: Routable,
<R as std::str::FromStr>::Err: std::fmt::Display,
H: HistoryProvider<R>,
{
fn parse_route(&self, route: &str) -> Result<Rc<dyn Any>, String> {
R::from_str(route)
.map_err(|err| err.to_string())
.map(|route| Rc::new(route) as Rc<dyn Any>)
}
fn current_route(&self) -> Rc<dyn Any> {
let route = self.inner.current_route();
Rc::new(route)
}
fn can_go_back(&self) -> bool {
self.inner.can_go_back()
}
fn go_back(&mut self) {
self.inner.go_back()
}
fn can_go_forward(&self) -> bool {
self.inner.can_go_forward()
}
fn go_forward(&mut self) {
self.inner.go_forward()
}
fn push(&mut self, route: Rc<dyn Any>) {
self.inner
.push(route.downcast::<R>().unwrap().as_ref().clone())
}
fn replace(&mut self, route: Rc<dyn Any>) {
self.inner
.replace(route.downcast::<R>().unwrap().as_ref().clone())
}
fn external(&mut self, url: String) -> bool {
self.inner.external(url)
}
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
self.inner.updater(callback)
}
#[cfg(feature = "liveview")]
fn is_liveview(&self) -> bool {
use std::any::TypeId;
TypeId::of::<H>() == TypeId::of::<LiveviewHistory<R>>()
}
}

View file

@ -0,0 +1,100 @@
use std::cell::RefCell;
use crate::History;
struct MemoryHistoryState {
current: String,
history: Vec<String>,
future: Vec<String>,
}
/// A [`History`] provider that stores all navigation information in memory.
pub struct MemoryHistory {
state: RefCell<MemoryHistoryState>,
}
impl Default for MemoryHistory {
fn default() -> Self {
Self::with_initial_path("/")
}
}
impl MemoryHistory {
/// Create a [`MemoryHistory`] starting at `path`.
///
/// ```rust
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[component]
/// # fn Index() -> Element { VNode::empty() }
/// # #[component]
/// # fn OtherPage() -> Element { VNode::empty() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
///
/// let mut history = dioxus_history::MemoryHistory::with_initial_path(Route::Index {});
/// assert_eq!(history.current_route(), Route::Index {}.to_string());
/// assert_eq!(history.can_go_back(), false);
/// ```
pub fn with_initial_path(path: impl ToString) -> Self {
Self {
state: MemoryHistoryState{
current: path.to_string().parse().unwrap_or_else(|err| {
panic!("index route does not exist:\n{err}\n use MemoryHistory::with_initial_path to set a custom path")
}),
history: Vec::new(),
future: Vec::new(),}.into()
}
}
}
impl History for MemoryHistory {
fn current_route(&self) -> String {
self.state.borrow().current.clone()
}
fn can_go_back(&self) -> bool {
!self.state.borrow().history.is_empty()
}
fn go_back(&self) {
let mut write = self.state.borrow_mut();
if let Some(last) = write.history.pop() {
let old = std::mem::replace(&mut write.current, last);
write.future.push(old);
}
}
fn can_go_forward(&self) -> bool {
!self.state.borrow().future.is_empty()
}
fn go_forward(&self) {
let mut write = self.state.borrow_mut();
if let Some(next) = write.future.pop() {
let old = std::mem::replace(&mut write.current, next);
write.history.push(old);
}
}
fn push(&self, new: String) {
let mut write = self.state.borrow_mut();
// don't push the same route twice
if write.current == new {
return;
}
let old = std::mem::replace(&mut write.current, new);
write.history.push(old);
write.future.clear();
}
fn replace(&self, path: String) {
let mut write = self.state.borrow_mut();
write.current = path;
}
}

View file

@ -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"] }

View file

@ -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::<QueryEngine>().unwrap();
let provider: Rc<dyn Document> = 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<serde_json::Value>,
@ -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::<QueryEngine>().unwrap();
let provider: Rc<dyn Document> = 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<dyn History> = 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))
}
}

View file

@ -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<R: Routable>
where
<R as FromStr>::Err: std::fmt::Display,
{
action_tx: tokio::sync::mpsc::UnboundedSender<Action<R>>,
timeline: Arc<Mutex<Timeline<R>>>,
pub(crate) struct LiveviewHistory {
action_tx: tokio::sync::mpsc::UnboundedSender<Action>,
timeline: Arc<Mutex<Timeline>>,
updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>>,
}
struct Timeline<R: Routable>
where
<R as FromStr>::Err: std::fmt::Display,
{
struct Timeline {
current_index: usize,
routes: BTreeMap<usize, R>,
routes: BTreeMap<usize, String>,
}
#[derive(Serialize, Deserialize, Debug)]
@ -30,28 +24,22 @@ struct State {
}
#[derive(Serialize, Deserialize, Debug)]
struct Session<R: Routable>
where
<R as FromStr>::Err: std::fmt::Display,
{
struct Session {
#[serde(with = "routes")]
routes: BTreeMap<usize, R>,
routes: BTreeMap<usize, String>,
last_visited: usize,
}
enum Action<R: Routable> {
enum Action {
GoBack,
GoForward,
Push(R),
Replace(R),
Push(String),
Replace(String),
External(String),
}
impl<R: Routable> Timeline<R>
where
<R as FromStr>::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<State>,
session: Option<Session<R>>,
session: Option<Session>,
depth: usize,
) -> State {
if let Some(session) = session {
@ -88,7 +76,7 @@ where
state
}
fn update(&mut self, route: R, state: Option<State>) -> State {
fn update(&mut self, route: String, state: Option<State>) -> 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<R> {
fn session(&self) -> Session {
Session {
routes: self.routes.clone(),
last_visited: self.current_index,
@ -128,30 +116,19 @@ where
}
}
impl<R: Routable> Default for LiveviewHistory<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self::new()
}
}
impl<R: Routable> LiveviewHistory<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
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<dyn Fn(&str) -> 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::<Action<R>>();
fn new_with_initial_path(initial_path: String, eval: Rc<dyn Fn(&str) -> Eval>) -> Self {
let (action_tx, mut action_rx) = tokio::sync::mpsc::unbounded_channel::<Action>();
let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
Arc::new(RwLock::new(Arc::new(|| {})));
let eval_provider = dioxus_lib::document::document();
let create_eval = Rc::new(move |script: &str| eval_provider.eval(script.to_string()))
as Rc<dyn Fn(&str) -> Eval>;
// Listen to server actions
spawn({
let timeline = timeline.clone();
let create_eval = create_eval.clone();
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<State>,
Option<Session<R>>,
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<State>, Option<Session>, 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<State>)>(event)
.expect("serializable state");
let Ok(route) = R::from_str(&route.to_string()) else {
return;
};
let mut timeline = timeline.lock().expect("unpoisoned mutex");
let state = timeline.update(route.clone(), state);
let state = serde_json::to_string(&state).expect("serializable state");
@ -322,34 +286,31 @@ where
}
}
impl<R: Routable> HistoryProvider<R> for LiveviewHistory<R>
where
<R as FromStr>::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<dyn Fn() + Send + Sync>) {
fn updater(&self, callback: Arc<dyn Fn() + Send + Sync>) {
let mut updater_callback = self.updater_callback.write().unwrap();
*updater_callback = callback;
}
}
mod routes {
use crate::prelude::Routable;
use core::str::FromStr;
use serde::de::{MapAccess, Visitor};
use serde::{ser::SerializeMap, Deserializer, Serializer};
use std::collections::BTreeMap;
pub fn serialize<S, R>(routes: &BTreeMap<usize, R>, serializer: S) -> Result<S::Ok, S::Error>
pub fn serialize<S>(routes: &BTreeMap<usize, String>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
R: Routable,
{
let mut map = serializer.serialize_map(Some(routes.len()))?;
for (index, route) in routes.iter() {
@ -402,22 +360,14 @@ mod routes {
map.end()
}
pub fn deserialize<'de, D, R>(deserializer: D) -> Result<BTreeMap<usize, R>, D::Error>
pub fn deserialize<'de, D>(deserializer: D) -> Result<BTreeMap<usize, String>, D::Error>
where
D: Deserializer<'de>,
R: Routable,
<R as FromStr>::Err: std::fmt::Display,
{
struct BTreeMapVisitor<R> {
marker: std::marker::PhantomData<R>,
}
struct BTreeMapVisitor {}
impl<'de, R> Visitor<'de> for BTreeMapVisitor<R>
where
R: Routable,
<R as FromStr>::Err: std::fmt::Display,
{
type Value = BTreeMap<usize, R>;
impl<'de> Visitor<'de> for BTreeMapVisitor {
type Value = BTreeMap<usize, String>;
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::<String, String>()? {
let index = index.parse::<usize>().map_err(serde::de::Error::custom)?;
let route = R::from_str(&route).map_err(serde::de::Error::custom)?;
routes.insert(index, route);
}
Ok(routes)
}
}
deserializer.deserialize_map(BTreeMapVisitor {
marker: std::marker::PhantomData,
})
deserializer.deserialize_map(BTreeMapVisitor {})
}
}

View file

@ -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;

View file

@ -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!

View file

@ -43,7 +43,6 @@ let dioxus = {
}"#;
/// Tracks what query ids are currently active
pub(crate) struct SharedSlab<T = ()> {
pub(crate) slab: Rc<RefCell<Slab<T>>>,
}

View file

@ -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(),
}
}
}
}
}

View file

@ -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],

View file

@ -11,24 +11,11 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies]
dioxus-lib = { workspace = true }
dioxus-history = { workspace = true }
dioxus-router-macro = { workspace = true }
gloo = { version = "0.8.0", optional = true }
tracing = { workspace = true }
urlencoding = "2.1.3"
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1.0.91", optional = true }
url = "2.3.1"
wasm-bindgen = { workspace = true, optional = true }
web-sys = { version = "0.3.60", optional = true, features = [
"ScrollRestoration",
] }
js-sys = { version = "0.3.63", optional = true }
gloo-utils = { version = "0.1.6", optional = true }
dioxus-liveview = { workspace = true, optional = true }
dioxus-ssr = { workspace = true, optional = true }
http = { workspace = true, optional = true }
dioxus-fullstack = { workspace = true, optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
dioxus-cli-config = { workspace = true }
rustversion = "1.0.17"
@ -36,18 +23,11 @@ rustversion = "1.0.17"
# dev-dependncey crates
[target.'cfg(target_family = "wasm")'.dev-dependencies]
console_error_panic_hook = "0.1.7"
dioxus-router = { workspace = true, features = ["web"] }
# dioxus-web = { workspace = true }
gloo = "0.8.0"
wasm-bindgen-test = "0.3.33"
[features]
default = []
ssr = []
liveview = ["dioxus-liveview", "dep:tokio", "dep:serde", "dep:serde_json"]
wasm_test = []
web = ["dep:gloo", "dep:web-sys", "dep:wasm-bindgen", "dep:gloo-utils", "dep:js-sys", "dioxus-router-macro/web"]
fullstack = ["dep:dioxus-fullstack"]
[dev-dependencies]
axum = { workspace = true, features = ["ws"] }
@ -57,6 +37,7 @@ criterion = { workspace = true, features = ["async_tokio", "html_reports"] }
ciborium = { version = "0.2.1" }
base64 = { version = "0.21.0" }
serde = { version = "1", features = ["derive"] }
tokio = { workspace = true, features = ["full"] }
[package.metadata.docs.rs]
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]

View file

@ -1,204 +0,0 @@
use dioxus::prelude::*;
use std::str::FromStr;
#[cfg(feature = "liveview")]
#[tokio::main]
async fn main() {
use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
let listen_address: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
let view = dioxus_liveview::LiveViewPool::new();
let app = Router::new()
.fallback(get(move || async move {
Html(format!(
r#"
<!DOCTYPE html>
<html>
<head></head>
<body><div id="main"></div></body>
{glue}
</html>
"#,
glue = dioxus_liveview::interpreter_glue(&format!("ws://{listen_address}/ws"))
))
}))
.route(
"/ws",
get(move |ws: WebSocketUpgrade| async move {
ws.on_upgrade(move |socket| async move {
_ = view.launch(dioxus_liveview::axum_socket(socket), app).await;
})
}),
);
println!("Listening on http://{listen_address}");
let listener = tokio::net::TcpListener::bind(&listen_address)
.await
.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "liveview"))]
fn main() {
dioxus::launch(app)
}
fn app() -> Element {
rsx! {
Router::<Route> {}
}
}
#[component]
fn UserFrame(user_id: usize) -> Element {
rsx! {
pre { "UserFrame{{\n\tuser_id:{user_id}\n}}" }
div { background_color: "rgba(0,0,0,50%)",
"children:"
Outlet::<Route> {}
}
}
}
#[component]
fn Route1(user_id: usize, dynamic: usize, query: String) -> Element {
rsx! {
pre {
"Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\tquery:{query}\n}}"
}
Link {
to: Route::Route1 {
user_id,
dynamic,
query: String::new(),
},
"Route1 with extra+\".\""
}
p { "Footer" }
Link {
to: Route::Route3 {
dynamic: String::new(),
},
"Home"
}
}
}
#[component]
fn Route2(user_id: usize) -> Element {
rsx! {
pre { "Route2{{\n\tuser_id:{user_id}\n}}" }
{(0..user_id).map(|i| rsx! { p { "{i}" } })}
p { "Footer" }
Link {
to: Route::Route3 {
dynamic: String::new(),
},
"Home"
}
}
}
#[component]
fn Route3(dynamic: String) -> Element {
let mut current_route_str = use_signal(String::new);
let current_route = use_route();
let parsed = Route::from_str(&current_route_str.read());
let site_map = Route::SITE_MAP
.iter()
.flat_map(|seg| seg.flatten().into_iter())
.collect::<Vec<_>>();
let navigator = use_navigator();
rsx! {
input {
oninput: move |evt: FormEvent| {
*current_route_str.write() = evt.value();
},
value: "{current_route_str}"
}
"dynamic: {dynamic}"
Link { to: Route::Route2 { user_id: 8888 }, "hello world link" }
button {
disabled: !navigator.can_go_back(),
onclick: move |_| {
navigator.go_back();
},
"go back"
}
button {
disabled: !navigator.can_go_forward(),
onclick: move |_| {
navigator.go_forward();
},
"go forward"
}
button {
onclick: move |_| {
navigator.push("https://www.google.com");
},
"google link"
}
p { "Site Map" }
pre { "{site_map:#?}" }
p { "Dynamic link" }
match parsed {
Ok(route) => {
if route != current_route {
rsx! {
Link {
to: route.clone(),
"{route}"
}
}
}
else {
VNode::empty()
}
}
Err(err) => {
rsx! {
pre {
color: "red",
"Invalid route:\n{err}"
}
}
}
}
}
}
#[rustfmt::skip]
#[derive(Clone, Debug, PartialEq, Routable)]
enum Route {
#[nest("/test")]
// Nests with parameters have types taken from child routes
#[nest("/user/:user_id")]
// Everything inside the nest has the added parameter `user_id: usize`
// UserFrame is a layout component that will receive the `user_id: usize` parameter
#[layout(UserFrame)]
#[route("/:dynamic?:query")]
Route1 {
// The type is taken from the first instance of the dynamic parameter
user_id: usize,
dynamic: usize,
query: String,
},
#[route("/hello_world")]
// You can opt out of the layout by using the `!` prefix
#[layout(!UserFrame)]
Route2 { user_id: usize },
#[end_layout]
#[end_nest]
#[end_nest]
#[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})]
#[route("/:dynamic")]
Route3 { dynamic: String },
}

View file

@ -0,0 +1,68 @@
/// Components that allow the macro to add child routers. This component provides a context
/// to the child router that maps child routes to root routes and vice versa.
use dioxus_lib::prelude::*;
use crate::prelude::Routable;
/// Maps a child route into the root router and vice versa
// NOTE: Currently child routers only support simple static prefixes, but this
// API could be expanded to support dynamic prefixes as well
pub(crate) struct ChildRouteMapping<R> {
format_route_as_root_route: fn(R) -> String,
parse_route_from_root_route: fn(&str) -> Option<R>,
}
impl<R: Routable> ChildRouteMapping<R> {
pub(crate) fn format_route_as_root_route(&self, route: R) -> String {
(self.format_route_as_root_route)(route)
}
pub(crate) fn parse_route_from_root_route(&self, route: &str) -> Option<R> {
(self.parse_route_from_root_route)(route)
}
}
/// Get the formatter that handles adding and stripping the prefix from a child route
pub(crate) fn consume_child_route_mapping<R: Routable>() -> Option<ChildRouteMapping<R>> {
try_consume_context()
}
impl<R> Clone for ChildRouteMapping<R> {
fn clone(&self) -> Self {
Self {
format_route_as_root_route: self.format_route_as_root_route,
parse_route_from_root_route: self.parse_route_from_root_route,
}
}
}
/// Props for the [`ChildHistoryProvider`] component.
#[derive(Props, Clone)]
pub struct ChildRouterProps<R: Routable> {
/// The child route to render
route: R,
/// Take a parent route and return a child route or none if the route is not part of the child
parse_route_from_root_route: fn(&str) -> Option<R>,
/// Take a child route and return a parent route
format_route_as_root_route: fn(R) -> String,
}
impl<R: Routable> PartialEq for ChildRouterProps<R> {
fn eq(&self, _: &Self) -> bool {
false
}
}
/// A component that provides a [`History`] to a child router. The `#[child]` attribute on the router macro will insert this automatically.
#[component]
#[allow(missing_docs)]
pub fn ChildRouter<R: Routable>(props: ChildRouterProps<R>) -> Element {
use_hook(|| {
provide_context(ChildRouteMapping {
format_route_as_root_route: props.format_route_as_root_route,
parse_route_from_root_route: props.parse_route_from_root_route,
})
});
props.route.render(0)
}

View file

@ -0,0 +1,20 @@
use dioxus_history::{provide_history_context, History};
use dioxus_lib::prelude::*;
use std::rc::Rc;
/// A component that provides a [`History`] for all child [`Router`] components. Renderers generally provide a default history automatically.
#[component]
#[allow(missing_docs)]
pub fn HistoryProvider(
/// The history to provide to child components.
history: Callback<(), Rc<dyn History>>,
/// The children to render within the history provider.
children: Element,
) -> Element {
use_hook(|| {
provide_history_context(history(()));
});
children
}

View file

@ -1,83 +1,14 @@
#![allow(clippy::type_complexity)]
use std::any::Any;
use std::fmt::Debug;
use std::rc::Rc;
use dioxus_lib::prelude::*;
use tracing::error;
use crate::navigation::NavigationTarget;
use crate::prelude::Routable;
use crate::utils::use_router_internal::use_router_internal;
use url::Url;
/// Something that can be converted into a [`NavigationTarget`].
#[derive(Clone)]
pub enum IntoRoutable {
/// A raw string target.
FromStr(String),
/// A internal target.
Route(Rc<dyn Any>),
}
impl PartialEq for IntoRoutable {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(IntoRoutable::FromStr(a), IntoRoutable::FromStr(b)) => a == b,
(IntoRoutable::Route(a), IntoRoutable::Route(b)) => Rc::ptr_eq(a, b),
_ => false,
}
}
}
impl<R: Routable> From<R> for IntoRoutable {
fn from(value: R) -> Self {
IntoRoutable::Route(Rc::new(value) as Rc<dyn Any>)
}
}
impl<R: Routable> From<NavigationTarget<R>> for IntoRoutable {
fn from(value: NavigationTarget<R>) -> Self {
match value {
NavigationTarget::Internal(route) => IntoRoutable::Route(Rc::new(route) as Rc<dyn Any>),
NavigationTarget::External(url) => IntoRoutable::FromStr(url),
}
}
}
impl From<String> for IntoRoutable {
fn from(value: String) -> Self {
IntoRoutable::FromStr(value)
}
}
impl From<&String> for IntoRoutable {
fn from(value: &String) -> Self {
IntoRoutable::FromStr(value.to_string())
}
}
impl From<&str> for IntoRoutable {
fn from(value: &str) -> Self {
IntoRoutable::FromStr(value.to_string())
}
}
impl From<Url> for IntoRoutable {
fn from(url: Url) -> Self {
IntoRoutable::FromStr(url.to_string())
}
}
impl From<&Url> for IntoRoutable {
fn from(url: &Url) -> Self {
IntoRoutable::FromStr(url.to_string())
}
}
/// The properties for a [`Link`].
#[derive(Props, Clone, PartialEq)]
pub struct LinkProps {
@ -119,7 +50,7 @@ pub struct LinkProps {
/// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
#[props(into)]
pub to: IntoRoutable,
pub to: NavigationTarget,
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
@ -227,12 +158,11 @@ pub fn Link(props: LinkProps) -> Element {
}
};
let current_url = router.current_route_string();
let current_url = router.full_route_string();
let href = match &to {
IntoRoutable::FromStr(url) => url.to_string(),
IntoRoutable::Route(route) => router.any_route_to_string(&**route),
NavigationTarget::Internal(url) => url.clone(),
NavigationTarget::External(route) => route.clone(),
};
let parsed_route: NavigationTarget<Rc<dyn Any>> = router.resolve_into_routable(to.clone());
let mut class_ = String::new();
if let Some(c) = class {
@ -257,7 +187,7 @@ pub fn Link(props: LinkProps) -> Element {
let tag_target = new_tab.then_some("_blank");
let is_external = matches!(parsed_route, NavigationTarget::External(_));
let is_external = matches!(to, NavigationTarget::External(_));
let is_router_nav = !is_external && !new_tab;
let rel = rel.or_else(|| is_external.then_some("noopener noreferrer".to_string()));
@ -281,7 +211,7 @@ pub fn Link(props: LinkProps) -> Element {
event.prevent_default();
if do_default && is_router_nav {
router.push_any(router.resolve_into_routable(to.clone()));
router.push_any(to.clone());
}
if let Some(handler) = onclick {
@ -301,8 +231,8 @@ pub fn Link(props: LinkProps) -> Element {
let liveview_prevent_default = {
// If the event is a click with the left mouse button and no modifiers, prevent the default action
// and navigate to the href with client side routing
router.is_liveview().then_some(
"if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) { event.preventDefault() }"
router.include_prevent_default().then_some(
"if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) { event.preventDefault() }"
)
};

View file

@ -58,8 +58,9 @@ use dioxus_lib::prelude::*;
/// # #[component]
/// # fn App() -> Element {
/// # rsx! {
/// # Router::<Route> {
/// # config: || RouterConfig::default().history(MemoryHistory::with_initial_path(Route::Child {}))
/// # dioxus_router::components::HistoryProvider {
/// # history: move |_| std::rc::Rc::new(dioxus_history::MemoryHistory::with_initial_path(Route::Child {}.to_string())) as std::rc::Rc<dyn dioxus_history::History>,
/// # Router::<Route> {}
/// # }
/// # }
/// # }

View file

@ -46,10 +46,7 @@ where
use crate::prelude::{outlet::OutletContext, RouterContext};
use_hook(|| {
provide_router_context(RouterContext::new(
props.config.call(()),
schedule_update_any(),
));
provide_router_context(RouterContext::new(props.config.call(())));
provide_context(OutletContext::<R> {
current_level: 0,

View file

@ -1,4 +1,4 @@
use crate::prelude::{ExternalNavigationFailure, IntoRoutable, RouterContext};
use crate::prelude::{ExternalNavigationFailure, NavigationTarget, RouterContext};
/// Acquire the navigator without subscribing to updates.
///
@ -48,14 +48,17 @@ impl Navigator {
/// Push a new location.
///
/// The previous location will be available to go back to.
pub fn push(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
pub fn push(&self, target: impl Into<NavigationTarget>) -> Option<ExternalNavigationFailure> {
self.0.push(target)
}
/// Replace the current location.
///
/// The previous location will **not** be available to go back to.
pub fn replace(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
pub fn replace(
&self,
target: impl Into<NavigationTarget>,
) -> Option<ExternalNavigationFailure> {
self.0.replace(target)
}
}

View file

@ -1,17 +1,14 @@
use std::{
any::Any,
collections::HashSet,
rc::Rc,
sync::{Arc, RwLock},
sync::{Arc, Mutex},
};
use dioxus_history::history;
use dioxus_lib::prelude::*;
use crate::{
navigation::NavigationTarget,
prelude::{AnyHistoryProvider, IntoRoutable, SiteMapSegment},
routable::Routable,
router_cfg::RouterConfig,
components::child_router::consume_child_route_mapping, navigation::NavigationTarget,
prelude::SiteMapSegment, routable::Routable, router_cfg::RouterConfig,
};
/// This context is set in the root of the virtual dom if there is a router present.
@ -47,38 +44,39 @@ pub struct ExternalNavigationFailure(pub String);
/// A function the router will call after every routing update.
pub(crate) type RoutingCallback<R> =
Arc<dyn Fn(GenericRouterContext<R>) -> Option<NavigationTarget<R>>>;
pub(crate) type AnyRoutingCallback =
Arc<dyn Fn(RouterContext) -> Option<NavigationTarget<Rc<dyn Any>>>>;
pub(crate) type AnyRoutingCallback = Arc<dyn Fn(RouterContext) -> Option<NavigationTarget>>;
struct RouterContextInner {
/// The current prefix.
prefix: Option<String>,
history: Box<dyn AnyHistoryProvider>,
unresolved_error: Option<ExternalNavigationFailure>,
subscribers: Arc<RwLock<HashSet<ScopeId>>>,
subscriber_update: Arc<dyn Fn(ScopeId)>,
subscribers: Arc<Mutex<HashSet<ReactiveContext>>>,
routing_callback: Option<AnyRoutingCallback>,
failure_external_navigation: fn() -> Element,
any_route_to_string: fn(&dyn Any) -> String,
internal_route: fn(&str) -> bool,
site_map: &'static [SiteMapSegment],
}
impl RouterContextInner {
fn update_subscribers(&self) {
let update = &self.subscriber_update;
for &id in self.subscribers.read().unwrap().iter() {
update(id);
for &id in self.subscribers.lock().unwrap().iter() {
id.mark_dirty();
}
}
fn subscribe_to_current_context(&self) {
if let Some(rc) = ReactiveContext::current() {
rc.subscribe(self.subscribers.clone());
}
}
fn external(&mut self, external: String) -> Option<ExternalNavigationFailure> {
match self.history.external(external.clone()) {
match history().external(external.clone()) {
true => None,
false => {
let failure = ExternalNavigationFailure(external);
@ -99,23 +97,17 @@ pub struct RouterContext {
}
impl RouterContext {
pub(crate) fn new<R: Routable + 'static>(
mut cfg: RouterConfig<R>,
mark_dirty: Arc<dyn Fn(ScopeId) + Sync + Send>,
) -> Self
pub(crate) fn new<R: Routable + 'static>(cfg: RouterConfig<R>) -> Self
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
let subscriber_update = mark_dirty.clone();
let subscribers = Arc::new(RwLock::new(HashSet::new()));
let subscribers = Arc::new(Mutex::new(HashSet::new()));
let mapping = consume_child_route_mapping();
let mut myself = RouterContextInner {
let myself = RouterContextInner {
prefix: Default::default(),
history: cfg.take_history(),
unresolved_error: None,
subscribers: subscribers.clone(),
subscriber_update,
routing_callback: cfg.on_update.map(|update| {
Arc::new(move |ctx| {
let ctx = GenericRouterContext {
@ -123,42 +115,30 @@ impl RouterContext {
_marker: std::marker::PhantomData,
};
update(ctx).map(|t| match t {
NavigationTarget::Internal(r) => {
NavigationTarget::Internal(Rc::new(r) as Rc<dyn Any>)
}
NavigationTarget::Internal(r) => match mapping.as_ref() {
Some(mapping) => {
NavigationTarget::Internal(mapping.format_route_as_root_route(r))
}
None => NavigationTarget::Internal(r.to_string()),
},
NavigationTarget::External(s) => NavigationTarget::External(s),
})
})
as Arc<dyn Fn(RouterContext) -> Option<NavigationTarget<Rc<dyn Any>>>>
}) as Arc<dyn Fn(RouterContext) -> Option<NavigationTarget>>
}),
failure_external_navigation: cfg.failure_external_navigation,
any_route_to_string: |route| {
route
.downcast_ref::<R>()
.unwrap_or_else(|| {
panic!(
"Route is not of the expected type: {}\n found typeid: {:?}\n expected typeid: {:?}",
std::any::type_name::<R>(),
route.type_id(),
std::any::TypeId::of::<R>()
)
})
.to_string()
},
internal_route: |route| R::from_str(route).is_ok(),
site_map: R::SITE_MAP,
};
// set the updater
{
myself.history.updater(Arc::new(move || {
for &id in subscribers.read().unwrap().iter() {
(mark_dirty)(id);
}
}));
}
history().updater(Arc::new(move || {
for &rc in subscribers.lock().unwrap().iter() {
rc.mark_dirty();
}
}));
Self {
inner: CopyValue::new_in_scope(myself, ScopeId::ROOT),
@ -167,41 +147,27 @@ impl RouterContext {
/// Check if the router is running in a liveview context
/// We do some slightly weird things for liveview because of the network boundary
pub fn is_liveview(&self) -> bool {
#[cfg(feature = "liveview")]
{
self.inner.read().history.is_liveview()
}
#[cfg(not(feature = "liveview"))]
{
false
}
}
pub(crate) fn route_from_str(&self, route: &str) -> Result<Rc<dyn Any>, String> {
self.inner.read().history.parse_route(route)
pub(crate) fn include_prevent_default(&self) -> bool {
history().include_prevent_default()
}
/// Check whether there is a previous page to navigate back to.
#[must_use]
pub fn can_go_back(&self) -> bool {
self.inner.read().history.can_go_back()
history().can_go_back()
}
/// Check whether there is a future page to navigate forward to.
#[must_use]
pub fn can_go_forward(&self) -> bool {
self.inner.read().history.can_go_forward()
history().can_go_forward()
}
/// Go back to the previous location.
///
/// Will fail silently if there is no previous location to go to.
pub fn go_back(&self) {
{
self.inner.write_unchecked().history.go_back();
}
history().go_back();
self.change_route();
}
@ -209,21 +175,15 @@ impl RouterContext {
///
/// Will fail silently if there is no next location to go to.
pub fn go_forward(&self) {
{
self.inner.write_unchecked().history.go_forward();
}
history().go_forward();
self.change_route();
}
pub(crate) fn push_any(
&self,
target: NavigationTarget<Rc<dyn Any>>,
) -> Option<ExternalNavigationFailure> {
pub(crate) fn push_any(&self, target: NavigationTarget) -> Option<ExternalNavigationFailure> {
{
let mut write = self.inner.write_unchecked();
match target {
NavigationTarget::Internal(p) => write.history.push(p),
NavigationTarget::Internal(p) => history().push(p),
NavigationTarget::External(e) => return write.external(e),
}
}
@ -234,12 +194,15 @@ impl RouterContext {
/// Push a new location.
///
/// The previous location will be available to go back to.
pub fn push(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
let target = self.resolve_into_routable(target.into());
pub fn push(&self, target: impl Into<NavigationTarget>) -> Option<ExternalNavigationFailure> {
let target = target.into();
{
let mut write = self.inner.write_unchecked();
match target {
NavigationTarget::Internal(p) => write.history.push(p),
NavigationTarget::Internal(p) => {
let history = history();
history.push(p)
}
NavigationTarget::External(e) => return write.external(e),
}
}
@ -250,13 +213,18 @@ impl RouterContext {
/// Replace the current location.
///
/// The previous location will **not** be available to go back to.
pub fn replace(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
let target = self.resolve_into_routable(target.into());
pub fn replace(
&self,
target: impl Into<NavigationTarget>,
) -> Option<ExternalNavigationFailure> {
let target = target.into();
{
let mut state = self.inner.write_unchecked();
match target {
NavigationTarget::Internal(p) => state.history.replace(p),
NavigationTarget::Internal(p) => {
let history = history();
history.replace(p)
}
NavigationTarget::External(e) => return state.external(e),
}
}
@ -266,56 +234,34 @@ impl RouterContext {
/// The route that is currently active.
pub fn current<R: Routable>(&self) -> R {
self.inner
.read()
.history
.current_route()
.downcast::<R>()
.unwrap()
.as_ref()
.clone()
}
/// The route that is currently active.
pub fn current_route_string(&self) -> String {
self.any_route_to_string(&*self.inner.read().history.current_route())
}
pub(crate) fn any_route_to_string(&self, route: &dyn Any) -> String {
(self.inner.read().any_route_to_string)(route)
}
pub(crate) fn resolve_into_routable(
&self,
into_routable: IntoRoutable,
) -> NavigationTarget<Rc<dyn Any>> {
match into_routable {
IntoRoutable::FromStr(url) => {
let parsed_route: NavigationTarget<Rc<dyn Any>> = match self.route_from_str(&url) {
Ok(route) => NavigationTarget::Internal(route),
Err(_) => NavigationTarget::External(url),
};
parsed_route
}
IntoRoutable::Route(route) => NavigationTarget::Internal(route),
let absolute_route = self.full_route_string();
// If this is a child route, map the absolute route to the child route before parsing
let mapping = consume_child_route_mapping::<R>();
match mapping.as_ref() {
Some(mapping) => mapping
.parse_route_from_root_route(&absolute_route)
.unwrap_or_else(|| {
panic!("route's display implementation must be parsable by FromStr")
}),
None => R::from_str(&absolute_route).unwrap_or_else(|_| {
panic!("route's display implementation must be parsable by FromStr")
}),
}
}
/// The full route that is currently active. If this is called from inside a child router, this will always return the parent's view of the route.
pub fn full_route_string(&self) -> String {
let inner = self.inner.read();
inner.subscribe_to_current_context();
let history = history();
history.current_route()
}
/// The prefix that is currently active.
pub fn prefix(&self) -> Option<String> {
self.inner.read().prefix.clone()
}
/// Manually subscribe to the current route
pub fn subscribe(&self, id: ScopeId) {
self.inner.read().subscribers.write().unwrap().insert(id);
}
/// Manually unsubscribe from the current route
pub fn unsubscribe(&self, id: ScopeId) {
self.inner.read().subscribers.write().unwrap().remove(&id);
}
/// Clear any unresolved errors
pub fn clear_error(&self) {
let mut write_inner = self.inner.write_unchecked();
@ -330,11 +276,12 @@ impl RouterContext {
}
pub(crate) fn render_error(&self) -> Option<Element> {
let inner_read = self.inner.write_unchecked();
inner_read
let inner_write = self.inner.write_unchecked();
inner_write.subscribe_to_current_context();
inner_write
.unresolved_error
.as_ref()
.map(|_| (inner_read.failure_external_navigation)())
.map(|_| (inner_write.failure_external_navigation)())
}
fn change_route(&self) -> Option<ExternalNavigationFailure> {
@ -346,7 +293,10 @@ impl RouterContext {
if let Some(new) = callback(myself) {
let mut self_write = self.inner.write_unchecked();
match new {
NavigationTarget::Internal(p) => self_write.history.replace(p),
NavigationTarget::Internal(p) => {
let history = history();
history.replace(p)
}
NavigationTarget::External(e) => return self_write.external(e),
}
}
@ -356,6 +306,10 @@ impl RouterContext {
None
}
pub(crate) fn internal_route(&self, route: &str) -> bool {
(self.inner.read().internal_route)(route)
}
}
pub struct GenericRouterContext<R> {
@ -426,16 +380,6 @@ where
self.inner.prefix()
}
/// Manually subscribe to the current route
pub fn subscribe(&self, id: ScopeId) {
self.inner.subscribe(id)
}
/// Manually unsubscribe from the current route
pub fn unsubscribe(&self, id: ScopeId) {
self.inner.unsubscribe(id)
}
/// Clear any unresolved errors
pub fn clear_error(&self) {
self.inner.clear_error()

View file

@ -1,103 +0,0 @@
use std::str::FromStr;
use crate::routable::Routable;
use super::HistoryProvider;
/// A [`HistoryProvider`] that stores all navigation information in memory.
pub struct MemoryHistory<R: Routable> {
current: R,
history: Vec<R>,
future: Vec<R>,
}
impl<R: Routable> MemoryHistory<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
/// Create a [`MemoryHistory`] starting at `path`.
///
/// ```rust
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[component]
/// # fn Index() -> Element { VNode::empty() }
/// # #[component]
/// # fn OtherPage() -> Element { VNode::empty() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
///
/// let mut history = MemoryHistory::<Route>::with_initial_path(Route::Index {});
/// assert_eq!(history.current_route(), Route::Index {});
/// assert_eq!(history.can_go_back(), false);
/// ```
pub fn with_initial_path(path: R) -> Self {
Self {
current: path,
history: Vec::new(),
future: Vec::new(),
}
}
}
impl<R: Routable> Default for MemoryHistory<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self {
current: "/".parse().unwrap_or_else(|err| {
panic!("index route does not exist:\n{err}\n use MemoryHistory::with_initial_path to set a custom path")
}),
history: Vec::new(),
future: Vec::new(),
}
}
}
impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
fn current_route(&self) -> R {
self.current.clone()
}
fn can_go_back(&self) -> bool {
!self.history.is_empty()
}
fn go_back(&mut self) {
if let Some(last) = self.history.pop() {
let old = std::mem::replace(&mut self.current, last);
self.future.push(old);
}
}
fn can_go_forward(&self) -> bool {
!self.future.is_empty()
}
fn go_forward(&mut self) {
if let Some(next) = self.future.pop() {
let old = std::mem::replace(&mut self.current, next);
self.history.push(old);
}
}
fn push(&mut self, new: R) {
// don't push the same route twice
if self.current.to_string() == new.to_string() {
return;
}
let old = std::mem::replace(&mut self.current, new);
self.history.push(old);
self.future.clear();
}
fn replace(&mut self, path: R) {
self.current = path;
}
}

View file

@ -1,211 +0,0 @@
use std::sync::{Arc, Mutex};
use gloo::{events::EventListener, render::AnimationFrame, utils::window};
use serde::{de::DeserializeOwned, Serialize};
use tracing::error;
use url::Url;
use web_sys::{History, ScrollRestoration, Window};
use crate::routable::Routable;
use super::HistoryProvider;
const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
/// A [`HistoryProvider`] that integrates with a browser via the [History API]. It uses the URLs
/// hash instead of its path.
///
/// Early web applications used the hash to store the current path because there was no other way
/// for them to interact with the history without triggering a browser navigation, as the
/// [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) did not yet exist. While this implementation could have been written that way, it
/// was not, because no browser supports WebAssembly without the [History API].
pub struct WebHashHistory<R: Serialize + DeserializeOwned> {
do_scroll_restoration: bool,
history: History,
listener_navigation: Option<EventListener>,
#[allow(dead_code)]
listener_scroll: Option<EventListener>,
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
window: Window,
phantom: std::marker::PhantomData<R>,
}
impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
/// Create a new [`WebHashHistory`].
///
/// If `do_scroll_restoration` is [`true`], [`WebHashHistory`] will take control of the history
/// state. It'll also set the browsers scroll restoration to `manual`.
pub fn new(do_scroll_restoration: bool) -> Self {
let window = window();
let history = window.history().expect("`window` has access to `history`");
history
.set_scroll_restoration(ScrollRestoration::Manual)
.expect("`history` can set scroll restoration");
let listener_scroll = match do_scroll_restoration {
true => {
history
.set_scroll_restoration(ScrollRestoration::Manual)
.expect("`history` can set scroll restoration");
let w = window.clone();
let h = history.clone();
let document = w.document().expect("`window` has access to `document`");
Some(EventListener::new(&document, "scroll", move |_| {
update_history(&w, &h);
}))
}
false => None,
};
Self {
do_scroll_restoration,
history,
listener_navigation: None,
listener_scroll,
listener_animation_frame: Default::default(),
window,
phantom: Default::default(),
}
}
}
impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
fn join_url_to_hash(&self, path: R) -> Option<String> {
let url = match self.url() {
Some(c) => match c.join(&path) {
Ok(new) => new,
Err(e) => {
error!("failed to join location with target: {e}");
return None;
}
},
None => {
error!("current location unknown");
return None;
}
};
Some(format!(
"#{path}{query}",
path = url.path(),
query = url.query().map(|q| format!("?{q}")).unwrap_or_default()
))
}
fn url(&self) -> Option<Url> {
let mut path = self.window.location().hash().ok()?;
if path.starts_with('#') {
path.remove(0);
}
if path.starts_with('/') {
path.remove(0);
}
match Url::parse(&format!("{INITIAL_URL}/{path}")) {
Ok(url) => Some(url),
Err(e) => {
error!("failed to parse hash path: {e}");
None
}
}
}
}
impl<R: Serialize + DeserializeOwned + Routable> HistoryProvider<R> for WebHashHistory<R> {
fn current_route(&self) -> R {
self.url()
.map(|url| url.path().to_string())
.unwrap_or(String::from("/"))
}
fn current_prefix(&self) -> Option<String> {
Some(String::from("#"))
}
fn go_back(&mut self) {
if let Err(e) = self.history.back() {
error!("failed to go back: {e:?}")
}
}
fn go_forward(&mut self) {
if let Err(e) = self.history.forward() {
error!("failed to go forward: {e:?}")
}
}
fn push(&mut self, path: R) {
let hash = match self.join_url_to_hash(path) {
Some(hash) => hash,
None => return,
};
let state = match self.do_scroll_restoration {
true => top_left(),
false => self.history.state().unwrap_or_default(),
};
let nav = self.history.push_state_with_url(&state, "", Some(&hash));
match nav {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => error!("failed to push state: {e:?}"),
}
}
fn replace(&mut self, path: R) {
let hash = match self.join_url_to_hash(path) {
Some(hash) => hash,
None => return,
};
let state = match self.do_scroll_restoration {
true => top_left(),
false => self.history.state().unwrap_or_default(),
};
let nav = self.history.replace_state_with_url(&state, "", Some(&hash));
match nav {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => error!("failed to replace state: {e:?}"),
}
}
fn external(&mut self, url: String) -> bool {
match self.window.location().set_href(&url) {
Ok(_) => true,
Err(e) => {
error!("failed to navigate to external url (`{url}): {e:?}");
false
}
}
}
fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
let w = self.window.clone();
let h = self.history.clone();
let s = self.listener_animation_frame.clone();
let d = self.do_scroll_restoration;
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
(*callback)();
if d {
let mut s = s.lock().expect("unpoisoned scroll mutex");
*s = Some(update_scroll(&w, &h));
}
}));
}
}

View file

@ -1,42 +0,0 @@
use gloo::console::error;
use wasm_bindgen::JsValue;
use web_sys::History;
pub(crate) fn replace_state_with_url(
history: &History,
value: &[f64; 2],
url: Option<&str>,
) -> Result<(), JsValue> {
let position = js_sys::Array::new();
position.push(&JsValue::from(value[0]));
position.push(&JsValue::from(value[1]));
history.replace_state_with_url(&position, "", url)
}
pub(crate) fn push_state_and_url(
history: &History,
value: &[f64; 2],
url: String,
) -> Result<(), JsValue> {
let position = js_sys::Array::new();
position.push(&JsValue::from(value[0]));
position.push(&JsValue::from(value[1]));
history.push_state_with_url(&position, "", Some(&url))
}
pub(crate) fn get_current(history: &History) -> Option<[f64; 2]> {
use wasm_bindgen::JsCast;
let state = history.state();
if let Err(err) = &state {
error!(err);
}
state.ok().and_then(|state| {
let state = state.dyn_into::<js_sys::Array>().ok()?;
let x = state.get(0).as_f64()?;
let y = state.get(1).as_f64()?;
Some([x, y])
})
}

View file

@ -1,22 +0,0 @@
use gloo::render::{request_animation_frame, AnimationFrame};
use web_sys::Window;
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct ScrollPosition {
pub x: f64,
pub y: f64,
}
impl ScrollPosition {
pub(crate) fn of_window(window: &Window) -> Self {
Self {
x: window.scroll_x().unwrap_or_default(),
y: window.scroll_y().unwrap_or_default(),
}
}
pub(crate) fn scroll_to(&self, window: Window) -> AnimationFrame {
let Self { x, y } = *self;
request_animation_frame(move |_| window.scroll_to_with_x_and_y(x, y))
}
}

View file

@ -24,6 +24,12 @@ pub mod components {
mod router;
pub use router::*;
mod history_provider;
pub use history_provider::*;
#[doc(hidden)]
pub mod child_router;
}
mod contexts {
@ -37,8 +43,6 @@ mod contexts {
mod router_cfg;
mod history;
/// Hooks for interacting with the router in components.
pub mod hooks {
mod use_router;
@ -55,9 +59,11 @@ pub use hooks::router;
/// A collection of useful items most applications might need.
pub mod prelude {
pub use crate::components::*;
pub use crate::components::{
GoBackButton, GoForwardButton, HistoryButtonProps, Link, LinkProps, Outlet, Router,
RouterProps,
};
pub use crate::contexts::*;
pub use crate::history::*;
pub use crate::hooks::*;
pub use crate::navigation::*;
pub use crate::routable::*;

View file

@ -7,11 +7,23 @@ use std::{
use url::{ParseError, Url};
use crate::routable::Routable;
use crate::{components::child_router::consume_child_route_mapping, routable::Routable, router};
impl<R: Routable> From<R> for NavigationTarget {
fn from(value: R) -> Self {
// If this is a child route, map it to the root route first
let mapping = consume_child_route_mapping();
match mapping.as_ref() {
Some(mapping) => NavigationTarget::Internal(mapping.format_route_as_root_route(value)),
// Otherwise, just use the internal route
None => NavigationTarget::Internal(value.to_string()),
}
}
}
/// A target for the router to navigate to.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum NavigationTarget<R> {
pub enum NavigationTarget<R = String> {
/// An internal path that the router can navigate to by itself.
///
/// ```rust
@ -80,6 +92,35 @@ impl<R: Routable> From<R> for NavigationTarget<R> {
}
}
impl From<&str> for NavigationTarget {
fn from(value: &str) -> Self {
let router = router();
match router.internal_route(value) {
true => NavigationTarget::Internal(value.to_string()),
false => NavigationTarget::External(value.to_string()),
}
}
}
impl From<String> for NavigationTarget {
fn from(value: String) -> Self {
let router = router();
match router.internal_route(&value) {
true => NavigationTarget::Internal(value),
false => NavigationTarget::External(value),
}
}
}
impl<R: Routable> From<NavigationTarget<R>> for NavigationTarget {
fn from(value: NavigationTarget<R>) -> Self {
match value {
NavigationTarget::Internal(r) => r.into(),
NavigationTarget::External(s) => Self::External(s),
}
}
}
impl<R: Routable> Display for NavigationTarget<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {

View file

@ -1,4 +1,4 @@
use crate::prelude::*;
use crate::{components::FailureExternalNavigation, prelude::*};
use dioxus_lib::prelude::*;
use std::sync::Arc;
@ -17,42 +17,29 @@ use std::sync::Arc;
/// #[route("/")]
/// Index {},
/// }
/// let cfg = RouterConfig::default().history(MemoryHistory::<Route>::default());
///
/// fn ExternalNavigationFailure() -> Element {
/// rsx! {
/// "Failed to navigate to external URL"
/// }
/// }
///
/// let cfg = RouterConfig::<Route>::default().failure_external_navigation(ExternalNavigationFailure);
/// ```
pub struct RouterConfig<R> {
pub(crate) failure_external_navigation: fn() -> Element,
pub(crate) history: Option<Box<dyn AnyHistoryProvider>>,
pub(crate) on_update: Option<RoutingCallback<R>>,
pub(crate) initial_route: Option<R>,
}
impl<R> Default for RouterConfig<R> {
fn default() -> Self {
Self {
failure_external_navigation: FailureExternalNavigation,
history: None,
on_update: None,
initial_route: None,
}
}
}
impl<R: Routable + Clone> RouterConfig<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
pub(crate) fn take_history(&mut self) -> Box<dyn AnyHistoryProvider> {
self.history
.take()
.unwrap_or_else(|| {
let initial_route = self.initial_route.clone().unwrap_or_else(|| "/".parse().unwrap_or_else(|err|
panic!("index route does not exist:\n{}\n use MemoryHistory::with_initial_path or RouterConfig::initial_route to set a custom path", err)
));
default_history(initial_route)
})
}
}
impl<R> RouterConfig<R>
where
R: Routable,
@ -81,24 +68,6 @@ where
}
}
/// The [`HistoryProvider`] the router should use.
///
/// Defaults to a different history provider depending on the target platform.
pub fn history(self, history: impl HistoryProvider<R> + 'static) -> Self {
Self {
history: Some(Box::new(AnyHistoryProviderImplWrapper::new(history))),
..self
}
}
/// The initial route the router should use if no history provider is set.
pub fn initial_route(self, route: R) -> Self {
Self {
initial_route: Some(route),
..self
}
}
/// A component to render when an external navigation fails.
///
/// Defaults to a router-internal component called [`FailureExternalNavigation`]
@ -109,47 +78,3 @@ where
}
}
}
/// Get the default history provider for the current platform.
#[allow(unreachable_code, unused)]
fn default_history<R: Routable + Clone>(initial_route: R) -> Box<dyn AnyHistoryProvider>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
// If we're on the web and have wasm, use the web history provider
#[cfg(all(target_arch = "wasm32", feature = "web"))]
return Box::new(AnyHistoryProviderImplWrapper::new(
WebHistory::<R>::default(),
));
// If we're using fullstack and server side rendering, use the memory history provider
#[cfg(all(feature = "fullstack", feature = "ssr"))]
return Box::new(AnyHistoryProviderImplWrapper::new(
MemoryHistory::<R>::with_initial_path(
dioxus_fullstack::prelude::server_context()
.request_parts()
.uri
.to_string()
.parse()
.unwrap_or_else(|err| {
tracing::error!("Failed to parse uri: {}", err);
"/".parse().unwrap_or_else(|err| {
panic!("Failed to parse uri: {}", err);
})
}),
),
));
// If liveview is enabled, use the liveview history provider
#[cfg(feature = "liveview")]
return Box::new(AnyHistoryProviderImplWrapper::new(
LiveviewHistory::new_with_initial_path(initial_route),
));
// If none of the above, use the memory history provider, which is a decent enough fallback
// Eventually we want to integrate with the mobile history provider, and other platform providers
Box::new(AnyHistoryProviderImplWrapper::new(
MemoryHistory::with_initial_path(initial_route),
))
}

View file

@ -11,17 +11,5 @@ use crate::prelude::*;
/// - [`None`], when the current component isn't a descendant of a [`Router`] component.
/// - Otherwise [`Some`].
pub(crate) fn use_router_internal() -> Option<RouterContext> {
let router = try_consume_context::<RouterContext>()?;
let id = current_scope_id().expect("use_router_internal called outside of a component");
use_drop({
to_owned![router];
move || {
router.unsubscribe(id);
}
});
use_hook(move || {
router.subscribe(id);
Some(router)
})
use_hook(try_consume_context)
}

View file

@ -1,13 +1,23 @@
use dioxus::prelude::*;
use std::str::FromStr;
use dioxus_history::{History, MemoryHistory};
use dioxus_router::components::HistoryProvider;
use std::{rc::Rc, str::FromStr};
fn prepare<R: Routable>() -> String
where
<R as FromStr>::Err: std::fmt::Display,
{
prepare_at::<R>("/")
}
fn prepare_at<R: Routable>(at: impl ToString) -> String
where
<R as FromStr>::Err: std::fmt::Display,
{
let mut vdom = VirtualDom::new_with_props(
App,
AppProps::<R> {
at: at.to_string(),
phantom: std::marker::PhantomData,
},
);
@ -16,12 +26,14 @@ where
#[derive(Props)]
struct AppProps<R: Routable> {
at: String,
phantom: std::marker::PhantomData<R>,
}
impl<R: Routable> Clone for AppProps<R> {
fn clone(&self) -> Self {
Self {
at: self.at.clone(),
phantom: std::marker::PhantomData,
}
}
@ -34,14 +46,15 @@ where
}
#[allow(non_snake_case)]
fn App<R: Routable>(_props: AppProps<R>) -> Element
fn App<R: Routable>(props: AppProps<R>) -> Element
where
<R as FromStr>::Err: std::fmt::Display,
{
rsx! {
h1 { "App" }
Router::<R> {
config: |_| RouterConfig::default().history(MemoryHistory::default())
HistoryProvider {
history: move |_| Rc::new(MemoryHistory::with_initial_path(props.at.clone())) as Rc<dyn History>,
Router::<R> {}
}
}
}
@ -345,3 +358,76 @@ fn with_rel() {
assert_eq!(prepare::<Route>(), expected);
}
#[test]
fn with_child_route() {
#[derive(Routable, Clone, PartialEq, Debug)]
enum ChildRoute {
#[route("/")]
ChildRoot {},
#[route("/:not_static")]
NotStatic { not_static: String },
}
#[derive(Routable, Clone, PartialEq, Debug)]
enum Route {
#[route("/")]
Root {},
#[route("/test")]
Test {},
#[child("/child")]
Nested { child: ChildRoute },
}
#[component]
fn Test() -> Element {
unimplemented!()
}
#[component]
fn Root() -> Element {
rsx! {
Link {
to: Route::Test {},
"Parent Link"
}
Link {
to: Route::Nested { child: ChildRoute::NotStatic { not_static: "this-is-a-child-route".to_string() } },
"Child Link"
}
}
}
#[component]
fn ChildRoot() -> Element {
rsx! {
Link {
to: Route::Test {},
"Parent Link"
}
Link {
to: ChildRoute::NotStatic { not_static: "this-is-a-child-route".to_string() },
"Child Link 1"
}
Link {
to: Route::Nested { child: ChildRoute::NotStatic { not_static: "this-is-a-child-route".to_string() } },
"Child Link 2"
}
}
}
#[component]
fn NotStatic(not_static: String) -> Element {
unimplemented!()
}
assert_eq!(
prepare_at::<Route>("/"),
"<h1>App</h1><a href=\"/test\">Parent Link</a><a href=\"/child/this-is-a-child-route\">Child Link</a>"
);
assert_eq!(
prepare_at::<Route>("/child"),
"<h1>App</h1><a href=\"/test\">Parent Link</a><a href=\"/child/this-is-a-child-route\">Child Link 1</a><a href=\"/child/this-is-a-child-route\">Child Link 2</a>"
);
}

View file

@ -1,6 +1,10 @@
#![allow(unused)]
use std::rc::Rc;
use dioxus::prelude::*;
use dioxus_history::{History, MemoryHistory};
use dioxus_router::components::HistoryProvider;
use dioxus_router::prelude::*;
fn prepare(path: impl Into<String>) -> VirtualDom {
@ -38,10 +42,9 @@ fn prepare(path: impl Into<String>) -> VirtualDom {
fn App(path: Route) -> Element {
rsx! {
h1 { "App" }
Router::<Route> {
config: move |_| {
RouterConfig::default().history(MemoryHistory::with_initial_path(path.clone()))
}
HistoryProvider {
history: move |_| Rc::new(MemoryHistory::with_initial_path(path.clone())) as Rc<dyn History>,
Router::<Route> {}
}
}
}

View file

@ -1,5 +1,7 @@
use dioxus::prelude::*;
use std::str::FromStr;
use dioxus_history::{History, MemoryHistory};
use dioxus_router::components::HistoryProvider;
use std::{rc::Rc, str::FromStr};
// Tests for regressions of <https://github.com/DioxusLabs/dioxus/issues/2549>
#[test]
@ -33,10 +35,9 @@ fn Home(lang: String) -> Element {
#[component]
fn App(path: Route) -> Element {
rsx! {
Router::<Route> {
config: {
move |_| RouterConfig::default().history(MemoryHistory::with_initial_path(path.clone()))
}
HistoryProvider {
history: move |_| Rc::new(MemoryHistory::with_initial_path(path.clone())) as Rc<dyn History>,
Router::<Route> {}
}
}
}

View file

@ -1,4 +1,8 @@
use std::rc::Rc;
use dioxus::prelude::*;
use dioxus_history::{History, MemoryHistory};
use dioxus_router::components::HistoryProvider;
// Tests for regressions of <https://github.com/DioxusLabs/dioxus/issues/2468>
#[test]
@ -32,10 +36,9 @@ fn Test() -> Element {
#[component]
fn App(path: Route) -> Element {
rsx! {
Router::<Route> {
config: {
move || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
}
HistoryProvider {
history: move |_| Rc::new(MemoryHistory::with_initial_path(path)) as Rc<dyn History>,
Router::<Route> {}
}
}
}

View file

@ -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"]

View file

@ -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",

View file

@ -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<dyn std::any::Any>,
@ -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<dyn Document> = Rc::new(WebDocument);
if ScopeId::ROOT.has_context::<Rc<dyn Document>>().is_none() {
ScopeId::ROOT.provide_context(provider);
}
let history_provider: Rc<dyn History> = Rc::new(WebHistory::default());
if ScopeId::ROOT.has_context::<Rc<dyn History>>().is_none() {
ScopeId::ROOT.provide_context(history_provider);
}
}
/// The web-target's document provider.

View file

@ -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<PathBuf> {
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<R>(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<R>(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<R: Routable> {
pub struct WebHistory {
do_scroll_restoration: bool,
history: History,
listener_navigation: Option<EventListener>,
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
prefix: Option<String>,
window: Window,
phantom: std::marker::PhantomData<R>,
}
impl<R: Routable> Default for WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
impl Default for WebHistory {
fn default() -> Self {
Self::new(None, true)
}
}
impl<R: Routable> WebHistory<R> {
impl WebHistory {
/// Create a new [`WebHistory`].
///
/// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
/// state. It'll also set the browsers scroll restoration to `manual`.
pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
pub fn new(prefix: Option<String>, 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(&current_url));
myself
}
fn new_inner(prefix: Option<String>, do_scroll_restoration: bool) -> Self
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn new_inner(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
let window = window().expect("access to `window`");
let history = window.history().expect("`window` has access to `history`");
@ -111,11 +88,8 @@ impl<R: Routable> WebHistory<R> {
Self {
do_scroll_restoration,
history,
listener_navigation: None,
listener_animation_frame: Default::default(),
prefix,
window,
phantom: Default::default(),
}
}
@ -125,17 +99,14 @@ impl<R: Routable> WebHistory<R> {
.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<R: Routable> WebHistory<R>
where
<R as std::str::FromStr>::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<R: Routable> HistoryProvider<R> for WebHistory<R>
where
<R as std::str::FromStr>::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::<R>(&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<dyn Fn() + Send + Sync>) {
fn updater(&self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
let w = self.window.clone();
let h = self.history.clone();
let s = self.listener_animation_frame.clone();
let d = self.do_scroll_restoration;
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
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<dyn FnMut(Event)>);
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::<js_sys::Array>().ok()?;
let x = state.get(0).as_f64()?;
let y = state.get(1).as_f64()?;
Some([x, y])
})
}

View file

@ -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<dyn FnMut()>
);
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");
}
}

View file

@ -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::*;