next_rs/router.rs
1use crate::log;
2use std::borrow::Cow;
3
4use crate::history::{AnyHistory, BrowserHistory, History, HistoryError, HistoryResult};
5use crate::prelude::*;
6use crate::use_context;
7use serde_json::Value;
8use std::collections::{HashMap, HashSet};
9use std::rc::Rc;
10use yew_router::prelude::Location;
11
12use gloo_net::http::Request;
13use web_sys::{EventListener, RequestCache};
14
15use wasm_bindgen_futures::spawn_local;
16use web_sys::js_sys::Function;
17
18/// Represents errors related to navigation.
19pub type NavigationError = HistoryError;
20
21/// Represents results of navigation operations.
22pub type NavigationResult<T> = HistoryResult<T>;
23
24/// Represents the context of the current location.
25#[derive(Clone)]
26pub struct LocationContext {
27 location: Location,
28 // Counter to force update.
29 ctr: u32,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33pub struct ComponentInfo {
34 pub component: Html,
35 pub err: &'static str,
36}
37
38impl LocationContext {
39 /// Returns the current location.
40 pub fn location(&self) -> Location {
41 self.location.clone()
42 }
43}
44
45impl PartialEq for LocationContext {
46 fn eq(&self, rhs: &Self) -> bool {
47 self.ctr == rhs.ctr
48 }
49}
50
51impl Reducible for LocationContext {
52 type Action = Location;
53
54 /// Reduces the state by applying the provided action.
55 ///
56 /// # Arguments
57 ///
58 /// * `action` - The action to apply to the state.
59 ///
60 /// # Returns
61 ///
62 /// (Rc<Self>): A new reference-counted state after applying the action.
63 fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
64 Self {
65 location: action,
66 ctr: self.ctr + 1,
67 }
68 .into()
69 }
70}
71
72/// Props for [`NextRouter`].
73#[derive(Properties, PartialEq, Clone)]
74pub struct RouterProps {
75 /// Children components to be rendered.
76 #[prop_or_default]
77 pub children: Html,
78 /// The history instance for navigation.
79 #[prop_or(AnyHistory::Browser(BrowserHistory::new()))]
80 pub history: AnyHistory,
81 /// The base URL for the router.
82 #[prop_or_default]
83 pub basename: &'static str,
84}
85
86/// The kind of Router Provider.
87#[derive(Debug, PartialEq, Eq, Clone, Copy)]
88pub enum RouterKind {
89 /// Browser History.
90 Browser,
91 /// Hash History.
92 Hash,
93 /// Memory History.
94 Memory,
95}
96
97/// Represents the context of the current router.
98#[derive(Clone, PartialEq)]
99pub struct RouterContext {
100 router: Router,
101}
102
103impl RouterContext {
104 /// Returns the current router instance.
105 pub fn router(&self) -> Router {
106 self.router.clone()
107 }
108}
109/// A struct representing the router for navigation.
110#[derive(Debug, Clone)]
111pub struct Router {
112 /// The history instance for navigation.
113 history: AnyHistory,
114
115 /// The base URL for the router.
116 basename: &'static str,
117
118 /// The current route of the router.
119 route: &'static str,
120
121 /// A mapping of route names to corresponding component information.
122 components: HashMap<&'static str, ComponentInfo>,
123
124 /// Set of routes currently being fetched or loaded.
125 fetching_routes: HashSet<String>,
126
127 /// Event listener for router events.
128 events: EventListener,
129
130 /// The error component to be rendered in case of errors.
131 error_component: Html,
132
133 /// The current pathname of the router.
134 pathname: &'static str,
135
136 /// The query parameters associated with the current route.
137 query: Value,
138
139 /// The path for the current route.
140 as_path: &'static str,
141
142 /// Subscriptions to router events with corresponding callbacks.
143 subscriptions: Vec<Callback<ComponentInfo>>,
144
145 /// Callback to cancel the loading of a component.
146 component_load_cancel: Callback<()>,
147}
148
149// Implement PartialEq manually for Router
150impl PartialEq for Router {
151 fn eq(&self, other: &Self) -> bool {
152 self.history == other.history
153 && self.basename == other.basename
154 && self.route == other.route
155 && self.components == other.components
156 && self.fetching_routes.len() == other.fetching_routes.len()
157 && self.events == other.events
158 && self.error_component == other.error_component
159 && self.pathname == other.pathname
160 && self.query == other.query
161 && self.as_path == other.as_path
162 && self.subscriptions.len() == other.subscriptions.len()
163 && self.component_load_cancel == other.component_load_cancel
164 }
165}
166
167impl Router {
168 /// Creates a new router instance.
169 ///
170 /// # Arguments
171 ///
172 /// * `history` - The history instance for navigation.
173 /// * `basename` - The base URL for the router.
174 /// * `route` - The default route for the router.
175 /// * `components` - A mapping of route names to component information.
176 /// * `fetching_routes` - Set of routes currently being fetched.
177 /// * `events` - Event listener for handling router events.
178 /// * `error_component` - The component to display in case of navigation errors.
179 /// * `pathname` - The current pathname of the router.
180 /// * `query` - The current query parameters of the router.
181 /// * `as_path` - The current path as a string.
182 /// * `subscriptions` - List of callbacks for component information updates.
183 /// * `component_load_cancel` - Callback for cancelling component loading.
184 ///
185 /// # Returns
186 ///
187 /// A new `Router` instance.
188 pub fn new(
189 history: AnyHistory,
190 basename: &'static str,
191 route: &'static str,
192 components: HashMap<&'static str, ComponentInfo>,
193 fetching_routes: HashSet<String>,
194 events: EventListener,
195 error_component: Html,
196 pathname: &'static str,
197 query: Value,
198 as_path: &'static str,
199 subscriptions: Vec<Callback<ComponentInfo>>,
200 component_load_cancel: Callback<()>,
201 ) -> Self {
202 Self {
203 history,
204 basename,
205 route,
206 components,
207 fetching_routes,
208 events,
209 error_component,
210 pathname,
211 query,
212 as_path,
213 subscriptions,
214 component_load_cancel,
215 }
216 }
217
218 /// Returns the basename of the current router.
219 pub fn basename(&self) -> &'static str {
220 self.basename
221 }
222
223 /// Navigates back by one page.
224 pub fn back(&self) {
225 self.go(-1);
226 }
227
228 /// Navigates forward by one page.
229 pub fn forward(&self) {
230 self.go(1);
231 }
232
233 /// Navigates to a specific page with a `delta` relative to the current page.
234 ///
235 /// # Arguments
236 ///
237 /// * `delta` - The number of pages to navigate (positive for forward, negative for backward).
238 ///
239 /// See: <https://developer.mozilla.org/en-US/docs/Web/API/History/go>
240 pub fn go(&self, delta: isize) {
241 self.history.go(delta);
242 }
243
244 /// Pushes a route onto the history stack.
245 ///
246 /// # Arguments
247 ///
248 /// * `route` - The route to be pushed.
249 pub fn push(&mut self, route: &'static str) {
250 self.route = route;
251 self.history.push(self.prefix_basename(route));
252 }
253
254 /// Replaces the current history entry with the provided route.
255 ///
256 /// # Arguments
257 ///
258 /// * `route` - The route to replace the current history entry.
259 pub fn replace(&mut self, route: &'static str) {
260 self.route = route;
261 self.history.replace(self.prefix_basename(route));
262 }
263
264 /// Pushes a route onto the history stack with state.
265 ///
266 /// # Arguments
267 ///
268 /// * `route` - The route to be pushed.
269 /// * `state` - The state to be associated with the route.
270 pub fn push_with_state(&mut self, route: &'static str, state: &'static str) {
271 self.route = route;
272 self.history
273 .push_with_state(self.prefix_basename(route), state);
274 }
275
276 /// Replaces the current history entry with the provided route and state.
277 ///
278 /// # Arguments
279 ///
280 /// * `route` - The route to replace the current history entry.
281 /// * `state` - The state to be associated with the route.
282 pub fn replace_with_state(&mut self, route: &'static str, state: &'static str) {
283 self.route = route;
284 self.history
285 .replace_with_state(self.prefix_basename(route), state);
286 }
287
288 /// Pushes a route onto the history stack with query parameters.
289 ///
290 /// # Arguments
291 ///
292 /// * `route` - The route to be pushed.
293 /// * `query` - The query parameters to be associated with the route.
294 ///
295 /// # Returns
296 ///
297 /// A `NavigationResult` indicating the success of the operation.
298 pub fn push_with_query(&mut self, route: &'static str, query: &Value) -> NavigationResult<()> {
299 self.route = route;
300 self.query = query.clone();
301 self.history
302 .push_with_query(self.prefix_basename(route), query)
303 }
304
305 /// Pushes a route onto the history stack with query parameters and state.
306 ///
307 /// # Arguments
308 ///
309 /// * `route` - The route to be pushed.
310 /// * `query` - The query parameters to be associated with the route.
311 /// * `state` - The state to be associated with the route.
312 ///
313 /// # Returns
314 ///
315 /// A `NavigationResult` indicating the success of the operation.
316 pub fn push_with_query_and_state(
317 &mut self,
318 route: &'static str,
319 query: &Value,
320 state: &'static str,
321 ) -> NavigationResult<()> {
322 self.route = route;
323 self.query = query.clone();
324 self.history
325 .push_with_query_and_state(self.prefix_basename(route), query, state)
326 }
327
328 /// Replaces the current history entry with the provided route, query parameters, and state.
329 ///
330 /// # Arguments
331 ///
332 /// * `route` - The route to replace the current history entry.
333 /// * `query` - The query parameters to be associated with the route.
334 /// * `state` - The state to be associated with the route.
335 ///
336 /// # Returns
337 ///
338 /// A `NavigationResult` indicating the success of the operation.
339 pub fn replace_with_query_and_state(
340 &mut self,
341 route: &'static str,
342 query: &Value,
343 state: Value,
344 ) -> NavigationResult<()> {
345 self.route = route;
346 self.query = query.clone();
347 self.history
348 .replace_with_query_and_state(self.prefix_basename(route), query, state)
349 }
350
351 /// Returns the kind of the router.
352 ///
353 /// # Returns
354 ///
355 /// A `RouterKind` enum representing the type of the router.
356 pub fn kind(&self) -> RouterKind {
357 match &self.history {
358 AnyHistory::Browser(_) => RouterKind::Browser,
359 AnyHistory::Hash(_) => RouterKind::Hash,
360 AnyHistory::Memory(_) => RouterKind::Memory,
361 }
362 }
363
364 /// Prefixes the basename to the route.
365 ///
366 /// # Arguments
367 ///
368 /// * `route_s` - The route to prefix with the basename.
369 ///
370 /// # Returns
371 ///
372 /// A `Cow<'a, str>` containing the combined route with the basename.
373 pub fn prefix_basename<'a>(&self, route_s: &'a str) -> Cow<'a, str> {
374 let base = self.basename();
375 if !base.is_empty() {
376 if route_s.is_empty() && route_s.is_empty() {
377 Cow::from("/")
378 } else {
379 Cow::from(format!("{base}{route_s}"))
380 }
381 } else {
382 route_s.into()
383 }
384 }
385
386 /// Strips the basename from the path.
387 ///
388 /// # Arguments
389 ///
390 /// * `path` - The path to strip the basename from.
391 ///
392 /// # Returns
393 ///
394 /// A `Cow<'a, str>` containing the path with the basename stripped.
395 pub fn strip_basename<'a>(&self, path: Cow<'a, str>) -> Cow<'a, str> {
396 let m = self.basename();
397 if !m.is_empty() {
398 let mut path = path
399 .strip_prefix(m)
400 .map(|m| Cow::from(m.to_owned()))
401 .unwrap_or(path);
402
403 if !path.starts_with('/') {
404 path = format!("/{m}").into();
405 }
406
407 path
408 } else {
409 path
410 }
411 }
412
413 /// Prefetches the specified URL by fetching its route information.
414 ///
415 /// # Arguments
416 ///
417 /// * `url` - The URL to prefetch.
418 pub fn prefetch(&mut self, url: &'static str) {
419 self.fetch_route(url.to_string());
420 }
421
422 /// Asynchronously fetches route information for the given URL.
423 ///
424 /// # Arguments
425 ///
426 /// * `url` - The URL for which to fetch route information.
427 ///
428 /// # Returns
429 ///
430 /// A `Result` containing `ComponentInfo` on success and an error `Value` on failure.
431 async fn fetch_gloo_net(url: &str) -> Result<ComponentInfo, Value> {
432 let response = match Request::get(url).cache(RequestCache::Reload).send().await {
433 Ok(res) => res,
434 Err(err) => {
435 return Err(err.to_string().into());
436 }
437 };
438
439 let _json_result = match response.json::<serde_json::Value>().await {
440 Ok(data) => data,
441 Err(err) => {
442 return Err(err.to_string().into());
443 }
444 };
445
446 Ok(ComponentInfo {
447 component: rsx! {},
448 err: "",
449 })
450 }
451 /// Initiates the fetching of route information for the specified route.
452 ///
453 /// # Arguments
454 ///
455 /// * `route` - The route to fetch.
456 fn fetch_route(&mut self, route: String) {
457 let url = format!("/{}/index.json", route);
458 let events = EventListener::new();
459 let subscriptions = self.subscriptions.clone();
460 let as_path = self.as_path;
461 let route = route.clone();
462 let self_route = self.route;
463 let fetching_routes = Callback::from(move |_: String| {
464 let url = url.clone();
465 let mut fetching_routes = HashSet::new();
466 let mut events = events.clone();
467 let subscriptions = subscriptions.clone();
468 let as_path = as_path;
469 let route = route.clone();
470 let self_route = self_route;
471 spawn_local(async move {
472 let result = match Self::fetch_gloo_net(&url).await {
473 Ok(component_info) => {
474 fetching_routes.insert(route.clone());
475 if self_route == route {
476 if !component_info.err.is_empty() {
477 events.handle_event(&Function::new_with_args(
478 "route_change_error",
479 as_path,
480 ));
481 }
482 Self::notify(subscriptions, component_info);
483 events.handle_event(&Function::new_with_args(
484 "route_change_complete",
485 as_path,
486 ));
487 }
488 Ok(())
489 }
490 Err(fetch_error) => {
491 fetching_routes.insert(route.clone());
492 log(&format!("Error fetching route: {:?}", fetch_error).into());
493 if self_route == route {
494 let component_info = ComponentInfo {
495 component: rsx! {},
496 err: "Error fetching route",
497 };
498 Self::notify(subscriptions, component_info);
499 events.handle_event(&Function::new_with_args(
500 "route_change_complete",
501 as_path,
502 ));
503 }
504 Err(fetch_error)
505 }
506 };
507
508 if let Err(error) = result {
509 log(&format!("Failed to handle fetch result: {:?}", error).into());
510 }
511 });
512 // fetching_routes.clone()
513 });
514 // self.fetching_routes = fetching_routes.emit("".to_string());
515 fetching_routes.emit("".to_string())
516 }
517
518 /// Notifies all subscribed callbacks with the provided route information.
519 ///
520 /// # Arguments
521 ///
522 /// * `subscriptions` - A vector of callbacks to notify.
523 /// * `data` - The route information to emit to the callbacks.
524 fn notify(subscriptions: Vec<Callback<ComponentInfo>>, data: ComponentInfo) {
525 subscriptions.iter().for_each(|callback| {
526 callback.emit(data.clone());
527 });
528 }
529
530 /// Subscribes to route change events and returns an unsubscribe callback.
531 ///
532 /// # Arguments
533 ///
534 /// * `callback` - The callback to be notified on route changes.
535 ///
536 /// # Returns
537 ///
538 /// A `Callback<()>` that can be used to unsubscribe from route change events.
539 fn _subscribe(&mut self, callback: Callback<ComponentInfo>) -> Callback<()> {
540 // Creates a Listener that will be notified when current state changes.
541 // self.history.listen(callback);
542 self.subscriptions.push(callback.clone());
543 Callback::from(move |_| {
544 // TODO: Implement unsubscribe, rm from subscriptions vec
545 })
546 }
547}
548
549/// The base router component.
550///
551/// This component ensures that `<Router />` has the same virtual DOM layout as `<BrowserRouter />`
552/// and `<HashRouter />`.
553///
554/// # Arguments
555///
556/// * `props` - The properties of the router.
557///
558/// # Returns
559///
560/// (Html): An HTML representation of the router component.
561///
562/// # Example
563/// ```
564/// use next_rs::prelude::*;
565/// use next_rs::router::*;
566/// use next_rs::history::{BrowserHistory, AnyHistory};
567///
568/// #[func]
569/// fn MyComponent() -> Html {
570/// rsx! {
571/// <BaseRouter basename="" history={AnyHistory::Browser(BrowserHistory::new())}>
572/// <div />
573/// </BaseRouter>
574/// }
575/// }
576/// ```
577#[func]
578pub fn BaseRouter(props: &RouterProps) -> Html {
579 let RouterProps {
580 history,
581 children,
582 basename,
583 } = props.clone();
584
585 let loc_ctx = use_reducer(|| LocationContext {
586 location: history.location(),
587 ctr: 0,
588 });
589
590 let trigger = use_force_update();
591 let prefetched_component = use_state(|| rsx! {<></>});
592 let component_value = (*prefetched_component).clone();
593
594 let basename = basename.strip_suffix('/').unwrap_or(basename);
595
596 let route = "/";
597 let components = HashMap::new();
598 let fetching_routes = HashSet::new();
599 let events = EventListener::new();
600 let error_component = Html::default();
601 let pathname = "";
602 let query = Value::default();
603 let as_path = "";
604 let mut subscriptions = Vec::new();
605 subscriptions.push(Callback::from(move |component: ComponentInfo| {
606 prefetched_component.set(component.component);
607 trigger.force_update();
608 log(&format!("prefetch callback...").into());
609 }));
610 let component_load_cancel = Callback::default();
611
612 let router = Router::new(
613 history.clone(),
614 basename,
615 route,
616 components,
617 fetching_routes,
618 events,
619 error_component,
620 pathname,
621 query,
622 as_path,
623 subscriptions,
624 component_load_cancel,
625 );
626 let navi_ctx = RouterContext {
627 router: router.clone(),
628 };
629
630 {
631 let loc_ctx_dispatcher = loc_ctx.dispatcher();
632
633 use_effect_with(history, move |history| {
634 let history = history.clone();
635 // Force location update when history changes.
636 loc_ctx_dispatcher.dispatch(history.location());
637
638 let history_cb = {
639 let history = history.clone();
640 move || loc_ctx_dispatcher.dispatch(history.location())
641 };
642
643 let listener = history.listen(history_cb);
644
645 // We hold the listener in the destructor.
646 move || {
647 std::mem::drop(listener);
648 }
649 });
650 }
651
652 rsx! {
653 <ContextProvider<RouterContext> context={navi_ctx}>
654 <ContextProvider<LocationContext> context={(*loc_ctx).clone()}>
655 {children}
656 {component_value}
657 </ContextProvider<LocationContext>>
658 </ContextProvider<RouterContext>>
659 }
660}
661
662/// Props for [`Switch`]
663#[derive(Properties, PartialEq, Clone)]
664pub struct SwitchProps {
665 /// Callback which returns [`Html`] to be rendered for the current route.
666 pub render: Callback<String, Html>,
667 #[prop_or_default]
668 pub pathname: &'static str,
669}
670
671/// A Switch that dispatches routes among variants of a [`Routable`].
672///
673/// When a route can't be matched, including when the path is matched but the deserialization fails,
674/// it looks for the route with the `not_found` attribute.
675/// If such a route is provided, it redirects to the specified route.
676/// Otherwise, an empty HTML element is rendered, and a message is logged to the console
677/// stating that no route can be matched.
678/// See the [crate level document][crate] for more information.
679///
680/// # Arguments
681///
682/// * `props` - The properties of the switch.
683///
684/// # Returns
685///
686/// (Html): An HTML representation of the switch component.
687///
688/// # Example
689/// ```
690/// use next_rs::prelude::*;
691/// use next_rs::router::*;
692///
693/// pub fn switch(route: String) -> Html {
694/// match route.as_str() {
695/// "/" => rsx! {<div />},
696/// _ => rsx! {<></>},
697/// }
698/// }
699///
700/// #[func]
701/// fn MySwitch() -> Html {
702/// rsx! {
703/// <Switch render={switch} />
704/// }
705/// }
706/// ```
707#[func]
708pub fn Switch(props: &SwitchProps) -> Html {
709 let mut route = use_route();
710
711 if route.is_empty() {
712 route = std::borrow::Cow::Borrowed(props.pathname);
713 }
714
715 if !route.is_empty() {
716 props.render.emit(route.to_string())
717 } else {
718 Html::default()
719 }
720}
721
722/// The NextRouter component.
723///
724/// This component provides location and navigator context to its children and switches.
725///
726/// If you are building a web application, you may want to consider using [`BrowserRouter`] instead.
727///
728/// You only need one `<Router />` for each application.
729///
730/// # Arguments
731///
732/// * `props` - The properties of the router.
733///
734/// # Returns
735///
736/// (Html): An HTML representation of the router component.
737///
738/// # Example
739/// ```
740/// use next_rs::prelude::*;
741/// use next_rs::router::*;
742/// use next_rs::history::{BrowserHistory, AnyHistory};
743///
744/// #[func]
745/// fn MyComponent() -> Html {
746/// rsx! {
747/// <NextRouter basename="" history={AnyHistory::Browser(BrowserHistory::new())}>
748/// <div />
749/// </NextRouter>
750/// }
751/// }
752/// ```
753#[func]
754pub fn NextRouter(props: &RouterProps) -> Html {
755 rsx! {
756 <BaseRouter ..props.clone() />
757 }
758}
759
760/// A hook to access the [`Router`] instance.
761///
762/// This hook allows components to access the router, which manages the application's navigation and routes.
763/// It retrieves the router from the current context and returns it.
764#[hook]
765pub fn use_router() -> Router {
766 use_context::<RouterContext>()
767 .map(|m| m.router())
768 .expect("router")
769}
770
771/// A hook to access the current [`Location`] information.
772///
773/// This hook provides components with access to the current location, including details such as the path and query parameters.
774/// It retrieves the location from the current context and returns it as an `Option`.
775#[hook]
776pub fn use_location() -> Option<Location> {
777 Some(use_context::<LocationContext>()?.location())
778}
779
780/// A hook to access the current route path with the basename stripped.
781///
782/// This hook is useful for components that need the current route path with the basename removed.
783/// It uses the `use_router` and `use_location` hooks to get the router and location information,
784/// then strips the basename from the location path, returning the stripped path as a `Cow<'static, str>`.
785#[hook]
786pub fn use_route() -> Cow<'static, str> {
787 let router = use_router();
788 let location = use_location().expect("location");
789
790 let stripped_path: Cow<'static, str> = router
791 .strip_basename(Cow::Borrowed(location.path()))
792 .into_owned()
793 .into();
794
795 stripped_path
796}