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.
- package/dist/native-document.dev.js +2671 -0
- package/dist/native-document.min.js +1 -0
- package/docs/anchor.md +208 -0
- package/docs/conditional-rendering.md +628 -0
- package/docs/contributing.md +51 -0
- package/docs/core-concepts.md +513 -0
- package/docs/elements.md +383 -0
- package/docs/getting-started.md +403 -0
- package/docs/lifecycle-events.md +106 -0
- package/docs/memory-management.md +90 -0
- package/docs/observables.md +265 -0
- package/docs/routing.md +817 -0
- package/docs/state-management.md +423 -0
- package/docs/validation.md +193 -0
- package/elements.js +3 -1
- package/index.js +2 -0
- package/package.json +1 -1
- package/readme.md +189 -425
- package/router.js +2 -0
- package/src/data/MemoryManager.js +15 -5
- package/src/data/Observable.js +35 -2
- package/src/data/ObservableChecker.js +3 -0
- package/src/data/ObservableItem.js +4 -0
- package/src/data/Store.js +6 -6
- package/src/router/Router.js +13 -13
- package/src/router/link.js +8 -5
- package/src/utils/plugins-manager.js +12 -0
- package/src/utils/prototypes.js +13 -0
- package/src/utils/validator.js +23 -1
- package/src/wrappers/AttributesWrapper.js +11 -1
- package/src/wrappers/HtmlElementWrapper.js +10 -1
package/docs/routing.md
ADDED
|
@@ -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
|