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:
1 parent
e5a1a62644
commit
281087469a
48 files changed
+875
-1419
No files matched your search
Generated
+16
-9
@@ -3169,6 +3169,7 @@ dependencies = [
|
|||||||
"dioxus-devtools",
|
"dioxus-devtools",
|
||||||
"dioxus-document",
|
"dioxus-document",
|
||||||
"dioxus-fullstack",
|
"dioxus-fullstack",
|
||||||
|
"dioxus-history",
|
||||||
"dioxus-hooks",
|
"dioxus-hooks",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
"dioxus-liveview",
|
"dioxus-liveview",
|
||||||
@@ -3374,6 +3375,7 @@ dependencies = [
|
|||||||
"dioxus-core",
|
"dioxus-core",
|
||||||
"dioxus-devtools",
|
"dioxus-devtools",
|
||||||
"dioxus-document",
|
"dioxus-document",
|
||||||
|
"dioxus-history",
|
||||||
"dioxus-hooks",
|
"dioxus-hooks",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
"dioxus-interpreter-js",
|
"dioxus-interpreter-js",
|
||||||
@@ -3530,6 +3532,15 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dioxus-history"
|
||||||
|
version = "0.6.0-alpha.3"
|
||||||
|
dependencies = [
|
||||||
|
"dioxus",
|
||||||
|
"dioxus-core",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dioxus-hooks"
|
name = "dioxus-hooks"
|
||||||
version = "0.6.0-alpha.3"
|
version = "0.6.0-alpha.3"
|
||||||
@@ -3628,6 +3639,7 @@ dependencies = [
|
|||||||
"dioxus-core",
|
"dioxus-core",
|
||||||
"dioxus-core-macro",
|
"dioxus-core-macro",
|
||||||
"dioxus-document",
|
"dioxus-document",
|
||||||
|
"dioxus-history",
|
||||||
"dioxus-hooks",
|
"dioxus-hooks",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
"dioxus-rsx",
|
"dioxus-rsx",
|
||||||
@@ -3644,6 +3656,7 @@ dependencies = [
|
|||||||
"dioxus-core",
|
"dioxus-core",
|
||||||
"dioxus-devtools",
|
"dioxus-devtools",
|
||||||
"dioxus-document",
|
"dioxus-document",
|
||||||
|
"dioxus-history",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
"dioxus-interpreter-js",
|
"dioxus-interpreter-js",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@@ -3733,26 +3746,18 @@ dependencies = [
|
|||||||
"criterion",
|
"criterion",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
"dioxus-cli-config",
|
"dioxus-cli-config",
|
||||||
"dioxus-fullstack",
|
"dioxus-history",
|
||||||
"dioxus-lib",
|
"dioxus-lib",
|
||||||
"dioxus-liveview",
|
|
||||||
"dioxus-router",
|
|
||||||
"dioxus-router-macro",
|
"dioxus-router-macro",
|
||||||
"dioxus-ssr",
|
"dioxus-ssr",
|
||||||
"gloo",
|
"gloo",
|
||||||
"gloo-utils 0.1.7",
|
|
||||||
"http 1.1.0",
|
|
||||||
"js-sys",
|
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-test",
|
"wasm-bindgen-test",
|
||||||
"web-sys",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3880,10 +3885,12 @@ dependencies = [
|
|||||||
"ciborium",
|
"ciborium",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
|
"dioxus-cli-config",
|
||||||
"dioxus-core",
|
"dioxus-core",
|
||||||
"dioxus-core-types",
|
"dioxus-core-types",
|
||||||
"dioxus-devtools",
|
"dioxus-devtools",
|
||||||
"dioxus-document",
|
"dioxus-document",
|
||||||
|
"dioxus-history",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
"dioxus-interpreter-js",
|
"dioxus-interpreter-js",
|
||||||
"dioxus-signals",
|
"dioxus-signals",
|
||||||
|
|||||||
+14
-13
@@ -38,6 +38,7 @@ members = [
|
|||||||
"packages/extension",
|
"packages/extension",
|
||||||
"packages/fullstack",
|
"packages/fullstack",
|
||||||
"packages/generational-box",
|
"packages/generational-box",
|
||||||
|
"packages/history",
|
||||||
"packages/hooks",
|
"packages/hooks",
|
||||||
"packages/html-internal-macro",
|
"packages/html-internal-macro",
|
||||||
"packages/html",
|
"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-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-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-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 = { path = "packages/router", version = "0.6.0-alpha.3" }
|
||||||
dioxus-router-macro = { path = "packages/router-macro", 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 = { 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-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-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-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-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-mobile = { path = "packages/mobile", version = "0.6.0-alpha.3" }
|
||||||
dioxus-interpreter-js = { path = "packages/interpreter", 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" }
|
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-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-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.6.0-alpha.3" }
|
||||||
dioxus-signals = { path = "packages/signals", 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" }
|
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" }
|
generational-box = { path = "packages/generational-box", version = "0.6.0-alpha.3" }
|
||||||
|
dioxus-devtools = { path = "packages/devtools", version = "0.6.0-alpha.3" }
|
||||||
manganis = { path = "packages/manganis/manganis", version = "0.6.0-alpha.3" }
|
dioxus-devtools-types = { path = "packages/devtools-types", version = "0.6.0-alpha.3" }
|
||||||
manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.0-alpha.3" }
|
dioxus-fullstack = { path = "packages/fullstack", version = "0.6.0-alpha.1" }
|
||||||
manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.0-alpha.3" }
|
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" }
|
warnings = { version = "0.2.0" }
|
||||||
|
|
||||||
|
|||||||
+20
-5
@@ -11,10 +11,25 @@ fn app() -> Element {
|
|||||||
// You can use the Meta component to render a meta tag into the head of the page
|
// 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
|
// 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
|
// 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 {
|
||||||
document::Meta { property: "og:type", content: "website" }
|
property: "og:title",
|
||||||
document::Meta { property: "og:url", content: "https://www.example.com" }
|
content: "My Site",
|
||||||
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: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"
|
async-trait = "0.1.68"
|
||||||
tao = { workspace = true, features = ["rwh_05"] }
|
tao = { workspace = true, features = ["rwh_05"] }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
|
dioxus-history.workspace = true
|
||||||
|
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use dioxus_core::{Runtime, ScopeId, VirtualDom};
|
use dioxus_core::{Runtime, ScopeId, VirtualDom};
|
||||||
use dioxus_document::Document;
|
use dioxus_document::Document;
|
||||||
|
use dioxus_history::{History, MemoryHistory};
|
||||||
use dioxus_hooks::to_owned;
|
use dioxus_hooks::to_owned;
|
||||||
use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData};
|
use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData};
|
||||||
use futures_util::{pin_mut, FutureExt};
|
use futures_util::{pin_mut, FutureExt};
|
||||||
@@ -392,9 +393,11 @@ impl WebviewInstance {
|
|||||||
// Provide the desktop context to the virtual dom and edit handler
|
// Provide the desktop context to the virtual dom and edit handler
|
||||||
edits.set_desktop_context(desktop_context.clone());
|
edits.set_desktop_context(desktop_context.clone());
|
||||||
let provider: Rc<dyn Document> = Rc::new(DesktopDocument::new(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(|| {
|
dom.in_runtime(|| {
|
||||||
ScopeId::ROOT.provide_context(desktop_context.clone());
|
ScopeId::ROOT.provide_context(desktop_context.clone());
|
||||||
ScopeId::ROOT.provide_context(provider);
|
ScopeId::ROOT.provide_context(provider);
|
||||||
|
ScopeId::ROOT.provide_context(history_provider);
|
||||||
});
|
});
|
||||||
|
|
||||||
WebviewInstance {
|
WebviewInstance {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ rust-version = "1.79.0"
|
|||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
dioxus-html = { workspace = true, optional = true }
|
dioxus-html = { workspace = true, optional = true }
|
||||||
dioxus-document = { workspace = true, optional = true }
|
dioxus-document = { workspace = true, optional = true }
|
||||||
|
dioxus-history = { workspace = true, optional = true }
|
||||||
dioxus-core-macro = { workspace = true, optional = true }
|
dioxus-core-macro = { workspace = true, optional = true }
|
||||||
dioxus-config-macro = { workspace = true, optional = true }
|
dioxus-config-macro = { workspace = true, optional = true }
|
||||||
dioxus-hooks = { workspace = true, optional = true }
|
dioxus-hooks = { workspace = true, optional = true }
|
||||||
@@ -27,7 +28,7 @@ dioxus = { workspace = true }
|
|||||||
default = ["macro", "html", "signals", "hooks"]
|
default = ["macro", "html", "signals", "hooks"]
|
||||||
signals = ["dep:dioxus-signals"]
|
signals = ["dep:dioxus-signals"]
|
||||||
macro = ["dep:dioxus-core-macro", "dep:dioxus-rsx", "dep:dioxus-config-macro"]
|
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"]
|
hooks = ["dep:dioxus-hooks"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ pub use dioxus_html as html;
|
|||||||
#[cfg(feature = "html")]
|
#[cfg(feature = "html")]
|
||||||
pub use dioxus_document as document;
|
pub use dioxus_document as document;
|
||||||
|
|
||||||
|
#[cfg(feature = "html")]
|
||||||
|
pub use dioxus_history as history;
|
||||||
|
|
||||||
#[cfg(feature = "macro")]
|
#[cfg(feature = "macro")]
|
||||||
pub use dioxus_rsx as rsx;
|
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 use dioxus_core_macro as core_macro;
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
|
#[cfg(feature = "html")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
|
||||||
|
pub use dioxus_history::{history, History};
|
||||||
|
|
||||||
#[cfg(feature = "hooks")]
|
#[cfg(feature = "hooks")]
|
||||||
pub use crate::hooks::*;
|
pub use crate::hooks::*;
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ rust-version = "1.79.0"
|
|||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
dioxus-html = { workspace = true, default-features = false, optional = true }
|
dioxus-html = { workspace = true, default-features = false, optional = true }
|
||||||
dioxus-document = { workspace = true, optional = true }
|
dioxus-document = { workspace = true, optional = true }
|
||||||
|
dioxus-history = { workspace = true, optional = true }
|
||||||
dioxus-core-macro = { workspace = true, optional = true }
|
dioxus-core-macro = { workspace = true, optional = true }
|
||||||
dioxus-config-macro = { workspace = true, optional = true }
|
dioxus-config-macro = { workspace = true, optional = true }
|
||||||
dioxus-hooks = { 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"]
|
mounted = ["dioxus-web?/mounted", "dioxus-html?/mounted"]
|
||||||
file_engine = ["dioxus-web?/file_engine"]
|
file_engine = ["dioxus-web?/file_engine"]
|
||||||
asset = ["dep:manganis"]
|
asset = ["dep:manganis"]
|
||||||
document = ["dioxus-web?/document", "dioxus-document"]
|
document = ["dioxus-web?/document", "dep:dioxus-document", "dep:dioxus-history"]
|
||||||
|
|
||||||
launch = ["dep:dioxus-config-macro"]
|
launch = ["dep:dioxus-config-macro"]
|
||||||
router = ["dep:dioxus-router"]
|
router = ["dep:dioxus-router"]
|
||||||
|
|
||||||
# Platforms
|
# 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"]
|
desktop = ["dep:dioxus-desktop", "dioxus-fullstack?/desktop", "dioxus-config-macro/desktop"]
|
||||||
mobile = ["dep:dioxus-mobile", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"]
|
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"]
|
web = ["dep:dioxus-web", "dioxus-fullstack?/web", "dioxus-static-site-generation?/web", "dioxus-config-macro/web"]
|
||||||
ssr = ["dep:dioxus-ssr", "dioxus-router?/ssr", "dioxus-config-macro/ssr"]
|
ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"]
|
||||||
liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview", "dioxus-router?/liveview"]
|
liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"]
|
||||||
static-generation = ["dep:dioxus-static-site-generation", "dioxus-config-macro/static-generation"]
|
static-generation = ["dep:dioxus-static-site-generation", "dioxus-config-macro/static-generation"]
|
||||||
axum = ["server"]
|
axum = ["server"]
|
||||||
server = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "dioxus-static-site-generation?/server", "ssr", "dioxus-liveview?/axum", "dep:axum"]
|
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")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "document")))]
|
||||||
pub use dioxus_document as 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(feature = "html")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "html")))]
|
||||||
pub use dioxus_html as html;
|
pub use dioxus_html as html;
|
||||||
@@ -67,6 +71,10 @@ pub mod prelude {
|
|||||||
#[cfg_attr(docsrs, doc(cfg(feature = "document")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "document")))]
|
||||||
pub use dioxus_document as 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(feature = "launch")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
|
||||||
pub use crate::launch::*;
|
pub use crate::launch::*;
|
||||||
|
|||||||
@@ -126,31 +126,26 @@ pub struct NoOpDocument;
|
|||||||
impl Document for NoOpDocument {
|
impl Document for NoOpDocument {
|
||||||
fn eval(&self, _: String) -> Eval {
|
fn eval(&self, _: String) -> Eval {
|
||||||
let owner = generational_box::Owner::default();
|
let owner = generational_box::Owner::default();
|
||||||
let boxed = owner.insert(Box::new(NoOpEvaluator {}) as Box<dyn Evaluator + 'static>);
|
struct NoOpEvaluator;
|
||||||
Eval::new(boxed)
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
/// An evaluator that does nothing
|
fn poll_recv(
|
||||||
#[derive(Default)]
|
&mut self,
|
||||||
pub struct NoOpEvaluator;
|
_: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
|
||||||
|
std::task::Poll::Ready(Err(EvalError::Unsupported))
|
||||||
|
}
|
||||||
|
|
||||||
impl Evaluator for NoOpEvaluator {
|
fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
|
||||||
fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
|
Err(EvalError::Unsupported)
|
||||||
Err(EvalError::Unsupported)
|
}
|
||||||
}
|
}
|
||||||
|
Eval::new(owner.insert(Box::new(NoOpEvaluator)))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
use dioxus_core::prelude::{provide_context, provide_root_context};
|
||||||
//!
|
use std::{rc::Rc, sync::Arc};
|
||||||
//! dioxus-router-core relies on [`HistoryProvider`]s to store the current Route, and possibly a
|
|
||||||
//! history (i.e. a browsers back button) and future (i.e. a browsers forward button).
|
|
||||||
//!
|
|
||||||
//! To integrate dioxus-router with a any type of history, all you have to do is implement the
|
|
||||||
//! [`HistoryProvider`] trait.
|
|
||||||
//!
|
|
||||||
//! dioxus-router contains two built in history providers:
|
|
||||||
//! 1) [`MemoryHistory`] for desktop/mobile/ssr platforms
|
|
||||||
//! 2) [`WebHistory`] for web platforms
|
|
||||||
|
|
||||||
use std::{any::Any, rc::Rc, sync::Arc};
|
|
||||||
|
|
||||||
mod memory;
|
mod memory;
|
||||||
pub use memory::*;
|
pub use memory::*;
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
/// Get the history provider for the current platform if the platform doesn't implement a history functionality.
|
||||||
mod web;
|
pub fn history() -> Rc<dyn History> {
|
||||||
#[cfg(feature = "web")]
|
match dioxus_core::prelude::try_consume_context::<Rc<dyn History>>() {
|
||||||
pub use web::*;
|
Some(history) => history,
|
||||||
#[cfg(feature = "web")]
|
None => {
|
||||||
pub(crate) mod web_history;
|
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")]
|
/// Provide a history context to the current component.
|
||||||
mod liveview;
|
pub fn provide_history_context(history: Rc<dyn History>) {
|
||||||
#[cfg(feature = "liveview")]
|
provide_context(history);
|
||||||
pub use liveview::*;
|
}
|
||||||
|
|
||||||
// #[cfg(feature = "web")]
|
pub trait History {
|
||||||
// mod web_hash;
|
|
||||||
// #[cfg(feature = "web")]
|
|
||||||
// pub use web_hash::*;
|
|
||||||
|
|
||||||
use crate::routable::Routable;
|
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
|
||||||
pub(crate) mod web_scroll;
|
|
||||||
|
|
||||||
/// An integration with some kind of navigation history.
|
|
||||||
///
|
|
||||||
/// Depending on your use case, your implementation may deviate from the described procedure. This
|
|
||||||
/// is fine, as long as both `current_route` and `current_query` match the described format.
|
|
||||||
///
|
|
||||||
/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
|
|
||||||
/// The described behaviors are designed to mimic a web browser, which most users should already
|
|
||||||
/// know. Deviations might confuse them.
|
|
||||||
pub trait HistoryProvider<R: Routable> {
|
|
||||||
/// Get the path of the current URL.
|
/// Get the path of the current URL.
|
||||||
///
|
///
|
||||||
/// **Must start** with `/`. **Must _not_ contain** the prefix.
|
/// **Must start** with `/`. **Must _not_ contain** the prefix.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use dioxus_router::prelude::*;
|
|
||||||
/// # use dioxus::prelude::*;
|
/// # use dioxus::prelude::*;
|
||||||
/// # #[component]
|
/// # #[component]
|
||||||
/// # fn Index() -> Element { VNode::empty() }
|
/// # fn Index() -> Element { VNode::empty() }
|
||||||
@@ -64,14 +38,14 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// #[route("/some-other-page")]
|
/// #[route("/some-other-page")]
|
||||||
/// OtherPage {},
|
/// OtherPage {},
|
||||||
/// }
|
/// }
|
||||||
/// let mut history = MemoryHistory::<Route>::default();
|
/// let mut history = dioxus::history::MemoryHistory::default();
|
||||||
/// assert_eq!(history.current_route().to_string(), "/");
|
/// assert_eq!(history.current_route(), "/");
|
||||||
///
|
///
|
||||||
/// history.push(Route::OtherPage {});
|
/// history.push(Route::OtherPage {}.to_string());
|
||||||
/// assert_eq!(history.current_route().to_string(), "/some-other-page");
|
/// assert_eq!(history.current_route(), "/some-other-page");
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn current_route(&self) -> R;
|
fn current_route(&self) -> String;
|
||||||
|
|
||||||
/// Get the current path prefix of the URL.
|
/// 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`].
|
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use dioxus_router::prelude::*;
|
|
||||||
/// # use dioxus::prelude::*;
|
/// # use dioxus::prelude::*;
|
||||||
/// # #[component]
|
/// # #[component]
|
||||||
/// # fn Index() -> Element { VNode::empty() }
|
/// # fn Index() -> Element { VNode::empty() }
|
||||||
@@ -101,10 +74,10 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// #[route("/other")]
|
/// #[route("/other")]
|
||||||
/// Other {},
|
/// Other {},
|
||||||
/// }
|
/// }
|
||||||
/// let mut history = MemoryHistory::<Route>::default();
|
/// let mut history = dioxus::history::MemoryHistory::default();
|
||||||
/// assert_eq!(history.can_go_back(), false);
|
/// assert_eq!(history.can_go_back(), false);
|
||||||
///
|
///
|
||||||
/// history.push(Route::Other {});
|
/// history.push(Route::Other {}.to_string());
|
||||||
/// assert_eq!(history.can_go_back(), true);
|
/// assert_eq!(history.can_go_back(), true);
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -118,7 +91,6 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// might be called, even if `can_go_back` returns [`false`].
|
/// might be called, even if `can_go_back` returns [`false`].
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use dioxus_router::prelude::*;
|
|
||||||
/// # use dioxus::prelude::*;
|
/// # use dioxus::prelude::*;
|
||||||
/// # #[component]
|
/// # #[component]
|
||||||
/// # fn Index() -> Element { VNode::empty() }
|
/// # fn Index() -> Element { VNode::empty() }
|
||||||
@@ -131,26 +103,25 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// #[route("/some-other-page")]
|
/// #[route("/some-other-page")]
|
||||||
/// OtherPage {},
|
/// OtherPage {},
|
||||||
/// }
|
/// }
|
||||||
/// let mut history = MemoryHistory::<Route>::default();
|
/// let mut history = dioxus::history::MemoryHistory::default();
|
||||||
/// assert_eq!(history.current_route().to_string(), "/");
|
/// assert_eq!(history.current_route(), "/");
|
||||||
///
|
///
|
||||||
/// history.go_back();
|
/// history.go_back();
|
||||||
/// assert_eq!(history.current_route().to_string(), "/");
|
/// assert_eq!(history.current_route(), "/");
|
||||||
///
|
///
|
||||||
/// history.push(Route::OtherPage {});
|
/// history.push(Route::OtherPage {}.to_string());
|
||||||
/// assert_eq!(history.current_route().to_string(), "/some-other-page");
|
/// assert_eq!(history.current_route(), "/some-other-page");
|
||||||
///
|
///
|
||||||
/// history.go_back();
|
/// 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.
|
/// Check whether there is a future page to navigate forward to.
|
||||||
///
|
///
|
||||||
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
|
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use dioxus_router::prelude::*;
|
|
||||||
/// # use dioxus::prelude::*;
|
/// # use dioxus::prelude::*;
|
||||||
/// # #[component]
|
/// # #[component]
|
||||||
/// # fn Index() -> Element { VNode::empty() }
|
/// # fn Index() -> Element { VNode::empty() }
|
||||||
@@ -163,10 +134,10 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// #[route("/some-other-page")]
|
/// #[route("/some-other-page")]
|
||||||
/// OtherPage {},
|
/// OtherPage {},
|
||||||
/// }
|
/// }
|
||||||
/// let mut history = MemoryHistory::<Route>::default();
|
/// let mut history = dioxus::history::MemoryHistory::default();
|
||||||
/// assert_eq!(history.can_go_forward(), false);
|
/// assert_eq!(history.can_go_forward(), false);
|
||||||
///
|
///
|
||||||
/// history.push(Route::OtherPage {});
|
/// history.push(Route::OtherPage {}.to_string());
|
||||||
/// assert_eq!(history.can_go_forward(), false);
|
/// assert_eq!(history.can_go_forward(), false);
|
||||||
///
|
///
|
||||||
/// history.go_back();
|
/// history.go_back();
|
||||||
@@ -183,7 +154,6 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// might be called, even if `can_go_forward` returns [`false`].
|
/// might be called, even if `can_go_forward` returns [`false`].
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use dioxus_router::prelude::*;
|
|
||||||
/// # use dioxus::prelude::*;
|
/// # use dioxus::prelude::*;
|
||||||
/// # #[component]
|
/// # #[component]
|
||||||
/// # fn Index() -> Element { VNode::empty() }
|
/// # fn Index() -> Element { VNode::empty() }
|
||||||
@@ -196,17 +166,17 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// #[route("/some-other-page")]
|
/// #[route("/some-other-page")]
|
||||||
/// OtherPage {},
|
/// OtherPage {},
|
||||||
/// }
|
/// }
|
||||||
/// let mut history = MemoryHistory::<Route>::default();
|
/// let mut history = dioxus::history::MemoryHistory::default();
|
||||||
/// history.push(Route::OtherPage {});
|
/// history.push(Route::OtherPage {}.to_string());
|
||||||
/// assert_eq!(history.current_route(), Route::OtherPage {});
|
/// assert_eq!(history.current_route(), Route::OtherPage {}.to_string());
|
||||||
///
|
///
|
||||||
/// history.go_back();
|
/// history.go_back();
|
||||||
/// assert_eq!(history.current_route(), Route::Index {});
|
/// assert_eq!(history.current_route(), Route::Index {}.to_string());
|
||||||
///
|
///
|
||||||
/// history.go_forward();
|
/// 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.
|
/// Go to another page.
|
||||||
///
|
///
|
||||||
@@ -216,7 +186,6 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// 3. Clear the navigation future.
|
/// 3. Clear the navigation future.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use dioxus_router::prelude::*;
|
|
||||||
/// # use dioxus::prelude::*;
|
/// # use dioxus::prelude::*;
|
||||||
/// # #[component]
|
/// # #[component]
|
||||||
/// # fn Index() -> Element { VNode::empty() }
|
/// # fn Index() -> Element { VNode::empty() }
|
||||||
@@ -229,14 +198,14 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// #[route("/some-other-page")]
|
/// #[route("/some-other-page")]
|
||||||
/// OtherPage {},
|
/// OtherPage {},
|
||||||
/// }
|
/// }
|
||||||
/// let mut history = MemoryHistory::<Route>::default();
|
/// let mut history = dioxus::history::MemoryHistory::default();
|
||||||
/// assert_eq!(history.current_route(), Route::Index {});
|
/// assert_eq!(history.current_route(), Route::Index {}.to_string());
|
||||||
///
|
///
|
||||||
/// history.push(Route::OtherPage {});
|
/// history.push(Route::OtherPage {}.to_string());
|
||||||
/// assert_eq!(history.current_route(), Route::OtherPage {});
|
/// assert_eq!(history.current_route(), Route::OtherPage {}.to_string());
|
||||||
/// assert!(history.can_go_back());
|
/// assert!(history.can_go_back());
|
||||||
/// ```
|
/// ```
|
||||||
fn push(&mut self, route: R);
|
fn push(&self, route: String);
|
||||||
|
|
||||||
/// Replace the current page with another one.
|
/// Replace the current page with another one.
|
||||||
///
|
///
|
||||||
@@ -245,7 +214,6 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// untouched.
|
/// untouched.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use dioxus_router::prelude::*;
|
|
||||||
/// # use dioxus::prelude::*;
|
/// # use dioxus::prelude::*;
|
||||||
/// # #[component]
|
/// # #[component]
|
||||||
/// # fn Index() -> Element { VNode::empty() }
|
/// # fn Index() -> Element { VNode::empty() }
|
||||||
@@ -258,14 +226,14 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// #[route("/some-other-page")]
|
/// #[route("/some-other-page")]
|
||||||
/// OtherPage {},
|
/// OtherPage {},
|
||||||
/// }
|
/// }
|
||||||
/// let mut history = MemoryHistory::<Route>::default();
|
/// let mut history = dioxus::history::MemoryHistory::default();
|
||||||
/// assert_eq!(history.current_route(), Route::Index {});
|
/// assert_eq!(history.current_route(), Route::Index {}.to_string());
|
||||||
///
|
///
|
||||||
/// history.replace(Route::OtherPage {});
|
/// history.replace(Route::OtherPage {}.to_string());
|
||||||
/// assert_eq!(history.current_route(), Route::OtherPage {});
|
/// assert_eq!(history.current_route(), Route::OtherPage {}.to_string());
|
||||||
/// assert!(!history.can_go_back());
|
/// assert!(!history.can_go_back());
|
||||||
/// ```
|
/// ```
|
||||||
fn replace(&mut self, path: R);
|
fn replace(&self, path: String);
|
||||||
|
|
||||||
/// Navigate to an external URL.
|
/// 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.
|
/// Returning [`false`] will cause the router to handle the external navigation failure.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
fn external(&mut self, url: String) -> bool {
|
fn external(&self, url: String) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,120 +251,11 @@ pub trait HistoryProvider<R: Routable> {
|
|||||||
/// Some [`HistoryProvider`]s may receive URL updates from outside the router. When such
|
/// 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.
|
/// updates are received, they should call `callback`, which will cause the router to update.
|
||||||
#[allow(unused_variables)]
|
#[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 {
|
/// Whether the router should include the legacy prevent default attribute instead of the new
|
||||||
fn parse_route(&self, route: &str) -> Result<Rc<dyn Any>, String>;
|
/// prevent default method. This should only be used by liveview.
|
||||||
|
fn include_prevent_default(&self) -> bool {
|
||||||
#[must_use]
|
|
||||||
fn current_route(&self) -> Rc<dyn Any>;
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
fn can_go_back(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn go_back(&mut self);
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
fn can_go_forward(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn go_forward(&mut self);
|
|
||||||
|
|
||||||
fn push(&mut self, route: Rc<dyn Any>);
|
|
||||||
|
|
||||||
fn replace(&mut self, path: Rc<dyn Any>);
|
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
fn external(&mut self, url: String) -> bool {
|
|
||||||
false
|
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>>()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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"
|
serde_json = "1.0.91"
|
||||||
dioxus-html = { workspace = true, features = ["serialize", "mounted"] }
|
dioxus-html = { workspace = true, features = ["serialize", "mounted"] }
|
||||||
dioxus-document = { workspace = true }
|
dioxus-document = { workspace = true }
|
||||||
|
dioxus-history = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||||
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
|
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }
|
||||||
|
|||||||
@@ -1,28 +1,12 @@
|
|||||||
use dioxus_core::ScopeId;
|
use dioxus_core::ScopeId;
|
||||||
use dioxus_document::{Document, Eval, EvalError, Evaluator};
|
use dioxus_document::{Document, Eval, EvalError, Evaluator};
|
||||||
|
use dioxus_history::History;
|
||||||
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::history::LiveviewHistory;
|
||||||
use crate::query::{Query, QueryEngine};
|
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.
|
/// Represents a liveview-target's JavaScript evaluator.
|
||||||
pub(crate) struct LiveviewEvaluator {
|
pub(crate) struct LiveviewEvaluator {
|
||||||
query: Query<serde_json::Value>,
|
query: Query<serde_json::Value>,
|
||||||
@@ -75,3 +59,28 @@ impl Evaluator for LiveviewEvaluator {
|
|||||||
.map_err(|e| EvalError::Communication(e.to_string()))
|
.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 dioxus_core::prelude::spawn;
|
||||||
use crate::routable::Routable;
|
use dioxus_document::Eval;
|
||||||
use dioxus_lib::document::Eval;
|
use dioxus_history::History;
|
||||||
use dioxus_lib::prelude::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::{Mutex, RwLock};
|
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.
|
/// A [`HistoryProvider`] that evaluates history through JS.
|
||||||
pub struct LiveviewHistory<R: Routable>
|
pub(crate) struct LiveviewHistory {
|
||||||
where
|
action_tx: tokio::sync::mpsc::UnboundedSender<Action>,
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
timeline: Arc<Mutex<Timeline>>,
|
||||||
{
|
|
||||||
action_tx: tokio::sync::mpsc::UnboundedSender<Action<R>>,
|
|
||||||
timeline: Arc<Mutex<Timeline<R>>>,
|
|
||||||
updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>>,
|
updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Timeline<R: Routable>
|
struct Timeline {
|
||||||
where
|
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
current_index: usize,
|
current_index: usize,
|
||||||
routes: BTreeMap<usize, R>,
|
routes: BTreeMap<usize, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -30,28 +24,22 @@ struct State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
struct Session<R: Routable>
|
struct Session {
|
||||||
where
|
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
#[serde(with = "routes")]
|
#[serde(with = "routes")]
|
||||||
routes: BTreeMap<usize, R>,
|
routes: BTreeMap<usize, String>,
|
||||||
last_visited: usize,
|
last_visited: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Action<R: Routable> {
|
enum Action {
|
||||||
GoBack,
|
GoBack,
|
||||||
GoForward,
|
GoForward,
|
||||||
Push(R),
|
Push(String),
|
||||||
Replace(R),
|
Replace(String),
|
||||||
External(String),
|
External(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Routable> Timeline<R>
|
impl Timeline {
|
||||||
where
|
fn new(initial_path: String) -> Self {
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
fn new(initial_path: R) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
current_index: 0,
|
current_index: 0,
|
||||||
routes: BTreeMap::from([(0, initial_path)]),
|
routes: BTreeMap::from([(0, initial_path)]),
|
||||||
@@ -60,9 +48,9 @@ where
|
|||||||
|
|
||||||
fn init(
|
fn init(
|
||||||
&mut self,
|
&mut self,
|
||||||
route: R,
|
route: String,
|
||||||
state: Option<State>,
|
state: Option<State>,
|
||||||
session: Option<Session<R>>,
|
session: Option<Session>,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
) -> State {
|
) -> State {
|
||||||
if let Some(session) = session {
|
if let Some(session) = session {
|
||||||
@@ -88,7 +76,7 @@ where
|
|||||||
state
|
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 {
|
if let Some(state) = state {
|
||||||
self.current_index = state.index;
|
self.current_index = state.index;
|
||||||
self.routes.insert(self.current_index, route);
|
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
|
// top of stack
|
||||||
let index = self.current_index + 1;
|
let index = self.current_index + 1;
|
||||||
self.current_index = index;
|
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);
|
self.routes.insert(self.current_index, route);
|
||||||
State {
|
State {
|
||||||
index: self.current_index,
|
index: self.current_index,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> &R {
|
fn current_route(&self) -> &str {
|
||||||
&self.routes[&self.current_index]
|
&self.routes[&self.current_index]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn session(&self) -> Session<R> {
|
fn session(&self) -> Session {
|
||||||
Session {
|
Session {
|
||||||
routes: self.routes.clone(),
|
routes: self.routes.clone(),
|
||||||
last_visited: self.current_index,
|
last_visited: self.current_index,
|
||||||
@@ -128,30 +116,19 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Routable> Default for LiveviewHistory<R>
|
impl LiveviewHistory {
|
||||||
where
|
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<R: Routable> LiveviewHistory<R>
|
|
||||||
where
|
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
/// Create a [`LiveviewHistory`] in the given scope.
|
/// Create a [`LiveviewHistory`] in the given scope.
|
||||||
/// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
|
/// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics if the function is not called in a dioxus runtime with a Liveview context.
|
/// 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(
|
Self::new_with_initial_path(
|
||||||
"/".parse().unwrap_or_else(|err| {
|
"/".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)
|
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
|
||||||
///
|
///
|
||||||
/// Panics if the function is not called in a dioxus runtime with a Liveview context.
|
/// 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 {
|
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<R>>();
|
let (action_tx, mut action_rx) = tokio::sync::mpsc::unbounded_channel::<Action>();
|
||||||
|
|
||||||
let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
|
let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
|
||||||
let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
|
let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
|
||||||
Arc::new(RwLock::new(Arc::new(|| {})));
|
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
|
// Listen to server actions
|
||||||
spawn({
|
spawn({
|
||||||
let timeline = timeline.clone();
|
let timeline = timeline.clone();
|
||||||
let create_eval = create_eval.clone();
|
let create_eval = eval.clone();
|
||||||
async move {
|
async move {
|
||||||
loop {
|
loop {
|
||||||
let eval = action_rx.recv().await.expect("sender to exist");
|
let eval = action_rx.recv().await.expect("sender to exist");
|
||||||
@@ -236,7 +208,7 @@ where
|
|||||||
spawn({
|
spawn({
|
||||||
let updater = updater_callback.clone();
|
let updater = updater_callback.clone();
|
||||||
let timeline = timeline.clone();
|
let timeline = timeline.clone();
|
||||||
let create_eval = create_eval.clone();
|
let create_eval = eval.clone();
|
||||||
async move {
|
async move {
|
||||||
let mut popstate_eval = {
|
let mut popstate_eval = {
|
||||||
let init_eval = create_eval(
|
let init_eval = create_eval(
|
||||||
@@ -249,16 +221,11 @@ where
|
|||||||
];
|
];
|
||||||
"#,
|
"#,
|
||||||
).await.expect("serializable state");
|
).await.expect("serializable state");
|
||||||
let (route, state, session, depth) = serde_json::from_value::<(
|
let (route, state, session, depth) =
|
||||||
String,
|
serde_json::from_value::<(String, Option<State>, Option<Session>, usize)>(
|
||||||
Option<State>,
|
init_eval,
|
||||||
Option<Session<R>>,
|
)
|
||||||
usize,
|
.expect("serializable state");
|
||||||
)>(init_eval)
|
|
||||||
.expect("serializable state");
|
|
||||||
let Ok(route) = R::from_str(&route.to_string()) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let mut timeline = timeline.lock().expect("unpoisoned mutex");
|
let mut timeline = timeline.lock().expect("unpoisoned mutex");
|
||||||
let state = timeline.init(route.clone(), state, session, depth);
|
let state = timeline.init(route.clone(), state, session, depth);
|
||||||
let state = serde_json::to_string(&state).expect("serializable state");
|
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)
|
let (route, state) = serde_json::from_value::<(String, Option<State>)>(event)
|
||||||
.expect("serializable state");
|
.expect("serializable state");
|
||||||
let Ok(route) = R::from_str(&route.to_string()) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let mut timeline = timeline.lock().expect("unpoisoned mutex");
|
let mut timeline = timeline.lock().expect("unpoisoned mutex");
|
||||||
let state = timeline.update(route.clone(), state);
|
let state = timeline.update(route.clone(), state);
|
||||||
let state = serde_json::to_string(&state).expect("serializable state");
|
let state = serde_json::to_string(&state).expect("serializable state");
|
||||||
@@ -322,34 +286,31 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Routable> HistoryProvider<R> for LiveviewHistory<R>
|
impl History for LiveviewHistory {
|
||||||
where
|
fn go_back(&self) {
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
fn go_back(&mut self) {
|
|
||||||
let _ = self.action_tx.send(Action::GoBack);
|
let _ = self.action_tx.send(Action::GoBack);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn go_forward(&mut self) {
|
fn go_forward(&self) {
|
||||||
let _ = self.action_tx.send(Action::GoForward);
|
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));
|
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));
|
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));
|
let _ = self.action_tx.send(Action::External(url));
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_route(&self) -> R {
|
fn current_route(&self) -> String {
|
||||||
let timeline = self.timeline.lock().expect("unpoisoned mutex");
|
let timeline = self.timeline.lock().expect("unpoisoned mutex");
|
||||||
timeline.current_route().clone()
|
timeline.current_route().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_go_back(&self) -> bool {
|
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();
|
let mut updater_callback = self.updater_callback.write().unwrap();
|
||||||
*updater_callback = callback;
|
*updater_callback = callback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod routes {
|
mod routes {
|
||||||
use crate::prelude::Routable;
|
|
||||||
use core::str::FromStr;
|
|
||||||
use serde::de::{MapAccess, Visitor};
|
use serde::de::{MapAccess, Visitor};
|
||||||
use serde::{ser::SerializeMap, Deserializer, Serializer};
|
use serde::{ser::SerializeMap, Deserializer, Serializer};
|
||||||
use std::collections::BTreeMap;
|
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
|
where
|
||||||
S: Serializer,
|
S: Serializer,
|
||||||
R: Routable,
|
|
||||||
{
|
{
|
||||||
let mut map = serializer.serialize_map(Some(routes.len()))?;
|
let mut map = serializer.serialize_map(Some(routes.len()))?;
|
||||||
for (index, route) in routes.iter() {
|
for (index, route) in routes.iter() {
|
||||||
@@ -402,22 +360,14 @@ mod routes {
|
|||||||
map.end()
|
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
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
R: Routable,
|
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
{
|
||||||
struct BTreeMapVisitor<R> {
|
struct BTreeMapVisitor {}
|
||||||
marker: std::marker::PhantomData<R>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de, R> Visitor<'de> for BTreeMapVisitor<R>
|
impl<'de> Visitor<'de> for BTreeMapVisitor {
|
||||||
where
|
type Value = BTreeMap<usize, String>;
|
||||||
R: Routable,
|
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
type Value = BTreeMap<usize, R>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
formatter.write_str("a map with indices and routable values")
|
formatter.write_str("a map with indices and routable values")
|
||||||
@@ -430,15 +380,12 @@ mod routes {
|
|||||||
let mut routes = BTreeMap::new();
|
let mut routes = BTreeMap::new();
|
||||||
while let Some((index, route)) = map.next_entry::<String, String>()? {
|
while let Some((index, route)) = map.next_entry::<String, String>()? {
|
||||||
let index = index.parse::<usize>().map_err(serde::de::Error::custom)?;
|
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);
|
routes.insert(index, route);
|
||||||
}
|
}
|
||||||
Ok(routes)
|
Ok(routes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deserializer.deserialize_map(BTreeMapVisitor {
|
deserializer.deserialize_map(BTreeMapVisitor {})
|
||||||
marker: std::marker::PhantomData,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,8 +13,9 @@ use dioxus_interpreter_js::NATIVE_JS;
|
|||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
pub use pool::*;
|
pub use pool::*;
|
||||||
mod config;
|
mod config;
|
||||||
mod eval;
|
mod document;
|
||||||
mod events;
|
mod events;
|
||||||
|
mod history;
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
#[cfg(feature = "axum")]
|
#[cfg(feature = "axum")]
|
||||||
pub mod launch;
|
pub mod launch;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
document::init_document,
|
||||||
element::LiveviewElement,
|
element::LiveviewElement,
|
||||||
eval::init_eval,
|
|
||||||
events::SerializedHtmlEventConverter,
|
events::SerializedHtmlEventConverter,
|
||||||
query::{QueryEngine, QueryResult},
|
query::{QueryEngine, QueryResult},
|
||||||
LiveViewError,
|
LiveViewError,
|
||||||
@@ -130,9 +130,9 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
|||||||
// Create the a proxy for query engine
|
// Create the a proxy for query engine
|
||||||
let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
let query_engine = QueryEngine::new(query_tx);
|
let query_engine = QueryEngine::new(query_tx);
|
||||||
vdom.in_runtime(|| {
|
vdom.runtime().on_scope(ScopeId::ROOT, || {
|
||||||
ScopeId::ROOT.provide_context(query_engine.clone());
|
provide_context(query_engine.clone());
|
||||||
init_eval();
|
init_document();
|
||||||
});
|
});
|
||||||
|
|
||||||
// pin the futures so we can use select!
|
// pin the futures so we can use select!
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ let dioxus = {
|
|||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
/// Tracks what query ids are currently active
|
/// Tracks what query ids are currently active
|
||||||
|
|
||||||
pub(crate) struct SharedSlab<T = ()> {
|
pub(crate) struct SharedSlab<T = ()> {
|
||||||
pub(crate) slab: Rc<RefCell<Slab<T>>>,
|
pub(crate) slab: Rc<RefCell<Slab<T>>>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,7 +242,23 @@ impl Route {
|
|||||||
quote! {
|
quote! {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
(#last_index.., Self::#name { #field_name, .. }) => {
|
(#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),
|
Redirect(&'a Redirect),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> RouteTreeSegmentData<'a> {
|
impl RouteTreeSegmentData<'_> {
|
||||||
pub fn to_tokens(
|
pub fn to_tokens(
|
||||||
&self,
|
&self,
|
||||||
nests: &[Nest],
|
nests: &[Nest],
|
||||||
|
|||||||
@@ -11,24 +11,11 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-lib = { workspace = true }
|
dioxus-lib = { workspace = true }
|
||||||
|
dioxus-history = { workspace = true }
|
||||||
dioxus-router-macro = { workspace = true }
|
dioxus-router-macro = { workspace = true }
|
||||||
gloo = { version = "0.8.0", optional = true }
|
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
|
||||||
serde_json = { version = "1.0.91", optional = true }
|
|
||||||
url = "2.3.1"
|
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 }
|
dioxus-cli-config = { workspace = true }
|
||||||
rustversion = "1.0.17"
|
rustversion = "1.0.17"
|
||||||
|
|
||||||
@@ -36,18 +23,11 @@ rustversion = "1.0.17"
|
|||||||
# dev-dependncey crates
|
# dev-dependncey crates
|
||||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||||
console_error_panic_hook = "0.1.7"
|
console_error_panic_hook = "0.1.7"
|
||||||
dioxus-router = { workspace = true, features = ["web"] }
|
|
||||||
# dioxus-web = { workspace = true }
|
|
||||||
gloo = "0.8.0"
|
gloo = "0.8.0"
|
||||||
wasm-bindgen-test = "0.3.33"
|
wasm-bindgen-test = "0.3.33"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
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]
|
[dev-dependencies]
|
||||||
axum = { workspace = true, features = ["ws"] }
|
axum = { workspace = true, features = ["ws"] }
|
||||||
@@ -57,6 +37,7 @@ criterion = { workspace = true, features = ["async_tokio", "html_reports"] }
|
|||||||
ciborium = { version = "0.2.1" }
|
ciborium = { version = "0.2.1" }
|
||||||
base64 = { version = "0.21.0" }
|
base64 = { version = "0.21.0" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
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 },
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/// Components that allow the macro to add child routers. This component provides a context
|
||||||
|
/// to the child router that maps child routes to root routes and vice versa.
|
||||||
|
use dioxus_lib::prelude::*;
|
||||||
|
|
||||||
|
use crate::prelude::Routable;
|
||||||
|
|
||||||
|
/// Maps a child route into the root router and vice versa
|
||||||
|
// NOTE: Currently child routers only support simple static prefixes, but this
|
||||||
|
// API could be expanded to support dynamic prefixes as well
|
||||||
|
pub(crate) struct ChildRouteMapping<R> {
|
||||||
|
format_route_as_root_route: fn(R) -> String,
|
||||||
|
parse_route_from_root_route: fn(&str) -> Option<R>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> ChildRouteMapping<R> {
|
||||||
|
pub(crate) fn format_route_as_root_route(&self, route: R) -> String {
|
||||||
|
(self.format_route_as_root_route)(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_route_from_root_route(&self, route: &str) -> Option<R> {
|
||||||
|
(self.parse_route_from_root_route)(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the formatter that handles adding and stripping the prefix from a child route
|
||||||
|
pub(crate) fn consume_child_route_mapping<R: Routable>() -> Option<ChildRouteMapping<R>> {
|
||||||
|
try_consume_context()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> Clone for ChildRouteMapping<R> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
format_route_as_root_route: self.format_route_as_root_route,
|
||||||
|
parse_route_from_root_route: self.parse_route_from_root_route,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Props for the [`ChildHistoryProvider`] component.
|
||||||
|
#[derive(Props, Clone)]
|
||||||
|
pub struct ChildRouterProps<R: Routable> {
|
||||||
|
/// The child route to render
|
||||||
|
route: R,
|
||||||
|
/// Take a parent route and return a child route or none if the route is not part of the child
|
||||||
|
parse_route_from_root_route: fn(&str) -> Option<R>,
|
||||||
|
/// Take a child route and return a parent route
|
||||||
|
format_route_as_root_route: fn(R) -> String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Routable> PartialEq for ChildRouterProps<R> {
|
||||||
|
fn eq(&self, _: &Self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A component that provides a [`History`] to a child router. The `#[child]` attribute on the router macro will insert this automatically.
|
||||||
|
#[component]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub fn ChildRouter<R: Routable>(props: ChildRouterProps<R>) -> Element {
|
||||||
|
use_hook(|| {
|
||||||
|
provide_context(ChildRouteMapping {
|
||||||
|
format_route_as_root_route: props.format_route_as_root_route,
|
||||||
|
parse_route_from_root_route: props.parse_route_from_root_route,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
props.route.render(0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
use dioxus_history::{provide_history_context, History};
|
||||||
|
use dioxus_lib::prelude::*;
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// A component that provides a [`History`] for all child [`Router`] components. Renderers generally provide a default history automatically.
|
||||||
|
#[component]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub fn HistoryProvider(
|
||||||
|
/// The history to provide to child components.
|
||||||
|
history: Callback<(), Rc<dyn History>>,
|
||||||
|
/// The children to render within the history provider.
|
||||||
|
children: Element,
|
||||||
|
) -> Element {
|
||||||
|
use_hook(|| {
|
||||||
|
provide_history_context(history(()));
|
||||||
|
});
|
||||||
|
|
||||||
|
children
|
||||||
|
}
|
||||||
@@ -1,83 +1,14 @@
|
|||||||
#![allow(clippy::type_complexity)]
|
#![allow(clippy::type_complexity)]
|
||||||
|
|
||||||
use std::any::Any;
|
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use dioxus_lib::prelude::*;
|
use dioxus_lib::prelude::*;
|
||||||
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::navigation::NavigationTarget;
|
use crate::navigation::NavigationTarget;
|
||||||
use crate::prelude::Routable;
|
|
||||||
use crate::utils::use_router_internal::use_router_internal;
|
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`].
|
/// The properties for a [`Link`].
|
||||||
#[derive(Props, Clone, PartialEq)]
|
#[derive(Props, Clone, PartialEq)]
|
||||||
pub struct LinkProps {
|
pub struct LinkProps {
|
||||||
@@ -119,7 +50,7 @@ pub struct LinkProps {
|
|||||||
|
|
||||||
/// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
|
/// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
|
||||||
#[props(into)]
|
#[props(into)]
|
||||||
pub to: IntoRoutable,
|
pub to: NavigationTarget,
|
||||||
|
|
||||||
#[props(extends = GlobalAttributes)]
|
#[props(extends = GlobalAttributes)]
|
||||||
attributes: Vec<Attribute>,
|
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 {
|
let href = match &to {
|
||||||
IntoRoutable::FromStr(url) => url.to_string(),
|
NavigationTarget::Internal(url) => url.clone(),
|
||||||
IntoRoutable::Route(route) => router.any_route_to_string(&**route),
|
NavigationTarget::External(route) => route.clone(),
|
||||||
};
|
};
|
||||||
let parsed_route: NavigationTarget<Rc<dyn Any>> = router.resolve_into_routable(to.clone());
|
|
||||||
|
|
||||||
let mut class_ = String::new();
|
let mut class_ = String::new();
|
||||||
if let Some(c) = class {
|
if let Some(c) = class {
|
||||||
@@ -257,7 +187,7 @@ pub fn Link(props: LinkProps) -> Element {
|
|||||||
|
|
||||||
let tag_target = new_tab.then_some("_blank");
|
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 is_router_nav = !is_external && !new_tab;
|
||||||
let rel = rel.or_else(|| is_external.then_some("noopener noreferrer".to_string()));
|
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();
|
event.prevent_default();
|
||||||
|
|
||||||
if do_default && is_router_nav {
|
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 {
|
if let Some(handler) = onclick {
|
||||||
@@ -301,8 +231,8 @@ pub fn Link(props: LinkProps) -> Element {
|
|||||||
let liveview_prevent_default = {
|
let liveview_prevent_default = {
|
||||||
// If the event is a click with the left mouse button and no modifiers, prevent the default action
|
// 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
|
// and navigate to the href with client side routing
|
||||||
router.is_liveview().then_some(
|
router.include_prevent_default().then_some(
|
||||||
"if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) { event.preventDefault() }"
|
"if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) { event.preventDefault() }"
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ use dioxus_lib::prelude::*;
|
|||||||
/// # #[component]
|
/// # #[component]
|
||||||
/// # fn App() -> Element {
|
/// # fn App() -> Element {
|
||||||
/// # rsx! {
|
/// # rsx! {
|
||||||
/// # Router::<Route> {
|
/// # dioxus_router::components::HistoryProvider {
|
||||||
/// # config: || RouterConfig::default().history(MemoryHistory::with_initial_path(Route::Child {}))
|
/// # 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 crate::prelude::{outlet::OutletContext, RouterContext};
|
||||||
|
|
||||||
use_hook(|| {
|
use_hook(|| {
|
||||||
provide_router_context(RouterContext::new(
|
provide_router_context(RouterContext::new(props.config.call(())));
|
||||||
props.config.call(()),
|
|
||||||
schedule_update_any(),
|
|
||||||
));
|
|
||||||
|
|
||||||
provide_context(OutletContext::<R> {
|
provide_context(OutletContext::<R> {
|
||||||
current_level: 0,
|
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.
|
/// Acquire the navigator without subscribing to updates.
|
||||||
///
|
///
|
||||||
@@ -48,14 +48,17 @@ impl Navigator {
|
|||||||
/// Push a new location.
|
/// Push a new location.
|
||||||
///
|
///
|
||||||
/// The previous location will be available to go back to.
|
/// 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)
|
self.0.push(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the current location.
|
/// Replace the current location.
|
||||||
///
|
///
|
||||||
/// The previous location will **not** be available to go back to.
|
/// 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)
|
self.0.replace(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
rc::Rc,
|
sync::{Arc, Mutex},
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use dioxus_history::history;
|
||||||
use dioxus_lib::prelude::*;
|
use dioxus_lib::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
navigation::NavigationTarget,
|
components::child_router::consume_child_route_mapping, navigation::NavigationTarget,
|
||||||
prelude::{AnyHistoryProvider, IntoRoutable, SiteMapSegment},
|
prelude::SiteMapSegment, routable::Routable, router_cfg::RouterConfig,
|
||||||
routable::Routable,
|
|
||||||
router_cfg::RouterConfig,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// This context is set in the root of the virtual dom if there is a router present.
|
/// 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.
|
/// A function the router will call after every routing update.
|
||||||
pub(crate) type RoutingCallback<R> =
|
pub(crate) type RoutingCallback<R> =
|
||||||
Arc<dyn Fn(GenericRouterContext<R>) -> Option<NavigationTarget<R>>>;
|
Arc<dyn Fn(GenericRouterContext<R>) -> Option<NavigationTarget<R>>>;
|
||||||
pub(crate) type AnyRoutingCallback =
|
pub(crate) type AnyRoutingCallback = Arc<dyn Fn(RouterContext) -> Option<NavigationTarget>>;
|
||||||
Arc<dyn Fn(RouterContext) -> Option<NavigationTarget<Rc<dyn Any>>>>;
|
|
||||||
|
|
||||||
struct RouterContextInner {
|
struct RouterContextInner {
|
||||||
/// The current prefix.
|
/// The current prefix.
|
||||||
prefix: Option<String>,
|
prefix: Option<String>,
|
||||||
|
|
||||||
history: Box<dyn AnyHistoryProvider>,
|
|
||||||
|
|
||||||
unresolved_error: Option<ExternalNavigationFailure>,
|
unresolved_error: Option<ExternalNavigationFailure>,
|
||||||
|
|
||||||
subscribers: Arc<RwLock<HashSet<ScopeId>>>,
|
subscribers: Arc<Mutex<HashSet<ReactiveContext>>>,
|
||||||
subscriber_update: Arc<dyn Fn(ScopeId)>,
|
|
||||||
routing_callback: Option<AnyRoutingCallback>,
|
routing_callback: Option<AnyRoutingCallback>,
|
||||||
|
|
||||||
failure_external_navigation: fn() -> Element,
|
failure_external_navigation: fn() -> Element,
|
||||||
|
|
||||||
any_route_to_string: fn(&dyn Any) -> String,
|
internal_route: fn(&str) -> bool,
|
||||||
|
|
||||||
site_map: &'static [SiteMapSegment],
|
site_map: &'static [SiteMapSegment],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RouterContextInner {
|
impl RouterContextInner {
|
||||||
fn update_subscribers(&self) {
|
fn update_subscribers(&self) {
|
||||||
let update = &self.subscriber_update;
|
for &id in self.subscribers.lock().unwrap().iter() {
|
||||||
for &id in self.subscribers.read().unwrap().iter() {
|
id.mark_dirty();
|
||||||
update(id);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
fn external(&mut self, external: String) -> Option<ExternalNavigationFailure> {
|
||||||
match self.history.external(external.clone()) {
|
match history().external(external.clone()) {
|
||||||
true => None,
|
true => None,
|
||||||
false => {
|
false => {
|
||||||
let failure = ExternalNavigationFailure(external);
|
let failure = ExternalNavigationFailure(external);
|
||||||
@@ -99,23 +97,17 @@ pub struct RouterContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RouterContext {
|
impl RouterContext {
|
||||||
pub(crate) fn new<R: Routable + 'static>(
|
pub(crate) fn new<R: Routable + 'static>(cfg: RouterConfig<R>) -> Self
|
||||||
mut cfg: RouterConfig<R>,
|
|
||||||
mark_dirty: Arc<dyn Fn(ScopeId) + Sync + Send>,
|
|
||||||
) -> Self
|
|
||||||
where
|
where
|
||||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||||
{
|
{
|
||||||
let subscriber_update = mark_dirty.clone();
|
let subscribers = Arc::new(Mutex::new(HashSet::new()));
|
||||||
let subscribers = Arc::new(RwLock::new(HashSet::new()));
|
let mapping = consume_child_route_mapping();
|
||||||
|
|
||||||
let mut myself = RouterContextInner {
|
let myself = RouterContextInner {
|
||||||
prefix: Default::default(),
|
prefix: Default::default(),
|
||||||
history: cfg.take_history(),
|
|
||||||
unresolved_error: None,
|
unresolved_error: None,
|
||||||
subscribers: subscribers.clone(),
|
subscribers: subscribers.clone(),
|
||||||
subscriber_update,
|
|
||||||
|
|
||||||
routing_callback: cfg.on_update.map(|update| {
|
routing_callback: cfg.on_update.map(|update| {
|
||||||
Arc::new(move |ctx| {
|
Arc::new(move |ctx| {
|
||||||
let ctx = GenericRouterContext {
|
let ctx = GenericRouterContext {
|
||||||
@@ -123,42 +115,30 @@ impl RouterContext {
|
|||||||
_marker: std::marker::PhantomData,
|
_marker: std::marker::PhantomData,
|
||||||
};
|
};
|
||||||
update(ctx).map(|t| match t {
|
update(ctx).map(|t| match t {
|
||||||
NavigationTarget::Internal(r) => {
|
NavigationTarget::Internal(r) => match mapping.as_ref() {
|
||||||
NavigationTarget::Internal(Rc::new(r) as Rc<dyn Any>)
|
Some(mapping) => {
|
||||||
}
|
NavigationTarget::Internal(mapping.format_route_as_root_route(r))
|
||||||
|
}
|
||||||
|
None => NavigationTarget::Internal(r.to_string()),
|
||||||
|
},
|
||||||
NavigationTarget::External(s) => NavigationTarget::External(s),
|
NavigationTarget::External(s) => NavigationTarget::External(s),
|
||||||
})
|
})
|
||||||
})
|
}) as Arc<dyn Fn(RouterContext) -> Option<NavigationTarget>>
|
||||||
as Arc<dyn Fn(RouterContext) -> Option<NavigationTarget<Rc<dyn Any>>>>
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
failure_external_navigation: cfg.failure_external_navigation,
|
failure_external_navigation: cfg.failure_external_navigation,
|
||||||
|
|
||||||
any_route_to_string: |route| {
|
internal_route: |route| R::from_str(route).is_ok(),
|
||||||
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()
|
|
||||||
},
|
|
||||||
|
|
||||||
site_map: R::SITE_MAP,
|
site_map: R::SITE_MAP,
|
||||||
};
|
};
|
||||||
|
|
||||||
// set the updater
|
// set the updater
|
||||||
{
|
history().updater(Arc::new(move || {
|
||||||
myself.history.updater(Arc::new(move || {
|
for &rc in subscribers.lock().unwrap().iter() {
|
||||||
for &id in subscribers.read().unwrap().iter() {
|
rc.mark_dirty();
|
||||||
(mark_dirty)(id);
|
}
|
||||||
}
|
}));
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
inner: CopyValue::new_in_scope(myself, ScopeId::ROOT),
|
inner: CopyValue::new_in_scope(myself, ScopeId::ROOT),
|
||||||
@@ -167,41 +147,27 @@ impl RouterContext {
|
|||||||
|
|
||||||
/// Check if the router is running in a liveview context
|
/// Check if the router is running in a liveview context
|
||||||
/// We do some slightly weird things for liveview because of the network boundary
|
/// We do some slightly weird things for liveview because of the network boundary
|
||||||
pub fn is_liveview(&self) -> bool {
|
pub(crate) fn include_prevent_default(&self) -> bool {
|
||||||
#[cfg(feature = "liveview")]
|
history().include_prevent_default()
|
||||||
{
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether there is a previous page to navigate back to.
|
/// Check whether there is a previous page to navigate back to.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn can_go_back(&self) -> bool {
|
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.
|
/// Check whether there is a future page to navigate forward to.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn can_go_forward(&self) -> bool {
|
pub fn can_go_forward(&self) -> bool {
|
||||||
self.inner.read().history.can_go_forward()
|
history().can_go_forward()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Go back to the previous location.
|
/// Go back to the previous location.
|
||||||
///
|
///
|
||||||
/// Will fail silently if there is no previous location to go to.
|
/// Will fail silently if there is no previous location to go to.
|
||||||
pub fn go_back(&self) {
|
pub fn go_back(&self) {
|
||||||
{
|
history().go_back();
|
||||||
self.inner.write_unchecked().history.go_back();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.change_route();
|
self.change_route();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,21 +175,15 @@ impl RouterContext {
|
|||||||
///
|
///
|
||||||
/// Will fail silently if there is no next location to go to.
|
/// Will fail silently if there is no next location to go to.
|
||||||
pub fn go_forward(&self) {
|
pub fn go_forward(&self) {
|
||||||
{
|
history().go_forward();
|
||||||
self.inner.write_unchecked().history.go_forward();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.change_route();
|
self.change_route();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn push_any(
|
pub(crate) fn push_any(&self, target: NavigationTarget) -> Option<ExternalNavigationFailure> {
|
||||||
&self,
|
|
||||||
target: NavigationTarget<Rc<dyn Any>>,
|
|
||||||
) -> Option<ExternalNavigationFailure> {
|
|
||||||
{
|
{
|
||||||
let mut write = self.inner.write_unchecked();
|
let mut write = self.inner.write_unchecked();
|
||||||
match target {
|
match target {
|
||||||
NavigationTarget::Internal(p) => write.history.push(p),
|
NavigationTarget::Internal(p) => history().push(p),
|
||||||
NavigationTarget::External(e) => return write.external(e),
|
NavigationTarget::External(e) => return write.external(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,12 +194,15 @@ impl RouterContext {
|
|||||||
/// Push a new location.
|
/// Push a new location.
|
||||||
///
|
///
|
||||||
/// The previous location will be available to go back to.
|
/// 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> {
|
||||||
let target = self.resolve_into_routable(target.into());
|
let target = target.into();
|
||||||
{
|
{
|
||||||
let mut write = self.inner.write_unchecked();
|
let mut write = self.inner.write_unchecked();
|
||||||
match target {
|
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),
|
NavigationTarget::External(e) => return write.external(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,13 +213,18 @@ impl RouterContext {
|
|||||||
/// Replace the current location.
|
/// Replace the current location.
|
||||||
///
|
///
|
||||||
/// The previous location will **not** be available to go back to.
|
/// The previous location will **not** be available to go back to.
|
||||||
pub fn replace(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
|
pub fn replace(
|
||||||
let target = self.resolve_into_routable(target.into());
|
&self,
|
||||||
|
target: impl Into<NavigationTarget>,
|
||||||
|
) -> Option<ExternalNavigationFailure> {
|
||||||
|
let target = target.into();
|
||||||
{
|
{
|
||||||
let mut state = self.inner.write_unchecked();
|
let mut state = self.inner.write_unchecked();
|
||||||
match target {
|
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),
|
NavigationTarget::External(e) => return state.external(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,56 +234,34 @@ impl RouterContext {
|
|||||||
|
|
||||||
/// The route that is currently active.
|
/// The route that is currently active.
|
||||||
pub fn current<R: Routable>(&self) -> R {
|
pub fn current<R: Routable>(&self) -> R {
|
||||||
self.inner
|
let absolute_route = self.full_route_string();
|
||||||
.read()
|
// If this is a child route, map the absolute route to the child route before parsing
|
||||||
.history
|
let mapping = consume_child_route_mapping::<R>();
|
||||||
.current_route()
|
match mapping.as_ref() {
|
||||||
.downcast::<R>()
|
Some(mapping) => mapping
|
||||||
.unwrap()
|
.parse_route_from_root_route(&absolute_route)
|
||||||
.as_ref()
|
.unwrap_or_else(|| {
|
||||||
.clone()
|
panic!("route's display implementation must be parsable by FromStr")
|
||||||
}
|
}),
|
||||||
|
None => R::from_str(&absolute_route).unwrap_or_else(|_| {
|
||||||
/// The route that is currently active.
|
panic!("route's display implementation must be parsable by FromStr")
|
||||||
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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
/// The prefix that is currently active.
|
||||||
pub fn prefix(&self) -> Option<String> {
|
pub fn prefix(&self) -> Option<String> {
|
||||||
self.inner.read().prefix.clone()
|
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
|
/// Clear any unresolved errors
|
||||||
pub fn clear_error(&self) {
|
pub fn clear_error(&self) {
|
||||||
let mut write_inner = self.inner.write_unchecked();
|
let mut write_inner = self.inner.write_unchecked();
|
||||||
@@ -330,11 +276,12 @@ impl RouterContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn render_error(&self) -> Option<Element> {
|
pub(crate) fn render_error(&self) -> Option<Element> {
|
||||||
let inner_read = self.inner.write_unchecked();
|
let inner_write = self.inner.write_unchecked();
|
||||||
inner_read
|
inner_write.subscribe_to_current_context();
|
||||||
|
inner_write
|
||||||
.unresolved_error
|
.unresolved_error
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|_| (inner_read.failure_external_navigation)())
|
.map(|_| (inner_write.failure_external_navigation)())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn change_route(&self) -> Option<ExternalNavigationFailure> {
|
fn change_route(&self) -> Option<ExternalNavigationFailure> {
|
||||||
@@ -346,7 +293,10 @@ impl RouterContext {
|
|||||||
if let Some(new) = callback(myself) {
|
if let Some(new) = callback(myself) {
|
||||||
let mut self_write = self.inner.write_unchecked();
|
let mut self_write = self.inner.write_unchecked();
|
||||||
match new {
|
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),
|
NavigationTarget::External(e) => return self_write.external(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,6 +306,10 @@ impl RouterContext {
|
|||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn internal_route(&self, route: &str) -> bool {
|
||||||
|
(self.inner.read().internal_route)(route)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GenericRouterContext<R> {
|
pub struct GenericRouterContext<R> {
|
||||||
@@ -426,16 +380,6 @@ where
|
|||||||
self.inner.prefix()
|
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
|
/// Clear any unresolved errors
|
||||||
pub fn clear_error(&self) {
|
pub fn clear_error(&self) {
|
||||||
self.inner.clear_error()
|
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;
|
mod router;
|
||||||
pub use router::*;
|
pub use router::*;
|
||||||
|
|
||||||
|
mod history_provider;
|
||||||
|
pub use history_provider::*;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub mod child_router;
|
||||||
}
|
}
|
||||||
|
|
||||||
mod contexts {
|
mod contexts {
|
||||||
@@ -37,8 +43,6 @@ mod contexts {
|
|||||||
|
|
||||||
mod router_cfg;
|
mod router_cfg;
|
||||||
|
|
||||||
mod history;
|
|
||||||
|
|
||||||
/// Hooks for interacting with the router in components.
|
/// Hooks for interacting with the router in components.
|
||||||
pub mod hooks {
|
pub mod hooks {
|
||||||
mod use_router;
|
mod use_router;
|
||||||
@@ -55,9 +59,11 @@ pub use hooks::router;
|
|||||||
|
|
||||||
/// A collection of useful items most applications might need.
|
/// A collection of useful items most applications might need.
|
||||||
pub mod prelude {
|
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::contexts::*;
|
||||||
pub use crate::history::*;
|
|
||||||
pub use crate::hooks::*;
|
pub use crate::hooks::*;
|
||||||
pub use crate::navigation::*;
|
pub use crate::navigation::*;
|
||||||
pub use crate::routable::*;
|
pub use crate::routable::*;
|
||||||
|
|||||||
@@ -7,11 +7,23 @@ use std::{
|
|||||||
|
|
||||||
use url::{ParseError, Url};
|
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.
|
/// A target for the router to navigate to.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[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.
|
/// An internal path that the router can navigate to by itself.
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```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> {
|
impl<R: Routable> Display for NavigationTarget<R> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::prelude::*;
|
use crate::{components::FailureExternalNavigation, prelude::*};
|
||||||
use dioxus_lib::prelude::*;
|
use dioxus_lib::prelude::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -17,42 +17,29 @@ use std::sync::Arc;
|
|||||||
/// #[route("/")]
|
/// #[route("/")]
|
||||||
/// Index {},
|
/// 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 struct RouterConfig<R> {
|
||||||
pub(crate) failure_external_navigation: fn() -> Element,
|
pub(crate) failure_external_navigation: fn() -> Element,
|
||||||
pub(crate) history: Option<Box<dyn AnyHistoryProvider>>,
|
|
||||||
pub(crate) on_update: Option<RoutingCallback<R>>,
|
pub(crate) on_update: Option<RoutingCallback<R>>,
|
||||||
pub(crate) initial_route: Option<R>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R> Default for RouterConfig<R> {
|
impl<R> Default for RouterConfig<R> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
failure_external_navigation: FailureExternalNavigation,
|
failure_external_navigation: FailureExternalNavigation,
|
||||||
history: None,
|
|
||||||
on_update: 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>
|
impl<R> RouterConfig<R>
|
||||||
where
|
where
|
||||||
R: Routable,
|
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.
|
/// A component to render when an external navigation fails.
|
||||||
///
|
///
|
||||||
/// Defaults to a router-internal component called [`FailureExternalNavigation`]
|
/// 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.
|
/// - [`None`], when the current component isn't a descendant of a [`Router`] component.
|
||||||
/// - Otherwise [`Some`].
|
/// - Otherwise [`Some`].
|
||||||
pub(crate) fn use_router_internal() -> Option<RouterContext> {
|
pub(crate) fn use_router_internal() -> Option<RouterContext> {
|
||||||
let router = try_consume_context::<RouterContext>()?;
|
use_hook(try_consume_context)
|
||||||
let id = current_scope_id().expect("use_router_internal called outside of a component");
|
|
||||||
use_drop({
|
|
||||||
to_owned![router];
|
|
||||||
move || {
|
|
||||||
router.unsubscribe(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
use_hook(move || {
|
|
||||||
router.subscribe(id);
|
|
||||||
|
|
||||||
Some(router)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
use dioxus::prelude::*;
|
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
|
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
|
where
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
<R as FromStr>::Err: std::fmt::Display,
|
||||||
{
|
{
|
||||||
let mut vdom = VirtualDom::new_with_props(
|
let mut vdom = VirtualDom::new_with_props(
|
||||||
App,
|
App,
|
||||||
AppProps::<R> {
|
AppProps::<R> {
|
||||||
|
at: at.to_string(),
|
||||||
phantom: std::marker::PhantomData,
|
phantom: std::marker::PhantomData,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -16,12 +26,14 @@ where
|
|||||||
|
|
||||||
#[derive(Props)]
|
#[derive(Props)]
|
||||||
struct AppProps<R: Routable> {
|
struct AppProps<R: Routable> {
|
||||||
|
at: String,
|
||||||
phantom: std::marker::PhantomData<R>,
|
phantom: std::marker::PhantomData<R>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Routable> Clone for AppProps<R> {
|
impl<R: Routable> Clone for AppProps<R> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
at: self.at.clone(),
|
||||||
phantom: std::marker::PhantomData,
|
phantom: std::marker::PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,14 +46,15 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
fn App<R: Routable>(_props: AppProps<R>) -> Element
|
fn App<R: Routable>(props: AppProps<R>) -> Element
|
||||||
where
|
where
|
||||||
<R as FromStr>::Err: std::fmt::Display,
|
<R as FromStr>::Err: std::fmt::Display,
|
||||||
{
|
{
|
||||||
rsx! {
|
rsx! {
|
||||||
h1 { "App" }
|
h1 { "App" }
|
||||||
Router::<R> {
|
HistoryProvider {
|
||||||
config: |_| RouterConfig::default().history(MemoryHistory::default())
|
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);
|
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)]
|
#![allow(unused)]
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_history::{History, MemoryHistory};
|
||||||
|
use dioxus_router::components::HistoryProvider;
|
||||||
use dioxus_router::prelude::*;
|
use dioxus_router::prelude::*;
|
||||||
|
|
||||||
fn prepare(path: impl Into<String>) -> VirtualDom {
|
fn prepare(path: impl Into<String>) -> VirtualDom {
|
||||||
@@ -38,10 +42,9 @@ fn prepare(path: impl Into<String>) -> VirtualDom {
|
|||||||
fn App(path: Route) -> Element {
|
fn App(path: Route) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
h1 { "App" }
|
h1 { "App" }
|
||||||
Router::<Route> {
|
HistoryProvider {
|
||||||
config: move |_| {
|
history: move |_| Rc::new(MemoryHistory::with_initial_path(path.clone())) as Rc<dyn History>,
|
||||||
RouterConfig::default().history(MemoryHistory::with_initial_path(path.clone()))
|
Router::<Route> {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use dioxus::prelude::*;
|
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>
|
// Tests for regressions of <https://github.com/DioxusLabs/dioxus/issues/2549>
|
||||||
#[test]
|
#[test]
|
||||||
@@ -33,10 +35,9 @@ fn Home(lang: String) -> Element {
|
|||||||
#[component]
|
#[component]
|
||||||
fn App(path: Route) -> Element {
|
fn App(path: Route) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
Router::<Route> {
|
HistoryProvider {
|
||||||
config: {
|
history: move |_| Rc::new(MemoryHistory::with_initial_path(path.clone())) as Rc<dyn History>,
|
||||||
move |_| RouterConfig::default().history(MemoryHistory::with_initial_path(path.clone()))
|
Router::<Route> {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_history::{History, MemoryHistory};
|
||||||
|
use dioxus_router::components::HistoryProvider;
|
||||||
|
|
||||||
// Tests for regressions of <https://github.com/DioxusLabs/dioxus/issues/2468>
|
// Tests for regressions of <https://github.com/DioxusLabs/dioxus/issues/2468>
|
||||||
#[test]
|
#[test]
|
||||||
@@ -32,10 +36,9 @@ fn Test() -> Element {
|
|||||||
#[component]
|
#[component]
|
||||||
fn App(path: Route) -> Element {
|
fn App(path: Route) -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
Router::<Route> {
|
HistoryProvider {
|
||||||
config: {
|
history: move |_| Rc::new(MemoryHistory::with_initial_path(path)) as Rc<dyn History>,
|
||||||
move || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
|
Router::<Route> {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ resolver = "2"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-fullstack = { workspace = true }
|
dioxus-fullstack = { workspace = true }
|
||||||
dioxus-lib.workspace = true
|
dioxus-lib.workspace = true
|
||||||
dioxus-router = { workspace = true, features = ["fullstack"]}
|
dioxus-router = { workspace = true }
|
||||||
dioxus-ssr = { workspace = true, optional = true }
|
dioxus-ssr = { workspace = true, optional = true }
|
||||||
dioxus-isrg = { workspace = true, optional = true }
|
dioxus-isrg = { workspace = true, optional = true }
|
||||||
axum = { workspace = true, features = ["ws", "macros"], optional = true }
|
axum = { workspace = true, features = ["ws", "macros"], optional = true }
|
||||||
@@ -32,8 +32,8 @@ criterion = { workspace = true }
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
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"]
|
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", "dioxus-router/web", "dep:dioxus-web"]
|
web = ["dioxus-fullstack/web", "dep:dioxus-web"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||||
@@ -12,7 +12,9 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
dioxus-core-types = { workspace = true }
|
dioxus-core-types = { workspace = true }
|
||||||
|
dioxus-cli-config = { workspace = true }
|
||||||
dioxus-html = { workspace = true }
|
dioxus-html = { workspace = true }
|
||||||
|
dioxus-history = { workspace = true }
|
||||||
dioxus-document = { workspace = true }
|
dioxus-document = { workspace = true }
|
||||||
dioxus-devtools = { workspace = true }
|
dioxus-devtools = { workspace = true }
|
||||||
dioxus-signals = { workspace = true }
|
dioxus-signals = { workspace = true }
|
||||||
@@ -55,6 +57,7 @@ features = [
|
|||||||
"Document",
|
"Document",
|
||||||
"DragEvent",
|
"DragEvent",
|
||||||
"FocusEvent",
|
"FocusEvent",
|
||||||
|
"History",
|
||||||
"HtmlElement",
|
"HtmlElement",
|
||||||
"HtmlFormElement",
|
"HtmlFormElement",
|
||||||
"HtmlInputElement",
|
"HtmlInputElement",
|
||||||
@@ -67,6 +70,7 @@ features = [
|
|||||||
"PointerEvent",
|
"PointerEvent",
|
||||||
"ResizeObserverEntry",
|
"ResizeObserverEntry",
|
||||||
"ResizeObserverSize",
|
"ResizeObserverSize",
|
||||||
|
"ScrollRestoration",
|
||||||
"Text",
|
"Text",
|
||||||
"Touch",
|
"Touch",
|
||||||
"TouchEvent",
|
"TouchEvent",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use dioxus_core::ScopeId;
|
use dioxus_core::ScopeId;
|
||||||
use dioxus_document::{Document, Eval, EvalError, Evaluator};
|
use dioxus_document::{Document, Eval, EvalError, Evaluator};
|
||||||
|
use dioxus_history::History;
|
||||||
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
||||||
use js_sys::Function;
|
use js_sys::Function;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -9,6 +10,8 @@ use std::pin::Pin;
|
|||||||
use std::{rc::Rc, str::FromStr};
|
use std::{rc::Rc, str::FromStr};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use crate::history::WebHistory;
|
||||||
|
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub struct JSOwner {
|
pub struct JSOwner {
|
||||||
_owner: Box<dyn std::any::Any>,
|
_owner: Box<dyn std::any::Any>,
|
||||||
@@ -53,12 +56,16 @@ extern "C" {
|
|||||||
pub async fn rust_recv(this: &WeakDioxusChannel) -> wasm_bindgen::JsValue;
|
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() {
|
pub fn init_document() {
|
||||||
let provider: Rc<dyn Document> = Rc::new(WebDocument);
|
let provider: Rc<dyn Document> = Rc::new(WebDocument);
|
||||||
if ScopeId::ROOT.has_context::<Rc<dyn Document>>().is_none() {
|
if ScopeId::ROOT.has_context::<Rc<dyn Document>>().is_none() {
|
||||||
ScopeId::ROOT.provide_context(provider);
|
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.
|
/// The web-target's document provider.
|
||||||
|
|||||||
@@ -1,35 +1,24 @@
|
|||||||
use std::{
|
use scroll::ScrollPosition;
|
||||||
path::PathBuf,
|
use std::path::PathBuf;
|
||||||
sync::{Arc, Mutex},
|
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};
|
mod scroll;
|
||||||
|
|
||||||
use wasm_bindgen::JsValue;
|
|
||||||
use web_sys::{window, History, ScrollRestoration, Window};
|
|
||||||
|
|
||||||
use crate::routable::Routable;
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
web_history::{get_current, push_state_and_url, replace_state_with_url},
|
|
||||||
web_scroll::ScrollPosition,
|
|
||||||
HistoryProvider,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn base_path() -> Option<PathBuf> {
|
fn base_path() -> Option<PathBuf> {
|
||||||
tracing::trace!(
|
let base_path = dioxus_cli_config::base_path();
|
||||||
"Using base_path from the CLI: {:?}",
|
tracing::trace!("Using base_path from the CLI: {:?}", base_path);
|
||||||
dioxus_cli_config::base_path()
|
base_path
|
||||||
);
|
|
||||||
dioxus_cli_config::base_path()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::extra_unused_type_parameters)]
|
#[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);
|
let scroll = ScrollPosition::of_window(window);
|
||||||
if let Err(err) = replace_state_with_url(history, &[scroll.x, scroll.y], None) {
|
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
|
/// 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.
|
/// 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,
|
do_scroll_restoration: bool,
|
||||||
history: History,
|
history: History,
|
||||||
listener_navigation: Option<EventListener>,
|
|
||||||
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
|
|
||||||
prefix: Option<String>,
|
prefix: Option<String>,
|
||||||
window: Window,
|
window: Window,
|
||||||
phantom: std::marker::PhantomData<R>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Routable> Default for WebHistory<R>
|
impl Default for WebHistory {
|
||||||
where
|
|
||||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new(None, true)
|
Self::new(None, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Routable> WebHistory<R> {
|
impl WebHistory {
|
||||||
/// Create a new [`WebHistory`].
|
/// Create a new [`WebHistory`].
|
||||||
///
|
///
|
||||||
/// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
|
/// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
|
||||||
/// state. It'll also set the browsers scroll restoration to `manual`.
|
/// state. It'll also set the browsers scroll restoration to `manual`.
|
||||||
pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self
|
pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
|
||||||
where
|
|
||||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
let myself = Self::new_inner(prefix, do_scroll_restoration);
|
let 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 current_route_str = current_route.to_string();
|
||||||
let prefix_str = myself.prefix.as_deref().unwrap_or("");
|
let prefix_str = myself.prefix.as_deref().unwrap_or("");
|
||||||
let current_url = format!("{prefix_str}{current_route_str}");
|
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));
|
let _ = replace_state_with_url(&myself.history, &state, Some(¤t_url));
|
||||||
|
|
||||||
myself
|
myself
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_inner(prefix: Option<String>, do_scroll_restoration: bool) -> Self
|
fn new_inner(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
|
||||||
where
|
|
||||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
let window = window().expect("access to `window`");
|
let window = window().expect("access to `window`");
|
||||||
let history = window.history().expect("`window` has access to `history`");
|
let history = window.history().expect("`window` has access to `history`");
|
||||||
|
|
||||||
@@ -111,11 +88,8 @@ impl<R: Routable> WebHistory<R> {
|
|||||||
Self {
|
Self {
|
||||||
do_scroll_restoration,
|
do_scroll_restoration,
|
||||||
history,
|
history,
|
||||||
listener_navigation: None,
|
|
||||||
listener_animation_frame: Default::default(),
|
|
||||||
prefix,
|
prefix,
|
||||||
window,
|
window,
|
||||||
phantom: Default::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,17 +99,14 @@ impl<R: Routable> WebHistory<R> {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_state(&self, _state: R) -> [f64; 2] {
|
fn create_state(&self) -> [f64; 2] {
|
||||||
let scroll = self.scroll_pos();
|
let scroll = self.scroll_pos();
|
||||||
[scroll.x, scroll.y]
|
[scroll.x, scroll.y]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Routable> WebHistory<R>
|
impl WebHistory {
|
||||||
where
|
fn route_from_location(&self) -> String {
|
||||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
fn route_from_location(&self) -> R {
|
|
||||||
let location = self.window.location();
|
let location = self.window.location();
|
||||||
let path = location.pathname().unwrap_or_else(|_| "/".into())
|
let path = location.pathname().unwrap_or_else(|_| "/".into())
|
||||||
+ &location.search().unwrap_or("".into())
|
+ &location.search().unwrap_or("".into())
|
||||||
@@ -148,12 +119,12 @@ where
|
|||||||
if path.is_empty() {
|
if path.is_empty() {
|
||||||
path = "/"
|
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 {
|
match &self.prefix {
|
||||||
None => format!("{state}"),
|
None => state.to_string(),
|
||||||
Some(prefix) => format!("{prefix}{state}"),
|
Some(prefix) => format!("{prefix}{state}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,26 +136,30 @@ where
|
|||||||
self.window.scroll_to_with_x_and_y(0.0, 0.0)
|
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) {
|
match self.window.location().set_href(&url) {
|
||||||
Ok(_) => true,
|
Ok(_) => true,
|
||||||
Err(e) => {
|
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
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Routable> HistoryProvider<R> for WebHistory<R>
|
impl dioxus_history::History for WebHistory {
|
||||||
where
|
fn current_route(&self) -> String {
|
||||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
|
||||||
{
|
|
||||||
fn current_route(&self) -> R {
|
|
||||||
self.route_from_location()
|
self.route_from_location()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,20 +167,20 @@ where
|
|||||||
self.prefix.clone()
|
self.prefix.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn go_back(&mut self) {
|
fn go_back(&self) {
|
||||||
if let Err(e) = self.history.back() {
|
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() {
|
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) {
|
fn push(&self, state: String) {
|
||||||
if state.to_string() == self.current_route().to_string() {
|
if state == self.current_route() {
|
||||||
// don't push the same state twice
|
// don't push the same state twice
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -214,39 +189,82 @@ where
|
|||||||
let h = w.history().expect("`window` has access to `history`");
|
let h = w.history().expect("`window` has access to `history`");
|
||||||
|
|
||||||
// update the scroll position before pushing the new state
|
// 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 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));
|
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 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)));
|
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)
|
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 w = self.window.clone();
|
||||||
let h = self.history.clone();
|
let h = self.history.clone();
|
||||||
let s = self.listener_animation_frame.clone();
|
|
||||||
let d = self.do_scroll_restoration;
|
let d = self.do_scroll_restoration;
|
||||||
|
|
||||||
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
|
let function = Closure::wrap(Box::new(move |_| {
|
||||||
(*callback)();
|
(*callback)();
|
||||||
if d {
|
if d {
|
||||||
let mut s = s.lock().expect("unpoisoned scroll mutex");
|
|
||||||
if let Some([x, y]) = get_current(&h) {
|
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])
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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")]
|
#[cfg(feature = "file_engine")]
|
||||||
mod file_engine;
|
mod file_engine;
|
||||||
#[cfg(feature = "document")]
|
#[cfg(feature = "document")]
|
||||||
|
mod history;
|
||||||
|
#[cfg(feature = "document")]
|
||||||
pub use document::WebDocument;
|
pub use document::WebDocument;
|
||||||
#[cfg(feature = "file_engine")]
|
#[cfg(feature = "file_engine")]
|
||||||
pub use file_engine::*;
|
pub use file_engine::*;
|
||||||
|
|||||||
Reference in new issue
Block a user