yew_router_nested/
service.rs1use yew::callback::Callback;
4
5use crate::route::{Route, RouteState};
6use cfg_if::cfg_if;
7use cfg_match::cfg_match;
8use std::marker::PhantomData;
9
10cfg_if! {
11 if #[cfg(feature = "std_web")] {
12 use stdweb::{
13 js,
14 unstable::{TryFrom, TryInto},
15 web::{event::PopStateEvent, window, EventListenerHandle, History, IEventTarget, Location},
16 Value,
17 };
18 } else if #[cfg(feature = "web_sys")] {
19 use web_sys::{History, Location, PopStateEvent};
20 use gloo::events::EventListener;
21 use wasm_bindgen::{JsValue as Value, JsCast};
22 }
23}
24
25#[derive(Debug)]
30pub struct RouteService<STATE = ()> {
31 history: History,
32 location: Location,
33 #[cfg(feature = "std_web")]
34 event_listener: Option<EventListenerHandle>,
35 #[cfg(feature = "web_sys")]
36 event_listener: Option<EventListener>,
37 phantom_data: PhantomData<STATE>,
38}
39
40impl<STATE> Default for RouteService<STATE>
41where
42 STATE: RouteState,
43{
44 fn default() -> Self {
45 RouteService::<STATE>::new()
46 }
47}
48
49impl<T> RouteService<T> {
50 pub fn new() -> RouteService<T> {
52 let (history, location) = cfg_match! {
53 feature = "std_web" => ({
54 (
55 window().history(),
56 window().location().expect("browser does not support location API")
57 )
58 }),
59 feature = "web_sys" => ({
60 let window = web_sys::window().unwrap();
61 (
62 window.history().expect("browser does not support history API"),
63 window.location()
64 )
65 }),
66 };
67
68 RouteService {
69 history,
70 location,
71 event_listener: None,
72 phantom_data: PhantomData,
73 }
74 }
75
76 #[inline]
77 fn get_route_from_location(location: &Location) -> String {
78 let path = location.pathname().unwrap();
79 let query = location.search().unwrap();
80 let fragment = location.hash().unwrap();
81 format_route_string(&path, &query, &fragment)
82 }
83
84 pub fn get_path(&self) -> String {
86 self.location.pathname().unwrap()
87 }
88
89 pub fn get_query(&self) -> String {
91 self.location.search().unwrap()
92 }
93
94 pub fn get_fragment(&self) -> String {
96 self.location.hash().unwrap()
97 }
98}
99
100impl<STATE> RouteService<STATE>
101where
102 STATE: RouteState,
103{
104 pub fn register_callback(&mut self, callback: Callback<Route<STATE>>) {
108 let cb = move |event: PopStateEvent| {
109 let state_value: Value = event.state();
110 let state_string: String = cfg_match! {
111 feature = "std_web" => String::try_from(state_value).unwrap_or_default(),
112 feature = "web_sys" => state_value.as_string().unwrap_or_default(),
113 };
114 let state: STATE = serde_json::from_str(&state_string).unwrap_or_else(|_| {
115 log::error!("Could not deserialize state string");
116 STATE::default()
117 });
118
119 let location: Location = cfg_match! {
122 feature = "std_web" => window().location().unwrap(),
123 feature = "web_sys" => web_sys::window().unwrap().location(),
124 };
125 let route: String = Self::get_route_from_location(&location);
126
127 callback.emit(Route { route, state })
128 };
129
130 cfg_if! {
131 if #[cfg(feature = "std_web")] {
132 self.event_listener = Some(window().add_event_listener(move |event: PopStateEvent| {
133 cb(event)
134 }));
135 } else if #[cfg(feature = "web_sys")] {
136 self.event_listener = Some(EventListener::new(web_sys::window().unwrap().as_ref(), "popstate", move |event| {
137 let event: PopStateEvent = event.clone().dyn_into().unwrap();
138 cb(event)
139 }));
140 }
141 };
142 }
143
144 pub fn set_route(&mut self, route: &str, state: STATE) {
149 let state_string: String = serde_json::to_string(&state).unwrap_or_else(|_| {
150 log::error!("Could not serialize state string");
151 "".to_string()
152 });
153 cfg_match! {
154 feature = "std_web" => ({
155 self.history.push_state(state_string, "", Some(route));
156 }),
157 feature = "web_sys" => ({
158 let _ = self.history.push_state_with_url(&Value::from_str(&state_string), "", Some(route));
159 }),
160 };
161 }
162
163 pub fn replace_route(&mut self, route: &str, state: STATE) {
166 let state_string: String = serde_json::to_string(&state).unwrap_or_else(|_| {
167 log::error!("Could not serialize state string");
168 "".to_string()
169 });
170 cfg_match! {
171 feature = "std_web" => ({
172 let _ = self.history.replace_state(state_string, "", Some(route));
173 }),
174 feature = "web_sys" => ({
175 let _ = self.history.replace_state_with_url(&Value::from_str(&state_string), "", Some(route));
176 }),
177 };
178 }
179
180 pub fn get_route(&self) -> Route<STATE> {
182 let route_string = Self::get_route_from_location(&self.location);
183 let state: STATE = get_state_string(&self.history)
184 .or_else(|| {
185 log::trace!("History state is empty");
186 None
187 })
188 .and_then(|state_string| -> Option<STATE> {
189 serde_json::from_str(&state_string)
190 .ok()
191 .or_else(|| {
192 log::error!("Could not deserialize state string");
193 None
194 })
195 .and_then(std::convert::identity) })
197 .unwrap_or_default();
198 Route {
199 route: route_string,
200 state,
201 }
202 }
203}
204
205pub(crate) fn format_route_string(path: &str, query: &str, fragment: &str) -> String {
210 format!(
211 "{path}{query}{fragment}",
212 path = path,
213 query = query,
214 fragment = fragment
215 )
216}
217
218fn get_state(history: &History) -> Value {
219 cfg_match! {
220 feature = "std_web" => js!(
221 return @{history}.state;
222 ),
223 feature = "web_sys" => history.state().unwrap(),
224 }
225}
226
227fn get_state_string(history: &History) -> Option<String> {
228 cfg_match! {
229 feature = "std_web" => get_state(history).try_into().ok(),
230 feature = "web_sys" => get_state(history).as_string(),
231 }
232}