1use crate::cfg::RouterCfg;
5use dioxus::core::{ScopeId, ScopeState, VirtualDom};
6use std::any::Any;
7use std::rc::Weak;
8use std::{
9 cell::{Cell, RefCell},
10 collections::{HashMap, HashSet},
11 rc::Rc,
12 str::FromStr,
13 sync::Arc,
14};
15use url::Url;
16
17pub type RouterContext = Rc<RouterService>;
19
20pub struct RouterService {
48 pub(crate) route_found: Cell<Option<ScopeId>>,
49
50 pub(crate) stack: RefCell<Vec<Arc<ParsedRoute>>>,
51
52 pub(crate) slots: Rc<RefCell<HashMap<ScopeId, String>>>,
53
54 pub(crate) ordering: Rc<RefCell<Vec<ScopeId>>>,
55
56 pub(crate) onchange_listeners: Rc<RefCell<HashSet<ScopeId>>>,
57
58 pub(crate) history: Box<dyn RouterProvider>,
59
60 pub(crate) regen_any_route: Arc<dyn Fn(ScopeId)>,
61
62 pub(crate) router_id: ScopeId,
63
64 pub(crate) cfg: RouterCfg,
65}
66
67#[derive(Debug, Clone)]
69pub struct ParsedRoute {
70 pub url: Url,
72
73 pub title: Option<String>,
75
76 pub serialized_state: Option<String>,
78}
79
80impl RouterService {
81 pub(crate) fn new(cx: &ScopeState, cfg: RouterCfg) -> RouterContext {
82 #[cfg(feature = "web")]
83 let history = Box::new(web::new());
84
85 #[cfg(not(feature = "web"))]
86 let history = Box::new(hash::new());
87
88 let route = match &cfg.initial_url {
89 Some(url) => Arc::new(ParsedRoute {
90 url: Url::from_str(url).unwrap_or_else(|_|
91 panic!(
92 "RouterCfg expects a valid initial_url, but got '{}'. Example: '{{scheme}}://{{?authority}}/{{?path}}'",
93 &url
94 )
95 ),
96 title: None,
97 serialized_state: None,
98 }),
99 None => Arc::new(history.init_location()),
100 };
101
102 let svc = Rc::new(Self {
103 cfg,
104 regen_any_route: cx.schedule_update_any(),
105 router_id: cx.scope_id(),
106 route_found: Cell::new(None),
107 stack: RefCell::new(vec![route]),
108 ordering: Default::default(),
109 slots: Default::default(),
110 onchange_listeners: Default::default(),
111 history,
112 });
113
114 svc.history.attach_listeners(Rc::downgrade(&svc));
115
116 svc
117 }
118
119 pub fn navigate_to(&self, route: &str) {
123 self.push_route(route, None, None);
124 }
125
126 pub fn push_route(&self, route: &str, title: Option<String>, serialized_state: Option<String>) {
132 let new_route = Arc::new(ParsedRoute {
133 url: self.current_location().url.join(route).ok().unwrap(),
134 title,
135 serialized_state,
136 });
137
138 self.history.push(&new_route);
139 self.stack.borrow_mut().push(new_route);
140
141 self.regen_routes();
142 }
143
144 pub fn replace_route(
146 &self,
147 route: &str,
148 title: Option<String>,
149 serialized_state: Option<String>,
150 ) {
151 let new_route = Arc::new(ParsedRoute {
152 url: self.current_location().url.join(route).ok().unwrap(),
153 title,
154 serialized_state,
155 });
156
157 self.history.replace(&new_route);
158 *self.stack.borrow_mut().last_mut().unwrap() = new_route;
159
160 self.regen_routes();
161 }
162
163 pub fn pop_route(&self) {
165 let mut stack = self.stack.borrow_mut();
166
167 if stack.len() > 1 {
168 stack.pop();
169 }
170
171 self.regen_routes();
172 }
173
174 pub fn regen_routes(&self) {
178 self.route_found.set(None);
179
180 (self.regen_any_route)(self.router_id);
181
182 for listener in self.onchange_listeners.borrow().iter() {
183 log::trace!("Regenerating scope {:?}", listener);
184 (self.regen_any_route)(*listener);
185 }
186
187 for route in self.ordering.borrow().iter().rev() {
188 (self.regen_any_route)(*route);
189 }
190 }
191
192 pub fn current_location(&self) -> Arc<ParsedRoute> {
194 self.stack.borrow().last().unwrap().clone()
195 }
196
197 pub fn native_location<T: 'static>(&self) -> Option<Box<T>> {
199 self.history.native_location().downcast::<T>().ok()
200 }
201
202 pub fn subscribe_onchange(&self, id: ScopeId) {
206 self.onchange_listeners.borrow_mut().insert(id);
207 }
208
209 pub fn unsubscribe_onchange(&self, id: ScopeId) {
213 self.onchange_listeners.borrow_mut().remove(&id);
214 }
215
216 pub(crate) fn register_total_route(&self, route: String, scope: ScopeId) {
217 let clean = clean_route(route);
218 self.slots.borrow_mut().insert(scope, clean);
219 self.ordering.borrow_mut().push(scope);
220 }
221
222 pub(crate) fn should_render(&self, scope: ScopeId) -> bool {
223 if let Some(root_id) = self.route_found.get() {
224 return root_id == scope;
225 }
226
227 let roots = self.slots.borrow();
228
229 if let Some(route) = roots.get(&scope) {
230 let cur = &self.current_location().url;
231 log::trace!("Checking if {} matches {}", cur, route);
232
233 if route_matches_path(cur, route, self.cfg.base_url.as_ref()) || route.is_empty() {
234 self.route_found.set(Some(scope));
235 true
236 } else {
237 false
238 }
239 } else {
240 false
241 }
242 }
243}
244
245pub fn get_router_from_vdom(dom: &VirtualDom, target_scope: ScopeId) -> Option<RouterContext> {
252 dom.get_scope(target_scope)
253 .and_then(|scope| scope.consume_context::<RouterContext>())
254}
255
256fn clean_route(route: String) -> String {
257 if route.as_str() == "/" {
258 return route;
259 }
260 route.trim_end_matches('/').to_string()
261}
262
263fn clean_path(path: &str) -> &str {
264 if path == "/" {
265 return path;
266 }
267 let sub = path.trim_end_matches('/');
268
269 if sub.starts_with('/') {
270 &path[1..]
271 } else {
272 sub
273 }
274}
275
276fn route_matches_path(cur: &Url, attempt: &str, base_url: Option<&String>) -> bool {
277 let cur_piece_iter = cur.path_segments().unwrap();
278
279 let mut cur_pieces = match base_url {
280 Some(_) => cur_piece_iter.skip(1).collect::<Vec<_>>(),
282 None => cur_piece_iter.collect::<Vec<_>>(),
283 };
284
285 if attempt == "/" && cur_pieces.len() == 1 && cur_pieces[0].is_empty() {
286 return true;
287 }
288
289 if cur_pieces.last() == Some(&"") {
291 cur_pieces.pop();
292 }
293
294 let attempt_pieces = clean_path(attempt).split('/').collect::<Vec<_>>();
295
296 if attempt_pieces.len() != cur_pieces.len() {
297 return false;
298 }
299
300 for (i, r) in attempt_pieces.iter().enumerate() {
301 if r.starts_with(':') {
304 continue;
305 }
306
307 if cur_pieces[i] != *r {
308 return false;
309 }
310 }
311
312 true
313}
314
315pub(crate) trait RouterProvider {
316 fn push(&self, route: &ParsedRoute);
317 fn replace(&self, route: &ParsedRoute);
318 fn native_location(&self) -> Box<dyn Any>;
319 fn init_location(&self) -> ParsedRoute;
320 fn attach_listeners(&self, svc: Weak<RouterService>);
321}
322
323#[cfg(not(feature = "web"))]
324mod hash {
325 use super::*;
326
327 pub fn new() -> HashRouter {
328 HashRouter {}
329 }
330
331 pub struct HashRouter {}
333
334 impl RouterProvider for HashRouter {
335 fn push(&self, _route: &ParsedRoute) {}
336
337 fn native_location(&self) -> Box<dyn Any> {
338 Box::new(())
339 }
340
341 fn init_location(&self) -> ParsedRoute {
342 ParsedRoute {
343 url: Url::parse("app:///").unwrap(),
344 title: None,
345 serialized_state: None,
346 }
347 }
348
349 fn replace(&self, _route: &ParsedRoute) {}
350
351 fn attach_listeners(&self, _svc: Weak<RouterService>) {}
352 }
353}
354
355#[cfg(feature = "web")]
356mod web {
357 use super::RouterProvider;
358 use crate::ParsedRoute;
359
360 use gloo_events::EventListener;
361 use std::{any::Any, cell::Cell};
362 use web_sys::History;
363
364 pub struct WebRouter {
365 _listener: Cell<Option<gloo_events::EventListener>>,
367
368 window: web_sys::Window,
369 history: History,
370 }
371
372 impl RouterProvider for WebRouter {
373 fn push(&self, route: &ParsedRoute) {
374 let ParsedRoute {
375 url,
376 title,
377 serialized_state,
378 } = route;
379
380 let _ = self.history.push_state_with_url(
381 &wasm_bindgen::JsValue::from_str(serialized_state.as_deref().unwrap_or("")),
382 title.as_deref().unwrap_or(""),
383 Some(url.as_str()),
384 );
385 }
386
387 fn replace(&self, route: &ParsedRoute) {
388 let ParsedRoute {
389 url,
390 title,
391 serialized_state,
392 } = route;
393
394 let _ = self.history.replace_state_with_url(
395 &wasm_bindgen::JsValue::from_str(serialized_state.as_deref().unwrap_or("")),
396 title.as_deref().unwrap_or(""),
397 Some(url.as_str()),
398 );
399 }
400
401 fn native_location(&self) -> Box<dyn Any> {
402 Box::new(self.window.location())
403 }
404
405 fn init_location(&self) -> ParsedRoute {
406 ParsedRoute {
407 url: url::Url::parse(&web_sys::window().unwrap().location().href().unwrap())
408 .unwrap(),
409 title: web_sys::window()
410 .unwrap()
411 .document()
412 .unwrap()
413 .title()
414 .into(),
415 serialized_state: None,
416 }
417 }
418
419 fn attach_listeners(&self, svc: std::rc::Weak<crate::RouterService>) {
420 self._listener.set(Some(EventListener::new(
421 &web_sys::window().unwrap(),
422 "popstate",
423 move |_| {
424 if let Some(svc) = svc.upgrade() {
425 svc.pop_route();
426 }
427 },
428 )));
429 }
430 }
431
432 pub(crate) fn new() -> WebRouter {
433 WebRouter {
434 history: web_sys::window().unwrap().history().unwrap(),
435 window: web_sys::window().unwrap(),
436 _listener: Cell::new(None),
437 }
438 }
439}