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:
1 parent
dd8e69635b
commit
08da9a1253
17 files changed
+550
-190
No files matched your search
@@ -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]}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user