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:
Evan Almloff 2024-04-25 13:30:25 -05:00 committed by GitHub
parent dd8e69635b
commit 08da9a1253
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 550 additions and 190 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -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"]

View 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]}"
}
}
}
}
}

View 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}" }
}
}
}

View file

@ -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);
}

View file

@ -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>,

View 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,
})
}
}

View file

@ -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('/') {

View file

@ -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))?;
}
} }
} }
} }

View file

@ -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,
}) })

View file

@ -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)]

View file

@ -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);
} }
} }

View file

@ -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(

View file

@ -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();

View file

@ -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) => {

View file

@ -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(())
} }

View file

@ -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;