native-document 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,817 @@
1
+ # Routing
2
+
3
+ NativeDocument's routing system enables building single-page applications with client-side navigation. The router automatically manages URL changes, renders appropriate components, and maintains application state without full page reloads.
4
+
5
+ ## Understanding Routing
6
+
7
+ The routing system works by matching URL patterns against registered routes and rendering corresponding components. When users navigate, the router updates the view while preserving application state and providing smooth transitions.
8
+
9
+ ```javascript
10
+ import { Router } from 'native-document/router';
11
+
12
+ // Create router and define routes
13
+ const router = Router.create({ mode: 'history' }, (router) => {
14
+ router.add('/', HomePage);
15
+ router.add('/about', AboutPage);
16
+ router.add('/users/{id}', UserProfile);
17
+ });
18
+
19
+ // Mount to DOM
20
+ router.mount('#app');
21
+ ```
22
+
23
+ ## Router Modes
24
+
25
+ NativeDocument supports three routing modes for different deployment scenarios:
26
+
27
+ ### History Mode (Recommended)
28
+
29
+ Uses HTML5 History API for clean URLs without hash symbols:
30
+
31
+ ```javascript
32
+ const router = Router.create({ mode: 'history' }, (router) => {
33
+ router.add('/', () => Div('Home Page'));
34
+ router.add('/products', () => Div('Product List'));
35
+ router.add('/contact', () => Div('Contact Us'));
36
+ });
37
+
38
+ // URLs: /products, /contact, /users/123
39
+ ```
40
+
41
+ ### Hash Mode
42
+
43
+ Uses URL fragments for compatibility with static hosting:
44
+
45
+ ```javascript
46
+ const router = Router.create({ mode: 'hash' }, (router) => {
47
+ router.add('/', () => Div('Home Page'));
48
+ router.add('/dashboard', () => Div('Dashboard'));
49
+ });
50
+
51
+ // URLs: #/dashboard, #/users/456
52
+ ```
53
+
54
+ ### Memory Mode
55
+
56
+ Keeps routing state in memory without URL changes (useful for testing):
57
+
58
+ ```javascript
59
+ const router = Router.create({ mode: 'memory' }, (router) => {
60
+ router.add('/', () => Div('Test Home'));
61
+ router.add('/test', () => Div('Test Page'));
62
+ });
63
+
64
+ // URLs don't change, navigation happens programmatically
65
+ ```
66
+
67
+ ## Basic Route Definition
68
+
69
+ Routes map URL patterns to component functions that receive navigation data:
70
+
71
+ ```javascript
72
+ const router = Router.create({ mode: 'history' }, (router) => {
73
+ // Static routes
74
+ router.add('/', () => Div('Welcome to our site'));
75
+ router.add('/about', () => Div('About our company'));
76
+
77
+ // Routes with parameters
78
+ router.add('/users/{id}', ({ params }) =>
79
+ Div(['User Profile for ID: ', params.id])
80
+ );
81
+
82
+ // Multiple parameters
83
+ router.add('/posts/{category}/{slug}', ({ params }) =>
84
+ Div([
85
+ 'Category: ', params.category,
86
+ ', Post: ', params.slug
87
+ ])
88
+ );
89
+ });
90
+ ```
91
+
92
+ ## Route Parameters
93
+
94
+ Extract dynamic values from URLs using parameter syntax:
95
+
96
+ ### Basic Parameters
97
+
98
+ ```javascript
99
+ router.add('/users/{userId}', ({ params }) => {
100
+ const user = getUserById(params.userId);
101
+ return UserProfileComponent(user);
102
+ });
103
+
104
+ router.add('/blog/{year}/{month}', ({ params }) => {
105
+ return BlogArchive({
106
+ year: parseInt(params.year),
107
+ month: parseInt(params.month)
108
+ });
109
+ });
110
+ ```
111
+
112
+ ### Parameter Validation
113
+
114
+ Define custom patterns for parameter validation:
115
+
116
+ ```javascript
117
+ // Define global patterns
118
+ RouteParamPatterns.number = '[0-9]+';
119
+
120
+ const router = Router.create({ mode: 'history' }, (router) => {
121
+ // Only numeric IDs - inline pattern
122
+ router.add('/users/{id}', ({ params }) =>
123
+ UserProfile(params.id),
124
+ { with: { id: '[0-9]+' }}
125
+ );
126
+
127
+ // OR using global pattern
128
+ router.add('/users/{id:number}', ({ params }) =>
129
+ UserProfile(params.id)
130
+ );
131
+
132
+ // Custom patterns - inline
133
+ router.add('/posts/{slug}', ({ params }) =>
134
+ BlogPost(params.slug),
135
+ { with: { slug: '[a-z0-9-]+' }}
136
+ );
137
+
138
+ // OR define and use pattern
139
+ RouteParamPatterns.slug = '[a-z0-9-]+';
140
+ router.add('/posts/{slug:slug}', ({ params }) =>
141
+ BlogPost(params.slug)
142
+ );
143
+ });
144
+ ```
145
+
146
+ ## Query Parameters
147
+
148
+ Access URL query strings through the query object:
149
+
150
+ ```javascript
151
+ router.add('/search', ({ query }) => {
152
+ const { term, category, page = 1 } = query;
153
+
154
+ return SearchResults({
155
+ searchTerm: term,
156
+ category: category,
157
+ currentPage: parseInt(page)
158
+ });
159
+ });
160
+
161
+ // URL: /search?term=javascript&category=tutorials&page=2
162
+ // query = { term: 'javascript', category: 'tutorials', page: '2' }
163
+ ```
164
+
165
+ ## Navigation
166
+
167
+ Navigate programmatically using router methods:
168
+
169
+ ### Push Navigation
170
+
171
+ Add new entries to browser history. **Specify router name** when using multiple routers:
172
+
173
+ ```javascript
174
+ const NavigationExample = Div([
175
+ Button('Go to About').nd.on.click(() => {
176
+ Router.push('/about'); // Uses default router
177
+ }),
178
+
179
+ Button('Go to About (Main Router)').nd.on.click(() => {
180
+ Router.push('/about', 'main'); // Uses named router
181
+ }),
182
+
183
+ Button('View User 123').nd.on.click(() => {
184
+ // Navigate in specific router
185
+ Router.push('/users/123', 'app');
186
+ }),
187
+
188
+ Button('Search Products').nd.on.click(() => {
189
+ Router.push('/search?term=laptop&category=electronics', 'main');
190
+ })
191
+ ]);
192
+ ```
193
+
194
+ ### Replace Navigation
195
+
196
+ Replace current history entry without adding to stack:
197
+
198
+ ```javascript
199
+ const LoginRedirect = () => {
200
+ // Redirect after login without allowing back navigation
201
+ Router.replace('/dashboard'); // Default router
202
+
203
+ // Or specify router
204
+ Router.replace('/dashboard', 'main');
205
+
206
+ return Div('Redirecting...');
207
+ };
208
+ ```
209
+
210
+ ### History Navigation
211
+
212
+ Navigate through browser history:
213
+
214
+ ```javascript
215
+ const HistoryControls = Div([
216
+ Button('Go Back').nd.on.click(() => Router.back()), // Default router
217
+ Button('Go Forward').nd.on.click(() => Router.forward()), // Default router
218
+
219
+ // Navigate specific router's history
220
+ Button('Back in Main').nd.on.click(() => Router.back('main')),
221
+ Button('Forward in Admin').nd.on.click(() => Router.forward('admin'))
222
+ ]);
223
+ ```
224
+
225
+ ## Named Routes
226
+
227
+ Name routes for easier URL generation and maintenance:
228
+
229
+ ```javascript
230
+ const router = Router.create({ mode: 'history' }, (router) => {
231
+ router.add('/', HomePage, { name: 'home' });
232
+ router.add('/users/{id}', UserProfile, { name: 'user.profile' });
233
+ router.add('/posts/{category}/{slug}', BlogPost, { name: 'blog.post' });
234
+ });
235
+
236
+ // Generate URLs by name
237
+ const userUrl = router.generateUrl('user.profile', { id: 123 });
238
+ // Result: '/users/123'
239
+
240
+ const blogUrl = router.generateUrl('blog.post', { category: 'javascript', slug: 'getting-started' }, { ref: 'newsletter' });
241
+ // Result: '/posts/javascript/getting-started?ref=newsletter'
242
+ ```
243
+
244
+ ### Navigation with Named Routes
245
+
246
+ ```javascript
247
+ const navigation = Div([
248
+ Button('Home').nd.on.click(() =>
249
+ Router.push({ name: 'home' }) // Uses router containing this route
250
+ ),
251
+
252
+ Button('My Profile').nd.on.click(() =>
253
+ Router.push({
254
+ name: 'user.profile',
255
+ params: { id: currentUser.id }
256
+ }, 'main') // Specify router if needed
257
+ ),
258
+
259
+ Button('Latest Post').nd.on.click(() =>
260
+ Router.push({
261
+ name: 'blog.post',
262
+ params: { category: 'news', slug: 'latest-update' },
263
+ query: { highlight: 'new-features' }
264
+ }, 'blog') // Navigate in blog router
265
+ )
266
+ ]);
267
+ ```
268
+
269
+ ## Route Groups
270
+
271
+ Organize related routes with shared configuration:
272
+
273
+ ```javascript
274
+ const router = Router.create({ mode: 'history' }, (router) => {
275
+ // Admin routes with auth middleware
276
+ router.group('/admin', { middlewares: [requireAuth, requireAdmin] }, () => {
277
+ router.add('/', AdminDashboard, { name: 'admin.dashboard' });
278
+ router.add('/users', AdminUsers, { name: 'admin.users' });
279
+ router.add('/settings', AdminSettings, { name: 'admin.settings' });
280
+ });
281
+
282
+ // User dashboard with nested naming
283
+ router.group('/dashboard', { name: 'dashboard' }, () => {
284
+ router.add('/', UserDashboard, { name: 'home' }); // dashboard.home
285
+ router.add('/profile', UserProfile, { name: 'profile' }); // dashboard.profile
286
+ router.add('/settings', UserSettings, { name: 'settings' }); // dashboard.settings
287
+ });
288
+ });
289
+ ```
290
+
291
+ ## Middleware
292
+
293
+ Add logic that runs before route components are rendered:
294
+
295
+ ### Authentication Middleware
296
+
297
+ ```javascript
298
+ const requireAuth = (context, next) => {
299
+ const { route, params, query, path } = context;
300
+
301
+ if (!isUserAuthenticated()) {
302
+ // Redirect to login with return path
303
+ Router.replace({
304
+ name: 'auth.login',
305
+ query: { redirect: path }
306
+ });
307
+ return;
308
+ }
309
+
310
+ // Continue to route component
311
+ next();
312
+ };
313
+
314
+ const requireAdmin = (context, next) => {
315
+ if (!isUserAdmin()) {
316
+ Router.replace({ name: 'errors.forbidden' });
317
+ return;
318
+ }
319
+ next();
320
+ };
321
+ ```
322
+
323
+ ### Loading States
324
+
325
+ ```javascript
326
+ const loadingMiddleware = (context, next) => {
327
+ // Show loading indicator
328
+ showGlobalLoader();
329
+
330
+ // Continue to route component
331
+ next();
332
+
333
+ // Hide loading indicator
334
+ setTimeout(() => hideGlobalLoader(), 100);
335
+ };
336
+ ```
337
+
338
+ ### Analytics Tracking
339
+
340
+ ```javascript
341
+ const analyticsMiddleware = (context, next) => {
342
+ const { route, params, path } = context;
343
+
344
+ // Track page view
345
+ analytics.track('page_view', {
346
+ path: path,
347
+ route_name: route.name(),
348
+ timestamp: Date.now()
349
+ });
350
+
351
+ next();
352
+ };
353
+ ```
354
+
355
+ ## Link Component
356
+
357
+ Create navigational links that integrate with the router:
358
+
359
+ ### Basic Links
360
+
361
+ ```javascript
362
+ import { Link } from 'native-document/router';
363
+
364
+ const Navigation = Nav([
365
+ Link({ to: '/' }, 'Home'),
366
+ Link({ to: '/about' }, 'About'),
367
+ Link({ to: '/contact' }, 'Contact'),
368
+
369
+ // External links open in new tab
370
+ Link.blank({ href: 'https://example.com' }, 'External Site')
371
+ ]);
372
+ ```
373
+
374
+ ### Links with Router Specification
375
+
376
+ ```javascript
377
+ const UserMenu = Div([
378
+ Link({
379
+ to: {
380
+ name: 'user.profile',
381
+ params: { id: currentUser.id },
382
+ router: 'main' // Specify which router to use
383
+ }
384
+ }, 'My Profile'),
385
+
386
+ Link({
387
+ to: {
388
+ name: 'user.settings',
389
+ params: { id: currentUser.id },
390
+ query: { tab: 'preferences' },
391
+ router: 'main'
392
+ }
393
+ }, 'Settings'),
394
+
395
+ // Cross-router navigation
396
+ Link({
397
+ to: {
398
+ name: 'admin.dashboard',
399
+ router: 'admin'
400
+ }
401
+ }, 'Admin Panel')
402
+ ]);
403
+ ```
404
+
405
+ ### Active Link Styling
406
+
407
+ ```javascript
408
+ const NavItem = (path, text, routerName = null) => {
409
+ const router = Router.get(routerName);
410
+ const isActive = Observable(router.currentState().path === path);
411
+ router.subscribe((state) => {
412
+ isActive.set(state.path === path);
413
+ });
414
+
415
+ return Link({
416
+ to: path,
417
+ class: { 'nav-link': true, 'active': isActive }
418
+ }, text);
419
+ };
420
+
421
+ const MainNav = Nav([
422
+ NavItem('/', 'Home', 'main'),
423
+ NavItem('/products', 'Products', 'main'),
424
+ NavItem('/services', 'Services', 'main')
425
+ ]);
426
+
427
+ // Multi-router navigation
428
+ const AppNav = Nav([
429
+ NavItem('/', 'Main App', 'main'),
430
+ NavItem('/admin', 'Admin', 'admin'),
431
+ NavItem('/blog', 'Blog', 'blog')
432
+ ]);
433
+
434
+ ```
435
+
436
+ ## Multiple Routers
437
+
438
+ Create separate router instances for different application areas. **Always name your routers** to avoid conflicts and enable proper navigation.
439
+
440
+ ```javascript
441
+ // Main application router (named)
442
+ const mainRouter = Router.create(
443
+ { mode: 'history', name: 'main' },
444
+ (router) => {
445
+ router.add('/', HomePage);
446
+ router.add('/products', ProductList);
447
+ router.add('/admin', AdminApp);
448
+ }
449
+ );
450
+
451
+ // Admin-specific router (named)
452
+ const adminRouter = Router.create(
453
+ { mode: 'history', name: 'admin', entry: '/admin' },
454
+ (router) => {
455
+ router.add('/', AdminDashboard);
456
+ router.add('/users', AdminUsers);
457
+ router.add('/reports', AdminReports);
458
+ }
459
+ );
460
+
461
+ // Access routers by name
462
+ const mainRouter = Router.get('main');
463
+ const adminRouter = Router.get('admin');
464
+
465
+ // Or access via routers object
466
+ const mainRouter = Router.routers.main;
467
+ const adminRouter = Router.routers.admin;
468
+
469
+ // Cross-router navigation
470
+ Button('Go to Admin').nd.on.click(() => {
471
+ Router.push('/admin/users', 'admin'); // Specify router name
472
+ });
473
+
474
+ Button('Back to Main').nd.on.click(() => {
475
+ Router.push('/', 'main'); // Navigate in main router
476
+ });
477
+ ```
478
+
479
+ ### Router Naming Best Practices
480
+
481
+ **Always name your routers** to prevent conflicts and enable reliable navigation:
482
+
483
+ ```javascript
484
+ // ❌ BAD: Unnamed router (becomes default)
485
+ const router1 = Router.create({ mode: 'history' }, (router) => {
486
+ // This becomes the default router
487
+ });
488
+
489
+ // ❌ BAD: Multiple unnamed routers cause conflicts
490
+ const router2 = Router.create({ mode: 'history' }, (router) => {
491
+ // This overwrites the default router!
492
+ });
493
+
494
+ // ✅ GOOD: Named routers
495
+ const appRouter = Router.create({ mode: 'history', name: 'app' }, (router) => {
496
+ router.add('/', HomePage);
497
+ });
498
+
499
+ const modalRouter = Router.create({ mode: 'memory', name: 'modal' }, (router) => {
500
+ router.add('/confirm', ConfirmDialog);
501
+ });
502
+ ```
503
+
504
+ ### Default Router Access
505
+
506
+ When no name is specified, Router.get() returns the router named "default":
507
+
508
+ ```javascript
509
+ // Create default router
510
+ Router.create({ mode: 'history' }, (router) => {
511
+ router.add('/', HomePage);
512
+ });
513
+
514
+ // Access default router (named "default" internally)
515
+ const defaultRouter = Router.get(); // Returns router named "default"
516
+ const defaultRouter = Router.get('default'); // Same as above
517
+ const defaultRouter = Router.routers.default; // Direct access
518
+
519
+ // Navigate using default router
520
+ Router.push('/'); // Uses "default" router
521
+ Router.push('/', 'default'); // Explicitly use default router
522
+ ```
523
+
524
+ ### Router Naming Best Practices
525
+
526
+ **Always name your routers explicitly** to avoid relying on the default:
527
+
528
+ ```javascript
529
+ // ❌ BAD: Unnamed router (becomes "default")
530
+ const router1 = Router.create({ mode: 'history' }, (router) => {
531
+ // This is stored as Router.routers.default
532
+ });
533
+
534
+ // ❌ BAD: Multiple unnamed routers cause conflicts
535
+ const router2 = Router.create({ mode: 'history' }, (router) => {
536
+ // This overwrites Router.routers.default!
537
+ });
538
+
539
+ // ✅ GOOD: Named routers
540
+ const appRouter = Router.create({ mode: 'history', name: 'app' }, (router) => {
541
+ router.add('/', HomePage);
542
+ // Stored as Router.routers.app
543
+ });
544
+
545
+ const modalRouter = Router.create({ mode: 'memory', name: 'modal' }, (router) => {
546
+ router.add('/confirm', ConfirmDialog);
547
+ // Stored as Router.routers.modal
548
+ });
549
+ ```
550
+
551
+ ## Route Guards and Data Loading
552
+
553
+ Implement route guards and data preloading:
554
+
555
+ ### Route-Specific Data Loading
556
+
557
+ ```javascript
558
+ const UserProfile = ({ params }) => {
559
+ const userId = params.id;
560
+ const user = Observable(null);
561
+ const loading = Observable(true);
562
+ const error = Observable(null);
563
+
564
+ // Load user data
565
+ fetchUser(userId)
566
+ .then(userData => {
567
+ user.set(userData);
568
+ loading.set(false);
569
+ })
570
+ .catch(err => {
571
+ error.set(err.message);
572
+ loading.set(false);
573
+ });
574
+
575
+ return Match(loading, {
576
+ true: LoadingSpinner,
577
+ false: () => Switch(error, ErrorMessage(error), UserProfileView(user) )
578
+ });
579
+ };
580
+
581
+ router.add('/users/{id}', UserProfile, { name: 'user.profile' });
582
+ ```
583
+
584
+ ### Global Route Guards
585
+
586
+ ```javascript
587
+ const authGuard = (context, next) => {
588
+ const { route } = context;
589
+ const protectedRoutes = ['dashboard', 'profile', 'settings'];
590
+
591
+ if (protectedRoutes.includes(route.name()) && !isAuthenticated()) {
592
+ Router.replace({
593
+ name: 'login',
594
+ query: { redirect: context.path }
595
+ });
596
+ return;
597
+ }
598
+
599
+ next();
600
+ };
601
+
602
+ // Add to all routes that need protection
603
+ router.group('/', { middlewares: [authGuard] }, () => {
604
+ router.add('/dashboard', Dashboard, { name: 'dashboard' });
605
+ router.add('/profile', Profile, { name: 'profile' });
606
+ router.add('/settings', Settings, { name: 'settings' });
607
+ });
608
+ ```
609
+
610
+ ## Error Handling
611
+
612
+ Handle routing errors gracefully:
613
+
614
+ ### 404 - Route Not Found
615
+
616
+ ```javascript
617
+ const router = Router.create({ mode: 'history' }, (router) => {
618
+ router.add('/', HomePage);
619
+ router.add('/about', AboutPage);
620
+
621
+ // Catch-all route for 404s (must be last)
622
+ router.add('.*', ({ params }) =>
623
+ NotFoundPage({ attemptedPath: params.path })
624
+ );
625
+ });
626
+
627
+ const NotFoundPage = ({ attemptedPath }) => Div([
628
+ H1('Page Not Found'),
629
+ P(['The page "', attemptedPath, '" could not be found.']),
630
+ Link({ to: '/' }, 'Return Home')
631
+ ]);
632
+ ```
633
+
634
+ ### Error Boundaries
635
+
636
+ ```javascript
637
+ const errorHandler = (context, next) => {
638
+ try {
639
+ next();
640
+ } catch (error) {
641
+ console.error('Route error:', error);
642
+
643
+ // Navigate to error page
644
+ Router.replace({
645
+ name: 'error',
646
+ query: { message: error.message }
647
+ });
648
+ }
649
+ };
650
+
651
+ router.add('/error', ({ query }) =>
652
+ ErrorPage({ message: query.message })
653
+ );
654
+ ```
655
+
656
+ ## Real-World Examples
657
+
658
+ ### E-commerce Application
659
+
660
+ ```javascript
661
+ const ecommerceRouter = Router.create({ mode: 'history' }, (router) => {
662
+ // Public routes
663
+ router.add('/', HomePage, { name: 'home' });
664
+ router.add('/products', ProductCatalog, { name: 'products' });
665
+ router.add('/products/{category}', CategoryPage, { name: 'category' });
666
+ router.add('/products/{category}/{id}', ProductDetail, { name: 'product' });
667
+
668
+ // User account routes
669
+ router.group('/account', { middlewares: [requireAuth] }, () => {
670
+ router.add('/', AccountDashboard, { name: 'account.dashboard' });
671
+ router.add('/orders', OrderHistory, { name: 'account.orders' });
672
+ router.add('/profile', UserProfile, { name: 'account.profile' });
673
+ router.add('/addresses', AddressBook, { name: 'account.addresses' });
674
+ });
675
+
676
+ // Shopping cart
677
+ router.add('/cart', ShoppingCart, { name: 'cart' });
678
+ router.add('/checkout', CheckoutProcess, { name: 'checkout', middlewares: [requireAuth] });
679
+
680
+ // Search and filters
681
+ router.add('/search', SearchResults, { name: 'search' });
682
+ // URL: /search?q=laptop&category=electronics&price_min=100&price_max=1000
683
+ });
684
+
685
+ // Product component with category navigation
686
+ const ProductDetail = ({ params, query }) => {
687
+ const { category, id } = params;
688
+ const product = Observable(null);
689
+
690
+ loadProduct(id).then(data => product.set(data));
691
+
692
+ return ShowIf(product, () => {
693
+ const p = product.val();
694
+ return Div([
695
+ // Breadcrumb navigation
696
+ Nav([
697
+ Link({ to: { name: 'home' } }, 'Home'),
698
+ ' > ',
699
+ Link({ to: { name: 'category', params: { category } } }, category),
700
+ ' > ',
701
+ Span(p.name)
702
+ ]),
703
+
704
+ // Product details
705
+ H1(p.name),
706
+ Div({ class: 'price' }, p.price),
707
+ P(p.description),
708
+
709
+ // Add to cart with redirect to login if needed
710
+ Button('Add to Cart').nd.on.click(() => {
711
+ if (!isAuthenticated()) {
712
+ Router.push({
713
+ name: 'login',
714
+ query: {
715
+ redirect: Router.get().currentState().path
716
+ }
717
+ });
718
+ } else {
719
+ addToCart(p.id);
720
+ Router.push({ name: 'cart' });
721
+ }
722
+ })
723
+ ]);
724
+ });
725
+ };
726
+ ```
727
+
728
+ ### Multi-Step Form with Navigation
729
+
730
+ ```javascript
731
+ const formRouter = Router.create({ mode: 'hash' }, (router) => {
732
+ const formData = Observable.object({
733
+ step1: { name: '', email: '' },
734
+ step2: { address: '', city: '' },
735
+ step3: { payment: '', terms: false }
736
+ });
737
+
738
+ router.add('/form/personal', () =>
739
+ PersonalInfoStep(formData),
740
+ { name: 'form.personal' }
741
+ );
742
+
743
+ router.add('/form/address', () =>
744
+ AddressStep(formData),
745
+ { name: 'form.address', middlewares: [validateStep1] }
746
+ );
747
+
748
+ router.add('/form/payment', () =>
749
+ PaymentStep(formData),
750
+ { name: 'form.payment', middlewares: [validateStep1, validateStep2] }
751
+ );
752
+
753
+ router.add('/form/review', () =>
754
+ ReviewStep(formData),
755
+ { name: 'form.review', middlewares: [validateAllSteps] }
756
+ );
757
+ });
758
+
759
+ const FormNavigation = (currentStep, formData) => {
760
+ const steps = ['personal', 'address', 'payment', 'review'];
761
+ const currentIndex = steps.indexOf(currentStep);
762
+
763
+ return Div({ class: 'form-navigation' }, [
764
+ // Step indicator
765
+ Div({ class: 'step-indicator' },
766
+ steps.map((step, index) =>
767
+ Span({
768
+ class: index <= currentIndex ? 'step active' : 'step'
769
+ }, step)
770
+ )
771
+ ),
772
+
773
+ // Navigation buttons
774
+ Div({ class: 'nav-buttons' }, [
775
+ ShowIf(Observable(currentIndex > 0),
776
+ Button('Previous').nd.on.click(() => {
777
+ const prevStep = steps[currentIndex - 1];
778
+ Router.push({ name: `form.${prevStep}` });
779
+ })
780
+ ),
781
+
782
+ ShowIf(Observable(currentIndex < steps.length - 1),
783
+ Button('Next').nd.on.click(() => {
784
+ const nextStep = steps[currentIndex + 1];
785
+ Router.push({ name: `form.${nextStep}` });
786
+ })
787
+ ),
788
+
789
+ ShowIf(Observable(currentIndex === steps.length - 1),
790
+ Button('Submit').nd.on.click(() => {
791
+ submitForm(formData.$val());
792
+ })
793
+ )
794
+ ])
795
+ ]);
796
+ };
797
+ ```
798
+
799
+ ## Best Practices
800
+
801
+ 1. **Use named routes** for maintainable URL generation
802
+ 2. **Group related routes** with shared middleware
803
+ 3. **Validate parameters** to prevent runtime errors
804
+ 4. **Handle loading states** in data-dependent routes
805
+ 5. **Implement proper error boundaries** for robust navigation
806
+ 6. **Use middleware** for cross-cutting concerns like authentication
807
+ 7. **Keep route components focused** - extract complex logic to separate modules
808
+ 8. **Test navigation flows** with different router modes
809
+
810
+ ## Next Steps
811
+
812
+ Explore these related topics to build complete applications:
813
+
814
+ - **[State Management](docs/state-management.md)** - Global state patterns
815
+ - **[Lifecycle Events](docs/lifecycle-events.md)** - Lifecycle events
816
+ - **[Memory Management](docs/memory-management.md)** - Memory management
817
+ - **[Anchor](docs/anchor.md)** - Anchor