Merge branch 'master' into feature/use-shared-state-better-diagnostics

This commit is contained in:
Jonathan Kelley authored and GitHub committed 2023-07-20 10:51:10 -07:00
commit 91d4207fa7
324 files changed
+13509 -6646

No files matched your search

+1
View File
@@ -1 +1,2 @@
dist/
static
+39 -38
View File
@@ -3,57 +3,58 @@ name = "dioxus-router"
version = "0.3.0"
edition = "2018"
description = "Cross-platform router for Dioxus apps"
license = "MIT/Apache-2.0"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com"
keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies]
dioxus = { workspace = true}
futures-channel = { workspace = true }
url = { version = "2.2.2", default-features = false }
# for wasm
web-sys = { version = "0.3", features = [
"Attr",
"Document",
"History",
"HtmlBaseElement",
"Event",
"NamedNodeMap",
"Url",
"UrlSearchParams",
"Window",
"Location",
], optional = true }
wasm-bindgen = { workspace = true, optional = true }
js-sys = { version = "0.3", optional = true }
gloo-events = { version = "0.1.1", optional = true }
anyhow = "1.0.66"
dioxus = { workspace = true }
dioxus-router-macro = { workspace = true }
gloo = { version = "0.8.0", optional = true }
log = { workspace = true }
thiserror = { workspace = true }
futures-util = { workspace = true }
serde = { version = "1", optional = true }
serde_urlencoded = { version = "0.7.1", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
thiserror = "1.0.37"
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-ssr = { workspace = true, optional = true }
tokio = { workspace = true, features = ["full"], optional = true }
[features]
default = ["query"]
web = ["web-sys", "gloo-events", "js-sys", "wasm-bindgen"]
query = ["serde", "serde_urlencoded"]
default = ["web"]
ssr = ["dioxus-ssr", "tokio"]
wasm_test = []
serde = ["dep:serde", "gloo-utils/serde"]
web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
[dev-dependencies]
dioxus = { path = "../dioxus" }
dioxus-ssr = { path = "../ssr" }
criterion = { verison = "0.5", features = ["async_tokio", "html_reports"] }
[[example]]
name = "static_generation"
required-features = ["ssr"]
[[bench]]
name = "incremental"
required-features = ["ssr"]
harness = false
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
dioxus-desktop = { path = "../desktop" }
[target.'cfg(target_family = "wasm")'.dev-dependencies]
console_error_panic_hook = "0.1.7"
wasm-logger = "0.2.0"
wasm-bindgen-test = "0.3"
gloo-utils = "0.1.2"
dioxus-web = { workspace = true }
# dioxus-desktop = { workspace = true, optional = true }
# not wasm
[target.wasm32-unknown-unknown.dev-dependencies]
dioxus-router = { workspace = true, features = ["web"] }
[dev-dependencies.web-sys]
version = "0.3"
features = ["Document"]
dioxus-web = { workspace = true }
gloo = "0.8.0"
wasm-bindgen-test = "0.3.33"
wasm-logger = "0.2.0"
-10
View File
@@ -1,10 +0,0 @@
[tasks.test-with-browser]
extend = "core::wasm-pack-base"
command = "wasm-pack"
args = [
"test",
"@@split(DIOXUS_TEST_FLAGS, )",
"--",
"--features",
"${DIOXUS_TEST_FEATURES}",
]
+73 -15
View File
@@ -7,35 +7,93 @@
[crates-badge]: https://img.shields.io/crates/v/dioxus-router.svg
[crates-url]: https://crates.io/crates/dioxus-router
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[Guides](https://dioxuslabs.com/docs/0.3/router/) |
[API Docs](https://docs.rs/dioxus-router/latest/dioxus_router) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
Dioxus Router is a first-party Router for all your Dioxus Apps. It provides a React-Router-style interface using somewhat loose typing rules.
Dioxus Router is a first-party Router for all your Dioxus Apps. It provides an
interface similar to React Router, but takes advantage of types for more
expressiveness.
```rust, ignore
fn app() {
cx.render(rsx! {
Router {
Route { to: "/", Component {} },
Route { to: "/blog", Blog {} },
Route { to: "/blog/:id", BlogPost {} },
```rust, no_run
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_router::prelude::*;
use std::str::FromStr;
#[rustfmt::skip]
#[derive(Clone, Debug, PartialEq, Routable)]
enum Route {
#[nest("/blog")]
#[layout(Blog)]
#[route("/")]
BlogList {},
#[route("/:blog_id")]
BlogPost { blog_id: usize },
#[end_layout]
#[end_nest]
#[route("/")]
Index {},
}
fn App(cx: Scope) -> Element {
render! {
Router { }
}
}
#[inline_props]
fn Index(cx: Scope) -> Element {
render! {
h1 { "Index" }
Link {
target: Route::BlogList {},
"Go to the blog"
}
})
}
}
#[inline_props]
fn Blog(cx: Scope) -> Element {
render! {
h1 { "Blog" }
Outlet { }
}
}
#[inline_props]
fn BlogList(cx: Scope) -> Element {
render! {
h2 { "List of blog posts" }
Link {
target: Route::BlogPost { blog_id: 0 },
"Blog post 1"
}
Link {
target: Route::BlogPost { blog_id: 1 },
"Blog post 2"
}
}
}
#[inline_props]
fn BlogPost(cx: Scope, blog_id: usize) -> Element {
render! {
h2 { "Blog Post" }
}
}
```
@@ -47,6 +105,7 @@ You need to enable the right features for the platform you're targeting since th
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
@@ -54,4 +113,3 @@ This project is licensed under the [MIT license].
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in Dioxus by you shall be licensed as MIT without any additional
terms or conditions.
+200
View File
@@ -0,0 +1,200 @@
#![allow(unused, non_snake_case)]
use std::time::Duration;
use dioxus::prelude::*;
use dioxus_router::prelude::*;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use dioxus_ssr::Renderer;
pub fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("build 1000 routes", |b| {
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
})
.static_dir("./static")
.invalidate_after(Duration::from_secs(10))
.build();
b.iter(|| {
tokio::runtime::Runtime::new().unwrap().block_on(async {
for id in 0..1000 {
render_route(
&mut renderer,
Route::Post { id },
&mut tokio::io::sink(),
|_| {},
)
.await
.unwrap();
}
})
})
});
c.bench_function("build 1000 routes no memory cache", |b| {
b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| async {
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
})
.static_dir("./static")
.memory_cache_limit(0)
.invalidate_after(Duration::from_secs(10))
.build();
for id in 0..1000 {
render_route(
&mut renderer,
Route::Post { id },
&mut tokio::io::sink(),
|_| {},
)
.await
.unwrap();
}
})
});
c.bench_function("build 1000 routes no cache", |b| {
let mut renderer = Renderer::default();
b.iter(|| {
for id in 0..1000 {
let mut vdom = VirtualDom::new_with_props(
RenderPath,
RenderPathProps::builder().path(Route::Post { id }).build(),
);
vdom.rebuild();
struct Ignore;
impl std::fmt::Write for Ignore {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
Ok(())
}
}
renderer.render_to(&mut Ignore, &vdom).unwrap();
}
})
});
c.bench_function("cache static", |b| {
b.to_async(tokio::runtime::Runtime::new().unwrap())
.iter(|| async {
let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
})
.static_dir("./static")
.build();
pre_cache_static_routes::<Route, _>(&mut renderer)
.await
.unwrap();
})
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
#[inline_props]
fn Blog(cx: Scope) -> Element {
render! {
div {
"Blog"
}
}
}
#[inline_props]
fn Post(cx: Scope, id: usize) -> Element {
render! {
for _ in 0..*id {
div {
"PostId: {id}"
}
}
}
}
#[inline_props]
fn PostHome(cx: Scope) -> Element {
render! {
div {
"Post"
}
}
}
#[inline_props]
fn Home(cx: Scope) -> Element {
render! {
div {
"Home"
}
}
}
#[rustfmt::skip]
#[derive(Clone, Debug, PartialEq, Routable)]
enum Route {
#[nest("/blog")]
#[route("/")]
Blog {},
#[route("/post/index")]
PostHome {},
#[route("/post/:id")]
Post {
id: usize,
},
#[end_nest]
#[route("/")]
Home {},
}
#[inline_props]
fn RenderPath(cx: Scope, path: Route) -> Element {
let path = path.clone();
render! {
Router {
config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
}
}
}
+141 -41
View File
@@ -1,52 +1,152 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_router::*;
use dioxus_router::prelude::*;
use std::str::FromStr;
fn main() {
dioxus_web::launch(app);
#[cfg(not(target_arch = "wasm32"))]
dioxus_desktop::launch(root);
#[cfg(target_arch = "wasm32")]
dioxus_web::launch(root);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
Router {
h1 { "Your app here" }
ul {
Link { to: "/", li { "home" } }
Link { to: "/blog", li { "blog" } }
Link { to: "/blog/tim", li { "tims' blog" } }
Link { to: "/blog/bill", li { "bills' blog" } }
Link { to: "/blog/james", li { "james amazing' blog" } }
Link { to: "/apples", li { "go to apples" } }
}
Route { to: "/", Home {} }
Route { to: "/blog/", BlogList {} }
Route { to: "/blog/:id/", BlogPost {} }
Route { to: "/oranges", "Oranges are not apples!" }
Redirect { from: "/apples", to: "/oranges" }
fn root(cx: Scope) -> Element {
render! {
Router {}
}
}
#[inline_props]
fn UserFrame(cx: Scope, user_id: usize) -> Element {
render! {
pre {
"UserFrame{{\n\tuser_id:{user_id}\n}}"
}
})
}
fn Home(cx: Scope) -> Element {
cx.render(rsx! { h1 { "Home" } })
}
fn BlogList(cx: Scope) -> Element {
cx.render(rsx! { div { "Blog List" } })
}
fn BlogPost(cx: Scope) -> Element {
let Some(id) = use_route(cx).segment("id") else {
return cx.render(rsx! { div { "No blog post id" } })
};
log::trace!("rendering blog post {}", id);
cx.render(rsx! {
div {
h3 { "blog post: {id:?}" }
Link { to: "/blog/", "back to blog list" }
background_color: "rgba(0,0,0,50%)",
"children:"
Outlet {}
}
})
}
}
#[inline_props]
fn Route1(cx: Scope, user_id: usize, dynamic: usize, query: String, extra: String) -> Element {
render! {
pre {
"Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\tquery:{query},\n\textra:{extra}\n}}"
}
Link {
target: Route::Route1 { user_id: *user_id, dynamic: *dynamic, query: String::new(), extra: extra.clone() + "." },
"Route1 with extra+\".\""
}
p { "Footer" }
Link {
target: Route::Route3 { dynamic: String::new() },
"Home"
}
}
}
#[inline_props]
fn Route2(cx: Scope, user_id: usize) -> Element {
render! {
pre {
"Route2{{\n\tuser_id:{user_id}\n}}"
}
(0..*user_id).map(|i| rsx!{ p { "{i}" } }),
p { "Footer" }
Link {
target: Route::Route3 { dynamic: String::new() },
"Home"
}
}
}
#[inline_props]
fn Route3(cx: Scope, dynamic: String) -> Element {
let navigator = use_navigator(cx);
let current_route = use_route(cx)?;
let current_route_str = use_ref(cx, String::new);
let parsed = Route::from_str(&current_route_str.read());
let site_map = Route::SITE_MAP
.iter()
.flat_map(|seg| seg.flatten().into_iter())
.collect::<Vec<_>>();
render! {
input {
oninput: move |evt| {
*current_route_str.write() = evt.value.clone();
},
value: "{current_route_str.read()}"
}
"dynamic: {dynamic}"
Link {
target: Route::Route2 { user_id: 8888 },
"hello world link"
}
button {
onclick: move |_| { navigator.push(NavigationTarget::External("https://www.google.com".to_string())); },
"google link"
}
p { "Site Map" }
pre { "{site_map:#?}" }
p { "Dynamic link" }
match parsed {
Ok(route) => {
if route != current_route {
render! {
Link {
target: route.clone(),
"{route}"
}
}
}
else {
None
}
}
Err(err) => {
render! {
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,
extra: 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,103 @@
#![allow(non_snake_case)]
use std::time::Duration;
use dioxus::prelude::*;
use dioxus_router::prelude::*;
use dioxus_ssr::incremental::{DefaultRenderer, IncrementalRendererConfig};
#[tokio::main]
async fn main() {
let mut renderer = IncrementalRendererConfig::new(DefaultRenderer {
before_body: r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<title>Dioxus Application</title>
</head>
<body>"#
.to_string(),
after_body: r#"</body>
</html>"#
.to_string(),
})
.static_dir("./static")
.invalidate_after(Duration::from_secs(10))
.build();
println!(
"SITE MAP:\n{}",
Route::SITE_MAP
.iter()
.flat_map(|route| route.flatten().into_iter())
.map(|route| {
route
.iter()
.map(|segment| segment.to_string())
.collect::<Vec<_>>()
.join("")
})
.collect::<Vec<_>>()
.join("\n")
);
pre_cache_static_routes::<Route, _>(&mut renderer)
.await
.unwrap();
}
#[inline_props]
fn Blog(cx: Scope) -> Element {
render! {
div {
"Blog"
}
}
}
#[inline_props]
fn Post(cx: Scope, id: usize) -> Element {
render! {
div {
"PostId: {id}"
}
}
}
#[inline_props]
fn PostHome(cx: Scope) -> Element {
render! {
div {
"Post"
}
}
}
#[inline_props]
fn Home(cx: Scope) -> Element {
render! {
div {
"Home"
}
}
}
#[rustfmt::skip]
#[derive(Clone, Debug, PartialEq, Routable)]
enum Route {
#[nest("/blog")]
#[route("/")]
Blog {},
#[route("/post/index")]
PostHome {},
#[route("/post/:id")]
Post {
id: usize,
},
#[end_nest]
#[route("/")]
Home {},
}
-6
View File
@@ -1,6 +0,0 @@
#[derive(Default)]
pub struct RouterCfg {
pub base_url: Option<String>,
pub active_class: Option<String>,
pub initial_url: Option<String>,
}
@@ -0,0 +1,22 @@
use crate::{hooks::use_generic_router, routable::Routable};
use dioxus::prelude::*;
/// The default component to render when an external navigation fails.
#[allow(non_snake_case)]
pub fn FailureExternalNavigation<R: Routable + Clone>(cx: Scope) -> Element {
let router = use_generic_router::<R>(cx);
render! {
h1 { "External Navigation Failure!" }
p {
"The application tried to programmatically navigate to an external page. This "
"operation has failed. Click the link below to complete the navigation manually."
}
a {
onclick: move |_| {
router.clear_error()
},
"Click here to go back"
}
}
}
@@ -0,0 +1,157 @@
use dioxus::prelude::*;
use log::error;
use crate::{prelude::*, utils::use_router_internal::use_router_internal};
/// The properties for a [`GenericGoBackButton`] or a [`GenericGoForwardButton`].
#[derive(Debug, Props)]
pub struct GenericHistoryButtonProps<'a> {
/// The children to render within the generated HTML button tag.
pub children: Element<'a>,
}
/// A button to go back through the navigation history. Similar to a browsers back button.
///
/// Only works as descendant of a [`GenericRouter`] component, otherwise it will be inactive.
///
/// The button will disable itself if it is known that no prior history is available.
///
/// # Panic
/// - When the [`GenericGoBackButton`] is not nested within a [`GenericRouter`] component
/// hook, but only in debug builds.
///
/// # Example
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_router::prelude::*;
/// #[derive(Clone, Routable)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// }
///
/// fn App(cx: Scope) -> Element {
/// render! {
/// Router {}
/// }
/// }
///
/// #[inline_props]
/// fn Index(cx: Scope) -> Element {
/// render! {
/// GoBackButton {
/// "go back"
/// }
/// }
/// }
/// #
/// # let mut vdom = VirtualDom::new(App);
/// # let _ = vdom.rebuild();
/// # assert_eq!(
/// # dioxus_ssr::render(&vdom),
/// # r#"<button disabled="true" dioxus-prevent-default="onclick">go back</button>"#
/// # );
/// ```
#[allow(non_snake_case)]
pub fn GenericGoBackButton<'a, R: Routable>(
cx: Scope<'a, GenericHistoryButtonProps<'a>>,
) -> Element {
let GenericHistoryButtonProps { children } = cx.props;
// hook up to router
let router = match use_router_internal::<R>(cx) {
Some(r) => r,
#[allow(unreachable_code)]
None => {
let msg = "`GoBackButton` must have access to a parent router";
error!("{msg}, will be inactive");
#[cfg(debug_assertions)]
panic!("{}", msg);
return None;
}
};
let disabled = !router.can_go_back();
render! {
button {
disabled: "{disabled}",
prevent_default: "onclick",
onclick: move |_| router.go_back(),
children
}
}
}
/// A button to go forward through the navigation history. Similar to a browsers forward button.
///
/// Only works as descendant of a [`GenericRouter`] component, otherwise it will be inactive.
///
/// The button will disable itself if it is known that no later history is available.
///
/// # Panic
/// - When the [`GenericGoForwardButton`] is not nested within a [`GenericRouter`] component
/// hook, but only in debug builds.
///
/// # Example
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_router::prelude::*;
/// #[derive(Clone, Routable)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// }
///
/// fn App(cx: Scope) -> Element {
/// render! {
/// Router {}
/// }
/// }
///
/// #[inline_props]
/// fn Index(cx: Scope) -> Element {
/// render! {
/// GoForwardButton {
/// "go forward"
/// }
/// }
/// }
/// #
/// # let mut vdom = VirtualDom::new(App);
/// # let _ = vdom.rebuild();
/// # assert_eq!(
/// # dioxus_ssr::render(&vdom),
/// # r#"<button disabled="true" dioxus-prevent-default="onclick">go forward</button>"#
/// # );
/// ```
#[allow(non_snake_case)]
pub fn GenericGoForwardButton<'a, R: Routable>(
cx: Scope<'a, GenericHistoryButtonProps<'a>>,
) -> Element {
let GenericHistoryButtonProps { children } = cx.props;
// hook up to router
let router = match use_router_internal::<R>(cx) {
Some(r) => r,
#[allow(unreachable_code)]
None => {
let msg = "`GoForwardButton` must have access to a parent router";
error!("{msg}, will be inactive");
#[cfg(debug_assertions)]
panic!("{}", msg);
return None;
}
};
let disabled = !router.can_go_back();
render! {
button {
disabled: "{disabled}",
prevent_default: "onclick",
onclick: move |_| router.go_forward(),
children
}
}
}
+167 -127
View File
@@ -1,152 +1,192 @@
use crate::{use_route, RouterContext};
use std::fmt::Debug;
use dioxus::prelude::*;
use log::error;
/// Props for the [`Link`](struct.Link.html) component.
use crate::navigation::NavigationTarget;
use crate::prelude::*;
use crate::utils::use_router_internal::use_router_internal;
/// The properties for a [`GenericLink`].
#[derive(Props)]
pub struct LinkProps<'a> {
/// The route to link to. This can be a relative path, or a full URL.
///
/// ```rust, ignore
/// // Absolute path
/// Link { to: "/home", "Go Home" }
///
/// // Relative path
/// Link { to: "../", "Go Up" }
/// ```
pub to: &'a str,
/// Set the class of the inner link ['a'](https://www.w3schools.com/tags/tag_a.asp) element.
///
/// This can be useful when styling the inner link element.
#[props(default, strip_option)]
pub class: Option<&'a str>,
/// Set the class added to the inner link when the current route is the same as the "to" route.
///
/// To set all of the active classes inside a Router at the same time use the `active_class`
/// prop on the Router component. If both the Router prop as well as this prop are provided then
/// this one has precedence. By default set to `"active"`.
#[props(default, strip_option)]
pub struct GenericLinkProps<'a, R: Routable> {
/// A class to apply to the generate HTML anchor tag if the `target` route is active.
pub active_class: Option<&'a str>,
/// Set the ID of the inner link ['a'](https://www.w3schools.com/tags/tag_a.asp) element.
///
/// This can be useful when styling the inner link element.
#[props(default, strip_option)]
pub id: Option<&'a str>,
/// Set the title of the window after the link is clicked..
#[props(default, strip_option)]
pub title: Option<&'a str>,
/// Autodetect if a link is external or not.
///
/// This is automatically set to `true` and will use http/https detection
#[props(default = true)]
pub autodetect: bool,
/// Is this link an external link?
#[props(default = false)]
pub external: bool,
/// New tab?
#[props(default = false)]
pub new_tab: bool,
/// Pass children into the `<a>` element
/// The children to render within the generated HTML anchor tag.
pub children: Element<'a>,
/// The class attribute for the generated HTML anchor tag.
///
/// If `active_class` is [`Some`] and the `target` route is active, `active_class` will be
/// appended at the end of `class`.
pub class: Option<&'a str>,
/// The id attribute for the generated HTML anchor tag.
pub id: Option<&'a str>,
/// When [`true`], the `target` route will be opened in a new tab.
///
/// This does not change whether the [`GenericLink`] is active or not.
#[props(default)]
pub new_tab: bool,
/// The onclick event handler.
pub onclick: Option<EventHandler<'a, MouseEvent>>,
#[props(default)]
/// Whether the default behavior should be executed if an `onclick` handler is provided.
///
/// 1. When `onclick` is [`None`] (default if not specified), `onclick_only` has no effect.
/// 2. If `onclick_only` is [`false`] (default if not specified), the provided `onclick` handler
/// will be executed after the links regular functionality.
/// 3. If `onclick_only` is [`true`], only the provided `onclick` handler will be executed.
pub onclick_only: bool,
/// The rel attribute for the generated HTML anchor tag.
///
/// For external `target`s, this defaults to `noopener noreferrer`.
pub rel: Option<&'a str>,
/// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
#[props(into)]
pub target: NavigationTarget<R>,
}
/// A component that renders a link to a route.
///
/// `Link` components are just [`<a>`](https://www.w3schools.com/tags/tag_a.asp) elements
/// that link to different pages *within* your single-page app.
///
/// If you need to link to a resource outside of your app, then just use a regular
/// `<a>` element directly.
///
/// # Examples
///
/// ```rust, ignore
/// fn Header(cx: Scope) -> Element {
/// cx.render(rsx!{
/// Link { to: "/home", "Go Home" }
/// })
/// }
/// ```
pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
let svc = use_context::<RouterContext>(cx);
impl<R: Routable> Debug for GenericLinkProps<'_, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LinkProps")
.field("active_class", &self.active_class)
.field("children", &self.children)
.field("class", &self.class)
.field("id", &self.id)
.field("new_tab", &self.new_tab)
.field("onclick", &self.onclick.as_ref().map(|_| "onclick is set"))
.field("onclick_only", &self.onclick_only)
.field("rel", &self.rel)
.field("target", &self.target.to_string())
.finish()
}
}
let LinkProps {
to,
/// A link to navigate to another route.
///
/// Only works as descendant of a [`GenericRouter`] component, otherwise it will be inactive.
///
/// Unlike a regular HTML anchor, a [`GenericLink`] allows the router to handle the navigation and doesn't
/// cause the browser to load a new page.
///
/// However, in the background a [`GenericLink`] still generates an anchor, which you can use for styling
/// as normal.
///
/// # External targets
/// When the [`GenericLink`]s target is an [`NavigationTarget::External`] target, that is used as the `href` directly. This
/// means that a [`GenericLink`] can always navigate to an [`NavigationTarget::External`] target, even if the [`HistoryProvider`] does not support it.
///
/// # Panic
/// - When the [`GenericLink`] is not nested within a [`GenericRouter`], but
/// only in debug builds.
///
/// # Example
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_router::prelude::*;
///
/// #[derive(Clone, Routable)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// }
///
/// fn App(cx: Scope) -> Element {
/// render! {
/// Router {}
/// }
/// }
///
/// #[inline_props]
/// fn Index(cx: Scope) -> Element {
/// render! {
/// render! {
/// Link {
/// active_class: "active",
/// class: "link_class",
/// id: "link_id",
/// new_tab: true,
/// rel: "link_rel",
/// target: Route::Index {},
///
/// "A fully configured link"
/// }
/// }
/// }
/// }
/// #
/// # let mut vdom = VirtualDom::new(App);
/// # let _ = vdom.rebuild();
/// # assert_eq!(
/// # dioxus_ssr::render(&vdom),
/// # r#"<a href="/" dioxus-prevent-default="" class="link_class active" id="link_id" rel="link_rel" target="_blank">A fully configured link</a>"#
/// # );
/// ```
#[allow(non_snake_case)]
pub fn GenericLink<'a, R: Routable + Clone>(cx: Scope<'a, GenericLinkProps<'a, R>>) -> Element {
let GenericLinkProps {
active_class,
children,
class,
id,
title,
autodetect,
external,
new_tab,
children,
active_class,
..
onclick,
onclick_only,
rel,
target,
} = cx.props;
let is_http = to.starts_with("http") || to.starts_with("https");
let outerlink = (*autodetect && is_http) || *external;
let prevent_default = if outerlink { "" } else { "onclick" };
let active_class_name = match active_class {
Some(c) => (*c).into(),
// hook up to router
let router = match use_router_internal::<R>(cx) {
Some(r) => r,
#[allow(unreachable_code)]
None => {
let active_from_router = match svc {
Some(service) => service.cfg.active_class.clone(),
None => None,
};
active_from_router.unwrap_or_else(|| "active".into())
let msg = "`Link` must have access to a parent router";
error!("{msg}, will be inactive");
#[cfg(debug_assertions)]
panic!("{}", msg);
return None;
}
};
let route = use_route(cx);
let url = route.url();
let path = url.path();
let active = path == cx.props.to;
let active_class = if active { active_class_name } else { "".into() };
let current_route = router.current();
let current_url = current_route.to_string();
let href = target.to_string();
let ac = active_class
.and_then(|active_class| (href == current_url).then(|| format!(" {active_class}")))
.unwrap_or_default();
cx.render(rsx! {
let id = id.unwrap_or_default();
let class = format!("{}{ac}", class.unwrap_or_default());
let tag_target = new_tab.then_some("_blank").unwrap_or_default();
let is_external = matches!(target, NavigationTarget::External(_));
let is_router_nav = !is_external && !new_tab;
let prevent_default = is_router_nav.then_some("onclick").unwrap_or_default();
let rel = rel
.or_else(|| is_external.then_some("noopener noreferrer"))
.unwrap_or_default();
let do_default = onclick.is_none() || !onclick_only;
let action = move |event| {
if do_default && is_router_nav {
router.push(target.clone());
}
if let Some(handler) = onclick {
handler.call(event);
}
};
render! {
a {
href: "{to}",
class: format_args!("{} {}", class.unwrap_or(""), active_class),
id: format_args!("{}", id.unwrap_or("")),
title: format_args!("{}", title.unwrap_or("")),
onclick: action,
href: "{href}",
prevent_default: "{prevent_default}",
target: format_args!("{}", if * new_tab { "_blank" } else { "" }),
onclick: move |evt| {
log::trace!("Clicked link to {}", to);
if !outerlink {
if let Some(service) = svc {
log::trace!("Pushing route to {}", to);
service.push_route(to, cx.props.title.map(|f| f.to_string()), None);
#[cfg(feature = "web")]
{
web_sys::window().unwrap().scroll_to_with_x_and_y(0.0, 0.0);
}
} else {
log::error!(
"Attempted to create a Link to {} outside of a Router context", cx.props
.to,
);
}
}
if let Some(onclick) = cx.props.onclick.as_ref() {
onclick.call(evt);
}
},
class: "{class}",
id: "{id}",
rel: "{rel}",
target: "{tag_target}",
children
}
})
}
}
+72
View File
@@ -0,0 +1,72 @@
use crate::prelude::{outlet::OutletContext, *};
use dioxus::prelude::*;
/// An outlet for the current content.
///
/// Only works as descendant of a [`GenericRouter`] component, otherwise it will be inactive.
///
/// The [`GenericOutlet`] is aware of how many [`GenericOutlet`]s it is nested within. It will render the content
/// of the active route that is __exactly as deep__.
///
/// # Panic
/// - When the [`GenericOutlet`] is not nested a [`GenericRouter`] component,
/// but only in debug builds.
///
/// # Example
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_router::prelude::*;
/// #[derive(Clone, Routable)]
/// #[rustfmt::skip]
/// enum Route {
/// #[nest("/wrap")]
/// #[layout(Wrapper)] // Every layout component must have one Outlet
/// #[route("/")]
/// Child {},
/// #[end_layout]
/// #[end_nest]
/// #[route("/")]
/// Index {},
/// }
///
/// #[inline_props]
/// fn Index(cx: Scope) -> Element {
/// render! {
/// div {
/// "Index"
/// }
/// }
/// }
///
/// #[inline_props]
/// fn Wrapper(cx: Scope) -> Element {
/// render! {
/// h1 { "App" }
/// Outlet {} // The content of child routes will be rendered here
/// }
/// }
///
/// #[inline_props]
/// fn Child(cx: Scope) -> Element {
/// render! {
/// p {
/// "Child"
/// }
/// }
/// }
///
/// # fn App(cx: Scope) -> Element {
/// # render! {
/// # Router {
/// # config: || RouterConfig::default().history(MemoryHistory::with_initial_path(Route::Child {}))
/// # }
/// # }
/// # }
/// #
/// # let mut vdom = VirtualDom::new(App);
/// # let _ = vdom.rebuild();
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Child</p>");
/// ```
pub fn GenericOutlet<R: Routable + Clone>(cx: Scope) -> Element {
OutletContext::<R>::render(cx)
}
@@ -1,51 +0,0 @@
use dioxus::prelude::*;
use crate::use_router;
/// The props for the [`Router`](fn.Router.html) component.
#[derive(Props)]
pub struct RedirectProps<'a> {
/// The route to link to. This can be a relative path, or a full URL.
///
/// ```rust, ignore
/// // Absolute path
/// Redirect { from: "", to: "/home" }
///
/// // Relative path
/// Redirect { from: "", to: "../" }
/// ```
pub to: &'a str,
/// The route to link from. This can be a relative path, or a full URL.
///
/// ```rust, ignore
/// // Absolute path
/// Redirect { from: "", to: "/home" }
///
/// // Relative path
/// Redirect { from: "", to: "../" }
/// ```
pub from: Option<&'a str>,
}
/// If this component is rendered, it will redirect the user to the given route.
///
/// It will replace the current route rather than pushing the current one to the stack.
pub fn Redirect<'a>(cx: Scope<'a, RedirectProps<'a>>) -> Element {
let router = use_router(cx);
let immediate_redirect = cx.use_hook(|| {
if let Some(from) = cx.props.from {
router.register_total_route(from.to_string(), cx.scope_id());
false
} else {
true
}
});
if *immediate_redirect || router.should_render(cx.scope_id()) {
router.replace_route(cx.props.to, None, None);
}
cx.render(rsx!(()))
}
-57
View File
@@ -1,57 +0,0 @@
use crate::{RouteContext, RouterContext};
use dioxus::prelude::*;
/// Props for the [`Route`](struct.Route.html) component.
#[derive(Props)]
pub struct RouteProps<'a> {
/// The path to match.
pub to: &'a str,
/// The component to render when the path matches.
pub children: Element<'a>,
}
/// A component that conditionally renders children based on the current location.
///
/// # Example
///
///```rust, ignore
/// rsx!(
/// Router {
/// Route { to: "/home", Home {} }
/// Route { to: "/about", About {} }
/// Route { to: "/Blog", Blog {} }
/// }
/// )
/// ```
pub fn Route<'a>(cx: Scope<'a, RouteProps<'a>>) -> Element {
let router_root = use_context::<RouterContext>(cx).unwrap();
let root_context = use_context::<RouteContext>(cx);
cx.use_hook(|| {
// create a bigger, better, longer route if one above us exists
let total_route = match root_context {
Some(ctx) => ctx.total_route.clone(),
None => cx.props.to.to_string(),
};
// provide our route context
let route_context = cx.provide_context(RouteContext {
declared_route: cx.props.to.to_string(),
total_route,
});
// submit our rout
router_root.register_total_route(route_context.total_route, cx.scope_id());
});
log::trace!("Checking Route: {:?}", cx.props.to);
if router_root.should_render(cx.scope_id()) {
log::trace!("Route should render: {:?}", cx.scope_id());
cx.render(rsx!(&cx.props.children))
} else {
log::trace!("Route should *not* render: {:?}", cx.scope_id());
cx.render(rsx!(()))
}
}
+160 -52
View File
@@ -1,61 +1,169 @@
use crate::{cfg::RouterCfg, RouterContext, RouterService};
use dioxus::prelude::*;
use std::{cell::RefCell, str::FromStr};
/// The props for the [`Router`](fn.Router.html) component.
#[derive(Props)]
pub struct RouterProps<'a> {
/// The routes and elements that should be rendered when the path matches.
///
/// If elements are not contained within Routes, the will be rendered
/// regardless of the path.
pub children: Element<'a>,
use crate::{
prelude::{GenericOutlet, GenericRouterContext},
routable::Routable,
router_cfg::RouterConfig,
};
/// The URL to point at
///
/// This will be used to trim any latent segments from the URL when your app is
/// not deployed to the root of the domain.
pub base_url: Option<&'a str>,
/// Hook into the router when the route is changed.
///
/// This lets you easily implement redirects
#[props(default)]
pub onchange: EventHandler<'a, RouterContext>,
/// Set the active class of all Link components contained in this router.
///
/// This is useful if you don't want to repeat the same `active_class` prop value in every Link.
/// By default set to `"active"`.
pub active_class: Option<&'a str>,
/// Set the initial url.
// This is Option<Option<String>> because we want to be able to either omit the prop or pass in Option<String>
#[props(into)]
pub initial_url: Option<Option<String>>,
/// The config for [`GenericRouter`].
pub struct RouterConfigFactory<R: Routable> {
#[allow(clippy::type_complexity)]
config: RefCell<Option<Box<dyn FnOnce() -> RouterConfig<R>>>>,
}
/// A component that conditionally renders children based on the current location of the app.
///
/// Uses BrowserRouter in the browser and HashRouter everywhere else.
///
/// Will fallback to HashRouter is BrowserRouter is not available, or through configuration.
#[allow(non_snake_case)]
pub fn Router<'a>(cx: Scope<'a, RouterProps<'a>>) -> Element {
let svc = cx.use_hook(|| {
cx.provide_context(RouterService::new(
cx,
RouterCfg {
base_url: cx.props.base_url.map(|s| s.to_string()),
active_class: cx.props.active_class.map(|s| s.to_string()),
initial_url: cx.props.initial_url.clone().flatten(),
},
))
#[cfg(feature = "serde")]
impl<R: Routable> Default for RouterConfigFactory<R>
where
<R as FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
fn default() -> Self {
Self::from(RouterConfig::default)
}
}
#[cfg(not(feature = "serde"))]
impl<R: Routable> Default for RouterConfigFactory<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self::from(RouterConfig::default)
}
}
impl<R: Routable, F: FnOnce() -> RouterConfig<R> + 'static> From<F> for RouterConfigFactory<R> {
fn from(value: F) -> Self {
Self {
config: RefCell::new(Some(Box::new(value))),
}
}
}
#[cfg(feature = "serde")]
/// The props for [`GenericRouter`].
#[derive(Props)]
pub struct GenericRouterProps<R: Routable>
where
<R as FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
#[props(default, into)]
config: RouterConfigFactory<R>,
}
#[cfg(not(feature = "serde"))]
/// The props for [`GenericRouter`].
#[derive(Props)]
pub struct GenericRouterProps<R: Routable>
where
<R as FromStr>::Err: std::fmt::Display,
{
#[props(default, into)]
config: RouterConfigFactory<R>,
}
#[cfg(not(feature = "serde"))]
impl<R: Routable> Default for GenericRouterProps<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self {
config: RouterConfigFactory::default(),
}
}
}
#[cfg(feature = "serde")]
impl<R: Routable> Default for GenericRouterProps<R>
where
<R as FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
fn default() -> Self {
Self {
config: RouterConfigFactory::default(),
}
}
}
#[cfg(not(feature = "serde"))]
impl<R: Routable> PartialEq for GenericRouterProps<R>
where
<R as FromStr>::Err: std::fmt::Display,
{
fn eq(&self, _: &Self) -> bool {
// prevent the router from re-rendering when the initial url or config changes
true
}
}
#[cfg(feature = "serde")]
impl<R: Routable> PartialEq for GenericRouterProps<R>
where
<R as FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
fn eq(&self, _: &Self) -> bool {
// prevent the router from re-rendering when the initial url or config changes
true
}
}
#[cfg(not(feature = "serde"))]
/// A component that renders the current route.
pub fn GenericRouter<R: Routable + Clone>(cx: Scope<GenericRouterProps<R>>) -> Element
where
<R as FromStr>::Err: std::fmt::Display,
{
use crate::prelude::outlet::OutletContext;
use_context_provider(cx, || {
GenericRouterContext::new(
(cx.props
.config
.config
.take()
.expect("use_context_provider ran twice"))(),
cx.schedule_update_any(),
)
});
use_context_provider(cx, || OutletContext::<R> {
current_level: 0,
_marker: std::marker::PhantomData,
});
// next time we run the rout_found will be filled
if svc.route_found.get().is_none() {
cx.props.onchange.call(svc.clone());
render! {
GenericOutlet::<R> {}
}
}
#[cfg(feature = "serde")]
/// A component that renders the current route.
pub fn GenericRouter<R: Routable + Clone>(cx: Scope<GenericRouterProps<R>>) -> Element
where
<R as FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
use_context_provider(cx, || {
GenericRouterContext::new(
(cx.props
.config
.config
.take()
.expect("use_context_provider ran twice"))(),
cx.schedule_update_any(),
)
});
use_context_provider(cx, || OutletContext::<R> {
current_level: 0,
_marker: std::marker::PhantomData,
});
render! {
GenericOutlet::<R> {}
}
cx.render(rsx!(&cx.props.children))
}
+53
View File
@@ -0,0 +1,53 @@
use crate::prelude::{ExternalNavigationFailure, GenericRouterContext, NavigationTarget, Routable};
/// A view into the navigation state of a router.
#[derive(Clone)]
pub struct GenericNavigator<R: Routable>(pub(crate) GenericRouterContext<R>);
impl<R: Routable> GenericNavigator<R> {
/// Check whether there is a previous page to navigate back to.
#[must_use]
pub fn can_go_back(&self) -> bool {
self.0.can_go_back()
}
/// Check whether there is a future page to navigate forward to.
#[must_use]
pub fn can_go_forward(&self) -> bool {
self.0.can_go_forward()
}
/// Go back to the previous location.
///
/// Will fail silently if there is no previous location to go to.
pub fn go_back(&self) {
self.0.go_back();
}
/// Go back to the next location.
///
/// Will fail silently if there is no next location to go to.
pub fn go_forward(&self) {
self.0.go_forward();
}
/// Push a new location.
///
/// The previous location will be available to go back to.
pub fn push(
&self,
target: impl Into<NavigationTarget<R>>,
) -> Option<ExternalNavigationFailure> {
self.0.push(target)
}
/// Replace the current location.
///
/// The previous location will **not** be available to go back to.
pub fn replace(
&self,
target: impl Into<NavigationTarget<R>>,
) -> Option<ExternalNavigationFailure> {
self.0.replace(target)
}
}
+56
View File
@@ -0,0 +1,56 @@
use dioxus::prelude::*;
use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
pub(crate) struct OutletContext<R> {
pub current_level: usize,
pub _marker: std::marker::PhantomData<R>,
}
impl<R> Clone for OutletContext<R> {
fn clone(&self) -> Self {
OutletContext {
current_level: self.current_level,
_marker: std::marker::PhantomData,
}
}
}
pub(crate) fn use_outlet_context<R: 'static>(cx: &ScopeState) -> &OutletContext<R> {
let outlet_context = cx.use_hook(|| {
cx.consume_context().unwrap_or(OutletContext::<R> {
current_level: 1,
_marker: std::marker::PhantomData,
})
});
outlet_context
}
impl<R> OutletContext<R> {
pub(crate) fn render(cx: Scope) -> Element<'_>
where
R: Routable + Clone,
{
let router = use_router_internal::<R>(cx)
.as_ref()
.expect("Outlet must be inside of a router");
let outlet: &OutletContext<R> = use_outlet_context(cx);
let current_level = outlet.current_level;
cx.provide_context({
OutletContext::<R> {
current_level: current_level + 1,
_marker: std::marker::PhantomData,
}
});
if let Some(error) = router.render_error(cx) {
if current_level == 0 {
return Some(error);
} else {
return None;
}
}
router.current().render(cx, current_level)
}
}
+256
View File
@@ -0,0 +1,256 @@
use std::{
collections::HashSet,
sync::{Arc, RwLock, RwLockWriteGuard},
};
use dioxus::prelude::*;
use crate::{
history::HistoryProvider, navigation::NavigationTarget, routable::Routable,
router_cfg::RouterConfig,
};
/// An error that can occur when navigating.
#[derive(Debug, Clone)]
pub struct ExternalNavigationFailure(String);
/// A function the router will call after every routing update.
pub(crate) type RoutingCallback<R> =
Arc<dyn Fn(GenericRouterContext<R>) -> Option<NavigationTarget<R>>>;
struct MutableRouterState<R>
where
R: Routable,
{
/// The current prefix.
prefix: Option<String>,
history: Box<dyn HistoryProvider<R>>,
unresolved_error: Option<ExternalNavigationFailure>,
}
/// A collection of router data that manages all routing functionality.
pub struct GenericRouterContext<R>
where
R: Routable,
{
state: Arc<RwLock<MutableRouterState<R>>>,
subscribers: Arc<RwLock<HashSet<ScopeId>>>,
subscriber_update: Arc<dyn Fn(ScopeId)>,
routing_callback: Option<RoutingCallback<R>>,
failure_external_navigation: fn(Scope) -> Element,
}
impl<R: Routable> Clone for GenericRouterContext<R> {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
subscribers: self.subscribers.clone(),
subscriber_update: self.subscriber_update.clone(),
routing_callback: self.routing_callback.clone(),
failure_external_navigation: self.failure_external_navigation,
}
}
}
impl<R> GenericRouterContext<R>
where
R: Routable,
{
pub(crate) fn new(
mut cfg: RouterConfig<R>,
mark_dirty: Arc<dyn Fn(ScopeId) + Sync + Send>,
) -> Self
where
R: Clone,
<R as std::str::FromStr>::Err: std::fmt::Display,
{
let state = Arc::new(RwLock::new(MutableRouterState {
prefix: Default::default(),
history: cfg.take_history(),
unresolved_error: None,
}));
let subscriber_update = mark_dirty.clone();
let subscribers = Arc::new(RwLock::new(HashSet::new()));
let myself = Self {
state,
subscribers: subscribers.clone(),
subscriber_update,
routing_callback: cfg.on_update,
failure_external_navigation: cfg.failure_external_navigation,
};
// set the updater
{
let mut state = myself.state.write().unwrap();
state.history.updater(Arc::new(move || {
for &id in subscribers.read().unwrap().iter() {
(mark_dirty)(id);
}
}));
}
myself
}
/// Check whether there is a previous page to navigate back to.
#[must_use]
pub fn can_go_back(&self) -> bool {
self.state.read().unwrap().history.can_go_back()
}
/// Check whether there is a future page to navigate forward to.
#[must_use]
pub fn can_go_forward(&self) -> bool {
self.state.read().unwrap().history.can_go_forward()
}
/// Go back to the previous location.
///
/// Will fail silently if there is no previous location to go to.
pub fn go_back(&self) {
{
self.state.write().unwrap().history.go_back();
}
self.change_route();
}
/// Go back to the next location.
///
/// Will fail silently if there is no next location to go to.
pub fn go_forward(&self) {
{
self.state.write().unwrap().history.go_forward();
}
self.change_route();
}
/// Push a new location.
///
/// The previous location will be available to go back to.
pub fn push(
&self,
target: impl Into<NavigationTarget<R>>,
) -> Option<ExternalNavigationFailure> {
let target = target.into();
match target {
NavigationTarget::Internal(p) => {
let mut state = self.state_mut();
state.history.push(p)
}
NavigationTarget::External(e) => return self.external(e),
}
self.change_route()
}
/// Replace the current location.
///
/// The previous location will **not** be available to go back to.
pub fn replace(
&self,
target: impl Into<NavigationTarget<R>>,
) -> Option<ExternalNavigationFailure> {
let target = target.into();
{
let mut state = self.state_mut();
match target {
NavigationTarget::Internal(p) => state.history.replace(p),
NavigationTarget::External(e) => return self.external(e),
}
}
self.change_route()
}
/// The route that is currently active.
pub fn current(&self) -> R
where
R: Clone,
{
self.state.read().unwrap().history.current_route()
}
/// The prefix that is currently active.
pub fn prefix(&self) -> Option<String> {
self.state.read().unwrap().prefix.clone()
}
fn external(&self, external: String) -> Option<ExternalNavigationFailure> {
let mut state = self.state_mut();
match state.history.external(external.clone()) {
true => None,
false => {
let failure = ExternalNavigationFailure(external);
state.unresolved_error = Some(failure.clone());
self.update_subscribers();
Some(failure)
}
}
}
fn state_mut(&self) -> RwLockWriteGuard<MutableRouterState<R>> {
self.state.write().unwrap()
}
/// Manually subscribe to the current route
pub fn subscribe(&self, id: ScopeId) {
self.subscribers.write().unwrap().insert(id);
}
/// Manually unsubscribe from the current route
pub fn unsubscribe(&self, id: ScopeId) {
self.subscribers.write().unwrap().remove(&id);
}
fn update_subscribers(&self) {
for &id in self.subscribers.read().unwrap().iter() {
(self.subscriber_update)(id);
}
}
/// Clear any unresolved errors
pub fn clear_error(&self) {
self.state.write().unwrap().unresolved_error = None;
self.update_subscribers();
}
pub(crate) fn render_error<'a>(&self, cx: Scope<'a>) -> Element<'a> {
self.state
.read()
.unwrap()
.unresolved_error
.as_ref()
.and_then(|_| (self.failure_external_navigation)(cx))
}
fn change_route(&self) -> Option<ExternalNavigationFailure> {
if let Some(callback) = &self.routing_callback {
let myself = self.clone();
if let Some(new) = callback(myself) {
let mut state = self.state_mut();
match new {
NavigationTarget::Internal(p) => state.history.replace(p),
NavigationTarget::External(e) => return self.external(e),
}
}
}
self.update_subscribers();
None
}
}
View File
Whitespace-only changes.
+99
View File
@@ -0,0 +1,99 @@
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::*;
/// # #[inline_props]
/// # fn Index(cx: Scope) -> Element { todo!() }
/// # #[inline_props]
/// # fn OtherPage(cx: Scope) -> Element { todo!() }
/// #[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{}\n use MemoryHistory::with_initial_path to set a custom path", err)
}),
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) {
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;
}
}
+279
View File
@@ -0,0 +1,279 @@
//! History Integration
//!
//! dioxus-router-core relies on [`HistoryProvider`]s to store the current Route, and possibly a
//! history (i.e. a browsers back button) and future (i.e. a browsers forward button).
//!
//! To integrate dioxus-router with a any type of history, all you have to do is implement the
//! [`HistoryProvider`] trait.
//!
//! dioxus-router contains two built in history providers:
//! 1) [`MemoryHistory`] for desktop/mobile/ssr platforms
//! 2) [`WebHistory`] for web platforms
use std::sync::Arc;
mod memory;
pub use memory::*;
#[cfg(feature = "web")]
mod web;
#[cfg(feature = "web")]
pub use web::*;
#[cfg(feature = "web")]
pub(crate) mod web_history;
// #[cfg(feature = "web")]
// mod web_hash;
// #[cfg(feature = "web")]
// pub use web_hash::*;
use crate::routable::Routable;
#[cfg(feature = "web")]
pub(crate) mod web_scroll;
/// An integration with some kind of navigation history.
///
/// Depending on your use case, your implementation may deviate from the described procedure. This
/// is fine, as long as both `current_route` and `current_query` match the described format.
///
/// However, you should document all deviations. Also, make sure the navigation is user-friendly.
/// The described behaviors are designed to mimic a web browser, which most users should already
/// know. Deviations might confuse them.
pub trait HistoryProvider<R: Routable> {
/// Get the path of the current URL.
///
/// **Must start** with `/`. **Must _not_ contain** the prefix.
///
/// ```rust
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[inline_props]
/// # fn Index(cx: Scope) -> Element { todo!() }
/// # #[inline_props]
/// # fn OtherPage(cx: Scope) -> Element { todo!() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::default();
/// assert_eq!(history.current_route().to_string(), "/");
///
/// history.push(Route::OtherPage {});
/// assert_eq!(history.current_route().to_string(), "/some-other-page");
/// ```
#[must_use]
fn current_route(&self) -> R;
/// Get the current path prefix of the URL.
///
/// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a
/// dioxus-router-core-routed application is not running on `/`. The [`HistoryProvider`] is responsible
/// for removing the prefix from the dioxus-router-core-internal path, and also for adding it back in
/// during navigation. This functions value is only used for creating `href`s (e.g. for SSR or
/// display (but not navigation) in a web app).
fn current_prefix(&self) -> Option<String> {
None
}
/// Check whether there is a previous page to navigate back to.
///
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
///
/// ```rust
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[inline_props]
/// # fn Index(cx: Scope) -> Element { todo!() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// }
/// let mut history = MemoryHistory::<Route>::default();
/// assert_eq!(history.can_go_back(), false);
///
/// history.push(Route::Index {});
/// assert_eq!(history.can_go_back(), true);
/// ```
#[must_use]
fn can_go_back(&self) -> bool {
true
}
/// Go back to a previous page.
///
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
/// might be called, even if `can_go_back` returns [`false`].
///
/// ```rust
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[inline_props]
/// # fn Index(cx: Scope) -> Element { todo!() }
/// # #[inline_props]
/// # fn OtherPage(cx: Scope) -> Element { todo!() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::default();
/// assert_eq!(history.current_route().to_string(), "/");
///
/// history.go_back();
/// assert_eq!(history.current_route().to_string(), "/");
///
/// history.push(Route::OtherPage {});
/// assert_eq!(history.current_route().to_string(), "/some-other-page");
///
/// history.go_back();
/// assert_eq!(history.current_route().to_string(), "/");
/// ```
fn go_back(&mut self);
/// Check whether there is a future page to navigate forward to.
///
/// If a [`HistoryProvider`] cannot know this, it should return [`true`].
///
/// ```rust
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[inline_props]
/// # fn Index(cx: Scope) -> Element { todo!() }
/// # #[inline_props]
/// # fn OtherPage(cx: Scope) -> Element { todo!() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::default();
/// assert_eq!(history.can_go_forward(), false);
///
/// history.push(Route::OtherPage {});
/// assert_eq!(history.can_go_forward(), false);
///
/// history.go_back();
/// assert_eq!(history.can_go_forward(), true);
/// ```
#[must_use]
fn can_go_forward(&self) -> bool {
true
}
/// Go forward to a future page.
///
/// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
/// might be called, even if `can_go_forward` returns [`false`].
///
/// ```rust
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[inline_props]
/// # fn Index(cx: Scope) -> Element { todo!() }
/// # #[inline_props]
/// # fn OtherPage(cx: Scope) -> Element { todo!() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::default();
/// history.push(Route::OtherPage {});
/// assert_eq!(history.current_route(), Route::OtherPage {});
///
/// history.go_back();
/// assert_eq!(history.current_route(), Route::Index {});
///
/// history.go_forward();
/// assert_eq!(history.current_route(), Route::OtherPage {});
/// ```
fn go_forward(&mut self);
/// Go to another page.
///
/// This should do three things:
/// 1. Merge the current URL with the `path` parameter (which may also include a query part).
/// 2. Remove the previous URL to the navigation history.
/// 3. Clear the navigation future.
///
/// ```rust
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[inline_props]
/// # fn Index(cx: Scope) -> Element { todo!() }
/// # #[inline_props]
/// # fn OtherPage(cx: Scope) -> Element { todo!() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::default();
/// assert_eq!(history.current_route(), Route::Index {});
///
/// history.push(Route::OtherPage {});
/// assert_eq!(history.current_route(), Route::OtherPage {});
/// assert!(history.can_go_back());
/// ```
fn push(&mut self, route: R);
/// Replace the current page with another one.
///
/// This should merge the current URL with the `path` parameter (which may also include a query
/// part). In contrast to the `push` function, the navigation history and future should stay
/// untouched.
///
/// ```rust
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[inline_props]
/// # fn Index(cx: Scope) -> Element { todo!() }
/// # #[inline_props]
/// # fn OtherPage(cx: Scope) -> Element { todo!() }
/// #[derive(Clone, Routable, Debug, PartialEq)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/some-other-page")]
/// OtherPage {},
/// }
/// let mut history = MemoryHistory::<Route>::default();
/// assert_eq!(history.current_route(), Route::Index {});
///
/// history.replace(Route::OtherPage {});
/// assert_eq!(history.current_route(), Route::OtherPage {});
/// assert!(!history.can_go_back());
/// ```
fn replace(&mut self, path: R);
/// Navigate to an external URL.
///
/// This should navigate to an external URL, which isn't controlled by the router. If a
/// [`HistoryProvider`] cannot do that, it should return [`false`], otherwise [`true`].
///
/// Returning [`false`] will cause the router to handle the external navigation failure.
#[allow(unused_variables)]
fn external(&mut self, url: String) -> bool {
false
}
/// Provide the [`HistoryProvider`] with an update callback.
///
/// Some [`HistoryProvider`]s may receive URL updates from outside the router. When such
/// updates are received, they should call `callback`, which will cause the router to update.
#[allow(unused_variables)]
fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {}
}
+401
View File
@@ -0,0 +1,401 @@
use std::sync::{Arc, Mutex};
use gloo::{console::error, events::EventListener, render::AnimationFrame};
use wasm_bindgen::JsValue;
use web_sys::{window, History, ScrollRestoration, Window};
use crate::routable::Routable;
use super::{
web_history::{get_current, push_state_and_url, replace_state_with_url},
web_scroll::ScrollPosition,
HistoryProvider,
};
#[cfg(not(feature = "serde"))]
#[allow(clippy::extra_unused_type_parameters)]
fn update_scroll<R>(window: &Window, history: &History) {
let scroll = ScrollPosition::of_window(window);
if let Err(err) = replace_state_with_url(history, &[scroll.x, scroll.y], None) {
error!(err);
}
}
#[cfg(feature = "serde")]
fn update_scroll<R: serde::Serialize + serde::de::DeserializeOwned + Routable>(
window: &Window,
history: &History,
) {
if let Some(WebHistoryState { state, .. }) = get_current::<WebHistoryState<R>>(history) {
let scroll = ScrollPosition::of_window(window);
let state = WebHistoryState { state, scroll };
if let Err(err) = replace_state_with_url(history, &state, None) {
error!(err);
}
}
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize, serde::Serialize)]
struct WebHistoryState<R> {
state: R,
scroll: ScrollPosition,
}
/// A [`HistoryProvider`] that integrates with a browser via the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API).
///
/// # Prefix
/// This [`HistoryProvider`] supports a prefix, which can be used for web apps that aren't located
/// at the root of their domain.
///
/// Application developers are responsible for ensuring that right after the prefix comes a `/`. If
/// that is not the case, this [`HistoryProvider`] will replace the first character after the prefix
/// with one.
///
/// Application developers are responsible for not rendering the router if the prefix is not present
/// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added.
pub struct WebHistory<R: Routable> {
do_scroll_restoration: bool,
history: History,
listener_navigation: Option<EventListener>,
#[allow(dead_code)]
listener_scroll: Option<EventListener>,
listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
prefix: Option<String>,
window: Window,
phantom: std::marker::PhantomData<R>,
}
#[cfg(not(feature = "serde"))]
impl<R: Routable> Default for WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self::new(None, true)
}
}
#[cfg(feature = "serde")]
impl<R: Routable> Default for WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
fn default() -> Self {
Self::new(None, true)
}
}
impl<R: Routable> WebHistory<R> {
#[cfg(not(feature = "serde"))]
/// Create a new [`WebHistory`].
///
/// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
/// state. It'll also set the browsers scroll restoration to `manual`.
pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
let w = window().expect("access to `window`");
let h = w.history().expect("`window` has access to `history`");
let document = w.document().expect("`window` has access to `document`");
let myself = Self::new_inner(
prefix,
do_scroll_restoration,
EventListener::new(&document, "scroll", {
let mut last_updated = 0.0;
move |evt| {
// the time stamp in milliseconds
let time_stamp = evt.time_stamp();
// throttle the scroll event to 100ms
if (time_stamp - last_updated) < 100.0 {
return;
}
update_scroll::<R>(&w, &h);
last_updated = time_stamp;
}
}),
);
let current_route = myself.current_route();
let current_url = current_route.to_string();
let state = myself.create_state(current_route);
let _ = replace_state_with_url(&myself.history, &state, Some(&current_url));
myself
}
#[cfg(feature = "serde")]
/// Create a new [`WebHistory`].
///
/// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
/// state. It'll also set the browsers scroll restoration to `manual`.
pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self
where
<R as std::str::FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
let w = window().expect("access to `window`");
let h = w.history().expect("`window` has access to `history`");
let document = w.document().expect("`window` has access to `document`");
let myself = Self::new_inner(
prefix,
do_scroll_restoration,
EventListener::new(&document, "scroll", {
let mut last_updated = 0.0;
move |evt| {
// the time stamp in milliseconds
let time_stamp = evt.time_stamp();
// throttle the scroll event to 100ms
if (time_stamp - last_updated) < 100.0 {
return;
}
update_scroll::<R>(&w, &h);
last_updated = time_stamp;
}
}),
);
let current_route = myself.current_route();
log::trace!("initial route: {:?}", current_route);
let current_url = current_route.to_string();
let state = myself.create_state(current_route);
let _ = replace_state_with_url(&myself.history, &state, Some(&current_url));
myself
}
fn new_inner(
prefix: Option<String>,
do_scroll_restoration: bool,
event_listener: EventListener,
) -> Self
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
let window = window().expect("access to `window`");
let history = window.history().expect("`window` has access to `history`");
let listener_scroll = match do_scroll_restoration {
true => {
history
.set_scroll_restoration(ScrollRestoration::Manual)
.expect("`history` can set scroll restoration");
Some(event_listener)
}
false => None,
};
Self {
do_scroll_restoration,
history,
listener_navigation: None,
listener_scroll,
listener_animation_frame: Default::default(),
prefix,
window,
phantom: Default::default(),
}
}
fn scroll_pos(&self) -> ScrollPosition {
self.do_scroll_restoration
.then(|| ScrollPosition::of_window(&self.window))
.unwrap_or_default()
}
#[cfg(not(feature = "serde"))]
fn create_state(&self, _state: R) -> [f64; 2] {
let scroll = self.scroll_pos();
[scroll.x, scroll.y]
}
#[cfg(feature = "serde")]
fn create_state(&self, state: R) -> WebHistoryState<R> {
let scroll = self.scroll_pos();
WebHistoryState { state, scroll }
}
}
impl<R: Routable> WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn route_from_location(&self) -> R {
R::from_str(
&self
.window
.location()
.pathname()
.unwrap_or_else(|_| String::from("/")),
)
.unwrap_or_else(|err| panic!("{}", err))
}
fn full_path(&self, state: &R) -> String {
match &self.prefix {
None => format!("{state}"),
Some(prefix) => format!("{prefix}{state}"),
}
}
fn handle_nav(&self, result: Result<(), JsValue>) {
match result {
Ok(_) => {
if self.do_scroll_restoration {
self.window.scroll_to_with_x_and_y(0.0, 0.0)
}
}
Err(e) => error!("failed to change state: ", e),
}
}
fn navigate_external(&mut self, url: String) -> bool {
match self.window.location().set_href(&url) {
Ok(_) => true,
Err(e) => {
error!("failed to navigate to external url (", url, "): ", e);
false
}
}
}
}
#[cfg(feature = "serde")]
impl<R: serde::Serialize + serde::de::DeserializeOwned + Routable> HistoryProvider<R>
for WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn current_route(&self) -> R {
match get_current::<WebHistoryState<_>>(&self.history) {
// Try to get the route from the history state
Some(route) => route.state,
// If that fails, get the route from the current URL
None => self.route_from_location(),
}
}
fn current_prefix(&self) -> Option<String> {
self.prefix.clone()
}
fn go_back(&mut self) {
if let Err(e) = self.history.back() {
error!("failed to go back: ", e)
}
}
fn go_forward(&mut self) {
if let Err(e) = self.history.forward() {
error!("failed to go forward: ", e)
}
}
fn push(&mut self, state: R) {
let path = self.full_path(&state);
let state = self.create_state(state);
self.handle_nav(push_state_and_url(&self.history, &state, path));
}
fn replace(&mut self, state: R) {
let path = match &self.prefix {
None => format!("{state}"),
Some(prefix) => format!("{prefix}{state}"),
};
let state = self.create_state(state);
self.handle_nav(replace_state_with_url(&self.history, &state, Some(&path)));
}
fn external(&mut self, url: String) -> bool {
self.navigate_external(url)
}
fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
let w = self.window.clone();
let h = self.history.clone();
let s = self.listener_animation_frame.clone();
let d = self.do_scroll_restoration;
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
(*callback)();
if d {
let mut s = s.lock().expect("unpoisoned scroll mutex");
if let Some(current_state) = get_current::<WebHistoryState<R>>(&h) {
*s = Some(current_state.scroll.scroll_to(w.clone()));
}
}
}));
}
}
#[cfg(not(feature = "serde"))]
impl<R: Routable> HistoryProvider<R> for WebHistory<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn current_route(&self) -> R {
self.route_from_location()
}
fn current_prefix(&self) -> Option<String> {
self.prefix.clone()
}
fn go_back(&mut self) {
if let Err(e) = self.history.back() {
error!("failed to go back: ", e)
}
}
fn go_forward(&mut self) {
if let Err(e) = self.history.forward() {
error!("failed to go forward: ", e)
}
}
fn push(&mut self, state: R) {
let path = self.full_path(&state);
let state: [f64; 2] = self.create_state(state);
self.handle_nav(push_state_and_url(&self.history, &state, path));
}
fn replace(&mut self, state: R) {
let path = match &self.prefix {
None => format!("{state}"),
Some(prefix) => format!("{prefix}{state}"),
};
let state = self.create_state(state);
self.handle_nav(replace_state_with_url(&self.history, &state, Some(&path)));
}
fn external(&mut self, url: String) -> bool {
self.navigate_external(url)
}
fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
let w = self.window.clone();
let h = self.history.clone();
let s = self.listener_animation_frame.clone();
let d = self.do_scroll_restoration;
self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
(*callback)();
if d {
let mut s = s.lock().expect("unpoisoned scroll mutex");
if let Some([x, y]) = get_current(&h) {
*s = Some(ScrollPosition { x, y }.scroll_to(w.clone()));
}
}
}));
}
}
+211
View File
@@ -0,0 +1,211 @@
use std::sync::{Arc, Mutex};
use gloo::{events::EventListener, render::AnimationFrame, utils::window};
use log::error;
use serde::{de::DeserializeOwned, Serialize};
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));
}
}));
}
}
@@ -0,0 +1,84 @@
use gloo::console::error;
#[cfg(feature = "serde")]
use gloo_utils::format::JsValueSerdeExt;
use wasm_bindgen::JsValue;
use web_sys::History;
#[cfg(not(feature = "serde"))]
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)
}
#[cfg(feature = "serde")]
pub(crate) fn replace_state_with_url<V: serde::Serialize>(
history: &History,
value: &V,
url: Option<&str>,
) -> Result<(), JsValue> {
let position = JsValue::from_serde(value).unwrap();
history.replace_state_with_url(&position, "", url)
}
#[cfg(not(feature = "serde"))]
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))
}
#[cfg(feature = "serde")]
pub(crate) fn push_state_and_url<V: serde::Serialize>(
history: &History,
value: &V,
url: String,
) -> Result<(), JsValue> {
let position = JsValue::from_serde(value).unwrap();
history.push_state_with_url(&position, "", Some(&url))
}
#[cfg(feature = "serde")]
pub(crate) fn get_current<V: serde::de::DeserializeOwned>(history: &History) -> Option<V> {
let state = history.state();
if let Err(err) = &state {
error!(err);
}
state.ok().and_then(|state| {
let deserialized = state.into_serde();
if let Err(err) = &deserialized {
error!(format!("{}", err));
}
deserialized.ok()
})
}
#[cfg(not(feature = "serde"))]
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])
})
}
+23
View File
@@ -0,0 +1,23 @@
use gloo::render::{request_animation_frame, AnimationFrame};
use web_sys::Window;
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
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))
}
}
@@ -0,0 +1,61 @@
use dioxus::prelude::ScopeState;
use crate::{
prelude::{GenericNavigator, GenericRouterContext},
routable::Routable,
};
/// A hook that provides access to the navigator to change the router history. Unlike [`use_router`], this hook will not cause a rerender when the current route changes
///
/// > The Routable macro will define a version of this hook with an explicit type.
///
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_router::prelude::*;
/// #[derive(Clone, Routable)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// #[route("/:id")]
/// Dynamic { id: usize },
/// }
///
/// fn App(cx: Scope) -> Element {
/// render! {
/// Router {}
/// }
/// }
///
/// #[inline_props]
/// fn Index(cx: Scope) -> Element {
/// let navigator = use_navigator(&cx);
///
/// render! {
/// button {
/// onclick: move |_| { navigator.push(Route::Dynamic { id: 1234 }); },
/// "Go to /1234"
/// }
/// }
/// }
///
/// #[inline_props]
/// fn Dynamic(cx: Scope, id: usize) -> Element {
/// render! {
/// p {
/// "Current ID: {id}"
/// }
/// }
/// }
///
/// # let mut vdom = VirtualDom::new(App);
/// # let _ = vdom.rebuild();
/// ```
pub fn use_generic_navigator<R: Routable + Clone>(cx: &ScopeState) -> &GenericNavigator<R> {
&*cx.use_hook(|| {
let router = cx
.consume_context::<GenericRouterContext<R>>()
.expect("Must be called in a descendant of a Router component");
GenericNavigator(router)
})
}
+53 -138
View File
@@ -1,143 +1,58 @@
use crate::{ParsedRoute, RouteContext, RouterContext};
use dioxus::core::{ScopeId, ScopeState};
use std::{borrow::Cow, str::FromStr, sync::Arc};
use url::Url;
use dioxus::prelude::ScopeState;
/// This hook provides access to information about the current location in the
/// context of a [`Router`]. If this function is called outside of a `Router`
/// component it will panic.
pub fn use_route(cx: &ScopeState) -> &UseRoute {
let router = cx
.consume_context::<RouterContext>()
.expect("Cannot call use_route outside the scope of a Router component");
use crate::prelude::*;
use crate::utils::use_router_internal::use_router_internal;
let handle = cx.use_hook(|| {
let route_context = cx.consume_context::<RouteContext>();
router.subscribe_onchange(cx.scope_id());
UseRouteListener {
state: UseRoute {
route_context,
route: router.current_location(),
},
router,
scope: cx.scope_id(),
}
});
handle.state.route = handle.router.current_location();
&handle.state
}
/// A handle to the current location of the router.
pub struct UseRoute {
pub(crate) route: Arc<ParsedRoute>,
/// If `use_route` is used inside a `Route` component this has some context otherwise `None`.
pub(crate) route_context: Option<RouteContext>,
}
impl UseRoute {
/// Get the underlying [`Url`] of the current location.
pub fn url(&self) -> &Url {
&self.route.url
}
/// Get the first query parameter given the parameter name.
///
/// If you need to get more than one parameter, use [`query_pairs`] on the [`Url`] instead.
#[cfg(feature = "query")]
pub fn query<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
let query = self.url().query()?;
serde_urlencoded::from_str(query).ok()
}
/// Get the first query parameter given the parameter name.
///
/// If you need to get more than one parameter, use [`query_pairs`] on the [`Url`] instead.
pub fn query_param(&self, param: &str) -> Option<Cow<str>> {
self.route
.url
.query_pairs()
.find(|(k, _)| k == param)
.map(|(_, v)| v)
}
/// Returns the nth segment in the path. Paths that end with a slash have
/// the slash removed before determining the segments. If the path has
/// fewer segments than `n` then this method returns `None`.
pub fn nth_segment(&self, n: usize) -> Option<&str> {
self.route.url.path_segments()?.nth(n)
}
/// Returns the last segment in the path. Paths that end with a slash have
/// the slash removed before determining the segments. The root path, `/`,
/// will return an empty string.
pub fn last_segment(&self) -> Option<&str> {
self.route.url.path_segments()?.last()
}
/// Get the named parameter from the path, as defined in your router. The
/// value will be parsed into the type specified by `T` by calling
/// `value.parse::<T>()`. This method returns `None` if the named
/// parameter does not exist in the current path.
pub fn segment(&self, name: &str) -> Option<&str> {
let total_route = match self.route_context {
None => self.route.url.path(),
Some(ref ctx) => &ctx.total_route,
};
let index = total_route
.trim_start_matches('/')
.split('/')
.position(|segment| segment.starts_with(':') && &segment[1..] == name)?;
self.route.url.path_segments()?.nth(index)
}
/// Get the named parameter from the path, as defined in your router. The
/// value will be parsed into the type specified by `T` by calling
/// `value.parse::<T>()`. This method returns `None` if the named
/// parameter does not exist in the current path.
pub fn parse_segment<T>(&self, name: &str) -> Option<Result<T, T::Err>>
where
T: FromStr,
{
self.segment(name).map(|value| value.parse::<T>())
}
/// Get the named parameter from the path, as defined in your router. The
/// value will be parsed into the type specified by `T` by calling
/// `value.parse::<T>()`. This method returns `None` if the named
/// parameter does not exist in the current path.
pub fn parse_segment_or_404<T>(&self, name: &str) -> Option<T>
where
T: FromStr,
{
match self.parse_segment(name) {
Some(Ok(val)) => Some(val),
_ => {
// todo: throw a 404
None
}
/// A hook that provides access to information about the current routing location.
///
/// > The Routable macro will define a version of this hook with an explicit type.
///
/// # Return values
/// - None, when not called inside a [`GenericRouter`] component.
/// - Otherwise the current route.
///
/// # Panic
/// - When the calling component is not nested within a [`GenericRouter`] component durring a debug build.
///
/// # Example
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_router::{prelude::*};
///
/// #[derive(Clone, Routable)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// }
///
/// fn App(cx: Scope) -> Element {
/// render! {
/// h1 { "App" }
/// Router {}
/// }
/// }
///
/// #[inline_props]
/// fn Index(cx: Scope) -> Element {
/// let path = use_route(&cx).unwrap();
/// render! {
/// h2 { "Current Path" }
/// p { "{path}" }
/// }
/// }
/// #
/// # let mut vdom = VirtualDom::new(App);
/// # let _ = vdom.rebuild();
/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><h2>Current Path</h2><p>/</p>")
/// ```
pub fn use_generic_route<R: Routable + Clone>(cx: &ScopeState) -> Option<R> {
match use_router_internal(cx) {
Some(r) => Some(r.current()),
None => {
#[cfg(debug_assertions)]
panic!("`use_route` must have access to a parent router");
#[allow(unreachable_code)]
None
}
}
}
// The entire purpose of this struct is to unubscribe this component when it is unmounted.
// The UseRoute can be cloned into async contexts, so we can't rely on its drop to unubscribe.
// Instead, we hide the drop implementation on this private type exclusive to the hook,
// and reveal our cached version of UseRoute to the component.
struct UseRouteListener {
state: UseRoute,
router: RouterContext,
scope: ScopeId,
}
impl Drop for UseRouteListener {
fn drop(&mut self) {
self.router.unsubscribe_onchange(self.scope)
}
}
+11 -6
View File
@@ -1,8 +1,13 @@
use crate::RouterContext;
use dioxus::{core::ScopeState, prelude::use_context};
use dioxus::prelude::ScopeState;
/// This hook provides access to the `RouterService` for the app.
pub fn use_router(cx: &ScopeState) -> &RouterContext {
use_context::<RouterContext>(cx)
.expect("Cannot call use_route outside the scope of a Router component")
use crate::{
prelude::GenericRouterContext, routable::Routable,
utils::use_router_internal::use_router_internal,
};
/// A hook that provides access to information about the router.
pub fn use_generic_router<R: Routable + Clone>(cx: &ScopeState) -> &GenericRouterContext<R> {
use_router_internal(cx)
.as_ref()
.expect("use_route must have access to a router")
}
+104
View File
@@ -0,0 +1,104 @@
//! Exentsions to the incremental renderer to support pre-caching static routes.
use std::str::FromStr;
use dioxus::prelude::*;
use dioxus_ssr::incremental::{
IncrementalRenderer, IncrementalRendererError, RenderFreshness, WrapBody,
};
use crate::prelude::*;
/// Pre-cache all static routes.
pub async fn pre_cache_static_routes<Rt, R: WrapBody + Send + Sync>(
renderer: &mut IncrementalRenderer,
wrapper: &R,
) -> Result<(), IncrementalRendererError>
where
Rt: Routable,
<Rt as FromStr>::Err: std::fmt::Display,
{
for route in Rt::SITE_MAP
.iter()
.flat_map(|seg| seg.flatten().into_iter())
{
// check if this is a static segment
let mut is_static = true;
let mut full_path = String::new();
for segment in &route {
match segment {
SegmentType::Child => {}
SegmentType::Static(s) => {
full_path += "/";
full_path += s;
}
_ => {
// skip routes with any dynamic segments
is_static = false;
break;
}
}
}
if is_static {
match Rt::from_str(&full_path) {
Ok(route) => {
render_route(
renderer,
route,
&mut tokio::io::sink(),
|vdom| {
let _ = vdom.rebuild();
},
wrapper,
)
.await?;
}
Err(e) => {
log::info!("@ route: {}", full_path);
log::error!("Error pre-caching static route: {}", e);
}
}
}
}
Ok(())
}
/// Render a route to a writer.
pub async fn render_route<R: WrapBody + Send + Sync, Rt, W, F: FnOnce(&mut VirtualDom)>(
renderer: &mut IncrementalRenderer,
route: Rt,
writer: &mut W,
modify_vdom: F,
wrapper: &R,
) -> Result<RenderFreshness, IncrementalRendererError>
where
Rt: Routable,
<Rt as FromStr>::Err: std::fmt::Display,
W: tokio::io::AsyncWrite + Unpin + Send,
{
#[inline_props]
fn RenderPath<R>(cx: Scope, path: R) -> Element
where
R: Routable,
<R as FromStr>::Err: std::fmt::Display,
{
let path = path.clone();
render! {
GenericRouter::<R> {
config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
}
}
}
renderer
.render(
route.to_string(),
RenderPath,
RenderPathProps { path: route },
writer,
modify_vdom,
wrapper,
)
.await
}
+74 -31
View File
@@ -1,41 +1,84 @@
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
// cannot use forbid, because props derive macro generates #[allow(missing_docs)]
#![deny(missing_docs)]
#![allow(non_snake_case)]
mod hooks {
mod use_route;
mod use_router;
pub use use_route::*;
pub use use_router::*;
}
pub use hooks::*;
pub mod navigation;
pub mod routable;
mod components {
#![allow(non_snake_case)]
#[cfg(feature = "ssr")]
pub mod incremental;
/// Components interacting with the router.
pub mod components {
mod default_errors;
pub use default_errors::*;
mod history_buttons;
pub use history_buttons::*;
mod link;
mod redirect;
mod route;
mod router;
pub use link::*;
pub use redirect::*;
pub use route::*;
mod outlet;
pub use outlet::*;
mod router;
pub use router::*;
}
pub use components::*;
mod cfg;
mod routecontext;
mod service;
pub use routecontext::*;
pub use service::*;
/// An error specific to the Router
#[derive(Debug)]
pub enum Error {
/// The route was not found while trying to navigate to it.
///
/// This will force the router to redirect to the 404 page.
NotFound,
mod contexts {
pub(crate) mod navigator;
pub(crate) mod outlet;
pub(crate) mod router;
pub use navigator::*;
pub(crate) use router::*;
}
mod router_cfg;
mod history;
/// Hooks for interacting with the router in components.
pub mod hooks {
mod use_router;
pub(crate) use use_router::*;
mod use_route;
pub use use_route::*;
mod use_navigator;
pub use use_navigator::*;
}
/// A collection of useful items most applications might need.
pub mod prelude {
pub use crate::components::*;
pub use crate::contexts::*;
pub use crate::history::*;
pub use crate::hooks::*;
pub use crate::navigation::*;
pub use crate::routable::*;
pub use crate::router_cfg::RouterConfig;
pub use dioxus_router_macro::Routable;
#[cfg(feature = "ssr")]
pub use crate::incremental::*;
#[cfg(feature = "ssr")]
pub use dioxus_ssr::incremental::*;
#[doc(hidden)]
/// A component with props used in the macro
pub trait HasProps {
/// The props type of the component.
type Props;
}
impl<P> HasProps for dioxus::prelude::Component<P> {
type Props = P;
}
}
mod utils {
pub(crate) mod use_router_internal;
}
+114
View File
@@ -0,0 +1,114 @@
//! Types pertaining to navigation.
use std::{
convert::TryFrom,
fmt::{Debug, Display},
str::FromStr,
};
use url::{ParseError, Url};
use crate::routable::Routable;
/// A target for the router to navigate to.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum NavigationTarget<R: Routable> {
/// An internal path that the router can navigate to by itself.
///
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_router::prelude::*;
/// # use dioxus_router::navigation::NavigationTarget;
/// # #[inline_props]
/// # fn Index(cx: Scope) -> Element {
/// # todo!()
/// # }
/// #[derive(Clone, Routable, PartialEq, Debug)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// }
/// let explicit = NavigationTarget::Internal(Route::Index {});
/// let implicit: NavigationTarget::<Route> = "/".parse().unwrap();
/// assert_eq!(explicit, implicit);
/// ```
Internal(R),
/// An external target that the router doesn't control.
///
/// ```rust
/// # use dioxus::prelude::*;
/// # use dioxus_router::prelude::*;
/// # use dioxus_router::navigation::NavigationTarget;
/// # #[inline_props]
/// # fn Index(cx: Scope) -> Element {
/// # todo!()
/// # }
/// #[derive(Clone, Routable, PartialEq, Debug)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// }
/// let explicit = NavigationTarget::<Route>::External(String::from("https://dioxuslabs.com/"));
/// let implicit: NavigationTarget::<Route> = "https://dioxuslabs.com/".parse().unwrap();
/// assert_eq!(explicit, implicit);
/// ```
External(String),
}
impl<R: Routable> TryFrom<&str> for NavigationTarget<R> {
type Error = NavigationTargetParseError<R>;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
impl<R: Routable> From<R> for NavigationTarget<R> {
fn from(value: R) -> Self {
Self::Internal(value)
}
}
impl<R: Routable> Display for NavigationTarget<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NavigationTarget::Internal(r) => write!(f, "{}", r),
NavigationTarget::External(s) => write!(f, "{}", s),
}
}
}
/// An error that can occur when parsing a [`NavigationTarget`].
pub enum NavigationTargetParseError<R: Routable> {
/// A URL that is not valid.
InvalidUrl(ParseError),
/// An internal URL that is not valid.
InvalidInternalURL(<R as FromStr>::Err),
}
impl<R: Routable> Debug for NavigationTargetParseError<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NavigationTargetParseError::InvalidUrl(e) => write!(f, "Invalid URL: {}", e),
NavigationTargetParseError::InvalidInternalURL(_) => {
write!(f, "Invalid internal URL")
}
}
}
}
impl<R: Routable> FromStr for NavigationTarget<R> {
type Err = NavigationTargetParseError<R>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match Url::parse(s) {
Ok(_) => Ok(Self::External(s.to_string())),
Err(ParseError::RelativeUrlWithoutBase) => {
Ok(Self::Internal(R::from_str(s).map_err(|e| {
NavigationTargetParseError::InvalidInternalURL(e)
})?))
}
Err(e) => Err(NavigationTargetParseError::InvalidUrl(e)),
}
}
}
+216
View File
@@ -0,0 +1,216 @@
//! # Routable
#![allow(non_snake_case)]
use dioxus::prelude::*;
use std::{fmt::Display, str::FromStr};
/// An error that occurs when parsing a route
#[derive(Debug, PartialEq)]
pub struct RouteParseError<E: std::fmt::Display> {
/// The attempted routes that failed to match
pub attempted_routes: Vec<E>,
}
impl<E: std::fmt::Display> std::fmt::Display for RouteParseError<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Route did not match:\nAttempted Matches:\n")?;
for (i, route) in self.attempted_routes.iter().enumerate() {
writeln!(f, "{}) {route}", i + 1)?;
}
Ok(())
}
}
/// Something that can be created from a query string
pub trait FromQuery {
/// Create an instance of `Self` from a query string
fn from_query(query: &str) -> Self;
}
impl<T: for<'a> From<&'a str>> FromQuery for T {
fn from_query(query: &str) -> Self {
T::from(query)
}
}
/// Something that can be created from a route segment
pub trait FromRouteSegment: Sized {
/// The error that can occur when parsing a route segment
type Err;
/// Create an instance of `Self` from a route segment
fn from_route_segment(route: &str) -> Result<Self, Self::Err>;
}
impl<T: FromStr> FromRouteSegment for T
where
<T as FromStr>::Err: std::fmt::Display,
{
type Err = <T as FromStr>::Err;
fn from_route_segment(route: &str) -> Result<Self, Self::Err> {
T::from_str(route)
}
}
/// Something that can be converted to route segments
pub trait ToRouteSegments {
/// Display the route segments
fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
}
impl<I, T: std::fmt::Display> ToRouteSegments for I
where
I: IntoIterator<Item = T>,
{
fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for segment in self {
write!(f, "/")?;
write!(f, "{}", segment)?;
}
Ok(())
}
}
/// Something that can be created from route segments
pub trait FromRouteSegments: Sized {
/// The error that can occur when parsing route segments
type Err;
/// Create an instance of `Self` from route segments
fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err>;
}
impl<I: std::iter::FromIterator<String>> FromRouteSegments for I {
type Err = <String as FromRouteSegment>::Err;
fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err> {
segments
.iter()
.map(|s| String::from_route_segment(s))
.collect()
}
}
/// Something that can be:
/// 1. Converted from a route
/// 2. Converted to a route
/// 3. Rendered as a component
///
/// This trait can be derived using the `#[derive(Routable)]` macro
pub trait Routable: std::fmt::Display + std::str::FromStr + Clone + 'static {
/// The error that can occur when parsing a route
const SITE_MAP: &'static [SiteMapSegment];
/// Render the route at the given level
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
/// Gets a list of all static routes
fn static_routes() -> Vec<Self> {
Self::SITE_MAP
.iter()
.flat_map(|segment| segment.flatten())
.filter_map(|route| {
if route
.iter()
.all(|segment| matches!(segment, SegmentType::Static(_)))
{
Self::from_str(
&route
.iter()
.map(|segment| match segment {
SegmentType::Static(s) => s.to_string(),
_ => unreachable!(),
})
.collect::<Vec<_>>()
.join("/"),
)
.ok()
} else {
None
}
})
.collect()
}
}
trait RoutableFactory {
type Err: std::fmt::Display;
type Routable: Routable + FromStr<Err = Self::Err>;
}
impl<R: Routable + FromStr> RoutableFactory for R
where
<R as FromStr>::Err: std::fmt::Display,
{
type Err = <R as FromStr>::Err;
type Routable = R;
}
trait RouteRenderable: std::fmt::Display + 'static {
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
}
impl<R: Routable> RouteRenderable for R
where
<R as FromStr>::Err: std::fmt::Display,
{
fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> {
self.render(cx, level)
}
}
/// A type erased map of the site structurens
#[derive(Debug, Clone, PartialEq)]
pub struct SiteMapSegment {
/// The type of the route segment
pub segment_type: SegmentType,
/// The children of the route segment
pub children: &'static [SiteMapSegment],
}
impl SiteMapSegment {
/// Take a map of the site structure and flatten it into a vector of routes
pub fn flatten(&self) -> Vec<Vec<SegmentType>> {
let mut routes = Vec::new();
self.flatten_inner(&mut routes, Vec::new());
routes
}
fn flatten_inner(&self, routes: &mut Vec<Vec<SegmentType>>, current: Vec<SegmentType>) {
let mut current = current;
current.push(self.segment_type.clone());
if self.children.is_empty() {
routes.push(current);
} else {
for child in self.children {
child.flatten_inner(routes, current.clone());
}
}
}
}
/// The type of a route segment
#[derive(Debug, Clone, PartialEq)]
pub enum SegmentType {
/// A static route segment
Static(&'static str),
/// A dynamic route segment
Dynamic(&'static str),
/// A catch all route segment
CatchAll(&'static str),
/// A child router
Child,
}
impl Display for SegmentType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
SegmentType::Static(s) => write!(f, "/{}", s),
SegmentType::Child => Ok(()),
SegmentType::Dynamic(s) => write!(f, "/:{}", s),
SegmentType::CatchAll(s) => write!(f, "/:...{}", s),
}
}
}
-24
View File
@@ -1,24 +0,0 @@
/// A `RouteContext` is a context that is provided by [`Route`](fn.Route.html) components.
///
/// This signals to all child [`Route`] and [`Link`] components that they are
/// currently nested under this route.
#[derive(Debug, Clone)]
pub struct RouteContext {
/// The `declared_route` is the sub-piece of the route that matches this pattern.
///
///
/// It follows this pattern:
/// ```ignore
/// "name/:id"
/// ```
pub declared_route: String,
/// The `total_route` is the full route that matches this pattern.
///
///
/// It follows this pattern:
/// ```ignore
/// "/level0/level1/:id"
/// ```
pub total_route: String,
}
+138
View File
@@ -0,0 +1,138 @@
use std::sync::Arc;
use crate::contexts::router::RoutingCallback;
use crate::history::HistoryProvider;
use crate::routable::Routable;
use dioxus::prelude::*;
use crate::prelude::*;
/// Global configuration options for the router.
///
/// This implements [`Default`] and follows the builder pattern, so you can use it like this:
/// ```rust,no_run
/// # use dioxus_router::prelude::*;
/// # use dioxus::prelude::*;
/// # #[inline_props]
/// # fn Index(cx: Scope) -> Element {
/// # todo!()
/// # }
/// #[derive(Clone, Routable)]
/// enum Route {
/// #[route("/")]
/// Index {},
/// }
/// let cfg = RouterConfig::default().history(WebHistory::<Route>::default());
/// ```
pub struct RouterConfig<R: Routable> {
pub(crate) failure_external_navigation: fn(Scope) -> Element,
pub(crate) history: Option<Box<dyn HistoryProvider<R>>>,
pub(crate) on_update: Option<RoutingCallback<R>>,
}
#[cfg(feature = "serde")]
impl<R: Routable + Clone> Default for RouterConfig<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
fn default() -> Self {
Self {
failure_external_navigation: FailureExternalNavigation::<R>,
history: None,
on_update: None,
}
}
}
#[cfg(feature = "serde")]
impl<R: Routable + Clone> RouterConfig<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
R: serde::Serialize + serde::de::DeserializeOwned,
{
pub(crate) fn get_history(self) -> Box<dyn HistoryProvider<R>> {
self.history.unwrap_or_else(|| {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let history = Box::<WebHistory<R>>::default();
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
let history = Box::<MemoryHistory<R>>::default();
history
})
}
}
#[cfg(not(feature = "serde"))]
impl<R: Routable + Clone> Default for RouterConfig<R>
where
<R as std::str::FromStr>::Err: std::fmt::Display,
{
fn default() -> Self {
Self {
failure_external_navigation: FailureExternalNavigation::<R>,
history: None,
on_update: None,
}
}
}
#[cfg(not(feature = "serde"))]
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 HistoryProvider<R>> {
self.history.take().unwrap_or_else(|| {
#[cfg(all(target_arch = "wasm32", feature = "web"))]
let history = Box::<WebHistory<R>>::default();
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
let history = Box::<MemoryHistory<R>>::default();
history
})
}
}
impl<R: Routable> RouterConfig<R> {
/// A function to be called whenever the routing is updated.
///
/// The callback is invoked after the routing is updated, but before components and hooks are
/// updated.
///
/// If the callback returns a [`NavigationTarget`] the router will replace the current location
/// with it. If no navigation failure was triggered, the router will then updated dependent
/// components and hooks.
///
/// The callback is called no more than once per rerouting. It will not be called if a
/// navigation failure occurs.
///
/// Defaults to [`None`].
pub fn on_update(
self,
callback: impl Fn(GenericRouterContext<R>) -> Option<NavigationTarget<R>> + 'static,
) -> Self {
Self {
on_update: Some(Arc::new(callback)),
..self
}
}
/// The [`HistoryProvider`] the router should use.
///
/// Defaults to a default [`MemoryHistory`].
pub fn history(self, history: impl HistoryProvider<R> + 'static) -> Self {
Self {
history: Some(Box::new(history)),
..self
}
}
/// A component to render when an external navigation fails.
///
/// Defaults to a router-internal component called [`FailureExternalNavigation`]
pub fn failure_external_navigation(self, component: fn(Scope) -> Element) -> Self {
Self {
failure_external_navigation: component,
..self
}
}
}
-439
View File
@@ -1,439 +0,0 @@
// todo: how does router work in multi-window contexts?
// does each window have its own router? probably, lol
use crate::cfg::RouterCfg;
use dioxus::core::{ScopeId, ScopeState, VirtualDom};
use std::any::Any;
use std::rc::Weak;
use std::{
cell::{Cell, RefCell},
collections::{HashMap, HashSet},
rc::Rc,
str::FromStr,
sync::Arc,
};
use url::Url;
/// A clonable handle to the router
pub type RouterContext = Rc<RouterService>;
/// An abstraction over the platform's history API.
///
/// The history is denoted using web-like semantics, with forward slashes delmitiing
/// routes and question marks denoting optional parameters.
///
/// This RouterService is exposed so you can modify the history directly. It
/// does not provide a high-level ergonomic API for your components. Instead,
/// you should consider using the components and hooks instead.
/// - [`Route`](struct.Route.html)
/// - [`Link`](struct.Link.html)
/// - [`UseRoute`](struct.UseRoute.html)
/// - [`Router`](struct.Router.html)
///
///
/// # Example
///
/// ```rust, ignore
/// let router = Router::new();
/// router.push_route("/home/custom");
/// cx.provide_context(router);
/// ```
///
/// # Platform Specific
///
/// - On the web, this is a [`BrowserHistory`](https://docs.rs/gloo/0.3.0/gloo/history/struct.BrowserHistory.html).
/// - On desktop, mobile, and SSR, this is just a Vec of Strings. Currently on
/// desktop, there is no way to tap into forward/back for the app unless explicitly set.
pub struct RouterService {
pub(crate) route_found: Cell<Option<ScopeId>>,
pub(crate) stack: RefCell<Vec<Arc<ParsedRoute>>>,
pub(crate) slots: Rc<RefCell<HashMap<ScopeId, String>>>,
pub(crate) ordering: Rc<RefCell<Vec<ScopeId>>>,
pub(crate) onchange_listeners: Rc<RefCell<HashSet<ScopeId>>>,
pub(crate) history: Box<dyn RouterProvider>,
pub(crate) regen_any_route: Arc<dyn Fn(ScopeId)>,
pub(crate) router_id: ScopeId,
pub(crate) cfg: RouterCfg,
}
/// A route is a combination of window title, saved state, and a URL.
#[derive(Debug, Clone)]
pub struct ParsedRoute {
/// The URL of the route.
pub url: Url,
/// The title of the route.
pub title: Option<String>,
/// The serialized state of the route.
pub serialized_state: Option<String>,
}
impl RouterService {
pub(crate) fn new(cx: &ScopeState, cfg: RouterCfg) -> RouterContext {
#[cfg(feature = "web")]
let history = Box::new(web::new());
#[cfg(not(feature = "web"))]
let history = Box::new(hash::new());
let route = match &cfg.initial_url {
Some(url) => Arc::new(ParsedRoute {
url: Url::from_str(url).unwrap_or_else(|_|
panic!(
"RouterCfg expects a valid initial_url, but got '{}'. Example: '{{scheme}}://{{?authority}}/{{?path}}'",
&url
)
),
title: None,
serialized_state: None,
}),
None => Arc::new(history.init_location()),
};
let svc = Rc::new(Self {
cfg,
regen_any_route: cx.schedule_update_any(),
router_id: cx.scope_id(),
route_found: Cell::new(None),
stack: RefCell::new(vec![route]),
ordering: Default::default(),
slots: Default::default(),
onchange_listeners: Default::default(),
history,
});
svc.history.attach_listeners(Rc::downgrade(&svc));
svc
}
/// Push a new route with no custom title or serialized state.
///
/// This is a convenience method for easily navigating.
pub fn navigate_to(&self, route: &str) {
self.push_route(route, None, None);
}
/// Push a new route to the history.
///
/// This will trigger a route change event.
///
/// This does not modify the current route
pub fn push_route(&self, route: &str, title: Option<String>, serialized_state: Option<String>) {
let new_route = Arc::new(ParsedRoute {
url: self.current_location().url.join(route).ok().unwrap(),
title,
serialized_state,
});
self.history.push(&new_route);
self.stack.borrow_mut().push(new_route);
self.regen_routes();
}
/// Instead of pushing a new route, replaces the current route.
pub fn replace_route(
&self,
route: &str,
title: Option<String>,
serialized_state: Option<String>,
) {
let new_route = Arc::new(ParsedRoute {
url: self.current_location().url.join(route).ok().unwrap(),
title,
serialized_state,
});
self.history.replace(&new_route);
*self.stack.borrow_mut().last_mut().unwrap() = new_route;
self.regen_routes();
}
/// Pop the current route from the history.
pub fn pop_route(&self) {
let mut stack = self.stack.borrow_mut();
if stack.len() > 1 {
stack.pop();
}
self.regen_routes();
}
/// Regenerate any routes that need to be regenerated, discarding the currently found route
///
/// You probably don't need this method
pub fn regen_routes(&self) {
self.route_found.set(None);
(self.regen_any_route)(self.router_id);
for listener in self.onchange_listeners.borrow().iter() {
log::trace!("Regenerating scope {:?}", listener);
(self.regen_any_route)(*listener);
}
for route in self.ordering.borrow().iter().rev() {
(self.regen_any_route)(*route);
}
}
/// Get the current location of the Router
pub fn current_location(&self) -> Arc<ParsedRoute> {
self.stack.borrow().last().unwrap().clone()
}
/// Get the current native location of the Router
pub fn native_location<T: 'static>(&self) -> Option<Box<T>> {
self.history.native_location().downcast::<T>().ok()
}
/// Registers a scope to regenerate on route change.
///
/// This is useful if you've built some abstraction on top of the router service.
pub fn subscribe_onchange(&self, id: ScopeId) {
self.onchange_listeners.borrow_mut().insert(id);
}
/// Unregisters a scope to regenerate on route change.
///
/// This is useful if you've built some abstraction on top of the router service.
pub fn unsubscribe_onchange(&self, id: ScopeId) {
self.onchange_listeners.borrow_mut().remove(&id);
}
pub(crate) fn register_total_route(&self, route: String, scope: ScopeId) {
let clean = clean_route(route);
self.slots.borrow_mut().insert(scope, clean);
self.ordering.borrow_mut().push(scope);
}
pub(crate) fn should_render(&self, scope: ScopeId) -> bool {
if let Some(root_id) = self.route_found.get() {
return root_id == scope;
}
let roots = self.slots.borrow();
if let Some(route) = roots.get(&scope) {
let cur = &self.current_location().url;
log::trace!("Checking if {} matches {}", cur, route);
if route_matches_path(cur, route, self.cfg.base_url.as_ref()) || route.is_empty() {
self.route_found.set(Some(scope));
true
} else {
false
}
} else {
false
}
}
}
/// Get the router service from an existing VirtualDom.
///
/// Takes an optional target_scope parameter to specify the scope to use if ScopeId is not the component
/// that owns the router.
///
/// This might change in the future.
pub fn get_router_from_vdom(dom: &VirtualDom, target_scope: ScopeId) -> Option<RouterContext> {
dom.get_scope(target_scope)
.and_then(|scope| scope.consume_context::<RouterContext>())
}
fn clean_route(route: String) -> String {
if route.as_str() == "/" {
return route;
}
route.trim_end_matches('/').to_string()
}
fn clean_path(path: &str) -> &str {
if path == "/" {
return path;
}
let sub = path.trim_end_matches('/');
if sub.starts_with('/') {
&path[1..]
} else {
sub
}
}
fn route_matches_path(cur: &Url, attempt: &str, base_url: Option<&String>) -> bool {
let cur_piece_iter = cur.path_segments().unwrap();
let mut cur_pieces = match base_url {
// baseurl is naive right now and doesn't support multiple nesting levels
Some(_) => cur_piece_iter.skip(1).collect::<Vec<_>>(),
None => cur_piece_iter.collect::<Vec<_>>(),
};
if attempt == "/" && cur_pieces.len() == 1 && cur_pieces[0].is_empty() {
return true;
}
// allow slashes at the end of the path
if cur_pieces.last() == Some(&"") {
cur_pieces.pop();
}
let attempt_pieces = clean_path(attempt).split('/').collect::<Vec<_>>();
if attempt_pieces.len() != cur_pieces.len() {
return false;
}
for (i, r) in attempt_pieces.iter().enumerate() {
// If this is a parameter then it matches as long as there's
// _any_thing in that spot in the path.
if r.starts_with(':') {
continue;
}
if cur_pieces[i] != *r {
return false;
}
}
true
}
pub(crate) trait RouterProvider {
fn push(&self, route: &ParsedRoute);
fn replace(&self, route: &ParsedRoute);
fn native_location(&self) -> Box<dyn Any>;
fn init_location(&self) -> ParsedRoute;
fn attach_listeners(&self, svc: Weak<RouterService>);
}
#[cfg(not(feature = "web"))]
mod hash {
use super::*;
pub fn new() -> HashRouter {
HashRouter {}
}
/// a simple cross-platform hash-based router
pub struct HashRouter {}
impl RouterProvider for HashRouter {
fn push(&self, _route: &ParsedRoute) {}
fn native_location(&self) -> Box<dyn Any> {
Box::new(())
}
fn init_location(&self) -> ParsedRoute {
ParsedRoute {
url: Url::parse("app:///").unwrap(),
title: None,
serialized_state: None,
}
}
fn replace(&self, _route: &ParsedRoute) {}
fn attach_listeners(&self, _svc: Weak<RouterService>) {}
}
}
#[cfg(feature = "web")]
mod web {
use super::RouterProvider;
use crate::ParsedRoute;
use gloo_events::EventListener;
use std::{any::Any, cell::Cell};
use web_sys::History;
pub struct WebRouter {
// keep it around so it drops when the router is dropped
_listener: Cell<Option<gloo_events::EventListener>>,
window: web_sys::Window,
history: History,
}
impl RouterProvider for WebRouter {
fn push(&self, route: &ParsedRoute) {
let ParsedRoute {
url,
title,
serialized_state,
} = route;
let _ = self.history.push_state_with_url(
&wasm_bindgen::JsValue::from_str(serialized_state.as_deref().unwrap_or("")),
title.as_deref().unwrap_or(""),
Some(url.as_str()),
);
}
fn replace(&self, route: &ParsedRoute) {
let ParsedRoute {
url,
title,
serialized_state,
} = route;
let _ = self.history.replace_state_with_url(
&wasm_bindgen::JsValue::from_str(serialized_state.as_deref().unwrap_or("")),
title.as_deref().unwrap_or(""),
Some(url.as_str()),
);
}
fn native_location(&self) -> Box<dyn Any> {
Box::new(self.window.location())
}
fn init_location(&self) -> ParsedRoute {
ParsedRoute {
url: url::Url::parse(&web_sys::window().unwrap().location().href().unwrap())
.unwrap(),
title: web_sys::window()
.unwrap()
.document()
.unwrap()
.title()
.into(),
serialized_state: None,
}
}
fn attach_listeners(&self, svc: std::rc::Weak<crate::RouterService>) {
self._listener.set(Some(EventListener::new(
&web_sys::window().unwrap(),
"popstate",
move |_| {
if let Some(svc) = svc.upgrade() {
svc.pop_route();
}
},
)));
}
}
pub(crate) fn new() -> WebRouter {
WebRouter {
history: web_sys::window().unwrap().history().unwrap(),
window: web_sys::window().unwrap(),
_listener: Cell::new(None),
}
}
}
@@ -0,0 +1,36 @@
use dioxus::prelude::{ScopeId, ScopeState};
use crate::{contexts::router::GenericRouterContext, prelude::*};
/// A private hook to subscribe to the router.
///
/// Used to reduce redundancy within other components/hooks. Safe to call multiple times for a
/// single component, but not recommended. Multiple subscriptions will be discarded.
///
/// # Return values
/// - [`None`], when the current component isn't a descendant of a [`GenericRouter`] component.
/// - Otherwise [`Some`].
pub(crate) fn use_router_internal<R: Routable>(
cx: &ScopeState,
) -> &Option<GenericRouterContext<R>> {
let inner = cx.use_hook(|| {
let router = cx.consume_context::<GenericRouterContext<R>>()?;
let id = cx.scope_id();
router.subscribe(id);
Some(Subscription { router, id })
});
cx.use_hook(|| inner.as_ref().map(|s| s.router.clone()))
}
struct Subscription<R: Routable> {
router: GenericRouterContext<R>,
id: ScopeId,
}
impl<R: Routable> Drop for Subscription<R> {
fn drop(&mut self) {
self.router.unsubscribe(self.id);
}
}
+378
View File
@@ -0,0 +1,378 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_router::prelude::*;
use std::str::FromStr;
fn prepare<R: Routable>() -> String
where
<R as FromStr>::Err: std::fmt::Display,
{
let mut vdom = VirtualDom::new_with_props(
App,
AppProps::<R> {
phantom: std::marker::PhantomData,
},
);
let _ = vdom.rebuild();
return dioxus_ssr::render(&vdom);
#[derive(Props)]
struct AppProps<R: Routable> {
phantom: std::marker::PhantomData<R>,
}
impl<R: Routable> PartialEq for AppProps<R> {
fn eq(&self, _other: &Self) -> bool {
false
}
}
fn App<R: Routable>(cx: Scope<AppProps<R>>) -> Element
where
<R as FromStr>::Err: std::fmt::Display,
{
render! {
h1 { "App" }
GenericRouter::<R> {
config: || RouterConfig::default().history(MemoryHistory::default())
}
}
}
}
#[test]
fn href_internal() {
#[derive(Routable, Clone)]
enum Route {
#[route("/")]
Root {},
#[route("/test")]
Test {},
}
#[inline_props]
fn Test(cx: Scope) -> Element {
todo!()
}
#[inline_props]
fn Root(cx: Scope) -> Element {
render! {
Link {
target: Route::Test {},
"Link"
}
}
}
let expected = format!(
"<h1>App</h1><a {href} {default} {class} {id} {rel} {target}>Link</a>",
href = r#"href="/test""#,
default = r#"dioxus-prevent-default="onclick""#,
class = r#"class="""#,
id = r#"id="""#,
rel = r#"rel="""#,
target = r#"target="""#
);
assert_eq!(prepare::<Route>(), expected);
}
#[test]
fn href_external() {
#[derive(Routable, Clone)]
enum Route {
#[route("/")]
Root {},
#[route("/test")]
Test {},
}
#[inline_props]
fn Test(cx: Scope) -> Element {
todo!()
}
#[inline_props]
fn Root(cx: Scope) -> Element {
render! {
Link {
target: NavigationTarget::External("https://dioxuslabs.com/".into()),
"Link"
}
}
}
let expected = format!(
"<h1>App</h1><a {href} {default} {class} {id} {rel} {target}>Link</a>",
href = r#"href="https://dioxuslabs.com/""#,
default = r#"dioxus-prevent-default="""#,
class = r#"class="""#,
id = r#"id="""#,
rel = r#"rel="noopener noreferrer""#,
target = r#"target="""#
);
assert_eq!(prepare::<Route>(), expected);
}
#[test]
fn with_class() {
#[derive(Routable, Clone)]
enum Route {
#[route("/")]
Root {},
#[route("/test")]
Test {},
}
#[inline_props]
fn Test(cx: Scope) -> Element {
todo!()
}
#[inline_props]
fn Root(cx: Scope) -> Element {
render! {
Link {
target: Route::Test {},
class: "test_class",
"Link"
}
}
}
let expected = format!(
"<h1>App</h1><a {href} {default} {class} {id} {rel} {target}>Link</a>",
href = r#"href="/test""#,
default = r#"dioxus-prevent-default="onclick""#,
class = r#"class="test_class""#,
id = r#"id="""#,
rel = r#"rel="""#,
target = r#"target="""#
);
assert_eq!(prepare::<Route>(), expected);
}
#[test]
fn with_active_class_active() {
#[derive(Routable, Clone)]
enum Route {
#[route("/")]
Root {},
}
#[inline_props]
fn Root(cx: Scope) -> Element {
render! {
Link {
target: Route::Root {},
active_class: "active_class",
class: "test_class",
"Link"
}
}
}
let expected = format!(
"<h1>App</h1><a {href} {default} {class} {id} {rel} {target}>Link</a>",
href = r#"href="/""#,
default = r#"dioxus-prevent-default="onclick""#,
class = r#"class="test_class active_class""#,
id = r#"id="""#,
rel = r#"rel="""#,
target = r#"target="""#
);
assert_eq!(prepare::<Route>(), expected);
}
#[test]
fn with_active_class_inactive() {
#[derive(Routable, Clone)]
enum Route {
#[route("/")]
Root {},
#[route("/test")]
Test {},
}
#[inline_props]
fn Test(cx: Scope) -> Element {
todo!()
}
#[inline_props]
fn Root(cx: Scope) -> Element {
render! {
Link {
target: Route::Test {},
active_class: "active_class",
class: "test_class",
"Link"
}
}
}
let expected = format!(
"<h1>App</h1><a {href} {default} {class} {id} {rel} {target}>Link</a>",
href = r#"href="/test""#,
default = r#"dioxus-prevent-default="onclick""#,
class = r#"class="test_class""#,
id = r#"id="""#,
rel = r#"rel="""#,
target = r#"target="""#
);
assert_eq!(prepare::<Route>(), expected);
}
#[test]
fn with_id() {
#[derive(Routable, Clone)]
enum Route {
#[route("/")]
Root {},
#[route("/test")]
Test {},
}
#[inline_props]
fn Test(cx: Scope) -> Element {
todo!()
}
#[inline_props]
fn Root(cx: Scope) -> Element {
render! {
Link {
target: Route::Test {},
id: "test_id",
"Link"
}
}
}
let expected = format!(
"<h1>App</h1><a {href} {default} {class} {id} {rel} {target}>Link</a>",
href = r#"href="/test""#,
default = r#"dioxus-prevent-default="onclick""#,
class = r#"class="""#,
id = r#"id="test_id""#,
rel = r#"rel="""#,
target = r#"target="""#
);
assert_eq!(prepare::<Route>(), expected);
}
#[test]
fn with_new_tab() {
#[derive(Routable, Clone)]
enum Route {
#[route("/")]
Root {},
#[route("/test")]
Test {},
}
#[inline_props]
fn Test(cx: Scope) -> Element {
todo!()
}
#[inline_props]
fn Root(cx: Scope) -> Element {
render! {
Link {
target: Route::Test {},
new_tab: true,
"Link"
}
}
}
let expected = format!(
"<h1>App</h1><a {href} {default} {class} {id} {rel} {target}>Link</a>",
href = r#"href="/test""#,
default = r#"dioxus-prevent-default="""#,
class = r#"class="""#,
id = r#"id="""#,
rel = r#"rel="""#,
target = r#"target="_blank""#
);
assert_eq!(prepare::<Route>(), expected);
}
#[test]
fn with_new_tab_external() {
#[derive(Routable, Clone)]
enum Route {
#[route("/")]
Root {},
}
#[inline_props]
fn Root(cx: Scope) -> Element {
render! {
Link {
target: NavigationTarget::External("https://dioxuslabs.com/".into()),
new_tab: true,
"Link"
}
}
}
let expected = format!(
"<h1>App</h1><a {href} {default} {class} {id} {rel} {target}>Link</a>",
href = r#"href="https://dioxuslabs.com/""#,
default = r#"dioxus-prevent-default="""#,
class = r#"class="""#,
id = r#"id="""#,
rel = r#"rel="noopener noreferrer""#,
target = r#"target="_blank""#
);
assert_eq!(prepare::<Route>(), expected);
}
#[test]
fn with_rel() {
#[derive(Routable, Clone)]
enum Route {
#[route("/")]
Root {},
#[route("/test")]
Test {},
}
#[inline_props]
fn Test(cx: Scope) -> Element {
todo!()
}
#[inline_props]
fn Root(cx: Scope) -> Element {
render! {
Link {
target: Route::Test {},
rel: "test_rel",
"Link"
}
}
}
let expected = format!(
"<h1>App</h1><a {href} {default} {class} {id} {rel} {target}>Link</a>",
href = r#"href="/test""#,
default = r#"dioxus-prevent-default="onclick""#,
class = r#"class="""#,
id = r#"id="""#,
rel = r#"rel="test_rel""#,
target = r#"target="""#
);
assert_eq!(prepare::<Route>(), expected);
}
+2
View File
@@ -0,0 +1,2 @@
mod link;
mod outlet;
+145
View File
@@ -0,0 +1,145 @@
#![allow(non_snake_case, unused)]
use dioxus::prelude::*;
use dioxus_router::prelude::*;
fn prepare(path: impl Into<String>) -> VirtualDom {
let mut vdom = VirtualDom::new_with_props(App, AppProps { path: path.into() });
let _ = vdom.rebuild();
return vdom;
#[derive(Routable, Clone)]
#[rustfmt::skip]
enum Route {
#[route("/")]
RootIndex {},
#[nest("/fixed")]
#[layout(Fixed)]
#[route("/")]
FixedIndex {},
#[route("/fixed")]
FixedFixed {},
#[end_layout]
#[end_nest]
#[nest("/:id")]
#[layout(Parameter)]
#[route("/")]
ParameterIndex { id: u8 },
#[route("/fixed")]
ParameterFixed { id: u8 },
}
#[derive(Debug, Props, PartialEq)]
struct AppProps {
path: String,
}
fn App(cx: Scope<AppProps>) -> Element {
render! {
h1 { "App" }
Router {
config: {
let path = cx.props.path.parse().unwrap();
move || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
}
}
}
}
#[inline_props]
fn RootIndex(cx: Scope) -> Element {
render! {
h2 { "Root Index" }
}
}
#[inline_props]
fn Fixed(cx: Scope) -> Element {
render! {
h2 { "Fixed" }
Outlet { }
}
}
#[inline_props]
fn FixedIndex(cx: Scope) -> Element {
render! {
h3 { "Fixed - Index" }
}
}
#[inline_props]
fn FixedFixed(cx: Scope) -> Element {
render! {
h3 { "Fixed - Fixed"}
}
}
#[inline_props]
fn Parameter(cx: Scope, id: u8) -> Element {
render! {
h2 { "Parameter {id}" }
Outlet { }
}
}
#[inline_props]
fn ParameterIndex(cx: Scope, id: u8) -> Element {
render! {
h3 { "Parameter - Index" }
}
}
#[inline_props]
fn ParameterFixed(cx: Scope, id: u8) -> Element {
render! {
h3 { "Parameter - Fixed" }
}
}
}
#[test]
fn root_index() {
let vdom = prepare("/");
let html = dioxus_ssr::render(&vdom);
assert_eq!(html, "<h1>App</h1><h2>Root Index</h2>");
}
#[test]
fn fixed() {
let vdom = prepare("/fixed");
let html = dioxus_ssr::render(&vdom);
assert_eq!(html, "<h1>App</h1><h2>Fixed</h2><h3>Fixed - Index</h3>");
}
#[test]
fn fixed_fixed() {
let vdom = prepare("/fixed/fixed");
let html = dioxus_ssr::render(&vdom);
assert_eq!(html, "<h1>App</h1><h2>Fixed</h2><h3>Fixed - Fixed</h3>");
}
#[test]
fn parameter() {
let vdom = prepare("/18");
let html = dioxus_ssr::render(&vdom);
assert_eq!(
html,
"<h1>App</h1><h2>Parameter 18</h2><h3>Parameter - Index</h3>"
);
}
#[test]
fn parameter_fixed() {
let vdom = prepare("/18/fixed");
let html = dioxus_ssr::render(&vdom);
assert_eq!(
html,
"<h1>App</h1><h2>Parameter 18</h2><h3>Parameter - Fixed</h3>"
);
}
-60
View File
@@ -1,60 +0,0 @@
#![cfg(target_arch = "wasm32")]
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_router::*;
use gloo_utils::document;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn simple_test() {
fn main() {
console_error_panic_hook::set_once();
wasm_logger::init(wasm_logger::Config::new(log::Level::Debug));
dioxus_web::launch(app);
}
fn app(cx: Scope) -> Element {
cx.render(rsx! {
Router {
onchange: move |router: RouterContext| log::trace!("route changed to {:?}", router.current_location()),
active_class: "is-active",
Route { to: "/", Home {} }
Route { to: "blog",
Route { to: "/", BlogList {} }
Route { to: ":id", BlogPost {} }
}
}
})
}
fn Home(cx: Scope) -> Element {
cx.render(rsx! {
div {
h1 { "Home" }
}
})
}
fn BlogList(cx: Scope) -> Element {
cx.render(rsx! {
div {
}
})
}
fn BlogPost(cx: Scope) -> Element {
let _id = use_route(cx).parse_segment::<usize>("id").unwrap();
cx.render(rsx! {
div { }
})
}
main();
let _ = document();
}
-221
View File
@@ -1,221 +0,0 @@
## Usage
Using Dioxus Router is pretty simple. Add a top-level Router to your app (not necessary but good practice) and then start adding routes, specifying the "to" field:
```rust
fn app() {
cx.render(rsx! {
Router {
Route { to: "/", Component {} },
Route { to: "/blog", Blog {} },
Route { to: "/about", Blog {} },
Route { to: "/contact", Blog {} },
Route { to: "/shop", Blog {} },
}
})
}
```
All Routes must start with a forward slash.
To have dynamic route segments, use the `:id` syntax. If concrete paths come *before* the dynamic syntax, then those will be chosen first.
```rust
cx.render(rsx! {
Router {
Route { to: "/", Component {} },
Route { to: "/blog", BlogList {} },
Route { to: "/blog/welcome", BlogWelcome {} },
Route { to: "/blog/:post", BlogPost {} },
}
})
```
### Nested `Routes`
Routes can be composed at various levels, so you don't just need top-level routes. To do this, simple add Routes inside other Routes
```rust
cx.render(rsx! {
Router {
Route { to: "/", Component {} },
Route { to: "/blog",
BlogContainer {
h1 { "blog" } // always renders as long as we're on the "blog" subroute
Route { to: "/", BlogList {} }
Route { to: "welcome", BlogWelcome {} }
Route { to: ":post", BlogPost {} }
}
},
}
})
```
### Navigating with `Links`
To navigate your app, regular, old, `a` tags are not going to work. We provide the `Link` component that wraps an `a` tag with the appropriate `href` attribute that generates semantic HTML. You can pass any children into this component and they will become clickable to the appropriate route.
```rust
Link { to: "/blog/welcome",
h1 { "Welcome to my blog!" }
}
```
#### Active `Links`
When your app has been navigated to a route that matches the route of a `Link`, this `Link` becomes 'active'.
Active links have a special class attached to them. By default it is simply called `"active"` but it can be
modified on the `Link` level or on the `Router` level. Both is done through the prop `active_class`.
If the active class is given on both, the `Router` and the `Link`, the one on the `Link` has precedence.
```rust
Router {
active_class: "custom-active", // All active links in this router get this class.
Link { to: "/", "Home" },
Link {
to: "/blog",
active_class: "is-active", // Only for this Link. Overwrites "custom-active" from Router.
"Blog"
},
}
```
#### Initial url
When working with ssr specially is crucial to have the ability to set an initial url. This is an optional property and defaults to `app:///` for desktop, mobile and ssr, and for web retrieves the url from the window current location.
You must provide a valid URL `{scheme}://{?authority}/{?path}`.
```rust
Router {
initial_url: "https://dioxuslab.com/blog", // Set the initial url.
Link { to: "/", "Home" },
Link { to: "/blog", "Blog" }, // The router will render this route.
}
```
### Segments
Each route in your app is comprised of segments and queries. Segments are the portions of the route delimited by forward slashes.
For the route `/dogs/breeds/yorkie/hugo` our "segment list" would be:
```rust
vec!["dogs", "breeds", "yorkie", "hugo"]
```
For any route, you can get a handle the current route with the `use_route` hook.
```rust
fn Title(cx: Scope) -> Element {
let route = use_route(cx);
assert_eq!(route.segments(), &["dogs", "breeds", "yorkie", "hugo"]);
assert_eq!(route.nth_segment(1), "breeds");
assert_eq!(route.last_segment(), "hugo");
}
```
As we've shown above, segments can also be named. We can get these named segments out by the last match at that route level:
```rust
// For this router:
Router {
Route { to: "/", Component {} },
Route { to: "/blog", BlogList {} },
Route { to: "/blog/:post", BlogPost {} },
}
fn BlogPost(cx: Scope) -> Element {
let route = use_route(cx);
match route.segment("post").and_then(parse) {
Some(post) => cx.render(rsx!{ div { "Post {post}" } })
None => cx.render(rsx!{ div { "Could not find that blog post" } }),
}
}
```
### Queries
### Listeners
It's possible to connect to route change events from the router by attaching a listener to the Router's `onchange` parameter. This listener is guaranteed to run before any of your routes are matched, so you can perform redirects, add some logging, fetch some data, or do anything that you might want to be synchronous with clicks on Links.
```rust
fn app() {
cx.render(rsx! {
Router {
onchange: move |router| {
let current = router.current_route();
log::debug!("App has navigated to {:?}", current);
// perform a redirect
if current == "404" {
router.navigate_to("/");
}
},
Route { to: "/", Component {} },
}
})
}
```
Listeners can also be attached downstream in your app with the `RouteListener` handler component:
```rust
fn TitleCard(cx: Scope) -> Element {
let (title, set_title) = use_state(cx, || "First");
cx.render(rsx!{
h1 { "render {title}" }
RouteListener { onchange: move |_| set_title("Last") }
})
}
```
### Working with Github Pages and other static hosts
Most "static" hosts will have issues with single-page-app (SPA) routers. To get around this, you can either generate an index.html for each route or hijack the 404 page.
For generating a static index.html, see `Generating a Route List`.
To hijack the 404 page, we can simply make a copy of our index.html page and call it 404.html. When Github Pages serves this 404 page, your app will be served instead and the router will render the right corresponding route.
<https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-custom-404-page-for-your-github-pages-site>
### Generating a SiteMap or Route List
If you want to statically generate and rehydrate all your pages, lean on Dioxus Router to do the heavy lifting.
For this feature to work properly, each route (and nested) route will need to be probed, but this can be done automatically.
```rust
let mut dom = VirtualDom::new(app);
dom.inject_root_context(RouterContext::new());
// populate the router
let _ = dom.rebuild();
// load the router context from the dom, generate a sitemap, and then pre-render each page
let mut prerendered_pages = dom
.consume_root_context::<RouterContext>()
.unwrap()
.sitemap()
.into_iter()
.map(|route| {
// update the root context
router.navigate_to(route);
// force our app to update
let _ = dom.rebuild();
// render the page and insert it into our map
(route, dioxus_ssr::render_vdom(&dom))
})
.collect::<HashMap<_, _>>();
```
-11
View File
@@ -1,11 +0,0 @@
{
"moz:firefoxOptions": {
"binary": "/usr/bin/firefox",
"prefs": {
"media.navigator.streams.fake": true,
"media.navigator.permission.disabled": true
},
"args": []
}
}