mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-24 21:23:07 +00:00
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:
parent
e5a1a62644
commit
281087469a
48 changed files with 877 additions and 1421 deletions
25
Cargo.lock
generated
25
Cargo.lock
generated
|
@ -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",
|
||||
|
|
27
Cargo.toml
27
Cargo.toml
|
@ -38,6 +38,7 @@ members = [
|
|||
"packages/extension",
|
||||
"packages/fullstack",
|
||||
"packages/generational-box",
|
||||
"packages/history",
|
||||
"packages/hooks",
|
||||
"packages/html-internal-macro",
|
||||
"packages/html",
|
||||
|
@ -116,15 +117,17 @@ dioxus-core = { path = "packages/core", version = "0.6.0-alpha.3" }
|
|||
dioxus-core-types = { path = "packages/core-types", version = "0.6.0-alpha.3" }
|
||||
dioxus-core-macro = { path = "packages/core-macro", version = "0.6.0-alpha.3" }
|
||||
dioxus-config-macro = { path = "packages/config-macro", version = "0.6.0-alpha.3" }
|
||||
dioxus-document = { path = "packages/document", version = "0.6.0-alpha.3" }
|
||||
dioxus-router = { path = "packages/router", version = "0.6.0-alpha.3" }
|
||||
dioxus-router-macro = { path = "packages/router-macro", version = "0.6.0-alpha.3" }
|
||||
dioxus-document = { path = "packages/document", version = "0.6.0-alpha.3", default-features = false }
|
||||
dioxus-history = { path = "packages/history", version = "0.6.0-alpha.3", default-features = false }
|
||||
dioxus-html = { path = "packages/html", version = "0.6.0-alpha.3", default-features = false }
|
||||
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.6.0-alpha.3" }
|
||||
dioxus-hooks = { path = "packages/hooks", version = "0.6.0-alpha.3" }
|
||||
dioxus-web = { path = "packages/web", version = "0.6.0-alpha.3", default-features = false }
|
||||
dioxus-isrg = { path = "packages/isrg", version = "0.6.0-alpha.3" }
|
||||
dioxus-ssr = { path = "packages/ssr", version = "0.6.0-alpha.3", default-features = false }
|
||||
dioxus-desktop = { path = "packages/desktop", version = "0.6.0-alpha.3" }
|
||||
dioxus-desktop = { path = "packages/desktop", version = "0.6.0-alpha.3", default-features = false }
|
||||
dioxus-mobile = { path = "packages/mobile", version = "0.6.0-alpha.3" }
|
||||
dioxus-interpreter-js = { path = "packages/interpreter", version = "0.6.0-alpha.3" }
|
||||
dioxus-liveview = { path = "packages/liveview", version = "0.6.0-alpha.3" }
|
||||
|
@ -134,19 +137,17 @@ dioxus-rsx = { path = "packages/rsx", version = "0.6.0-alpha.3" }
|
|||
dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.6.0-alpha.3" }
|
||||
dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.6.0-alpha.3" }
|
||||
dioxus-signals = { path = "packages/signals", version = "0.6.0-alpha.3" }
|
||||
dioxus-devtools = { path = "packages/devtools", version = "0.6.0-alpha.3" }
|
||||
dioxus-devtools-types = { path = "packages/devtools-types", version = "0.6.0-alpha.3" }
|
||||
dioxus-fullstack = { path = "packages/fullstack", version = "0.6.0-alpha.3", default-features = false }
|
||||
dioxus-static-site-generation = { path = "packages/static-generation", version = "0.6.0-alpha.3" }
|
||||
dioxus_server_macro = { path = "packages/server-macro", version = "0.6.0-alpha.3", default-features = false }
|
||||
dioxus-isrg = { path = "packages/isrg", version = "0.6.0-alpha.3" }
|
||||
lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.6.0-alpha.3" }
|
||||
dioxus-cli-config = { path = "packages/cli-config", version = "0.6.0-alpha.3" }
|
||||
generational-box = { path = "packages/generational-box", version = "0.6.0-alpha.3" }
|
||||
|
||||
manganis = { path = "packages/manganis/manganis", version = "0.6.0-alpha.3" }
|
||||
manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.0-alpha.3" }
|
||||
manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.0-alpha.3" }
|
||||
dioxus-devtools = { path = "packages/devtools", version = "0.6.0-alpha.3" }
|
||||
dioxus-devtools-types = { path = "packages/devtools-types", version = "0.6.0-alpha.3" }
|
||||
dioxus-fullstack = { path = "packages/fullstack", version = "0.6.0-alpha.1" }
|
||||
dioxus-static-site-generation = { path = "packages/static-generation", version = "0.6.0-alpha.3" }
|
||||
dioxus_server_macro = { path = "packages/server-macro", version = "0.6.0-alpha.3", default-features = false }
|
||||
lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.6.0-alpha.3" }
|
||||
manganis = { path = "packages/manganis/manganis", version = "0.6.0-alpha.3" }
|
||||
manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.0-alpha.3" }
|
||||
manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.0-alpha.3" }
|
||||
|
||||
warnings = { version = "0.2.0" }
|
||||
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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::*;
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
11
packages/history/Cargo.toml
Normal file
11
packages/history/Cargo.toml
Normal 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"] }
|
|
@ -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>>()
|
||||
}
|
||||
}
|
100
packages/history/src/memory.rs
Normal file
100
packages/history/src/memory.rs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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"] }
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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 {})
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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>>>,
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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(¤t_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 },
|
||||
}
|
68
packages/router/src/components/child_router.rs
Normal file
68
packages/router/src/components/child_router.rs
Normal 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)
|
||||
}
|
20
packages/router/src/components/history_provider.rs
Normal file
20
packages/router/src/components/history_provider.rs
Normal 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
|
||||
}
|
|
@ -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() }"
|
||||
)
|
||||
};
|
||||
|
||||
|
|
|
@ -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> {}
|
||||
/// # }
|
||||
/// # }
|
||||
/// # }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,211 +0,0 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use gloo::{events::EventListener, render::AnimationFrame, utils::window};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use tracing::error;
|
||||
use url::Url;
|
||||
use web_sys::{History, ScrollRestoration, Window};
|
||||
|
||||
use crate::routable::Routable;
|
||||
|
||||
use super::HistoryProvider;
|
||||
|
||||
const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
|
||||
|
||||
/// A [`HistoryProvider`] that integrates with a browser via the [History API]. It uses the URLs
|
||||
/// hash instead of its path.
|
||||
///
|
||||
/// Early web applications used the hash to store the current path because there was no other way
|
||||
/// for them to interact with the history without triggering a browser navigation, as the
|
||||
/// [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) did not yet exist. While this implementation could have been written that way, it
|
||||
/// was not, because no browser supports WebAssembly without the [History API].
|
||||
pub struct WebHashHistory<R: Serialize + DeserializeOwned> {
|
||||
do_scroll_restoration: bool,
|
||||
history: History,
|
||||
listener_navigation: Option<EventListener>,
|
||||
#[allow(dead_code)]
|
||||
listener_scroll: Option<EventListener>,
|
||||
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
|
||||
window: Window,
|
||||
phantom: std::marker::PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
|
||||
/// Create a new [`WebHashHistory`].
|
||||
///
|
||||
/// If `do_scroll_restoration` is [`true`], [`WebHashHistory`] will take control of the history
|
||||
/// state. It'll also set the browsers scroll restoration to `manual`.
|
||||
pub fn new(do_scroll_restoration: bool) -> Self {
|
||||
let window = window();
|
||||
let history = window.history().expect("`window` has access to `history`");
|
||||
|
||||
history
|
||||
.set_scroll_restoration(ScrollRestoration::Manual)
|
||||
.expect("`history` can set scroll restoration");
|
||||
|
||||
let listener_scroll = match do_scroll_restoration {
|
||||
true => {
|
||||
history
|
||||
.set_scroll_restoration(ScrollRestoration::Manual)
|
||||
.expect("`history` can set scroll restoration");
|
||||
let w = window.clone();
|
||||
let h = history.clone();
|
||||
let document = w.document().expect("`window` has access to `document`");
|
||||
|
||||
Some(EventListener::new(&document, "scroll", move |_| {
|
||||
update_history(&w, &h);
|
||||
}))
|
||||
}
|
||||
false => None,
|
||||
};
|
||||
|
||||
Self {
|
||||
do_scroll_restoration,
|
||||
history,
|
||||
listener_navigation: None,
|
||||
listener_scroll,
|
||||
listener_animation_frame: Default::default(),
|
||||
window,
|
||||
phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
|
||||
fn join_url_to_hash(&self, path: R) -> Option<String> {
|
||||
let url = match self.url() {
|
||||
Some(c) => match c.join(&path) {
|
||||
Ok(new) => new,
|
||||
Err(e) => {
|
||||
error!("failed to join location with target: {e}");
|
||||
return None;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
error!("current location unknown");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(format!(
|
||||
"#{path}{query}",
|
||||
path = url.path(),
|
||||
query = url.query().map(|q| format!("?{q}")).unwrap_or_default()
|
||||
))
|
||||
}
|
||||
|
||||
fn url(&self) -> Option<Url> {
|
||||
let mut path = self.window.location().hash().ok()?;
|
||||
|
||||
if path.starts_with('#') {
|
||||
path.remove(0);
|
||||
}
|
||||
|
||||
if path.starts_with('/') {
|
||||
path.remove(0);
|
||||
}
|
||||
|
||||
match Url::parse(&format!("{INITIAL_URL}/{path}")) {
|
||||
Ok(url) => Some(url),
|
||||
Err(e) => {
|
||||
error!("failed to parse hash path: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Serialize + DeserializeOwned + Routable> HistoryProvider<R> for WebHashHistory<R> {
|
||||
fn current_route(&self) -> R {
|
||||
self.url()
|
||||
.map(|url| url.path().to_string())
|
||||
.unwrap_or(String::from("/"))
|
||||
}
|
||||
|
||||
fn current_prefix(&self) -> Option<String> {
|
||||
Some(String::from("#"))
|
||||
}
|
||||
|
||||
fn go_back(&mut self) {
|
||||
if let Err(e) = self.history.back() {
|
||||
error!("failed to go back: {e:?}")
|
||||
}
|
||||
}
|
||||
|
||||
fn go_forward(&mut self) {
|
||||
if let Err(e) = self.history.forward() {
|
||||
error!("failed to go forward: {e:?}")
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, path: R) {
|
||||
let hash = match self.join_url_to_hash(path) {
|
||||
Some(hash) => hash,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let state = match self.do_scroll_restoration {
|
||||
true => top_left(),
|
||||
false => self.history.state().unwrap_or_default(),
|
||||
};
|
||||
|
||||
let nav = self.history.push_state_with_url(&state, "", Some(&hash));
|
||||
|
||||
match nav {
|
||||
Ok(_) => {
|
||||
if self.do_scroll_restoration {
|
||||
self.window.scroll_to_with_x_and_y(0.0, 0.0)
|
||||
}
|
||||
}
|
||||
Err(e) => error!("failed to push state: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn replace(&mut self, path: R) {
|
||||
let hash = match self.join_url_to_hash(path) {
|
||||
Some(hash) => hash,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let state = match self.do_scroll_restoration {
|
||||
true => top_left(),
|
||||
false => self.history.state().unwrap_or_default(),
|
||||
};
|
||||
|
||||
let nav = self.history.replace_state_with_url(&state, "", Some(&hash));
|
||||
|
||||
match nav {
|
||||
Ok(_) => {
|
||||
if self.do_scroll_restoration {
|
||||
self.window.scroll_to_with_x_and_y(0.0, 0.0)
|
||||
}
|
||||
}
|
||||
Err(e) => error!("failed to replace state: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn external(&mut self, url: String) -> bool {
|
||||
match self.window.location().set_href(&url) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
error!("failed to navigate to external url (`{url}): {e:?}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
|
||||
let w = self.window.clone();
|
||||
let h = self.history.clone();
|
||||
let s = self.listener_animation_frame.clone();
|
||||
let d = self.do_scroll_restoration;
|
||||
|
||||
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
|
||||
(*callback)();
|
||||
if d {
|
||||
let mut s = s.lock().expect("unpoisoned scroll mutex");
|
||||
*s = Some(update_scroll(&w, &h));
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
use gloo::console::error;
|
||||
use wasm_bindgen::JsValue;
|
||||
use web_sys::History;
|
||||
|
||||
pub(crate) fn replace_state_with_url(
|
||||
history: &History,
|
||||
value: &[f64; 2],
|
||||
url: Option<&str>,
|
||||
) -> Result<(), JsValue> {
|
||||
let position = js_sys::Array::new();
|
||||
position.push(&JsValue::from(value[0]));
|
||||
position.push(&JsValue::from(value[1]));
|
||||
|
||||
history.replace_state_with_url(&position, "", url)
|
||||
}
|
||||
|
||||
pub(crate) fn push_state_and_url(
|
||||
history: &History,
|
||||
value: &[f64; 2],
|
||||
url: String,
|
||||
) -> Result<(), JsValue> {
|
||||
let position = js_sys::Array::new();
|
||||
position.push(&JsValue::from(value[0]));
|
||||
position.push(&JsValue::from(value[1]));
|
||||
|
||||
history.push_state_with_url(&position, "", Some(&url))
|
||||
}
|
||||
|
||||
pub(crate) fn get_current(history: &History) -> Option<[f64; 2]> {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
let state = history.state();
|
||||
if let Err(err) = &state {
|
||||
error!(err);
|
||||
}
|
||||
state.ok().and_then(|state| {
|
||||
let state = state.dyn_into::<js_sys::Array>().ok()?;
|
||||
let x = state.get(0).as_f64()?;
|
||||
let y = state.get(1).as_f64()?;
|
||||
Some([x, y])
|
||||
})
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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::*;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::prelude::*;
|
||||
use crate::{components::FailureExternalNavigation, prelude::*};
|
||||
use dioxus_lib::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -17,42 +17,29 @@ use std::sync::Arc;
|
|||
/// #[route("/")]
|
||||
/// Index {},
|
||||
/// }
|
||||
/// let cfg = RouterConfig::default().history(MemoryHistory::<Route>::default());
|
||||
///
|
||||
/// fn ExternalNavigationFailure() -> Element {
|
||||
/// rsx! {
|
||||
/// "Failed to navigate to external URL"
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let cfg = RouterConfig::<Route>::default().failure_external_navigation(ExternalNavigationFailure);
|
||||
/// ```
|
||||
pub struct RouterConfig<R> {
|
||||
pub(crate) failure_external_navigation: fn() -> Element,
|
||||
pub(crate) history: Option<Box<dyn AnyHistoryProvider>>,
|
||||
pub(crate) on_update: Option<RoutingCallback<R>>,
|
||||
pub(crate) initial_route: Option<R>,
|
||||
}
|
||||
|
||||
impl<R> Default for RouterConfig<R> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
failure_external_navigation: FailureExternalNavigation,
|
||||
history: None,
|
||||
on_update: None,
|
||||
initial_route: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Routable + Clone> RouterConfig<R>
|
||||
where
|
||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
pub(crate) fn take_history(&mut self) -> Box<dyn AnyHistoryProvider> {
|
||||
self.history
|
||||
.take()
|
||||
.unwrap_or_else(|| {
|
||||
let initial_route = self.initial_route.clone().unwrap_or_else(|| "/".parse().unwrap_or_else(|err|
|
||||
panic!("index route does not exist:\n{}\n use MemoryHistory::with_initial_path or RouterConfig::initial_route to set a custom path", err)
|
||||
));
|
||||
default_history(initial_route)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> RouterConfig<R>
|
||||
where
|
||||
R: Routable,
|
||||
|
@ -81,24 +68,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// The [`HistoryProvider`] the router should use.
|
||||
///
|
||||
/// Defaults to a different history provider depending on the target platform.
|
||||
pub fn history(self, history: impl HistoryProvider<R> + 'static) -> Self {
|
||||
Self {
|
||||
history: Some(Box::new(AnyHistoryProviderImplWrapper::new(history))),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// The initial route the router should use if no history provider is set.
|
||||
pub fn initial_route(self, route: R) -> Self {
|
||||
Self {
|
||||
initial_route: Some(route),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// A component to render when an external navigation fails.
|
||||
///
|
||||
/// Defaults to a router-internal component called [`FailureExternalNavigation`]
|
||||
|
@ -109,47 +78,3 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default history provider for the current platform.
|
||||
#[allow(unreachable_code, unused)]
|
||||
fn default_history<R: Routable + Clone>(initial_route: R) -> Box<dyn AnyHistoryProvider>
|
||||
where
|
||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
// If we're on the web and have wasm, use the web history provider
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "web"))]
|
||||
return Box::new(AnyHistoryProviderImplWrapper::new(
|
||||
WebHistory::<R>::default(),
|
||||
));
|
||||
|
||||
// If we're using fullstack and server side rendering, use the memory history provider
|
||||
#[cfg(all(feature = "fullstack", feature = "ssr"))]
|
||||
return Box::new(AnyHistoryProviderImplWrapper::new(
|
||||
MemoryHistory::<R>::with_initial_path(
|
||||
dioxus_fullstack::prelude::server_context()
|
||||
.request_parts()
|
||||
.uri
|
||||
.to_string()
|
||||
.parse()
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::error!("Failed to parse uri: {}", err);
|
||||
"/".parse().unwrap_or_else(|err| {
|
||||
panic!("Failed to parse uri: {}", err);
|
||||
})
|
||||
}),
|
||||
),
|
||||
));
|
||||
|
||||
// If liveview is enabled, use the liveview history provider
|
||||
#[cfg(feature = "liveview")]
|
||||
return Box::new(AnyHistoryProviderImplWrapper::new(
|
||||
LiveviewHistory::new_with_initial_path(initial_route),
|
||||
));
|
||||
|
||||
// If none of the above, use the memory history provider, which is a decent enough fallback
|
||||
// Eventually we want to integrate with the mobile history provider, and other platform providers
|
||||
Box::new(AnyHistoryProviderImplWrapper::new(
|
||||
MemoryHistory::with_initial_path(initial_route),
|
||||
))
|
||||
}
|
||||
|
|
|
@ -11,17 +11,5 @@ use crate::prelude::*;
|
|||
/// - [`None`], when the current component isn't a descendant of a [`Router`] component.
|
||||
/// - Otherwise [`Some`].
|
||||
pub(crate) fn use_router_internal() -> Option<RouterContext> {
|
||||
let router = try_consume_context::<RouterContext>()?;
|
||||
let id = current_scope_id().expect("use_router_internal called outside of a component");
|
||||
use_drop({
|
||||
to_owned![router];
|
||||
move || {
|
||||
router.unsubscribe(id);
|
||||
}
|
||||
});
|
||||
use_hook(move || {
|
||||
router.subscribe(id);
|
||||
|
||||
Some(router)
|
||||
})
|
||||
use_hook(try_consume_context)
|
||||
}
|
||||
|
|
|
@ -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>"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use dioxus::prelude::*;
|
||||
use std::str::FromStr;
|
||||
use dioxus_history::{History, MemoryHistory};
|
||||
use dioxus_router::components::HistoryProvider;
|
||||
use std::{rc::Rc, str::FromStr};
|
||||
|
||||
// Tests for regressions of <https://github.com/DioxusLabs/dioxus/issues/2549>
|
||||
#[test]
|
||||
|
@ -33,10 +35,9 @@ fn Home(lang: String) -> Element {
|
|||
#[component]
|
||||
fn App(path: Route) -> Element {
|
||||
rsx! {
|
||||
Router::<Route> {
|
||||
config: {
|
||||
move |_| RouterConfig::default().history(MemoryHistory::with_initial_path(path.clone()))
|
||||
}
|
||||
HistoryProvider {
|
||||
history: move |_| Rc::new(MemoryHistory::with_initial_path(path.clone())) as Rc<dyn History>,
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_history::{History, MemoryHistory};
|
||||
use dioxus_router::components::HistoryProvider;
|
||||
|
||||
// Tests for regressions of <https://github.com/DioxusLabs/dioxus/issues/2468>
|
||||
#[test]
|
||||
|
@ -32,10 +36,9 @@ fn Test() -> Element {
|
|||
#[component]
|
||||
fn App(path: Route) -> Element {
|
||||
rsx! {
|
||||
Router::<Route> {
|
||||
config: {
|
||||
move || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
|
||||
}
|
||||
HistoryProvider {
|
||||
history: move |_| Rc::new(MemoryHistory::with_initial_path(path)) as Rc<dyn History>,
|
||||
Router::<Route> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(¤t_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])
|
||||
})
|
||||
}
|
28
packages/web/src/history/scroll.rs
Normal file
28
packages/web/src/history/scroll.rs
Normal 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");
|
||||
}
|
||||
}
|
|
@ -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::*;
|
||||
|
|
Loading…
Reference in a new issue