1use std::marker::PhantomData;
2
3use yew::prelude::*;
4use yew_router::{
5 history::{BrowserHistory, History},
6 prelude::*
7};
8
9#[derive(Clone, Debug)]
19pub struct Navigation<R>
20where
21 R: Routable + Clone + 'static
22{
23 pub go_back: Callback<()>,
25 pub go_forward: Callback<()>,
27 pub _marker: PhantomData<R>
29}
30
31impl<R> Navigation<R>
32where
33 R: Routable + Clone + 'static
34{
35 pub fn push_callback(&self, route: R) -> Callback<()> {
37 Callback::from(move |()| {
38 let path = route.to_path();
39 BrowserHistory::new().push(&path);
40 })
41 }
42
43 pub fn replace_callback(&self, route: R) -> Callback<()> {
45 Callback::from(move |()| {
46 let path = route.to_path();
47 BrowserHistory::new().replace(&path);
48 })
49 }
50
51 #[must_use]
52 pub fn go_callback(&self, delta: isize) -> Callback<()> {
54 Callback::from(move |()| {
55 BrowserHistory::new().go(delta);
56 })
57 }
58}
59
60#[hook]
89pub fn use_navigation<R>() -> Navigation<R>
90where
91 R: Routable + Clone + 'static
92{
93 let go_back = Callback::from(|()| {
94 BrowserHistory::new().back();
95 });
96
97 let go_forward = Callback::from(|()| {
98 BrowserHistory::new().forward();
99 });
100
101 Navigation {
102 go_back,
103 go_forward,
104 _marker: PhantomData
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use std::marker::PhantomData;
111
112 use super::*;
113
114 #[test]
115 fn navigation_struct_creation() {
116 #[derive(Clone, PartialEq, Debug, Routable)]
117 enum TestRoute {
118 #[at("/test")]
119 Test,
120 #[at("/")]
121 Home
122 }
123
124 let nav = Navigation::<TestRoute> {
125 go_back: Callback::from(|()| {}),
126 go_forward: Callback::from(|()| {}),
127 _marker: PhantomData
128 };
129
130 let _ = nav.go_back;
131 let _ = nav.go_forward;
132 }
133
134 #[test]
135 fn navigation_clone() {
136 #[derive(Clone, PartialEq, Debug, Routable)]
137 enum TestRoute {
138 #[at("/test")]
139 Test
140 }
141
142 let nav1 = Navigation::<TestRoute> {
143 go_back: Callback::from(|()| {}),
144 go_forward: Callback::from(|()| {}),
145 _marker: PhantomData
146 };
147
148 let nav2 = nav1;
149 let _ = nav2.go_back;
150 let _ = nav2.go_forward;
151 }
152
153 #[test]
154 fn navigation_debug() {
155 #[derive(Clone, PartialEq, Debug, Routable)]
156 enum TestRoute {
157 #[at("/test")]
158 Test
159 }
160
161 let nav = Navigation::<TestRoute> {
162 go_back: Callback::from(|()| {}),
163 go_forward: Callback::from(|()| {}),
164 _marker: PhantomData
165 };
166
167 let debug_str = format!("{nav:?}");
168 assert!(debug_str.contains("Navigation"));
169 }
170
171 #[test]
172 fn navigation_partial_eq() {
173 #[derive(Clone, PartialEq, Debug, Routable)]
174 enum TestRoute {
175 #[at("/test")]
176 Test
177 }
178
179 let nav1 = Navigation::<TestRoute> {
180 go_back: Callback::from(|()| {}),
181 go_forward: Callback::from(|()| {}),
182 _marker: PhantomData
183 };
184 let nav2 = nav1;
185 let _ = nav1.go_back;
186 let _ = nav2.go_back;
187 }
188
189 #[test]
190 fn navigation_push_callback() {
191 #[derive(Clone, PartialEq, Debug, Routable)]
192 enum TestRoute {
193 #[at("/")]
194 Home
195 }
196
197 let nav = Navigation::<TestRoute> {
198 go_back: Callback::from(|()| {}),
199 go_forward: Callback::from(|()| {}),
200 _marker: PhantomData
201 };
202 let _ = nav.push_callback(TestRoute::Home);
203 }
204
205 #[test]
206 fn navigation_replace_callback() {
207 #[derive(Clone, PartialEq, Debug, Routable)]
208 enum TestRoute {
209 #[at("/")]
210 Home
211 }
212
213 let nav = Navigation::<TestRoute> {
214 go_back: Callback::from(|()| {}),
215 go_forward: Callback::from(|()| {}),
216 _marker: PhantomData
217 };
218 let _ = nav.replace_callback(TestRoute::Home);
219 }
220
221 #[test]
222 fn navigation_go_callback() {
223 #[derive(Clone, PartialEq, Debug, Routable)]
224 enum TestRoute {
225 #[at("/")]
226 Home
227 }
228
229 let nav = Navigation::<TestRoute> {
230 go_back: Callback::from(|()| {}),
231 go_forward: Callback::from(|()| {}),
232 _marker: PhantomData
233 };
234 let _ = nav.go_callback(-1);
235 }
236
237 #[test]
238 fn navigation_go_callback_with_positive_delta() {
239 #[derive(Clone, PartialEq, Debug, Routable)]
240 enum TestRoute {
241 #[at("/")]
242 Home
243 }
244
245 let nav = Navigation::<TestRoute> {
246 go_back: Callback::from(|()| {}),
247 go_forward: Callback::from(|()| {}),
248 _marker: PhantomData
249 };
250 let callback = nav.go_callback(1);
251 let _ = callback;
252 }
253
254 #[test]
255 fn navigation_go_callback_with_zero_delta() {
256 #[derive(Clone, PartialEq, Debug, Routable)]
257 enum TestRoute {
258 #[at("/")]
259 Home
260 }
261
262 let nav = Navigation::<TestRoute> {
263 go_back: Callback::from(|()| {}),
264 go_forward: Callback::from(|()| {}),
265 _marker: PhantomData
266 };
267 let callback = nav.go_callback(0);
268 let _ = callback;
269 }
270
271 #[test]
272 fn navigation_go_callback_with_large_negative_delta() {
273 #[derive(Clone, PartialEq, Debug, Routable)]
274 enum TestRoute {
275 #[at("/")]
276 Home
277 }
278
279 let nav = Navigation::<TestRoute> {
280 go_back: Callback::from(|()| {}),
281 go_forward: Callback::from(|()| {}),
282 _marker: PhantomData
283 };
284 let callback = nav.go_callback(-10);
285 let _ = callback;
286 }
287
288 #[test]
289 fn navigation_go_callback_with_large_positive_delta() {
290 #[derive(Clone, PartialEq, Debug, Routable)]
291 enum TestRoute {
292 #[at("/")]
293 Home
294 }
295
296 let nav = Navigation::<TestRoute> {
297 go_back: Callback::from(|()| {}),
298 go_forward: Callback::from(|()| {}),
299 _marker: PhantomData
300 };
301 let callback = nav.go_callback(10);
302 let _ = callback;
303 }
304
305 #[test]
306 fn navigation_push_callback_with_struct_route() {
307 #[derive(Clone, PartialEq, Debug, Routable)]
308 enum TestRoute {
309 #[at("/users/:id")]
310 User { id: String }
311 }
312
313 let nav = Navigation::<TestRoute> {
314 go_back: Callback::from(|()| {}),
315 go_forward: Callback::from(|()| {}),
316 _marker: PhantomData
317 };
318
319 let route = TestRoute::User {
320 id: "123".to_string()
321 };
322 let callback = nav.push_callback(route);
323 let _ = callback;
324 }
325
326 #[test]
327 fn navigation_push_callback_with_tuple_route() {
328 #[derive(Clone, PartialEq, Debug, Routable)]
329 enum TestRoute {
330 #[at("/posts/:year")]
331 Post { year: u32 }
332 }
333
334 let nav = Navigation::<TestRoute> {
335 go_back: Callback::from(|()| {}),
336 go_forward: Callback::from(|()| {}),
337 _marker: PhantomData
338 };
339
340 let route = TestRoute::Post {
341 year: 2024
342 };
343 let callback = nav.push_callback(route);
344 let _ = callback;
345 }
346
347 #[test]
348 fn navigation_replace_callback_with_struct_route() {
349 #[derive(Clone, PartialEq, Debug, Routable)]
350 enum TestRoute {
351 #[at("/settings/:section")]
352 Settings { section: String }
353 }
354
355 let nav = Navigation::<TestRoute> {
356 go_back: Callback::from(|()| {}),
357 go_forward: Callback::from(|()| {}),
358 _marker: PhantomData
359 };
360
361 let route = TestRoute::Settings {
362 section: "profile".to_string()
363 };
364 let callback = nav.replace_callback(route);
365 let _ = callback;
366 }
367
368 #[test]
369 fn navigation_replace_callback_with_tuple_route() {
370 #[derive(Clone, PartialEq, Debug, Routable)]
371 enum TestRoute {
372 #[at("/api/:version")]
373 Api { version: u32 }
374 }
375
376 let nav = Navigation::<TestRoute> {
377 go_back: Callback::from(|()| {}),
378 go_forward: Callback::from(|()| {}),
379 _marker: PhantomData
380 };
381
382 let route = TestRoute::Api {
383 version: 1
384 };
385 let callback = nav.replace_callback(route);
386 let _ = callback;
387 }
388
389 #[test]
390 fn navigation_callbacks_are_distinct() {
391 #[derive(Clone, PartialEq, Debug, Routable)]
392 enum TestRoute {
393 #[at("/")]
394 Home,
395 #[at("/about")]
396 About
397 }
398
399 let nav = Navigation::<TestRoute> {
400 go_back: Callback::from(|()| {}),
401 go_forward: Callback::from(|()| {}),
402 _marker: PhantomData
403 };
404
405 let push_callback = nav.push_callback(TestRoute::Home);
406 let replace_callback = nav.replace_callback(TestRoute::About);
407 let go_callback = nav.go_callback(-1);
408
409 let _ = push_callback;
410 let _ = replace_callback;
411 let _ = go_callback;
412 }
413
414 #[test]
415 fn navigation_multiple_callbacks_same_route() {
416 #[derive(Clone, PartialEq, Debug, Routable)]
417 enum TestRoute {
418 #[at("/")]
419 Home
420 }
421
422 let nav = Navigation::<TestRoute> {
423 go_back: Callback::from(|()| {}),
424 go_forward: Callback::from(|()| {}),
425 _marker: PhantomData
426 };
427
428 let callback1 = nav.push_callback(TestRoute::Home);
429 let callback2 = nav.push_callback(TestRoute::Home);
430 let callback3 = nav.push_callback(TestRoute::Home);
431
432 let _ = callback1;
433 let _ = callback2;
434 let _ = callback3;
435 }
436
437 #[test]
438 fn navigation_callbacks_with_complex_route() {
439 #[derive(Clone, PartialEq, Debug, Routable)]
440 enum ComplexRoute {
441 #[at("/")]
442 Home,
443 #[at("/users")]
444 Users,
445 #[at("/admin")]
446 Admin,
447 #[at("/date")]
448 Date
449 }
450
451 let nav = Navigation::<ComplexRoute> {
452 go_back: Callback::from(|()| {}),
453 go_forward: Callback::from(|()| {}),
454 _marker: PhantomData
455 };
456
457 let _ = nav.push_callback(ComplexRoute::Home);
458 let _ = nav.push_callback(ComplexRoute::Users);
459 let _ = nav.push_callback(ComplexRoute::Admin);
460 let _ = nav.push_callback(ComplexRoute::Date);
461 }
462
463 #[test]
464 fn navigation_callbacks_preserve_route_data() {
465 #[derive(Clone, PartialEq, Debug, Routable)]
466 enum TestRoute {
467 #[at("/search/:query")]
468 Search { query: String }
469 }
470
471 let nav = Navigation::<TestRoute> {
472 go_back: Callback::from(|()| {}),
473 go_forward: Callback::from(|()| {}),
474 _marker: PhantomData
475 };
476
477 let query = "rust programming".to_string();
478 let route = TestRoute::Search {
479 query: query.clone()
480 };
481 let _callback = nav.push_callback(route);
482
483 assert_eq!(query, "rust programming");
484 }
485
486 #[test]
487 fn navigation_go_back_callback_exists() {
488 #[derive(Clone, PartialEq, Debug, Routable)]
489 enum TestRoute {
490 #[at("/")]
491 Home
492 }
493
494 let nav = Navigation::<TestRoute> {
495 go_back: Callback::from(|()| {}),
496 go_forward: Callback::from(|()| {}),
497 _marker: PhantomData
498 };
499
500 let _ = nav.go_back;
501 }
502
503 #[test]
504 fn navigation_go_forward_callback_exists() {
505 #[derive(Clone, PartialEq, Debug, Routable)]
506 enum TestRoute {
507 #[at("/")]
508 Home
509 }
510
511 let nav = Navigation::<TestRoute> {
512 go_back: Callback::from(|()| {}),
513 go_forward: Callback::from(|()| {}),
514 _marker: PhantomData
515 };
516
517 let _ = nav.go_forward;
518 }
519
520 #[test]
521 fn navigation_struct_with_multiple_variants() {
522 #[derive(Clone, PartialEq, Debug, Routable)]
523 enum MultiVariantRoute {
524 #[at("/")]
525 Home,
526 #[at("/about")]
527 About,
528 #[at("/contact")]
529 Contact,
530 #[at("/products")]
531 Products,
532 #[at("/services")]
533 Services,
534 #[at("/blog")]
535 Blog,
536 #[at("/faq")]
537 Faq
538 }
539
540 let nav = Navigation::<MultiVariantRoute> {
541 go_back: Callback::from(|()| {}),
542 go_forward: Callback::from(|()| {}),
543 _marker: PhantomData
544 };
545
546 let _ = nav.push_callback(MultiVariantRoute::Home);
547 let _ = nav.push_callback(MultiVariantRoute::About);
548 let _ = nav.push_callback(MultiVariantRoute::Contact);
549 let _ = nav.push_callback(MultiVariantRoute::Products);
550 let _ = nav.push_callback(MultiVariantRoute::Services);
551 let _ = nav.push_callback(MultiVariantRoute::Blog);
552 let _ = nav.push_callback(MultiVariantRoute::Faq);
553 }
554
555 #[test]
556 fn navigation_go_callback_edge_cases() {
557 #[derive(Clone, PartialEq, Debug, Routable)]
558 enum TestRoute {
559 #[at("/")]
560 Home
561 }
562
563 let nav = Navigation::<TestRoute> {
564 go_back: Callback::from(|()| {}),
565 go_forward: Callback::from(|()| {}),
566 _marker: PhantomData
567 };
568
569 let min_delta = nav.go_callback(isize::MIN);
570 let max_delta = nav.go_callback(isize::MAX);
571 let neg_one = nav.go_callback(-1);
572 let pos_one = nav.go_callback(1);
573
574 let _ = min_delta;
575 let _ = max_delta;
576 let _ = neg_one;
577 let _ = pos_one;
578 }
579
580 #[test]
581 fn navigation_callbacks_with_empty_string_route() {
582 #[derive(Clone, PartialEq, Debug, Routable)]
583 enum TestRoute {
584 #[at("/item/:name")]
585 Item { name: String }
586 }
587
588 let nav = Navigation::<TestRoute> {
589 go_back: Callback::from(|()| {}),
590 go_forward: Callback::from(|()| {}),
591 _marker: PhantomData
592 };
593
594 let route = TestRoute::Item {
595 name: String::new()
596 };
597 let callback = nav.push_callback(route);
598 let _ = callback;
599 }
600
601 #[test]
602 fn navigation_callbacks_with_special_characters() {
603 #[derive(Clone, PartialEq, Debug, Routable)]
604 enum TestRoute {
605 #[at("/tag/:name")]
606 Tag { name: String }
607 }
608
609 let nav = Navigation::<TestRoute> {
610 go_back: Callback::from(|()| {}),
611 go_forward: Callback::from(|()| {}),
612 _marker: PhantomData
613 };
614
615 let route = TestRoute::Tag {
616 name: "rust-lang".to_string()
617 };
618 let callback = nav.push_callback(route);
619 let _ = callback;
620 }
621
622 #[test]
623 fn navigation_callbacks_with_unicode() {
624 #[derive(Clone, PartialEq, Debug, Routable)]
625 enum TestRoute {
626 #[at("/emoji/:emoji")]
627 Emoji { emoji: String }
628 }
629
630 let nav = Navigation::<TestRoute> {
631 go_back: Callback::from(|()| {}),
632 go_forward: Callback::from(|()| {}),
633 _marker: PhantomData
634 };
635
636 let route = TestRoute::Emoji {
637 emoji: "🦀".to_string()
638 };
639 let callback = nav.push_callback(route);
640 let _ = callback;
641 }
642
643 #[test]
644 fn navigation_callbacks_with_numbers() {
645 #[derive(Clone, PartialEq, Debug, Routable)]
646 enum TestRoute {
647 #[at("/number/:num")]
648 Number { num: u64 }
649 }
650
651 let nav = Navigation::<TestRoute> {
652 go_back: Callback::from(|()| {}),
653 go_forward: Callback::from(|()| {}),
654 _marker: PhantomData
655 };
656
657 let route = TestRoute::Number {
658 num: 0
659 };
660 let callback = nav.push_callback(route);
661 let _ = callback;
662
663 let route = TestRoute::Number {
664 num: u64::MAX
665 };
666 let callback = nav.push_callback(route);
667 let _ = callback;
668 }
669
670 #[test]
671 fn navigation_struct_debug_format() {
672 #[derive(Clone, PartialEq, Debug, Routable)]
673 enum TestRoute {
674 #[at("/")]
675 Home
676 }
677
678 let nav = Navigation::<TestRoute> {
679 go_back: Callback::from(|()| {}),
680 go_forward: Callback::from(|()| {}),
681 _marker: PhantomData
682 };
683
684 let debug_str = format!("{nav:?}");
685 assert!(debug_str.contains("Navigation"));
686 assert!(debug_str.contains("go_back"));
687 assert!(debug_str.contains("go_forward"));
688 }
689
690 #[test]
691 fn navigation_callbacks_chainability() {
692 #[derive(Clone, PartialEq, Debug, Routable)]
693 enum TestRoute {
694 #[at("/")]
695 Home,
696 #[at("/step1")]
697 Step1,
698 #[at("/step2")]
699 Step2,
700 #[at("/step3")]
701 Step3
702 }
703
704 let nav = Navigation::<TestRoute> {
705 go_back: Callback::from(|()| {}),
706 go_forward: Callback::from(|()| {}),
707 _marker: PhantomData
708 };
709
710 let step1_callback = nav.push_callback(TestRoute::Step1);
711 let step2_callback = nav.push_callback(TestRoute::Step2);
712 let step3_callback = nav.push_callback(TestRoute::Step3);
713 let back_callback = nav.go_callback(-1);
714
715 let _ = step1_callback;
716 let _ = step2_callback;
717 let _ = step3_callback;
718 let _ = back_callback;
719 }
720
721 #[test]
722 fn navigation_replace_vs_push_same_route() {
723 #[derive(Clone, PartialEq, Debug, Routable)]
724 enum TestRoute {
725 #[at("/")]
726 Home
727 }
728
729 let nav = Navigation::<TestRoute> {
730 go_back: Callback::from(|()| {}),
731 go_forward: Callback::from(|()| {}),
732 _marker: PhantomData
733 };
734
735 let push_cb = nav.push_callback(TestRoute::Home);
736 let replace_cb = nav.replace_callback(TestRoute::Home);
737
738 let _ = push_cb;
739 let _ = replace_cb;
740 }
741
742 #[test]
743 fn navigation_callbacks_with_nested_route_params() {
744 #[derive(Clone, PartialEq, Debug, Routable)]
745 enum TestRoute {
746 #[at("/org/:org_id/team/:team_id/member/:member_id")]
747 Member {
748 org_id: String,
749 team_id: String,
750 member_id: String
751 }
752 }
753
754 let nav = Navigation::<TestRoute> {
755 go_back: Callback::from(|()| {}),
756 go_forward: Callback::from(|()| {}),
757 _marker: PhantomData
758 };
759
760 let route = TestRoute::Member {
761 org_id: "org1".to_string(),
762 team_id: "team1".to_string(),
763 member_id: "member1".to_string()
764 };
765 let callback = nav.push_callback(route);
766 let _ = callback;
767 }
768
769 #[test]
770 fn navigation_callbacks_with_optional_like_params() {
771 #[derive(Clone, PartialEq, Debug, Routable)]
772 enum TestRoute {
773 #[at("/filter/:min/:max")]
774 Filter { min: u32, max: u32 }
775 }
776
777 let nav = Navigation::<TestRoute> {
778 go_back: Callback::from(|()| {}),
779 go_forward: Callback::from(|()| {}),
780 _marker: PhantomData
781 };
782
783 let route = TestRoute::Filter {
784 min: 0, max: 100
785 };
786 let callback = nav.push_callback(route);
787 let _ = callback;
788 }
789
790 #[test]
791 fn navigation_go_callback_returns_valid_callback() {
792 #[derive(Clone, PartialEq, Debug, Routable)]
793 enum TestRoute {
794 #[at("/")]
795 Home
796 }
797
798 let nav = Navigation::<TestRoute> {
799 go_back: Callback::from(|()| {}),
800 go_forward: Callback::from(|()| {}),
801 _marker: PhantomData
802 };
803
804 let callback = nav.go_callback(-5);
805 let _ = callback;
806 }
807
808 #[test]
809 fn navigation_struct_fields_accessible() {
810 #[derive(Clone, PartialEq, Debug, Routable)]
811 enum TestRoute {
812 #[at("/")]
813 Home
814 }
815
816 let nav = Navigation::<TestRoute> {
817 go_back: Callback::from(|()| {}),
818 go_forward: Callback::from(|()| {}),
819 _marker: PhantomData
820 };
821
822 let _nav_back = &nav.go_back;
823 let _nav_forward = &nav.go_forward;
824 let _ = nav._marker;
825 }
826
827 #[test]
828 fn navigation_with_multiple_routable_types() {
829 #[derive(Clone, PartialEq, Debug, Routable)]
830 enum Route1 {
831 #[at("/")]
832 Home
833 }
834
835 #[derive(Clone, PartialEq, Debug, Routable)]
836 enum Route2 {
837 #[at("/")]
838 Page
839 }
840
841 let nav1 = Navigation::<Route1> {
842 go_back: Callback::from(|()| {}),
843 go_forward: Callback::from(|()| {}),
844 _marker: PhantomData
845 };
846
847 let nav2 = Navigation::<Route2> {
848 go_back: Callback::from(|()| {}),
849 go_forward: Callback::from(|()| {}),
850 _marker: PhantomData
851 };
852
853 let _ = nav1.push_callback(Route1::Home);
854 let _ = nav2.push_callback(Route2::Page);
855 }
856
857 #[test]
858 fn navigation_push_callback_multiple_times() {
859 #[derive(Clone, PartialEq, Debug, Routable)]
860 enum TestRoute {
861 #[at("/")]
862 Home
863 }
864
865 let nav = Navigation::<TestRoute> {
866 go_back: Callback::from(|()| {}),
867 go_forward: Callback::from(|()| {}),
868 _marker: PhantomData
869 };
870
871 let _ = nav.push_callback(TestRoute::Home);
872 let _ = nav.push_callback(TestRoute::Home);
873 let _ = nav.push_callback(TestRoute::Home);
874 }
875
876 #[test]
877 fn navigation_replace_callback_multiple_times() {
878 #[derive(Clone, PartialEq, Debug, Routable)]
879 enum TestRoute {
880 #[at("/")]
881 Home
882 }
883
884 let nav = Navigation::<TestRoute> {
885 go_back: Callback::from(|()| {}),
886 go_forward: Callback::from(|()| {}),
887 _marker: PhantomData
888 };
889
890 let _ = nav.replace_callback(TestRoute::Home);
891 let _ = nav.replace_callback(TestRoute::Home);
892 let _ = nav.replace_callback(TestRoute::Home);
893 }
894}