Files
dioxus/packages/router/src/routable.rs
T
Evan AlmloffandJonathan Kelley 022e4ad203 Suspense boundaries/out of order streaming/anyhow like error handling (#2365)
* create static site generation helpers in the router crate

* work on integrating static site generation into fullstack

* move ssg into a separate crate

* integrate ssg with the launch builder

* simplify ssg example

* fix static_routes for child routes

* move CLI hot reloading websocket code into dioxus-hot-reload

* fix some unused imports

* use the same hot reloading websocket code for fullstack

* fix fullstack hot reloading

* move cli hot reloading logic into the hot reload crate

* ssg example working with dx serve

* add more examples

* fix clippy

* switch to a result for Element

* fix formatting

* fix hot reload doctest imports

* fix axum imports

* add show method to error context

* implement retaining nodes during suspense

* fix unterminated if statements

* communicate between tasks and suspense boundaries

* make suspense placeholders easier to use

* implement IntoDynNode and IntoVNode for more wrappers

* fix clippy examples

* fix rsx tests

* add streaming html utilities to the ssr package

* unify hydration and non-hydration ssr cache

* fix router with Result Element

* don't run server doc tests

* Fix hot reload websocket doc examples

* simple apps working with fullstack streaming

* fix preloading wasm

* Report errors encountered while streaming

* remove async from incremental renderer

* document new VirtualDom suspense methods

* make streaming work with incremental rendering

* fix static site generation

* document suspense structs

* create closure type; allow async event handlers in props; allow shorthand event handlers

* test forwarding event handlers with the shorthand syntax

* fix clippy

* fix imports in spawn async doctest

* fix empty rsx

* fix async result event handlers

* fix mounting router in multiple places

* Fix task dead cancel race condition

* simplify diffing before adding suspense

* fix binary size increase

* fix attribute diffing

* more diffing fixes

* create minimal fullstack feature

* smaller fullstack bundles

* allow mounting nodes that are already created and creating nodes without mounting them

* fix hot reload feature

* fix replacing components

* don't reclaim virtual nodes

* client side suspense working!

* fix CLI

* slightly smaller fullstack builds

* fix multiple suspended scopes

* fix merge errors

* yield back to tokio every few polls to fix suspending on many tasks at once

* remove logs

* document suspense boundary and update suspense example

* fix ssg

* make streaming optional

* fix some router and core tests

* fix suspense example

* fix serialization with out of order server futures

* add incremental streaming hackernews demo

* fix hackernews demo

* fix root hackernews redirect

* fix formatting

* add tests for suspense cases

* slightly smaller binaries

* slightly smaller

* improve error handling docs

* fix errors example link

* fix doc tests

* remove log file

* fix ssr cache type inference

* remove index.html

* fix ssg render template

* fix assigning ids on elements with dynamic attributes

* add desktop feature to the workspace examples

* remove router static generation example; ssg lives in the dioxus-static-generation package

* add a test for effects during suspense

* only run effects on mounted nodes

* fix multiple suspense roots

* fix node iterator

* fix closures without arguments

* fix dioxus-core readme doctest

* remove suspense logs

* fix scope stack

* fix clippy

* remove unused suspense boundary from hackernews

* assert that launch never returns for better compiler errors

* fix static generation launch function

* fix web renderer

* pass context providers into server functions

* add an example for FromContext

* clean up DioxusRouterExt

* fix server function context

* fix fullstack desktop example

* forward CLI serve settings to fullstack

* re-export serve config at the root of fullstack

* forward env directly instead of using a guard

* just set the port in the CLI for fullstack playwright tests

* fix fullstack dioxus-cli-config feature

* fix launch server merge conflicts

* fix fullstack launch context

* Merge branch 'main' into suspense-2.0

* fix fullstack html data

* remove drop virtual dom feature

* add a comment about only_write_templates binary size workaround

* remove explicit dependencies from use_server_future

* make ErrorContext and SuspenseContext more similar

* Tweak: small tweaks to tomls to make diff smaller

* only rerun components under suspense after the initial placeholders are sent to the client

* add module docs for suspense

* keep track of when suspense boundaries are resolved

* start implementing JS out of order streaming

* fix core tests

* implement the server side of suspense with js

* fix streaming ssr with nested suspense

* move streaming ssr code into fullstack

* revert minification changes

* serialize server future data as the html streams

* start loading scripts wasm immediately instead of defering the script

* very basic nested suspense example working with minimal html updates

* clean up some suspense/error docs

* fix hydrating nested pending server futures

* sort resolved boundaries by height

* Fix disconnecting clients while streaming

* fix static generation crate

* don't insert extra divs when hydrating streamed chunks

* wait to swap in the elements until they are hydrated

* remove inline streaming script

* hackernews partially working

* fix spa mode

* banish the open shadow dom

* fix removing placeholder

* set up streaming playwright test

* run web playwright tests on 9999 to avoid port conflicts with other local servers

* remove suspense nodes if the suspense boundary is replaced before the suspense resolves on the server

* ignore hydration of removed suspense boundaries

* use path based indexing to fix hydrating suspense after parent suspense with child is removed

* re-export dioxus error

* remove resolved suspense divs if the suspense boundary has been removed

* Fix client side initialized server futures

* ignore comment nodes while traversing nodes in core to avoid lists getting swapped out with suspense

* Pass initial hydration data to the client

* hide pre nodes

* don't panic if reclaiming an element fails

* fix scope stack when polling tasks

* improve deserialization out of length message

* Ok(VNode::placeholder()) -> VNode::empty()

* fix typo in rsx usage

* restore testing changes from suspense example

* clean up some logs and comments

* fix playwright tests

* clean up more changes in core

* clean up core tests

* remove anymap dependency

* clean up changes to hooks

* clean up changes in the router, rsx, and web

* revert changes to axum-hello-world

* fix use_server_future

* fix clippy in dioxus-core

* check that the next or previous node exist before checking if we should ignore them

* fix formatting

* fix suspense playwright test

* remove unused suspense code

* add more suspense playwright tests

* add more docs for error boundaries

* fix suspense core tests

* fix ErrorBoundary example

* remove a bunch of debug logging in js

* fix router failure_external_navigation

* use absolute paths in the interpreter build.rs

* strip '\r' while hashing ts files

* add a wrapper with a default error boundary and suspense boundary

* restore hot reloading

* ignore non-ts files when hashing

* sort ts files before hashing them

* fix rsx tests

* fix fullstack doc tests

* fix core tests

* fix axum auth example

* update suspense hydration diagram

* longer playwright build limit

* tiny fixes - spelling, formatting

* update diagram link

* remove comment and template nodes for suspense placeholders

* remove comment nodes as we hydrate text

* simplify hackernews example

* clean up hydrating text nodes

* switch to a separate environment variable for the base path for smaller binaries

* clean up file system html trait

* fix form data

* move streaming code into fullstack

* implement serialize and deserialize for CapturedError

* remove waits in the nested suspense playwright spec

* force sequential fullstack builds for CI

* longer nested suspense delay for CI

* fix --force-sequential flag

* wait to launch server until client build is done

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
2024-07-01 20:50:36 -07:00

800 lines
25 KiB
Rust

//! # Routable
#![allow(non_snake_case)]
use dioxus_lib::prelude::*;
use std::iter::FlatMap;
use std::slice::Iter;
use std::{fmt::Display, str::FromStr};
/// An error that occurs when parsing a route.
#[derive(Debug, PartialEq)]
pub struct RouteParseError<E: Display> {
/// The attempted routes that failed to match.
pub attempted_routes: Vec<E>,
}
impl<E: Display> 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 an entire query string. This trait must be implemented for any type that is spread into the query segment like `#[route("/?:..query")]`.
///
///
/// **This trait is automatically implemented for any types that implement `From<&str>`.**
///
/// ```rust
/// use dioxus::prelude::*;
///
/// #[derive(Routable, Clone, PartialEq, Debug)]
/// enum Route {
/// // FromQuery must be implemented for any types you spread into the query segment
/// #[route("/?:..query")]
/// Home {
/// query: CustomQuery
/// },
/// }
///
/// #[derive(Default, Clone, PartialEq, Debug)]
/// struct CustomQuery {
/// count: i32,
/// }
///
/// // We implement From<&str> for CustomQuery so that FromQuery is implemented automatically
/// impl From<&str> for CustomQuery {
/// fn from(query: &str) -> Self {
/// CustomQuery {
/// count: query.parse().unwrap_or(0),
/// }
/// }
/// }
///
/// // We also need to implement Display for CustomQuery which will be used to format the query string into the URL
/// impl std::fmt::Display for CustomQuery {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// write!(f, "{}", self.count)
/// }
/// }
///
/// # #[component]
/// # fn Home(query: CustomQuery) -> Element {
/// # unimplemented!()
/// # }
/// ```
#[rustversion::attr(
since(1.78.0),
diagnostic::on_unimplemented(
message = "`FromQuery` is not implemented for `{Self}`",
label = "spread query",
note = "FromQuery is automatically implemented for types that implement `From<&str>`. You need to either implement From<&str> or implement FromQuery manually."
)
)]
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 query argument. This trait must be implemented for any type that is used as a query argument like `#[route("/?:query")]`.
///
/// **This trait is automatically implemented for any types that implement `FromStr` and `Default`.**
///
/// ```rust
/// use dioxus::prelude::*;
///
/// #[derive(Routable, Clone, PartialEq, Debug)]
/// enum Route {
/// // FromQuerySegment must be implemented for any types you use in the query segment
/// // When you don't spread the query, you can parse multiple values form the query
/// // This url will be in the format `/?query=123&other=456`
/// #[route("/?:query&:other")]
/// Home {
/// query: CustomQuery,
/// other: i32,
/// },
/// }
///
/// // We can derive Default for CustomQuery
/// // If the router fails to parse the query value, it will use the default value instead
/// #[derive(Default, Clone, PartialEq, Debug)]
/// struct CustomQuery {
/// count: i32,
/// }
///
/// // We implement FromStr for CustomQuery so that FromQuerySegment is implemented automatically
/// impl std::str::FromStr for CustomQuery {
/// type Err = <i32 as std::str::FromStr>::Err;
///
/// fn from_str(query: &str) -> Result<Self, Self::Err> {
/// Ok(CustomQuery {
/// count: query.parse()?,
/// })
/// }
/// }
///
/// // We also need to implement Display for CustomQuery which will be used to format the query string into the URL
/// impl std::fmt::Display for CustomQuery {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// write!(f, "{}", self.count)
/// }
/// }
///
/// # #[component]
/// # fn Home(query: CustomQuery, other: i32) -> Element {
/// # unimplemented!()
/// # }
/// ```
#[rustversion::attr(
since(1.78.0),
diagnostic::on_unimplemented(
message = "`FromQueryArgument` is not implemented for `{Self}`",
label = "query argument",
note = "FromQueryArgument is automatically implemented for types that implement `FromStr` and `Default`. You need to either implement FromStr and Default or implement FromQueryArgument manually."
)
)]
pub trait FromQueryArgument: Default {
/// The error that can occur when parsing a query argument.
type Err;
/// Create an instance of `Self` from a query string.
fn from_query_argument(argument: &str) -> Result<Self, Self::Err>;
}
impl<T: Default + FromStr> FromQueryArgument for T
where
<T as FromStr>::Err: Display,
{
type Err = <T as FromStr>::Err;
fn from_query_argument(argument: &str) -> Result<Self, Self::Err> {
match T::from_str(argument) {
Ok(result) => Ok(result),
Err(err) => {
tracing::error!("Failed to parse query argument: {}", err);
Err(err)
}
}
}
}
/// Something that can be created from an entire hash fragment. This must be implemented for any type that is used as a hash fragment like `#[route("/#:hash_fragment")]`.
///
///
/// **This trait is automatically implemented for any types that implement `FromStr` and `Default`.**
///
/// # Example
///
/// ```rust
/// use dioxus::prelude::*;
///
/// #[derive(Routable, Clone)]
/// #[rustfmt::skip]
/// enum Route {
/// // State is stored in the url hash
/// #[route("/#:url_hash")]
/// Home {
/// url_hash: State,
/// },
/// }
///
/// #[component]
/// fn Home(url_hash: State) -> Element {
/// unimplemented!()
/// }
///
///
/// #[derive(Clone, PartialEq, Default)]
/// struct State {
/// count: usize,
/// other_count: usize
/// }
///
/// // The hash segment will be displayed as a string (this will be url encoded automatically)
/// impl std::fmt::Display for State {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// write!(f, "{}-{}", self.count, self.other_count)
/// }
/// }
///
/// // We need to parse the hash fragment into a struct from the string (this will be url decoded automatically)
/// impl FromHashFragment for State {
/// fn from_hash_fragment(hash: &str) -> Self {
/// let Some((first, second)) = hash.split_once('-') else {
/// // URL fragment parsing shouldn't fail. You can return a default value if you want
/// return Default::default();
/// };
///
/// let first = first.parse().unwrap();
/// let second = second.parse().unwrap();
///
/// State {
/// count: first,
/// other_count: second,
/// }
/// }
/// }
/// ```
#[rustversion::attr(
since(1.78.0),
diagnostic::on_unimplemented(
message = "`FromHashFragment` is not implemented for `{Self}`",
label = "hash fragment",
note = "FromHashFragment is automatically implemented for types that implement `FromStr` and `Default`. You need to either implement FromStr and Default or implement FromHashFragment manually."
)
)]
pub trait FromHashFragment {
/// Create an instance of `Self` from a hash fragment.
fn from_hash_fragment(hash: &str) -> Self;
}
impl<T> FromHashFragment for T
where
T: FromStr + Default,
T::Err: std::fmt::Display,
{
fn from_hash_fragment(hash: &str) -> Self {
match T::from_str(hash) {
Ok(value) => value,
Err(err) => {
tracing::error!("Failed to parse hash fragment: {}", err);
Default::default()
}
}
}
}
/// Something that can be created from a single route segment. This must be implemented for any type that is used as a route segment like `#[route("/:route_segment")]`.
///
///
/// **This trait is automatically implemented for any types that implement `FromStr` and `Default`.**
///
/// ```rust
/// use dioxus::prelude::*;
///
/// #[derive(Routable, Clone, PartialEq, Debug)]
/// enum Route {
/// // FromRouteSegment must be implemented for any types you use in the route segment
/// // When you don't spread the route, you can parse multiple values from the route
/// // This url will be in the format `/123/456`
/// #[route("/:route_segment_one/:route_segment_two")]
/// Home {
/// route_segment_one: CustomRouteSegment,
/// route_segment_two: i32,
/// },
/// }
///
/// // We can derive Default for CustomRouteSegment
/// // If the router fails to parse the route segment, it will use the default value instead
/// #[derive(Default, PartialEq, Clone, Debug)]
/// struct CustomRouteSegment {
/// count: i32,
/// }
///
/// // We implement FromStr for CustomRouteSegment so that FromRouteSegment is implemented automatically
/// impl std::str::FromStr for CustomRouteSegment {
/// type Err = <i32 as std::str::FromStr>::Err;
///
/// fn from_str(route_segment: &str) -> Result<Self, Self::Err> {
/// Ok(CustomRouteSegment {
/// count: route_segment.parse()?,
/// })
/// }
/// }
///
/// // We also need to implement Display for CustomRouteSegment which will be used to format the route segment into the URL
/// impl std::fmt::Display for CustomRouteSegment {
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// write!(f, "{}", self.count)
/// }
/// }
///
/// # #[component]
/// # fn Home(route_segment_one: CustomRouteSegment, route_segment_two: i32) -> Element {
/// # unimplemented!()
/// # }
/// ```
#[rustversion::attr(
since(1.78.0),
diagnostic::on_unimplemented(
message = "`FromRouteSegment` is not implemented for `{Self}`",
label = "route segment",
note = "FromRouteSegment is automatically implemented for types that implement `FromStr` and `Default`. You need to either implement FromStr and Default or implement FromRouteSegment manually."
)
)]
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: Display,
{
type Err = <T as FromStr>::Err;
fn from_route_segment(route: &str) -> Result<Self, Self::Err> {
T::from_str(route)
}
}
#[test]
fn full_circle() {
let route = "testing 1234 hello world";
assert_eq!(String::from_route_segment(route).unwrap(), route);
}
/// Something that can be converted into multiple route segments. This must be implemented for any type that is spread into the route segment like `#[route("/:..route_segments")]`.
///
///
/// **This trait is automatically implemented for any types that implement `IntoIterator<Item=impl Display>`.**
///
/// ```rust
/// use dioxus::prelude::*;
///
/// #[derive(Routable, Clone, PartialEq, Debug)]
/// enum Route {
/// // FromRouteSegments must be implemented for any types you use in the route segment
/// // When you spread the route, you can parse multiple values from the route
/// // This url will be in the format `/123/456/789`
/// #[route("/:..numeric_route_segments")]
/// Home {
/// numeric_route_segments: NumericRouteSegments,
/// },
/// }
///
/// // We can derive Default for NumericRouteSegments
/// // If the router fails to parse the route segment, it will use the default value instead
/// #[derive(Default, PartialEq, Clone, Debug)]
/// struct NumericRouteSegments {
/// numbers: Vec<i32>,
/// }
///
/// // Implement ToRouteSegments for NumericRouteSegments so that we can display the route segments
/// impl ToRouteSegments for NumericRouteSegments {
/// fn display_route_segments(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// for number in &self.numbers {
/// write!(f, "/{}", number)?;
/// }
/// Ok(())
/// }
/// }
///
/// // We also need to parse the route segments with `FromRouteSegments`
/// impl FromRouteSegments for NumericRouteSegments {
/// type Err = <i32 as std::str::FromStr>::Err;
///
/// fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err> {
/// let mut numbers = Vec::new();
/// for segment in segments {
/// numbers.push(segment.parse()?);
/// }
/// Ok(NumericRouteSegments { numbers })
/// }
/// }
///
/// # #[component]
/// # fn Home(numeric_route_segments: NumericRouteSegments) -> Element {
/// # unimplemented!()
/// # }
/// ```
pub trait ToRouteSegments {
/// Display the route segments with each route segment separated by a `/`. This should not start with a `/`.
///
fn display_route_segments(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
}
// Implement ToRouteSegments for any type that can turn &self into an iterator of &T where T: Display
impl<I, T: Display> ToRouteSegments for I
where
for<'a> &'a I: IntoIterator<Item = &'a T>,
{
fn display_route_segments(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for segment in self {
write!(f, "/")?;
let segment = segment.to_string();
let encoded = urlencoding::encode(&segment);
write!(f, "{}", encoded)?;
}
Ok(())
}
}
#[test]
fn to_route_segments() {
struct DisplaysRoute;
impl Display for DisplaysRoute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let segments = vec!["hello", "world"];
segments.display_route_segments(f)
}
}
assert_eq!(DisplaysRoute.to_string(), "/hello/world");
}
/// Something that can be created from multiple route segments. This must be implemented for any type that is spread into the route segment like `#[route("/:..route_segments")]`.
///
///
/// **This trait is automatically implemented for any types that implement `FromIterator<impl Display>`.**
///
/// ```rust
/// use dioxus::prelude::*;
///
/// #[derive(Routable, Clone, PartialEq, Debug)]
/// enum Route {
/// // FromRouteSegments must be implemented for any types you use in the route segment
/// // When you spread the route, you can parse multiple values from the route
/// // This url will be in the format `/123/456/789`
/// #[route("/:..numeric_route_segments")]
/// Home {
/// numeric_route_segments: NumericRouteSegments,
/// },
/// }
///
/// // We can derive Default for NumericRouteSegments
/// // If the router fails to parse the route segment, it will use the default value instead
/// #[derive(Default, Clone, PartialEq, Debug)]
/// struct NumericRouteSegments {
/// numbers: Vec<i32>,
/// }
///
/// // Implement ToRouteSegments for NumericRouteSegments so that we can display the route segments
/// impl ToRouteSegments for NumericRouteSegments {
/// fn display_route_segments(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
/// for number in &self.numbers {
/// write!(f, "/{}", number)?;
/// }
/// Ok(())
/// }
/// }
///
/// // We also need to parse the route segments with `FromRouteSegments`
/// impl FromRouteSegments for NumericRouteSegments {
/// type Err = <i32 as std::str::FromStr>::Err;
///
/// fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err> {
/// let mut numbers = Vec::new();
/// for segment in segments {
/// numbers.push(segment.parse()?);
/// }
/// Ok(NumericRouteSegments { numbers })
/// }
/// }
///
/// # #[component]
/// # fn Home(numeric_route_segments: NumericRouteSegments) -> Element {
/// # unimplemented!()
/// # }
/// ```
#[rustversion::attr(
since(1.78.0),
diagnostic::on_unimplemented(
message = "`FromRouteSegments` is not implemented for `{Self}`",
label = "spread route segments",
note = "FromRouteSegments is automatically implemented for types that implement `FromIterator` with an `Item` type that implements `Display`. You need to either implement FromIterator or implement FromRouteSegments manually."
)
)]
pub trait FromRouteSegments: Sized {
/// The error that can occur when parsing route segments.
type Err: std::fmt::Display;
/// Create an instance of `Self` from route segments.
///
/// NOTE: This method must parse the output of `ToRouteSegments::display_route_segments` into the type `Self`.
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()
}
}
/// A flattened version of [`Routable::SITE_MAP`].
/// This essentially represents a `Vec<Vec<SegmentType>>`, which you can collect it into.
type SiteMapFlattened<'a> = FlatMap<
Iter<'a, SiteMapSegment>,
Vec<Vec<SegmentType>>,
fn(&SiteMapSegment) -> Vec<Vec<SegmentType>>,
>;
/// The Routable trait is implemented for types that can be converted to and from a route and be rendered as a page.
///
/// A Routable object is something that can be:
/// 1. Converted from a route.
/// 2. Converted to a route.
/// 3. Rendered as a component.
///
/// This trait should generally be derived using the [`dioxus_router_macro::Routable`] macro which has more information about the syntax.
///
/// ## Example
/// ```rust
/// use dioxus::prelude::*;
///
/// fn App() -> Element {
/// rsx! {
/// Router::<Route> { }
/// }
/// }
///
/// // Routes are generally enums that derive `Routable`
/// #[derive(Routable, Clone, PartialEq, Debug)]
/// enum Route {
/// // Each enum has an associated url
/// #[route("/")]
/// Home {},
/// // Routes can include dynamic segments that are parsed from the url
/// #[route("/blog/:blog_id")]
/// Blog { blog_id: usize },
/// // Or query segments that are parsed from the url
/// #[route("/edit?:blog_id")]
/// Edit { blog_id: usize },
/// // Or hash segments that are parsed from the url
/// #[route("/hashtag/#:hash")]
/// Hash { hash: String },
/// }
///
/// // Each route variant defaults to rendering a component of the same name
/// #[component]
/// fn Home() -> Element {
/// rsx! {
/// h1 { "Home" }
/// }
/// }
///
/// // If the variant has dynamic parameters, those are passed to the component
/// #[component]
/// fn Blog(blog_id: usize) -> Element {
/// rsx! {
/// h1 { "Blog" }
/// }
/// }
///
/// #[component]
/// fn Edit(blog_id: usize) -> Element {
/// rsx! {
/// h1 { "Edit" }
/// }
/// }
///
/// #[component]
/// fn Hash(hash: String) -> Element {
/// rsx! {
/// h1 { "Hashtag #{hash}" }
/// }
/// }
/// ```
#[rustversion::attr(
since(1.78.0),
diagnostic::on_unimplemented(
message = "`Routable` is not implemented for `{Self}`",
label = "Route",
note = "Routable should generally be derived using the `#[derive(Routable)]` macro."
)
)]
pub trait Routable: FromStr + Display + 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(&self, level: usize) -> Element;
/// Checks if this route is a child of the given route.
///
/// # Example
/// ```rust
/// use dioxus_router::prelude::*;
/// use dioxus::prelude::*;
///
/// #[component]
/// fn Home() -> Element { VNode::empty() }
/// #[component]
/// fn About() -> Element { VNode::empty() }
///
/// #[derive(Routable, Clone, PartialEq, Debug)]
/// enum Route {
/// #[route("/")]
/// Home {},
/// #[route("/about")]
/// About {},
/// }
///
/// let route = Route::About {};
/// let parent = Route::Home {};
/// assert!(route.is_child_of(&parent));
/// ```
fn is_child_of(&self, other: &Self) -> bool {
let self_str = self.to_string();
let self_str = self_str.trim_matches('/');
let other_str = other.to_string();
let other_str = other_str.trim_matches('/');
if other_str.is_empty() {
return true;
}
let self_segments = self_str.split('/');
let other_segments = other_str.split('/');
for (self_seg, other_seg) in self_segments.zip(other_segments) {
if self_seg != other_seg {
return false;
}
}
true
}
/// Get the parent route of this route.
///
/// # Example
/// ```rust
/// use dioxus_router::prelude::*;
/// use dioxus::prelude::*;
///
/// #[component]
/// fn Home() -> Element { VNode::empty() }
/// #[component]
/// fn About() -> Element { VNode::empty() }
///
/// #[derive(Routable, Clone, PartialEq, Debug)]
/// enum Route {
/// #[route("/home")]
/// Home {},
/// #[route("/home/about")]
/// About {},
/// }
///
/// let route = Route::About {};
/// let parent = route.parent().unwrap();
/// assert_eq!(parent, Route::Home {});
/// ```
fn parent(&self) -> Option<Self> {
let as_str = self.to_string();
let as_str = as_str.trim_matches('/');
let segments = as_str.split('/');
let segment_count = segments.clone().count();
let new_route = segments
.take(segment_count - 1)
.fold(String::new(), |mut acc, segment| {
acc.push('/');
acc.push_str(segment);
acc
});
Self::from_str(&new_route).ok()
}
/// Returns a flattened version of [`Self::SITE_MAP`].
fn flatten_site_map<'a>() -> SiteMapFlattened<'a> {
Self::SITE_MAP.iter().flat_map(SiteMapSegment::flatten)
}
/// Gets a list of all the static routes.
/// Example static route: `#[route("/static/route")]`
fn static_routes() -> Vec<Self> {
Self::flatten_site_map()
.filter_map(|segments| {
let mut route = String::new();
for segment in segments.iter() {
match segment {
SegmentType::Static(s) => {
route.push('/');
route.push_str(s)
}
SegmentType::Child => {}
_ => return None,
}
}
route.parse().ok()
})
.collect()
}
}
/// A type erased map of the site structure.
#[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)]
#[non_exhaustive]
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 SegmentType {
/// Try to convert this segment into a static segment.
pub fn to_static(&self) -> Option<&'static str> {
match self {
SegmentType::Static(s) => Some(*s),
_ => None,
}
}
/// Try to convert this segment into a dynamic segment.
pub fn to_dynamic(&self) -> Option<&'static str> {
match self {
SegmentType::Dynamic(s) => Some(*s),
_ => None,
}
}
/// Try to convert this segment into a catch all segment.
pub fn to_catch_all(&self) -> Option<&'static str> {
match self {
SegmentType::CatchAll(s) => Some(*s),
_ => None,
}
}
/// Try to convert this segment into a child segment.
pub fn to_child(&self) -> Option<()> {
match self {
SegmentType::Child => Some(()),
_ => None,
}
}
}
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),
}
}
}