mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
Implement hash fragments in the router (#2320)
* implement hash fragments in the router * clean up query and search example --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
parent
dd8e69635b
commit
08da9a1253
17 changed files with 550 additions and 190 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2335,6 +2335,8 @@ dependencies = [
|
||||||
name = "dioxus-examples"
|
name = "dioxus-examples"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"ciborium",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
"dioxus-ssr",
|
"dioxus-ssr",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
|
|
|
@ -157,6 +157,8 @@ publish = false
|
||||||
manganis = { workspace = true, optional = true }
|
manganis = { workspace = true, optional = true }
|
||||||
reqwest = { version = "0.11.9", features = ["json"], optional = true }
|
reqwest = { version = "0.11.9", features = ["json"], optional = true }
|
||||||
http-range = { version = "0.1.5", optional = true }
|
http-range = { version = "0.1.5", optional = true }
|
||||||
|
ciborium = { version = "0.2.1", optional = true }
|
||||||
|
base64 = { version = "0.21.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dioxus = { workspace = true, features = ["router"] }
|
dioxus = { workspace = true, features = ["router"] }
|
||||||
|
@ -193,7 +195,6 @@ web = ["dioxus/web"]
|
||||||
collect-assets = ["manganis"]
|
collect-assets = ["manganis"]
|
||||||
http = ["reqwest", "http-range"]
|
http = ["reqwest", "http-range"]
|
||||||
|
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "login_form"
|
name = "login_form"
|
||||||
required-features = ["http"]
|
required-features = ["http"]
|
||||||
|
@ -217,3 +218,7 @@ required-features = ["http"]
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "image_generator_openai"
|
name = "image_generator_openai"
|
||||||
required-features = ["http"]
|
required-features = ["http"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "hash_fragment_state"
|
||||||
|
required-features = ["ciborium", "base64"]
|
||||||
|
|
130
examples/hash_fragment_state.rs
Normal file
130
examples/hash_fragment_state.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
//! This example shows how to use the hash segment to store state in the url.
|
||||||
|
//!
|
||||||
|
//! You can set up two way data binding between the url hash and signals.
|
||||||
|
//!
|
||||||
|
//! Run this example on desktop with
|
||||||
|
//! ```sh
|
||||||
|
//! dx serve --example hash_fragment_state --features=ciborium,base64
|
||||||
|
//! ```
|
||||||
|
//! Or on web with
|
||||||
|
//! ```sh
|
||||||
|
//! dx serve --platform web --features web --example hash_fragment_state --features=ciborium,base64 -- --no-default-features
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use base64::engine::general_purpose::STANDARD;
|
||||||
|
use base64::Engine;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
launch(|| {
|
||||||
|
rsx! {
|
||||||
|
Router::<Route> {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Routable, Clone, Debug, PartialEq)]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
enum Route {
|
||||||
|
#[route("/#:url_hash")]
|
||||||
|
Home {
|
||||||
|
url_hash: State,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can use a custom type with the hash segment as long as it implements Display, FromStr and Default
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
|
||||||
|
struct State {
|
||||||
|
counters: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the state in a way that can be parsed by FromStr
|
||||||
|
impl Display for State {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut serialized = Vec::new();
|
||||||
|
if ciborium::into_writer(self, &mut serialized).is_ok() {
|
||||||
|
write!(f, "{}", STANDARD.encode(serialized))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum StateParseError {
|
||||||
|
DecodeError(base64::DecodeError),
|
||||||
|
CiboriumError(ciborium::de::Error<std::io::Error>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for StateParseError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::DecodeError(err) => write!(f, "Failed to decode base64: {}", err),
|
||||||
|
Self::CiboriumError(err) => write!(f, "Failed to deserialize: {}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the state from a string that was created by Display
|
||||||
|
impl FromStr for State {
|
||||||
|
type Err = StateParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let decompressed = STANDARD
|
||||||
|
.decode(s.as_bytes())
|
||||||
|
.map_err(StateParseError::DecodeError)?;
|
||||||
|
let parsed = ciborium::from_reader(std::io::Cursor::new(decompressed))
|
||||||
|
.map_err(StateParseError::CiboriumError)?;
|
||||||
|
Ok(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Home(url_hash: ReadOnlySignal<State>) -> Element {
|
||||||
|
// The initial state of the state comes from the url hash
|
||||||
|
let mut state = use_signal(&*url_hash);
|
||||||
|
|
||||||
|
// Change the state signal when the url hash changes
|
||||||
|
use_memo(move || {
|
||||||
|
if *state.peek() != *url_hash.read() {
|
||||||
|
state.set(url_hash());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change the url hash when the state changes
|
||||||
|
use_memo(move || {
|
||||||
|
if *state.read() != *url_hash.peek() {
|
||||||
|
navigator().replace(Route::Home { url_hash: state() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
button {
|
||||||
|
onclick: move |_| state.write().counters.clear(),
|
||||||
|
"Reset"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
state.write().counters.push(0);
|
||||||
|
},
|
||||||
|
"Add Counter"
|
||||||
|
}
|
||||||
|
for counter in 0..state.read().counters.len() {
|
||||||
|
div {
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
state.write().counters.remove(counter);
|
||||||
|
},
|
||||||
|
"Remove"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
state.write().counters[counter] += 1;
|
||||||
|
},
|
||||||
|
"Count: {state.read().counters[counter]}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
117
examples/query_segment_search.rs
Normal file
117
examples/query_segment_search.rs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
//! This example shows how to access and use query segments present in an url on the web.
|
||||||
|
//!
|
||||||
|
//! The enum router makes it easy to use your route as state in your app. This example shows how to use the router to encode search text into the url and decode it back into a string.
|
||||||
|
//!
|
||||||
|
//! Run this example on desktop with
|
||||||
|
//! ```sh
|
||||||
|
//! dx serve --example query_segment_search
|
||||||
|
//! ```
|
||||||
|
//! Or on web with
|
||||||
|
//! ```sh
|
||||||
|
//! dx serve --platform web --features web --example query_segment_search -- --no-default-features
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
launch(|| {
|
||||||
|
rsx! {
|
||||||
|
Router::<Route> {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Routable, Clone, Debug, PartialEq)]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
enum Route {
|
||||||
|
#[route("/")]
|
||||||
|
Home {},
|
||||||
|
|
||||||
|
// The each query segment must implement <https://docs.rs/dioxus-router/latest/dioxus_router/routable/trait.FromQueryArgument.html> and Display.
|
||||||
|
// You can use multiple query segments separated by `&`s.
|
||||||
|
#[route("/search?:query&:word_count")]
|
||||||
|
Search {
|
||||||
|
query: String,
|
||||||
|
word_count: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Home() -> Element {
|
||||||
|
// Display a list of example searches in the home page
|
||||||
|
rsx! {
|
||||||
|
ul {
|
||||||
|
li {
|
||||||
|
Link {
|
||||||
|
to: Route::Search {
|
||||||
|
query: "hello".to_string(),
|
||||||
|
word_count: 1
|
||||||
|
},
|
||||||
|
"Search for results containing 'hello' and at least one word"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
Link {
|
||||||
|
to: Route::Search {
|
||||||
|
query: "dioxus".to_string(),
|
||||||
|
word_count: 2
|
||||||
|
},
|
||||||
|
"Search for results containing 'dioxus' and at least two word"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of accepting String and usize directly, we use ReadOnlySignal to make the parameters `Copy` and let us subscribe to them automatically inside the meme
|
||||||
|
#[component]
|
||||||
|
fn Search(query: ReadOnlySignal<String>, word_count: ReadOnlySignal<usize>) -> Element {
|
||||||
|
const ITEMS: &[&str] = &[
|
||||||
|
"hello",
|
||||||
|
"world",
|
||||||
|
"hello world",
|
||||||
|
"hello dioxus",
|
||||||
|
"hello dioxus-router",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Find all results that contain the query and the right number of words
|
||||||
|
// This memo will automatically rerun when the query or word count changes because we read the signals inside the closure
|
||||||
|
let results = use_memo(move || {
|
||||||
|
ITEMS
|
||||||
|
.iter()
|
||||||
|
.filter(|item| {
|
||||||
|
item.contains(&*query.read()) && item.split_whitespace().count() >= word_count()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
h1 { "Search for {query}" }
|
||||||
|
input {
|
||||||
|
oninput: move |e| {
|
||||||
|
// Every time the query changes, we change the current route to the new query
|
||||||
|
navigator().replace(Route::Search {
|
||||||
|
query: e.value(),
|
||||||
|
word_count: word_count(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
value: "{query}",
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
r#type: "number",
|
||||||
|
oninput: move |e| {
|
||||||
|
// Every time the word count changes, we change the current route to the new query
|
||||||
|
if let Ok(word_count) = e.value().parse() {
|
||||||
|
navigator().replace(Route::Search {
|
||||||
|
query: query(),
|
||||||
|
word_count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value: "{word_count}",
|
||||||
|
}
|
||||||
|
for result in results.read().iter() {
|
||||||
|
div { "{result}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,85 +0,0 @@
|
||||||
//! Example: Url query segments usage
|
|
||||||
//! ------------------------------------
|
|
||||||
//!
|
|
||||||
//! This example shows how to access and use multiple query segments present in an url on the web.
|
|
||||||
//!
|
|
||||||
//! Run `dx serve` and navigate to `http://localhost:8080/blog?name=John&surname=Doe`
|
|
||||||
use dioxus::prelude::*;
|
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
#[derive(Routable, Clone)]
|
|
||||||
#[rustfmt::skip]
|
|
||||||
enum Route {
|
|
||||||
// segments that start with ?:.. are query segments that capture the entire query
|
|
||||||
#[route("/blog?:..query_params")]
|
|
||||||
BlogPost {
|
|
||||||
// You must include query segments in child variants
|
|
||||||
query_params: ManualBlogQuerySegments,
|
|
||||||
},
|
|
||||||
|
|
||||||
// segments that follow the ?:field&:other_field syntax are query segments that follow the standard url query syntax
|
|
||||||
#[route("/autoblog?:name&:surname")]
|
|
||||||
AutomaticBlogPost {
|
|
||||||
name: String,
|
|
||||||
surname: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
struct ManualBlogQuerySegments {
|
|
||||||
name: String,
|
|
||||||
surname: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The display impl needs to display the query in a way that can be parsed:
|
|
||||||
impl Display for ManualBlogQuerySegments {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "name={}&surname={}", self.name, self.surname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The query segment is anything that implements <https://docs.rs/dioxus-router/latest/dioxus::router/routable/trait.FromQuery.html>. You can implement that trait for a struct if you want to parse multiple query parameters.
|
|
||||||
impl FromQuery for ManualBlogQuerySegments {
|
|
||||||
fn from_query(query: &str) -> Self {
|
|
||||||
let mut name = None;
|
|
||||||
let mut surname = None;
|
|
||||||
let pairs = form_urlencoded::parse(query.as_bytes());
|
|
||||||
pairs.for_each(|(key, value)| {
|
|
||||||
if key == "name" {
|
|
||||||
name = Some(value.clone().into());
|
|
||||||
}
|
|
||||||
if key == "surname" {
|
|
||||||
surname = Some(value.clone().into());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Self {
|
|
||||||
name: name.unwrap(),
|
|
||||||
surname: surname.unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn BlogPost(query_params: ManualBlogQuerySegments) -> Element {
|
|
||||||
rsx! {
|
|
||||||
div { "This is your blogpost with a query segment:" }
|
|
||||||
div { "{query_params:?}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn AutomaticBlogPost(name: String, surname: String) -> Element {
|
|
||||||
rsx! {
|
|
||||||
div { "This is your blogpost with a query segment:" }
|
|
||||||
div { "name={name}&surname={surname}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn App() -> Element {
|
|
||||||
rsx! { Router::<Route> {} }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
launch(App);
|
|
||||||
}
|
|
|
@ -392,9 +392,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
fn deserialize_leaky<'a, 'de, T: serde::Deserialize<'de>, D>(
|
fn deserialize_leaky<'a, 'de, T, D>(deserializer: D) -> Result<&'a [T], D::Error>
|
||||||
deserializer: D,
|
|
||||||
) -> Result<&'a [T], D::Error>
|
|
||||||
where
|
where
|
||||||
T: serde::Deserialize<'de>,
|
T: serde::Deserialize<'de>,
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
|
|
62
packages/router-macro/src/hash.rs
Normal file
62
packages/router-macro/src/hash.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{Ident, Type};
|
||||||
|
|
||||||
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HashFragment {
|
||||||
|
pub ident: Ident,
|
||||||
|
pub ty: Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HashFragment {
|
||||||
|
pub fn contains_ident(&self, ident: &Ident) -> bool {
|
||||||
|
self.ident == *ident
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(&self) -> TokenStream2 {
|
||||||
|
let ident = &self.ident;
|
||||||
|
let ty = &self.ty;
|
||||||
|
quote! {
|
||||||
|
let #ident = <#ty as dioxus_router::routable::FromHashFragment>::from_hash_fragment(&*hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&self) -> TokenStream2 {
|
||||||
|
let ident = &self.ident;
|
||||||
|
quote! {
|
||||||
|
write!(f, "#{}", #ident)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_from_str<'a>(
|
||||||
|
route_span: proc_macro2::Span,
|
||||||
|
mut fields: impl Iterator<Item = (&'a Ident, &'a Type)>,
|
||||||
|
hash: &str,
|
||||||
|
) -> syn::Result<Self> {
|
||||||
|
// check if the route has a hash string
|
||||||
|
let Some(hash) = hash.strip_prefix(':') else {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
route_span,
|
||||||
|
"Failed to parse `:`. Hash fragments must be in the format '#:<field>'",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let hash_ident = Ident::new(hash, proc_macro2::Span::call_site());
|
||||||
|
let field = fields.find(|(name, _)| *name == &hash_ident);
|
||||||
|
|
||||||
|
let ty = if let Some((_, ty)) = field {
|
||||||
|
ty.clone()
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
route_span,
|
||||||
|
format!("Could not find a field with the name '{}'", hash_ident),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
ident: hash_ident,
|
||||||
|
ty,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
|
||||||
use crate::{layout::LayoutId, route_tree::RouteTree};
|
use crate::{layout::LayoutId, route_tree::RouteTree};
|
||||||
|
|
||||||
|
mod hash;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod nest;
|
mod nest;
|
||||||
mod query;
|
mod query;
|
||||||
|
@ -523,6 +524,11 @@ impl RouteEnum {
|
||||||
from_route = true
|
from_route = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(hash) = &route.hash {
|
||||||
|
if hash.contains_ident(field) {
|
||||||
|
from_route = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -576,9 +582,10 @@ impl RouteEnum {
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
let route = s;
|
let route = s;
|
||||||
let (route, _hash) = route.split_once('#').unwrap_or((route, ""));
|
let (route, hash) = route.split_once('#').unwrap_or((route, ""));
|
||||||
let (route, query) = route.split_once('?').unwrap_or((route, ""));
|
let (route, query) = route.split_once('?').unwrap_or((route, ""));
|
||||||
let query = dioxus_router::exports::urlencoding::decode(query).unwrap_or(query.into());
|
let query = dioxus_router::exports::urlencoding::decode(query).unwrap_or(query.into());
|
||||||
|
let hash = dioxus_router::exports::urlencoding::decode(hash).unwrap_or(hash.into());
|
||||||
let mut segments = route.split('/').map(|s| dioxus_router::exports::urlencoding::decode(s).unwrap_or(s.into()));
|
let mut segments = route.split('/').map(|s| dioxus_router::exports::urlencoding::decode(s).unwrap_or(s.into()));
|
||||||
// skip the first empty segment
|
// skip the first empty segment
|
||||||
if s.starts_with('/') {
|
if s.starts_with('/') {
|
||||||
|
|
|
@ -51,6 +51,66 @@ impl QuerySegment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_from_str<'a>(
|
||||||
|
route_span: proc_macro2::Span,
|
||||||
|
mut fields: impl Iterator<Item = (&'a Ident, &'a Type)>,
|
||||||
|
query: &str,
|
||||||
|
) -> syn::Result<Self> {
|
||||||
|
// check if the route has a query string
|
||||||
|
if let Some(query) = query.strip_prefix(":..") {
|
||||||
|
let query_ident = Ident::new(query, proc_macro2::Span::call_site());
|
||||||
|
let field = fields.find(|(name, _)| *name == &query_ident);
|
||||||
|
|
||||||
|
let ty = if let Some((_, ty)) = field {
|
||||||
|
ty.clone()
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
route_span,
|
||||||
|
format!("Could not find a field with the name '{}'", query_ident),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(QuerySegment::Single(FullQuerySegment {
|
||||||
|
ident: query_ident,
|
||||||
|
ty,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
let mut query_arguments = Vec::new();
|
||||||
|
for segment in query.split('&') {
|
||||||
|
if segment.is_empty() {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
route_span,
|
||||||
|
"Query segments should be non-empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(query_argument) = segment.strip_prefix(':') {
|
||||||
|
let query_ident = Ident::new(query_argument, proc_macro2::Span::call_site());
|
||||||
|
let field = fields.find(|(name, _)| *name == &query_ident);
|
||||||
|
|
||||||
|
let ty = if let Some((_, ty)) = field {
|
||||||
|
ty.clone()
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
route_span,
|
||||||
|
format!("Could not find a field with the name '{}'", query_ident),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
query_arguments.push(QueryArgument {
|
||||||
|
ident: query_ident,
|
||||||
|
ty,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
route_span,
|
||||||
|
"Query segments should be a : followed by the name of the query argument",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(QuerySegment::Segments(query_arguments))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -71,7 +131,10 @@ impl FullQuerySegment {
|
||||||
pub fn write(&self) -> TokenStream2 {
|
pub fn write(&self) -> TokenStream2 {
|
||||||
let ident = &self.ident;
|
let ident = &self.ident;
|
||||||
quote! {
|
quote! {
|
||||||
write!(f, "?{}", #ident)?;
|
{
|
||||||
|
let as_string = #ident.to_string();
|
||||||
|
write!(f, "?{}", dioxus_router::exports::urlencoding::encode(&as_string))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,7 +160,10 @@ impl QueryArgument {
|
||||||
pub fn write(&self) -> TokenStream2 {
|
pub fn write(&self) -> TokenStream2 {
|
||||||
let ident = &self.ident;
|
let ident = &self.ident;
|
||||||
quote! {
|
quote! {
|
||||||
write!(f, "{}={}", stringify!(#ident), #ident)?;
|
{
|
||||||
|
let as_string = #ident.to_string();
|
||||||
|
write!(f, "{}={}", stringify!(#ident), dioxus_router::exports::urlencoding::encode(&as_string))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use quote::{format_ident, quote};
|
||||||
use syn::LitStr;
|
use syn::LitStr;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
hash::HashFragment,
|
||||||
nest::NestId,
|
nest::NestId,
|
||||||
query::QuerySegment,
|
query::QuerySegment,
|
||||||
segment::{create_error_type, parse_route_segments, RouteSegment},
|
segment::{create_error_type, parse_route_segments, RouteSegment},
|
||||||
|
@ -14,6 +15,7 @@ pub(crate) struct Redirect {
|
||||||
pub nests: Vec<NestId>,
|
pub nests: Vec<NestId>,
|
||||||
pub segments: Vec<RouteSegment>,
|
pub segments: Vec<RouteSegment>,
|
||||||
pub query: Option<QuerySegment>,
|
pub query: Option<QuerySegment>,
|
||||||
|
pub hash: Option<HashFragment>,
|
||||||
pub function: syn::ExprClosure,
|
pub function: syn::ExprClosure,
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
}
|
}
|
||||||
|
@ -40,6 +42,13 @@ impl Redirect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_hash(&self) -> TokenStream {
|
||||||
|
match &self.hash {
|
||||||
|
Some(hash) => hash.parse(),
|
||||||
|
None => quote! {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse(
|
pub fn parse(
|
||||||
input: syn::parse::ParseStream,
|
input: syn::parse::ParseStream,
|
||||||
active_nests: Vec<NestId>,
|
active_nests: Vec<NestId>,
|
||||||
|
@ -73,7 +82,7 @@ impl Redirect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (segments, query) = parse_route_segments(
|
let (segments, query, hash) = parse_route_segments(
|
||||||
path.span(),
|
path.span(),
|
||||||
#[allow(clippy::map_identity)]
|
#[allow(clippy::map_identity)]
|
||||||
closure_arguments.iter().map(|(name, ty)| (name, ty)),
|
closure_arguments.iter().map(|(name, ty)| (name, ty)),
|
||||||
|
@ -85,6 +94,7 @@ impl Redirect {
|
||||||
nests: active_nests,
|
nests: active_nests,
|
||||||
segments,
|
segments,
|
||||||
query,
|
query,
|
||||||
|
hash,
|
||||||
function,
|
function,
|
||||||
index,
|
index,
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,6 +9,7 @@ use syn::{Ident, LitStr};
|
||||||
|
|
||||||
use proc_macro2::TokenStream as TokenStream2;
|
use proc_macro2::TokenStream as TokenStream2;
|
||||||
|
|
||||||
|
use crate::hash::HashFragment;
|
||||||
use crate::layout::Layout;
|
use crate::layout::Layout;
|
||||||
use crate::layout::LayoutId;
|
use crate::layout::LayoutId;
|
||||||
use crate::nest::Nest;
|
use crate::nest::Nest;
|
||||||
|
@ -56,6 +57,7 @@ pub(crate) struct Route {
|
||||||
pub route: String,
|
pub route: String,
|
||||||
pub segments: Vec<RouteSegment>,
|
pub segments: Vec<RouteSegment>,
|
||||||
pub query: Option<QuerySegment>,
|
pub query: Option<QuerySegment>,
|
||||||
|
pub hash: Option<HashFragment>,
|
||||||
pub nests: Vec<NestId>,
|
pub nests: Vec<NestId>,
|
||||||
pub layouts: Vec<LayoutId>,
|
pub layouts: Vec<LayoutId>,
|
||||||
fields: Vec<(Ident, Type)>,
|
fields: Vec<(Ident, Type)>,
|
||||||
|
@ -144,7 +146,7 @@ impl Route {
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (route_segments, query) = {
|
let (route_segments, query, hash) = {
|
||||||
parse_route_segments(
|
parse_route_segments(
|
||||||
variant.ident.span(),
|
variant.ident.span(),
|
||||||
fields.iter().map(|f| (&f.0, &f.1)),
|
fields.iter().map(|f| (&f.0, &f.1)),
|
||||||
|
@ -158,6 +160,7 @@ impl Route {
|
||||||
segments: route_segments,
|
segments: route_segments,
|
||||||
route,
|
route,
|
||||||
query,
|
query,
|
||||||
|
hash,
|
||||||
nests,
|
nests,
|
||||||
layouts,
|
layouts,
|
||||||
fields,
|
fields,
|
||||||
|
@ -167,7 +170,8 @@ impl Route {
|
||||||
pub fn display_match(&self, nests: &[Nest]) -> TokenStream2 {
|
pub fn display_match(&self, nests: &[Nest]) -> TokenStream2 {
|
||||||
let name = &self.route_name;
|
let name = &self.route_name;
|
||||||
let dynamic_segments = self.dynamic_segments();
|
let dynamic_segments = self.dynamic_segments();
|
||||||
let write_query = self.query.as_ref().map(|q| q.write());
|
let write_query: Option<TokenStream2> = self.query.as_ref().map(|q| q.write());
|
||||||
|
let write_hash = self.hash.as_ref().map(|q| q.write());
|
||||||
|
|
||||||
match &self.ty {
|
match &self.ty {
|
||||||
RouteType::Child(field) => {
|
RouteType::Child(field) => {
|
||||||
|
@ -200,6 +204,7 @@ impl Route {
|
||||||
#(#write_nests)*
|
#(#write_nests)*
|
||||||
#(#write_segments)*
|
#(#write_segments)*
|
||||||
#write_query
|
#write_query
|
||||||
|
#write_hash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -286,6 +291,11 @@ impl Route {
|
||||||
from_route = true
|
from_route = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(hash) = &self.hash {
|
||||||
|
if hash.contains_ident(name) {
|
||||||
|
from_route = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if from_route {
|
if from_route {
|
||||||
quote! {#name}
|
quote! {#name}
|
||||||
|
@ -337,6 +347,13 @@ impl Route {
|
||||||
None => quote! {},
|
None => quote! {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_hash(&self) -> TokenStream2 {
|
||||||
|
match &self.hash {
|
||||||
|
Some(hash) => hash.parse(),
|
||||||
|
None => quote! {},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
@ -337,6 +337,7 @@ impl<'a> RouteTreeSegmentData<'a> {
|
||||||
|
|
||||||
let construct_variant = route.construct(nests, enum_name);
|
let construct_variant = route.construct(nests, enum_name);
|
||||||
let parse_query = route.parse_query();
|
let parse_query = route.parse_query();
|
||||||
|
let parse_hash = route.parse_hash();
|
||||||
|
|
||||||
let insure_not_trailing = match route.ty {
|
let insure_not_trailing = match route.ty {
|
||||||
RouteType::Leaf { .. } => route
|
RouteType::Leaf { .. } => route
|
||||||
|
@ -356,6 +357,7 @@ impl<'a> RouteTreeSegmentData<'a> {
|
||||||
enum_variant,
|
enum_variant,
|
||||||
&variant_parse_error,
|
&variant_parse_error,
|
||||||
parse_query,
|
parse_query,
|
||||||
|
parse_hash,
|
||||||
),
|
),
|
||||||
&error_enum_name,
|
&error_enum_name,
|
||||||
enum_variant,
|
enum_variant,
|
||||||
|
@ -426,6 +428,7 @@ impl<'a> RouteTreeSegmentData<'a> {
|
||||||
.skip_while(|(_, seg)| matches!(seg, RouteSegment::Static(_)));
|
.skip_while(|(_, seg)| matches!(seg, RouteSegment::Static(_)));
|
||||||
|
|
||||||
let parse_query = redirect.parse_query();
|
let parse_query = redirect.parse_query();
|
||||||
|
let parse_hash = redirect.parse_hash();
|
||||||
|
|
||||||
let insure_not_trailing = redirect
|
let insure_not_trailing = redirect
|
||||||
.segments
|
.segments
|
||||||
|
@ -454,6 +457,7 @@ impl<'a> RouteTreeSegmentData<'a> {
|
||||||
enum_variant,
|
enum_variant,
|
||||||
&variant_parse_error,
|
&variant_parse_error,
|
||||||
parse_query,
|
parse_query,
|
||||||
|
parse_hash,
|
||||||
),
|
),
|
||||||
&error_enum_name,
|
&error_enum_name,
|
||||||
enum_variant,
|
enum_variant,
|
||||||
|
@ -501,6 +505,7 @@ fn return_constructed(
|
||||||
enum_variant: &Ident,
|
enum_variant: &Ident,
|
||||||
variant_parse_error: &Ident,
|
variant_parse_error: &Ident,
|
||||||
parse_query: TokenStream,
|
parse_query: TokenStream,
|
||||||
|
parse_hash: TokenStream,
|
||||||
) -> TokenStream {
|
) -> TokenStream {
|
||||||
if insure_not_trailing {
|
if insure_not_trailing {
|
||||||
quote! {
|
quote! {
|
||||||
|
@ -514,6 +519,7 @@ fn return_constructed(
|
||||||
// This is the last segment, return the parsed route
|
// This is the last segment, return the parsed route
|
||||||
(None, _) | (Some(""), None) => {
|
(None, _) | (Some(""), None) => {
|
||||||
#parse_query
|
#parse_query
|
||||||
|
#parse_hash
|
||||||
return Ok(#construct_variant);
|
return Ok(#construct_variant);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -530,6 +536,7 @@ fn return_constructed(
|
||||||
} else {
|
} else {
|
||||||
quote! {
|
quote! {
|
||||||
#parse_query
|
#parse_query
|
||||||
|
#parse_hash
|
||||||
return Ok(#construct_variant);
|
return Ok(#construct_variant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use syn::{Ident, Type};
|
||||||
|
|
||||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||||
|
|
||||||
use crate::query::{FullQuerySegment, QueryArgument, QuerySegment};
|
use crate::{hash::HashFragment, query::QuerySegment};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum RouteSegment {
|
pub enum RouteSegment {
|
||||||
|
@ -24,7 +24,12 @@ impl RouteSegment {
|
||||||
pub fn write_segment(&self) -> TokenStream2 {
|
pub fn write_segment(&self) -> TokenStream2 {
|
||||||
match self {
|
match self {
|
||||||
Self::Static(segment) => quote! { write!(f, "/{}", #segment)?; },
|
Self::Static(segment) => quote! { write!(f, "/{}", #segment)?; },
|
||||||
Self::Dynamic(ident, _) => quote! { write!(f, "/{}", #ident)?; },
|
Self::Dynamic(ident, _) => quote! {
|
||||||
|
{
|
||||||
|
let as_string = #ident.to_string();
|
||||||
|
write!(f, "/{}", dioxus_router::exports::urlencoding::encode(&as_string))?;
|
||||||
|
}
|
||||||
|
},
|
||||||
Self::CatchAll(ident, _) => quote! { #ident.display_route_segments(f)?; },
|
Self::CatchAll(ident, _) => quote! { #ident.display_route_segments(f)?; },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,15 +135,38 @@ pub fn static_segment_idx(idx: usize) -> Ident {
|
||||||
|
|
||||||
pub fn parse_route_segments<'a>(
|
pub fn parse_route_segments<'a>(
|
||||||
route_span: Span,
|
route_span: Span,
|
||||||
fields: impl Iterator<Item = (&'a Ident, &'a Type)> + Clone,
|
mut fields: impl Iterator<Item = (&'a Ident, &'a Type)> + Clone,
|
||||||
route: &str,
|
route: &str,
|
||||||
) -> syn::Result<(Vec<RouteSegment>, Option<QuerySegment>)> {
|
) -> syn::Result<(
|
||||||
|
Vec<RouteSegment>,
|
||||||
|
Option<QuerySegment>,
|
||||||
|
Option<HashFragment>,
|
||||||
|
)> {
|
||||||
let mut route_segments = Vec::new();
|
let mut route_segments = Vec::new();
|
||||||
|
|
||||||
let (route_string, query) = match route.rsplit_once('?') {
|
let (route_string, hash) = match route.rsplit_once('#') {
|
||||||
Some((route, query)) => (route, Some(query)),
|
Some((route, hash)) => (
|
||||||
|
route,
|
||||||
|
Some(HashFragment::parse_from_str(
|
||||||
|
route_span,
|
||||||
|
fields.clone(),
|
||||||
|
hash,
|
||||||
|
)?),
|
||||||
|
),
|
||||||
None => (route, None),
|
None => (route, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (route_string, query) = match route_string.rsplit_once('?') {
|
||||||
|
Some((route, query)) => (
|
||||||
|
route,
|
||||||
|
Some(QuerySegment::parse_from_str(
|
||||||
|
route_span,
|
||||||
|
fields.clone(),
|
||||||
|
query,
|
||||||
|
)?),
|
||||||
|
),
|
||||||
|
None => (route_string, None),
|
||||||
|
};
|
||||||
let mut iterator = route_string.split('/');
|
let mut iterator = route_string.split('/');
|
||||||
|
|
||||||
// skip the first empty segment
|
// skip the first empty segment
|
||||||
|
@ -198,66 +226,7 @@ pub fn parse_route_segments<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the route has a query string
|
Ok((route_segments, query, hash))
|
||||||
let parsed_query = match query {
|
|
||||||
Some(query) => {
|
|
||||||
if let Some(query) = query.strip_prefix(":..") {
|
|
||||||
let query_ident = Ident::new(query, Span::call_site());
|
|
||||||
let field = fields.clone().find(|(name, _)| *name == &query_ident);
|
|
||||||
|
|
||||||
let ty = if let Some((_, ty)) = field {
|
|
||||||
ty.clone()
|
|
||||||
} else {
|
|
||||||
return Err(syn::Error::new(
|
|
||||||
route_span,
|
|
||||||
format!("Could not find a field with the name '{}'", query_ident),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(QuerySegment::Single(FullQuerySegment {
|
|
||||||
ident: query_ident,
|
|
||||||
ty,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
let mut query_arguments = Vec::new();
|
|
||||||
for segment in query.split('&') {
|
|
||||||
if segment.is_empty() {
|
|
||||||
return Err(syn::Error::new(
|
|
||||||
route_span,
|
|
||||||
"Query segments should be non-empty",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if let Some(query_argument) = segment.strip_prefix(':') {
|
|
||||||
let query_ident = Ident::new(query_argument, Span::call_site());
|
|
||||||
let field = fields.clone().find(|(name, _)| *name == &query_ident);
|
|
||||||
|
|
||||||
let ty = if let Some((_, ty)) = field {
|
|
||||||
ty.clone()
|
|
||||||
} else {
|
|
||||||
return Err(syn::Error::new(
|
|
||||||
route_span,
|
|
||||||
format!("Could not find a field with the name '{}'", query_ident),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
query_arguments.push(QueryArgument {
|
|
||||||
ident: query_ident,
|
|
||||||
ty,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return Err(syn::Error::new(
|
|
||||||
route_span,
|
|
||||||
"Query segments should be a : followed by the name of the query argument",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(QuerySegment::Segments(query_arguments))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((route_segments, parsed_query))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn create_error_type(
|
pub(crate) fn create_error_type(
|
||||||
|
|
|
@ -76,7 +76,6 @@ impl RouterContext {
|
||||||
mark_dirty: Arc<dyn Fn(ScopeId) + Sync + Send>,
|
mark_dirty: Arc<dyn Fn(ScopeId) + Sync + Send>,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
R: Clone,
|
|
||||||
<R as std::str::FromStr>::Err: std::fmt::Display,
|
<R as std::str::FromStr>::Err: std::fmt::Display,
|
||||||
{
|
{
|
||||||
let subscriber_update = mark_dirty.clone();
|
let subscriber_update = mark_dirty.clone();
|
||||||
|
|
|
@ -128,7 +128,8 @@ where
|
||||||
fn route_from_location(&self) -> R {
|
fn route_from_location(&self) -> R {
|
||||||
let location = self.window.location();
|
let location = self.window.location();
|
||||||
let path = location.pathname().unwrap_or_else(|_| "/".into())
|
let path = location.pathname().unwrap_or_else(|_| "/".into())
|
||||||
+ &location.search().unwrap_or("".into());
|
+ &location.search().unwrap_or("".into())
|
||||||
|
+ &location.hash().unwrap_or("".into());
|
||||||
let path = match self.prefix {
|
let path = match self.prefix {
|
||||||
None => path,
|
None => path,
|
||||||
Some(ref prefix) => {
|
Some(ref prefix) => {
|
||||||
|
|
|
@ -36,7 +36,7 @@ pub trait FromQuery {
|
||||||
|
|
||||||
impl<T: for<'a> From<&'a str>> FromQuery for T {
|
impl<T: for<'a> From<&'a str>> FromQuery for T {
|
||||||
fn from_query(query: &str) -> Self {
|
fn from_query(query: &str) -> Self {
|
||||||
T::from(&*urlencoding::decode(query).expect("Failed to decode url encoding"))
|
T::from(query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,14 +58,7 @@ where
|
||||||
type Err = <T as FromStr>::Err;
|
type Err = <T as FromStr>::Err;
|
||||||
|
|
||||||
fn from_query_argument(argument: &str) -> Result<Self, Self::Err> {
|
fn from_query_argument(argument: &str) -> Result<Self, Self::Err> {
|
||||||
let result = match urlencoding::decode(argument) {
|
match T::from_str(argument) {
|
||||||
Ok(argument) => T::from_str(&argument),
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("Failed to decode url encoding: {}", err);
|
|
||||||
T::from_str(argument)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match result {
|
|
||||||
Ok(result) => Ok(result),
|
Ok(result) => Ok(result),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::error!("Failed to parse query argument: {}", err);
|
tracing::error!("Failed to parse query argument: {}", err);
|
||||||
|
@ -75,6 +68,82 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Something that can be created from an entire hash fragment.
|
||||||
|
///
|
||||||
|
/// This trait needs to be implemented if you want to turn a hash fragment into a struct.
|
||||||
|
///
|
||||||
|
/// # 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 {
|
||||||
|
/// todo!()
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// #[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,
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
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 route segment.
|
/// Something that can be created from a route segment.
|
||||||
pub trait FromRouteSegment: Sized {
|
pub trait FromRouteSegment: Sized {
|
||||||
/// The error that can occur when parsing a route segment.
|
/// The error that can occur when parsing a route segment.
|
||||||
|
@ -91,13 +160,7 @@ where
|
||||||
type Err = <T as FromStr>::Err;
|
type Err = <T as FromStr>::Err;
|
||||||
|
|
||||||
fn from_route_segment(route: &str) -> Result<Self, Self::Err> {
|
fn from_route_segment(route: &str) -> Result<Self, Self::Err> {
|
||||||
match urlencoding::decode(route) {
|
T::from_str(route)
|
||||||
Ok(segment) => T::from_str(&segment),
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("Failed to decode url encoding: {}", err);
|
|
||||||
T::from_str(route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +172,7 @@ fn full_circle() {
|
||||||
|
|
||||||
/// Something that can be converted to route segments.
|
/// Something that can be converted to route segments.
|
||||||
pub trait ToRouteSegments {
|
pub trait ToRouteSegments {
|
||||||
/// Display the route segments.
|
/// Display the route segments. You must url encode the segments.
|
||||||
fn display_route_segments(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
|
fn display_route_segments(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,13 +184,8 @@ where
|
||||||
for segment in self {
|
for segment in self {
|
||||||
write!(f, "/")?;
|
write!(f, "/")?;
|
||||||
let segment = segment.to_string();
|
let segment = segment.to_string();
|
||||||
match urlencoding::decode(&segment) {
|
let encoded = urlencoding::encode(&segment);
|
||||||
Ok(segment) => write!(f, "{}", segment)?,
|
write!(f, "{}", encoded)?;
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("Failed to decode url encoding: {}", err);
|
|
||||||
write!(f, "{}", segment)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
use crate::contexts::router::RoutingCallback;
|
|
||||||
use crate::history::HistoryProvider;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::routable::Routable;
|
|
||||||
use dioxus_lib::prelude::*;
|
use dioxus_lib::prelude::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue