hash_fragment_state.rs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. //! This example shows how to use the hash segment to store state in the url.
  2. //!
  3. //! You can set up two way data binding between the url hash and signals.
  4. //!
  5. //! Run this example on desktop with
  6. //! ```sh
  7. //! dx serve --example hash_fragment_state --features=ciborium,base64
  8. //! ```
  9. //! Or on web with
  10. //! ```sh
  11. //! dx serve --platform web --features web --example hash_fragment_state --features=ciborium,base64 -- --no-default-features
  12. //! ```
  13. use std::{fmt::Display, str::FromStr};
  14. use base64::engine::general_purpose::STANDARD;
  15. use base64::Engine;
  16. use dioxus::prelude::*;
  17. use serde::{Deserialize, Serialize};
  18. fn main() {
  19. dioxus::launch(|| {
  20. rsx! {
  21. Router::<Route> {}
  22. }
  23. });
  24. }
  25. #[derive(Routable, Clone, Debug, PartialEq)]
  26. #[rustfmt::skip]
  27. enum Route {
  28. #[route("/#:url_hash")]
  29. Home {
  30. url_hash: State,
  31. },
  32. }
  33. // You can use a custom type with the hash segment as long as it implements Display, FromStr and Default
  34. #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
  35. struct State {
  36. counters: Vec<usize>,
  37. }
  38. // Display the state in a way that can be parsed by FromStr
  39. impl Display for State {
  40. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  41. let mut serialized = Vec::new();
  42. if ciborium::into_writer(self, &mut serialized).is_ok() {
  43. write!(f, "{}", STANDARD.encode(serialized))?;
  44. }
  45. Ok(())
  46. }
  47. }
  48. enum StateParseError {
  49. DecodeError(base64::DecodeError),
  50. CiboriumError(ciborium::de::Error<std::io::Error>),
  51. }
  52. impl std::fmt::Display for StateParseError {
  53. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  54. match self {
  55. Self::DecodeError(err) => write!(f, "Failed to decode base64: {}", err),
  56. Self::CiboriumError(err) => write!(f, "Failed to deserialize: {}", err),
  57. }
  58. }
  59. }
  60. // Parse the state from a string that was created by Display
  61. impl FromStr for State {
  62. type Err = StateParseError;
  63. fn from_str(s: &str) -> Result<Self, Self::Err> {
  64. let decompressed = STANDARD
  65. .decode(s.as_bytes())
  66. .map_err(StateParseError::DecodeError)?;
  67. let parsed = ciborium::from_reader(std::io::Cursor::new(decompressed))
  68. .map_err(StateParseError::CiboriumError)?;
  69. Ok(parsed)
  70. }
  71. }
  72. #[component]
  73. fn Home(url_hash: ReadOnlySignal<State>) -> Element {
  74. // The initial state of the state comes from the url hash
  75. let mut state = use_signal(&*url_hash);
  76. // Change the state signal when the url hash changes
  77. use_memo(move || {
  78. if *state.peek() != *url_hash.read() {
  79. state.set(url_hash());
  80. }
  81. });
  82. // Change the url hash when the state changes
  83. use_memo(move || {
  84. if *state.read() != *url_hash.peek() {
  85. navigator().replace(Route::Home { url_hash: state() });
  86. }
  87. });
  88. rsx! {
  89. button {
  90. onclick: move |_| state.write().counters.clear(),
  91. "Reset"
  92. }
  93. button {
  94. onclick: move |_| {
  95. state.write().counters.push(0);
  96. },
  97. "Add Counter"
  98. }
  99. for counter in 0..state.read().counters.len() {
  100. div {
  101. button {
  102. onclick: move |_| {
  103. state.write().counters.remove(counter);
  104. },
  105. "Remove"
  106. }
  107. button {
  108. onclick: move |_| {
  109. state.write().counters[counter] += 1;
  110. },
  111. "Count: {state.read().counters[counter]}"
  112. }
  113. }
  114. }
  115. }
  116. }