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