@real-router/helpers 0.1.1 → 0.1.3
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/README.md +82 -434
- package/dist/cjs/index.d.ts +234 -13
- package/dist/esm/index.d.mts +234 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
# @real-router/helpers
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@real-router/helpers)
|
|
4
3
|
[](https://opensource.org/licenses/MIT)
|
|
5
4
|
[](https://www.typescriptlang.org/)
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## Overview
|
|
10
|
-
|
|
11
|
-
`@real-router/helpers` provides efficient utilities for testing route name segments in Real-Router applications. These helpers are essential for:
|
|
12
|
-
|
|
13
|
-
- Conditional rendering based on route hierarchy
|
|
14
|
-
- Active navigation menu items
|
|
15
|
-
- Breadcrumb construction
|
|
16
|
-
- Route-based access guards
|
|
17
|
-
- UI component visibility logic
|
|
6
|
+
Route segment testing utilities for Real-Router. Useful for navigation menus, breadcrumbs, conditional rendering, and route guards.
|
|
18
7
|
|
|
19
8
|
## Installation
|
|
20
9
|
|
|
@@ -38,247 +27,113 @@ import {
|
|
|
38
27
|
areRoutesRelated,
|
|
39
28
|
} from "@real-router/helpers";
|
|
40
29
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Check if route ends with segment
|
|
46
|
-
endsWithSegment("users.profile.edit", "edit"); // true
|
|
47
|
-
endsWithSegment("users.profile.edit", "view"); // false
|
|
48
|
-
|
|
49
|
-
// Check if route includes segment anywhere
|
|
50
|
-
includesSegment("users.profile.edit", "profile"); // true
|
|
51
|
-
includesSegment("users.profile.edit", "admin"); // false
|
|
52
|
-
|
|
53
|
-
// Check if routes are related (parent-child or same)
|
|
54
|
-
areRoutesRelated("users", "users.list"); // true
|
|
55
|
-
areRoutesRelated("users", "admin"); // false
|
|
30
|
+
startsWithSegment("users.profile.edit", "users"); // true
|
|
31
|
+
endsWithSegment("users.profile.edit", "edit"); // true
|
|
32
|
+
includesSegment("users.profile.edit", "profile"); // true
|
|
33
|
+
areRoutesRelated("users", "users.profile"); // true
|
|
56
34
|
```
|
|
57
35
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
### `startsWithSegment(route, segment?)`
|
|
61
|
-
|
|
62
|
-
Tests if a route name starts with the given segment.
|
|
63
|
-
|
|
64
|
-
**Parameters:**
|
|
65
|
-
|
|
66
|
-
- `route: State | string` - Route state object or route name string
|
|
67
|
-
- `segment?: string | null` - Segment to test (optional for curried form)
|
|
36
|
+
---
|
|
68
37
|
|
|
69
|
-
|
|
38
|
+
## API
|
|
70
39
|
|
|
71
|
-
|
|
72
|
-
- `(segment: string) => boolean` - Tester function if segment omitted
|
|
73
|
-
- `false` - If segment is null or empty string
|
|
40
|
+
### `startsWithSegment(route: State | string, segment?: string | null): boolean | ((segment: string) => boolean)`
|
|
74
41
|
|
|
75
|
-
|
|
42
|
+
Tests if route name starts with segment.\
|
|
43
|
+
`route: State | string` — route state object or route name string\
|
|
44
|
+
`segment?: string | null` — segment to test (optional for curried form)\
|
|
45
|
+
Returns: `boolean` (true if starts with segment, false if segment is null/empty) or `(segment: string) => boolean` (tester function if segment omitted)
|
|
76
46
|
|
|
77
47
|
```typescript
|
|
78
48
|
// Direct usage
|
|
79
|
-
startsWithSegment("users.list", "users");
|
|
80
|
-
startsWithSegment("users.
|
|
81
|
-
|
|
82
|
-
// Multi-segment matching
|
|
83
|
-
startsWithSegment("users.profile.edit", "users.profile"); // true
|
|
49
|
+
startsWithSegment("users.list", "users"); // true
|
|
50
|
+
startsWithSegment("users.profile", "users.profile"); // true (multi-segment)
|
|
84
51
|
|
|
85
52
|
// With State object
|
|
86
|
-
|
|
87
|
-
startsWithSegment(state, "users"); // true
|
|
53
|
+
startsWithSegment({ name: "users.list", params: {}, path: "/" }, "users"); // true
|
|
88
54
|
|
|
89
55
|
// Curried form
|
|
90
56
|
const tester = startsWithSegment("users.profile.edit");
|
|
91
|
-
tester("users");
|
|
92
|
-
tester("admin");
|
|
93
|
-
|
|
94
|
-
// Edge cases
|
|
95
|
-
startsWithSegment("users", ""); // false
|
|
96
|
-
startsWithSegment("users", null); // false
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
**Use Cases:**
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
// Active navigation menu
|
|
103
|
-
const isAdminSection = startsWithSegment(currentRoute, 'admin');
|
|
104
|
-
|
|
105
|
-
// Conditional rendering
|
|
106
|
-
{startsWithSegment(route, 'users') && <UsersToolbar />}
|
|
107
|
-
|
|
108
|
-
// Access guards
|
|
109
|
-
if (!startsWithSegment(route, 'admin')) {
|
|
110
|
-
throw new UnauthorizedError();
|
|
111
|
-
}
|
|
57
|
+
tester("users"); // true
|
|
58
|
+
tester("admin"); // false
|
|
112
59
|
```
|
|
113
60
|
|
|
114
|
-
|
|
61
|
+
### `endsWithSegment(route: State | string, segment?: string | null): boolean | ((segment: string) => boolean)`
|
|
115
62
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
**Parameters:**
|
|
121
|
-
|
|
122
|
-
- `route: State | string` - Route state object or route name string
|
|
123
|
-
- `segment?: string | null` - Segment to test (optional for curried form)
|
|
124
|
-
|
|
125
|
-
**Returns:**
|
|
126
|
-
|
|
127
|
-
- `boolean` - True if route ends with segment
|
|
128
|
-
- `(segment: string) => boolean` - Tester function if segment omitted
|
|
129
|
-
- `false` - If segment is null or empty string
|
|
130
|
-
|
|
131
|
-
**Examples:**
|
|
63
|
+
Tests if route name ends with segment.\
|
|
64
|
+
`route: State | string` — route state object or route name string\
|
|
65
|
+
`segment?: string | null` — segment to test (optional for curried form)\
|
|
66
|
+
Returns: `boolean` (true if ends with segment, false if segment is null/empty) or `(segment: string) => boolean` (tester function if segment omitted)
|
|
132
67
|
|
|
133
68
|
```typescript
|
|
134
|
-
//
|
|
135
|
-
endsWithSegment("
|
|
136
|
-
endsWithSegment("users.profile.edit", "view"); // false
|
|
137
|
-
|
|
138
|
-
// Multi-segment matching
|
|
139
|
-
endsWithSegment("a.b.c.d", "c.d"); // true
|
|
69
|
+
endsWithSegment("users.profile.edit", "edit"); // true
|
|
70
|
+
endsWithSegment("a.b.c.d", "c.d"); // true (multi-segment)
|
|
140
71
|
|
|
141
72
|
// Curried form
|
|
142
73
|
const tester = endsWithSegment("users.list");
|
|
143
|
-
tester("list");
|
|
144
|
-
tester("edit"); // false
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
**Use Cases:**
|
|
148
|
-
|
|
149
|
-
```typescript
|
|
150
|
-
// Detect edit pages
|
|
151
|
-
const isEditPage = endsWithSegment(route, 'edit');
|
|
152
|
-
|
|
153
|
-
// Page type detection
|
|
154
|
-
const pageType = ['view', 'edit', 'create'].find(
|
|
155
|
-
type => endsWithSegment(route, type)
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
// Conditional toolbar
|
|
159
|
-
{endsWithSegment(route, 'edit') && <EditToolbar />}
|
|
74
|
+
tester("list"); // true
|
|
160
75
|
```
|
|
161
76
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
### `includesSegment(route, segment?)`
|
|
165
|
-
|
|
166
|
-
Tests if a route name includes the given segment anywhere in its path.
|
|
167
|
-
|
|
168
|
-
**Parameters:**
|
|
169
|
-
|
|
170
|
-
- `route: State | string` - Route state object or route name string
|
|
171
|
-
- `segment?: string | null` - Segment to test (optional for curried form)
|
|
172
|
-
|
|
173
|
-
**Returns:**
|
|
77
|
+
### `includesSegment(route: State | string, segment?: string | null): boolean | ((segment: string) => boolean)`
|
|
174
78
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
**Examples:**
|
|
79
|
+
Tests if route name includes segment anywhere.\
|
|
80
|
+
`route: State | string` — route state object or route name string\
|
|
81
|
+
`segment?: string | null` — segment to test (optional for curried form)\
|
|
82
|
+
Returns: `boolean` (true if includes segment, false if segment is null/empty) or `(segment: string) => boolean` (tester function if segment omitted)
|
|
180
83
|
|
|
181
84
|
```typescript
|
|
182
|
-
//
|
|
183
|
-
includesSegment("
|
|
184
|
-
includesSegment("
|
|
185
|
-
includesSegment("admin.users.profile", "settings"); // false
|
|
186
|
-
|
|
187
|
-
// Multi-segment matching (contiguous)
|
|
188
|
-
includesSegment("a.b.c.d", "b.c"); // true
|
|
189
|
-
includesSegment("a.b.c.d", "a.c"); // false (not contiguous)
|
|
85
|
+
includesSegment("admin.users.profile", "users"); // true
|
|
86
|
+
includesSegment("a.b.c.d", "b.c"); // true (contiguous)
|
|
87
|
+
includesSegment("a.b.c.d", "a.c"); // false (not contiguous)
|
|
190
88
|
|
|
191
89
|
// Curried form
|
|
192
90
|
const tester = includesSegment("admin.users.profile");
|
|
193
|
-
tester("users");
|
|
194
|
-
tester("settings");
|
|
91
|
+
tester("users"); // true
|
|
92
|
+
tester("settings"); // false
|
|
195
93
|
```
|
|
196
94
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
// Check if anywhere in users section
|
|
201
|
-
const inUsersSection = includesSegment(route, "users");
|
|
202
|
-
|
|
203
|
-
// Feature detection
|
|
204
|
-
const hasProfileFeature = includesSegment(route, "profile");
|
|
205
|
-
|
|
206
|
-
// Analytics tracking
|
|
207
|
-
if (includesSegment(route, "checkout")) {
|
|
208
|
-
analytics.track("checkout_flow");
|
|
209
|
-
}
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
---
|
|
95
|
+
### `areRoutesRelated(route1: string, route2: string): boolean`
|
|
213
96
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
**Parameters:**
|
|
219
|
-
|
|
220
|
-
- `route1: string` - First route name
|
|
221
|
-
- `route2: string` - Second route name
|
|
222
|
-
|
|
223
|
-
**Returns:**
|
|
224
|
-
|
|
225
|
-
- `boolean` - True if routes are related
|
|
226
|
-
|
|
227
|
-
**Examples:**
|
|
97
|
+
Tests if routes are in same hierarchy (parent-child, child-parent, or same).\
|
|
98
|
+
`route1: string` — first route name\
|
|
99
|
+
`route2: string` — second route name\
|
|
100
|
+
Returns: `boolean` — true if routes are related (same, parent-child, or child-parent)
|
|
228
101
|
|
|
229
102
|
```typescript
|
|
230
|
-
// Same route
|
|
231
|
-
areRoutesRelated("users", "users"); // true
|
|
232
|
-
|
|
233
103
|
// Parent-child relationship
|
|
234
|
-
areRoutesRelated("users", "users.list");
|
|
104
|
+
areRoutesRelated("users", "users.list"); // true
|
|
235
105
|
areRoutesRelated("users", "users.profile.edit"); // true
|
|
236
106
|
|
|
237
107
|
// Child-parent relationship
|
|
238
|
-
areRoutesRelated("users.list", "users");
|
|
108
|
+
areRoutesRelated("users.list", "users"); // true
|
|
239
109
|
areRoutesRelated("users.profile.edit", "users"); // true
|
|
240
110
|
|
|
241
|
-
//
|
|
242
|
-
areRoutesRelated("users", "
|
|
243
|
-
areRoutesRelated("users.list", "admin.dashboard"); // false
|
|
111
|
+
// Same route
|
|
112
|
+
areRoutesRelated("users", "users"); // true
|
|
244
113
|
|
|
245
114
|
// Siblings (not related)
|
|
246
|
-
areRoutesRelated("users.list", "users.view");
|
|
247
|
-
areRoutesRelated("users.profile", "users.settings"); // false
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
**Use Cases:**
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
// Optimized re-rendering (only update when route is related)
|
|
254
|
-
const shouldUpdate = areRoutesRelated(newRoute, watchedRoute);
|
|
255
|
-
|
|
256
|
-
// Navigation menu highlighting
|
|
257
|
-
const isInSection = areRoutesRelated(currentRoute, "admin");
|
|
115
|
+
areRoutesRelated("users.list", "users.view"); // false
|
|
258
116
|
|
|
259
|
-
//
|
|
260
|
-
|
|
117
|
+
// Different branches (not related)
|
|
118
|
+
areRoutesRelated("users", "admin"); // false
|
|
261
119
|
```
|
|
262
120
|
|
|
263
121
|
---
|
|
264
122
|
|
|
265
|
-
##
|
|
266
|
-
|
|
267
|
-
### Active Navigation Menu
|
|
123
|
+
## Usage Examples
|
|
268
124
|
|
|
269
|
-
|
|
270
|
-
import { startsWithSegment } from '@real-router/helpers';
|
|
125
|
+
### Navigation Menu
|
|
271
126
|
|
|
127
|
+
```tsx
|
|
272
128
|
function NavigationMenu({ currentRoute }) {
|
|
273
|
-
const
|
|
274
|
-
{ name:
|
|
275
|
-
{ name:
|
|
276
|
-
{ name: 'Settings', route: 'settings' },
|
|
129
|
+
const items = [
|
|
130
|
+
{ name: "Dashboard", route: "dashboard" },
|
|
131
|
+
{ name: "Users", route: "users" },
|
|
277
132
|
];
|
|
278
133
|
|
|
279
134
|
return (
|
|
280
135
|
<nav>
|
|
281
|
-
{
|
|
136
|
+
{items.map((item) => (
|
|
282
137
|
<MenuItem
|
|
283
138
|
key={item.route}
|
|
284
139
|
active={startsWithSegment(currentRoute, item.route)}
|
|
@@ -291,44 +146,11 @@ function NavigationMenu({ currentRoute }) {
|
|
|
291
146
|
}
|
|
292
147
|
```
|
|
293
148
|
|
|
294
|
-
###
|
|
295
|
-
|
|
296
|
-
```typescript
|
|
297
|
-
import { startsWithSegment } from '@real-router/helpers';
|
|
298
|
-
|
|
299
|
-
function Breadcrumbs({ currentRoute }) {
|
|
300
|
-
const segments = currentRoute.split('.');
|
|
301
|
-
const breadcrumbs = segments.reduce((acc, segment, index) => {
|
|
302
|
-
const path = segments.slice(0, index + 1).join('.');
|
|
303
|
-
return [...acc, {
|
|
304
|
-
path,
|
|
305
|
-
label: segment,
|
|
306
|
-
isActive: path === currentRoute,
|
|
307
|
-
}];
|
|
308
|
-
}, []);
|
|
309
|
-
|
|
310
|
-
return (
|
|
311
|
-
<nav>
|
|
312
|
-
{breadcrumbs.map(crumb => (
|
|
313
|
-
<Breadcrumb
|
|
314
|
-
key={crumb.path}
|
|
315
|
-
active={crumb.isActive}
|
|
316
|
-
>
|
|
317
|
-
{crumb.label}
|
|
318
|
-
</Breadcrumb>
|
|
319
|
-
))}
|
|
320
|
-
</nav>
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### Route Guards
|
|
149
|
+
### Route Guard
|
|
326
150
|
|
|
327
151
|
```typescript
|
|
328
|
-
import { startsWithSegment } from "@real-router/helpers";
|
|
329
|
-
|
|
330
152
|
const adminGuard = (router) => (toState, fromState, done) => {
|
|
331
|
-
if (startsWithSegment(toState, "admin") && !
|
|
153
|
+
if (startsWithSegment(toState, "admin") && !isAdmin()) {
|
|
332
154
|
done({ redirect: { name: "unauthorized" } });
|
|
333
155
|
} else {
|
|
334
156
|
done();
|
|
@@ -340,236 +162,62 @@ router.useMiddleware(adminGuard);
|
|
|
340
162
|
|
|
341
163
|
### Conditional Rendering
|
|
342
164
|
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
function PageLayout({ route, children }) {
|
|
165
|
+
```tsx
|
|
166
|
+
function Layout({ route, children }) {
|
|
347
167
|
return (
|
|
348
168
|
<div>
|
|
349
|
-
{
|
|
350
|
-
{
|
|
351
|
-
|
|
352
|
-
{/* Show edit toolbar on edit pages */}
|
|
353
|
-
{endsWithSegment(route, 'edit') && <EditToolbar />}
|
|
354
|
-
|
|
355
|
-
{/* Show user profile widget in user-related pages */}
|
|
356
|
-
{includesSegment(route, 'users') && <UserProfileWidget />}
|
|
357
|
-
|
|
169
|
+
{startsWithSegment(route, "admin") && <AdminSidebar />}
|
|
170
|
+
{endsWithSegment(route, "edit") && <EditToolbar />}
|
|
358
171
|
<main>{children}</main>
|
|
359
172
|
</div>
|
|
360
173
|
);
|
|
361
174
|
}
|
|
362
175
|
```
|
|
363
176
|
|
|
364
|
-
### Analytics Tracking
|
|
365
|
-
|
|
366
|
-
```typescript
|
|
367
|
-
import { startsWithSegment, includesSegment } from "@real-router/helpers";
|
|
368
|
-
|
|
369
|
-
router.subscribe((state) => {
|
|
370
|
-
const routeName = state.route.name;
|
|
371
|
-
|
|
372
|
-
// Track section
|
|
373
|
-
if (startsWithSegment(routeName, "admin")) {
|
|
374
|
-
analytics.track("admin_section_view");
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Track feature usage
|
|
378
|
-
if (includesSegment(routeName, "checkout")) {
|
|
379
|
-
analytics.track("checkout_flow_step", {
|
|
380
|
-
step: routeName.split(".").pop(),
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
```
|
|
385
|
-
|
|
386
177
|
---
|
|
387
178
|
|
|
388
|
-
## Validation
|
|
389
|
-
|
|
390
|
-
### Automatic Validation
|
|
391
|
-
|
|
392
|
-
All segment inputs are automatically validated for security and correctness:
|
|
393
|
-
|
|
394
|
-
**Character Whitelist:**
|
|
395
|
-
|
|
396
|
-
- Allowed: `a-z`, `A-Z`, `0-9`, `.` (dot), `-` (dash), `_` (underscore)
|
|
397
|
-
- Disallowed: Special characters, spaces, slashes, etc.
|
|
398
|
-
|
|
399
|
-
**Length Limits:**
|
|
400
|
-
|
|
401
|
-
- Maximum segment length: 10,000 characters
|
|
402
|
-
- Empty segments are rejected
|
|
403
|
-
|
|
404
|
-
**Examples:**
|
|
405
|
-
|
|
406
|
-
```typescript
|
|
407
|
-
// Valid segments
|
|
408
|
-
startsWithSegment("route", "users"); // OK
|
|
409
|
-
startsWithSegment("route", "admin-panel"); // OK
|
|
410
|
-
startsWithSegment("route", "users_v2"); // OK
|
|
411
|
-
startsWithSegment("route", "app.settings"); // OK
|
|
412
|
-
|
|
413
|
-
// Invalid segments (throw TypeError)
|
|
414
|
-
startsWithSegment("route", "invalid!char"); // Throws
|
|
415
|
-
startsWithSegment("route", "has space"); // Throws
|
|
416
|
-
startsWithSegment("route", "path/segment"); // Throws
|
|
417
|
-
startsWithSegment("route", "ns:segment"); // Throws
|
|
418
|
-
|
|
419
|
-
// Too long (throw RangeError)
|
|
420
|
-
startsWithSegment("route", "a".repeat(10001)); // Throws
|
|
421
|
-
|
|
422
|
-
// Empty (returns false)
|
|
423
|
-
startsWithSegment("route", ""); // false
|
|
424
|
-
startsWithSegment("route", null); // false
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### Error Handling
|
|
428
|
-
|
|
429
|
-
```typescript
|
|
430
|
-
try {
|
|
431
|
-
startsWithSegment("route", "invalid!segment");
|
|
432
|
-
} catch (error) {
|
|
433
|
-
if (error instanceof TypeError) {
|
|
434
|
-
console.error("Invalid segment:", error.message);
|
|
435
|
-
// "Segment contains invalid characters. Allowed: a-z, A-Z, 0-9, dot (.), dash (-), underscore (_)"
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
startsWithSegment("route", "a".repeat(10001));
|
|
441
|
-
} catch (error) {
|
|
442
|
-
if (error instanceof RangeError) {
|
|
443
|
-
console.error("Segment too long:", error.message);
|
|
444
|
-
// "Segment exceeds maximum length of 10000 characters"
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
---
|
|
450
|
-
|
|
451
|
-
## TypeScript Support
|
|
452
|
-
|
|
453
|
-
Full TypeScript support with type definitions included.
|
|
454
|
-
|
|
455
|
-
```typescript
|
|
456
|
-
import type { State } from "@real-router/core";
|
|
457
|
-
import { startsWithSegment } from "@real-router/helpers";
|
|
458
|
-
|
|
459
|
-
// Type inference works correctly
|
|
460
|
-
const result: boolean = startsWithSegment("route", "segment");
|
|
179
|
+
## Validation
|
|
461
180
|
|
|
462
|
-
|
|
463
|
-
const tester: (segment: string) => boolean = startsWithSegment("route");
|
|
181
|
+
Segments are validated for security:
|
|
464
182
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
### Type Definitions
|
|
183
|
+
- **Allowed:** `a-z`, `A-Z`, `0-9`, `.`, `-`, `_`
|
|
184
|
+
- **Max length:** 10,000 characters
|
|
185
|
+
- **Empty/null:** Returns `false`
|
|
186
|
+
- **Invalid chars:** Throws `TypeError`
|
|
471
187
|
|
|
472
188
|
```typescript
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
(route: State | string, segment: null): false;
|
|
477
|
-
(
|
|
478
|
-
route: State | string,
|
|
479
|
-
segment?: string | null,
|
|
480
|
-
): boolean | ((segment: string) => boolean);
|
|
481
|
-
}
|
|
189
|
+
startsWithSegment("route", "valid-segment_v2"); // OK
|
|
190
|
+
startsWithSegment("route", "invalid!char"); // Throws TypeError
|
|
191
|
+
startsWithSegment("route", ""); // false
|
|
482
192
|
```
|
|
483
193
|
|
|
484
194
|
---
|
|
485
195
|
|
|
486
|
-
## Documentation
|
|
487
|
-
|
|
488
|
-
Full documentation available on the [Real-Router Wiki](https://github.com/greydragon888/real-router/wiki):
|
|
489
|
-
|
|
490
|
-
- [startsWithSegment](https://github.com/greydragon888/real-router/wiki/startsWithSegment)
|
|
491
|
-
- [endsWithSegment](https://github.com/greydragon888/real-router/wiki/endsWithSegment)
|
|
492
|
-
- [includesSegment](https://github.com/greydragon888/real-router/wiki/includesSegment)
|
|
493
|
-
- [areRoutesRelated](https://github.com/greydragon888/real-router/wiki/areRoutesRelated)
|
|
494
|
-
|
|
495
|
-
---
|
|
496
|
-
|
|
497
|
-
## Related Packages
|
|
498
|
-
|
|
499
|
-
- [@real-router/core](https://www.npmjs.com/package/@real-router/core) - Core router
|
|
500
|
-
- [@real-router/react](https://www.npmjs.com/package/@real-router/react) - React integration
|
|
501
|
-
- [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) - Browser integration
|
|
502
|
-
|
|
503
|
-
---
|
|
504
|
-
|
|
505
196
|
## Migration from router5-helpers
|
|
506
197
|
|
|
507
|
-
### Import Changes
|
|
508
|
-
|
|
509
|
-
```diff
|
|
510
|
-
- import { startsWithSegment, endsWithSegment, includesSegment } from 'router5-helpers';
|
|
511
|
-
+ import { startsWithSegment, endsWithSegment, includesSegment } from '@real-router/helpers';
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### Removed: `redirect`
|
|
515
|
-
|
|
516
|
-
The `redirect` helper has been removed. Use router guards instead:
|
|
517
|
-
|
|
518
|
-
```diff
|
|
519
|
-
- import { redirect } from 'router5-helpers';
|
|
520
|
-
-
|
|
521
|
-
- router.canActivate('protected', () => redirect('login'));
|
|
522
|
-
+ router.canActivate('protected', (toState, fromState, done) => {
|
|
523
|
-
+ done({ redirect: { name: 'login' } });
|
|
524
|
-
+ });
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
### New: `areRoutesRelated`
|
|
528
|
-
|
|
529
|
-
New helper function for checking route hierarchy:
|
|
530
|
-
|
|
531
|
-
```typescript
|
|
532
|
-
import { areRoutesRelated } from "@real-router/helpers";
|
|
533
|
-
|
|
534
|
-
areRoutesRelated("users", "users.profile"); // true (parent-child)
|
|
535
|
-
areRoutesRelated("users.profile", "users"); // true (child-parent)
|
|
536
|
-
areRoutesRelated("users", "admin"); // false (different branches)
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
### New: Input Validation
|
|
540
|
-
|
|
541
|
-
@real-router/helpers validates segment inputs for security:
|
|
542
|
-
|
|
543
|
-
```typescript
|
|
544
|
-
// Valid segments
|
|
545
|
-
startsWithSegment("route", "users");
|
|
546
|
-
startsWithSegment("route", "admin-panel");
|
|
547
|
-
|
|
548
|
-
// Throws TypeError (invalid characters)
|
|
549
|
-
startsWithSegment("route", "invalid!char");
|
|
550
|
-
startsWithSegment("route", "has space");
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
### Full Migration Example
|
|
554
|
-
|
|
555
198
|
```diff
|
|
556
199
|
- import { startsWithSegment, redirect } from 'router5-helpers';
|
|
557
200
|
+ import { startsWithSegment } from '@real-router/helpers';
|
|
558
201
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
// ...
|
|
562
|
-
}
|
|
202
|
+
// Segment testing — unchanged
|
|
203
|
+
startsWithSegment(route, 'admin');
|
|
563
204
|
|
|
564
|
-
|
|
565
|
-
- router.canActivate('old
|
|
566
|
-
+ router.canActivate('old
|
|
567
|
-
+ done({ redirect: { name: 'new
|
|
205
|
+
// redirect removed — use guards:
|
|
206
|
+
- router.canActivate('old', () => redirect('new'));
|
|
207
|
+
+ router.canActivate('old', (to, from, done) => {
|
|
208
|
+
+ done({ redirect: { name: 'new' } });
|
|
568
209
|
+ });
|
|
569
210
|
```
|
|
570
211
|
|
|
212
|
+
**New:** `areRoutesRelated()` for hierarchy checks.
|
|
213
|
+
|
|
571
214
|
---
|
|
572
215
|
|
|
216
|
+
## Related Packages
|
|
217
|
+
|
|
218
|
+
- [@real-router/core](https://www.npmjs.com/package/@real-router/core) — Core router
|
|
219
|
+
- [@real-router/react](https://www.npmjs.com/package/@real-router/react) — React integration
|
|
220
|
+
|
|
573
221
|
## License
|
|
574
222
|
|
|
575
223
|
MIT © [Oleg Ivanov](https://github.com/greydragon888)
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,16 +1,238 @@
|
|
|
1
|
-
|
|
1
|
+
// Generated by dts-bundle-generator v9.5.1
|
|
2
2
|
|
|
3
|
+
export interface State<P extends Params = Params, MP extends Params = Params> {
|
|
4
|
+
name: string;
|
|
5
|
+
params: P;
|
|
6
|
+
path: string;
|
|
7
|
+
meta?: StateMeta<MP> | undefined;
|
|
8
|
+
}
|
|
9
|
+
export interface StateMeta<P extends Params = Params> {
|
|
10
|
+
id: number;
|
|
11
|
+
params: P;
|
|
12
|
+
options: NavigationOptions;
|
|
13
|
+
redirected: boolean;
|
|
14
|
+
source?: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Configuration options that control navigation transition behavior.
|
|
18
|
+
*
|
|
19
|
+
* @description
|
|
20
|
+
* NavigationOptions provides fine-grained control over how the router performs navigation
|
|
21
|
+
* transitions. These options affect history management, transition lifecycle execution,
|
|
22
|
+
* guard enforcement, and state comparison logic.
|
|
23
|
+
*
|
|
24
|
+
* All options are optional and have sensible defaults. Options can be combined to achieve
|
|
25
|
+
* complex navigation behaviors. The options object is stored in state.meta.options and is
|
|
26
|
+
* available to middleware, guards, and event listeners.
|
|
27
|
+
*
|
|
28
|
+
* @see {@link Router.navigate} for navigation method that accepts these options
|
|
29
|
+
* @see {@link State.meta} for where options are stored after navigation
|
|
30
|
+
*/
|
|
31
|
+
export interface NavigationOptions {
|
|
32
|
+
[key: string]: string | number | boolean | Record<string, unknown> | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Replace the current history entry instead of pushing a new one.
|
|
35
|
+
*
|
|
36
|
+
* @description
|
|
37
|
+
* When `true`, the navigation will replace the current entry in browser history instead
|
|
38
|
+
* of adding a new entry. This is typically used by history plugins (browser plugin) to
|
|
39
|
+
* control how navigation affects the browser's back/forward buttons.
|
|
40
|
+
*
|
|
41
|
+
* @default false
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Redirect after login - prevent back button to login page
|
|
45
|
+
* router.navigate('dashboard', {}, { replace: true });
|
|
46
|
+
*
|
|
47
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState}
|
|
48
|
+
*/
|
|
49
|
+
replace?: boolean | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Force reload of the current route even if states are equal.
|
|
52
|
+
*
|
|
53
|
+
* @description
|
|
54
|
+
* When `true`, bypasses the "same state" check that normally prevents navigation when
|
|
55
|
+
* the target state equals the current state. This forces a full transition lifecycle
|
|
56
|
+
* execution, allowing route components to reload with the same parameters.
|
|
57
|
+
*
|
|
58
|
+
* Without `reload`:
|
|
59
|
+
* - Navigation to current route throws SAME_STATES error
|
|
60
|
+
* - No lifecycle hooks or middleware execute
|
|
61
|
+
* - No events are fired
|
|
62
|
+
*
|
|
63
|
+
* With `reload`:
|
|
64
|
+
* - Full transition executes (deactivate → activate → middleware)
|
|
65
|
+
* - All lifecycle hooks run again
|
|
66
|
+
* - TRANSITION_SUCCESS event fires with same state
|
|
67
|
+
* - State object is recreated (new reference)
|
|
68
|
+
*
|
|
69
|
+
* @default false
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* // Refresh current page data
|
|
73
|
+
* router.navigate(currentRoute.name, currentRoute.params, { reload: true });
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Force re-fetch on same route with different query params
|
|
77
|
+
* // Note: query params are in path, not checked for equality
|
|
78
|
+
* router.navigate('search', { term: 'react' }, { reload: true });
|
|
79
|
+
*
|
|
80
|
+
* @see {@link force} for alternative that forces transition
|
|
81
|
+
* @see {@link Router.areStatesEqual} for state comparison logic
|
|
82
|
+
*/
|
|
83
|
+
reload?: boolean | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Preview navigation without any side effects (dry-run mode).
|
|
86
|
+
*
|
|
87
|
+
* @description
|
|
88
|
+
* When `true`, returns the would-be target state via callback WITHOUT:
|
|
89
|
+
* - Executing canDeactivate/canActivate guards
|
|
90
|
+
* - Executing middleware
|
|
91
|
+
* - Updating router state (`router.getState()` remains unchanged)
|
|
92
|
+
* - Emitting any transition events (TRANSITION_START, TRANSITION_SUCCESS, etc.)
|
|
93
|
+
*
|
|
94
|
+
* The callback receives `(undefined, toState)` where `toState` is the computed
|
|
95
|
+
* target state that WOULD result from this navigation.
|
|
96
|
+
*
|
|
97
|
+
* @default false
|
|
98
|
+
*
|
|
99
|
+
* @remarks
|
|
100
|
+
* This option is useful for:
|
|
101
|
+
* - Validating that a route exists and params are correct
|
|
102
|
+
* - SSR: previewing state for pre-rendering without side effects
|
|
103
|
+
* - Dry-run before actual navigation
|
|
104
|
+
*
|
|
105
|
+
* @deprecated Consider using `router.buildState()` + `router.makeState()` instead
|
|
106
|
+
* for clearer intent. This option may be removed in a future major version.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* // Preview navigation - router.getState() is NOT changed
|
|
110
|
+
* router.navigate('users.view', { id: 123 }, { skipTransition: true }, (err, previewState) => {
|
|
111
|
+
* console.log(previewState); // { name: 'users.view', params: { id: 123 }, path: '/users/view/123', ... }
|
|
112
|
+
* console.log(router.getState()); // Still the previous state!
|
|
113
|
+
* });
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* // Recommended alternative (clearer intent)
|
|
117
|
+
* const route = router.buildState('users.view', { id: 123 });
|
|
118
|
+
* if (route) {
|
|
119
|
+
* const path = router.buildPath(route.name, route.params);
|
|
120
|
+
* const previewState = router.makeState(route.name, route.params, path, { params: route.meta });
|
|
121
|
+
* }
|
|
122
|
+
*
|
|
123
|
+
* @see {@link forceDeactivate} for skipping only canDeactivate guards
|
|
124
|
+
* @see {@link force} for forcing navigation while preserving lifecycle
|
|
125
|
+
*/
|
|
126
|
+
skipTransition?: boolean | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* Force navigation even if target state equals current state.
|
|
129
|
+
*
|
|
130
|
+
* @description
|
|
131
|
+
* When `true`, bypasses the "same state" equality check but still executes the full
|
|
132
|
+
* transition lifecycle (unlike `skipTransition`). Similar to `reload` but can be used
|
|
133
|
+
* for any forced navigation scenario.
|
|
134
|
+
*
|
|
135
|
+
* Difference from `reload`:
|
|
136
|
+
* - `reload`: semantic meaning is "refresh current route"
|
|
137
|
+
* - `force`: general-purpose bypass of equality check
|
|
138
|
+
* - Both have identical implementation effect
|
|
139
|
+
*
|
|
140
|
+
* The equality check compares:
|
|
141
|
+
* - state.name (route name)
|
|
142
|
+
* - state.params (route parameters, shallow comparison)
|
|
143
|
+
*
|
|
144
|
+
* @default false
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* // Force transition for tracking even if params didn't change
|
|
148
|
+
* router.navigate('analytics', { event: 'pageview' }, { force: true });
|
|
149
|
+
*
|
|
150
|
+
* @see {@link reload} for semantic equivalent (preferred for refresh scenarios)
|
|
151
|
+
* @see {@link skipTransition} for bypassing entire lifecycle
|
|
152
|
+
*/
|
|
153
|
+
force?: boolean | undefined;
|
|
154
|
+
/**
|
|
155
|
+
* Skip canDeactivate guards during transition.
|
|
156
|
+
*
|
|
157
|
+
* @description
|
|
158
|
+
* When `true`, bypasses only the canDeactivate lifecycle hooks for segments being
|
|
159
|
+
* deactivated. canActivate guards and middleware still execute normally. This allows
|
|
160
|
+
* forcing navigation away from routes with confirmation dialogs or unsaved changes.
|
|
161
|
+
*
|
|
162
|
+
* Skipped vs executed:
|
|
163
|
+
* ```
|
|
164
|
+
* // Normal transition
|
|
165
|
+
* deactivate(fromSegments) → activate(toSegments) → middleware → success
|
|
166
|
+
*
|
|
167
|
+
* // With forceDeactivate: true
|
|
168
|
+
* [skip deactivate] → activate(toSegments) → middleware → success
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* ⚠️ Data loss risk: Bypassing canDeactivate means unsaved changes will be lost
|
|
172
|
+
*
|
|
173
|
+
* @default false
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* // Force logout even with unsaved changes
|
|
177
|
+
* function forceLogout() {
|
|
178
|
+
* router.navigate('login', {}, {
|
|
179
|
+
* forceDeactivate: true,
|
|
180
|
+
* replace: true
|
|
181
|
+
* });
|
|
182
|
+
* }
|
|
183
|
+
*
|
|
184
|
+
* @see {@link skipTransition} for bypassing all guards and middleware
|
|
185
|
+
* @see {@link Router.clearCanDeactivate} for programmatically clearing guards
|
|
186
|
+
*/
|
|
187
|
+
forceDeactivate?: boolean | undefined;
|
|
188
|
+
/**
|
|
189
|
+
* Internal flag indicating navigation is result of a redirect.
|
|
190
|
+
*
|
|
191
|
+
* @internal
|
|
192
|
+
*
|
|
193
|
+
* @description
|
|
194
|
+
* Automatically set by the router when a navigation is triggered by a redirect from
|
|
195
|
+
* middleware or lifecycle hooks. This flag is used internally to track redirect chains
|
|
196
|
+
* and is stored in state.meta.redirected.
|
|
197
|
+
*
|
|
198
|
+
* @default false (auto-set by router during redirects)
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* // Middleware triggers automatic redirect
|
|
202
|
+
* router.useMiddleware((toState, fromState, opts) => {
|
|
203
|
+
* if (!isAuthenticated && toState.name !== 'login') {
|
|
204
|
+
* // Router will automatically set redirected: true
|
|
205
|
+
* return { name: 'login', params: { next: toState.path } };
|
|
206
|
+
* }
|
|
207
|
+
* });
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* // Accessing redirect flag in lifecycle
|
|
211
|
+
* router.canActivate('dashboard', (toState, fromState) => {
|
|
212
|
+
* if (toState.meta?.redirected) {
|
|
213
|
+
* console.log('This navigation is from a redirect');
|
|
214
|
+
* }
|
|
215
|
+
* return true;
|
|
216
|
+
* });
|
|
217
|
+
*
|
|
218
|
+
* @see {@link Router.navigate} for redirect handling implementation
|
|
219
|
+
* @see {@link State.meta.redirected} for redirect flag in state
|
|
220
|
+
*/
|
|
221
|
+
redirected?: boolean | undefined;
|
|
222
|
+
}
|
|
223
|
+
export interface Params {
|
|
224
|
+
[key: string]: string | string[] | number | number[] | boolean | boolean[] | Params | Params[] | Record<string, string | number | boolean> | null | undefined;
|
|
225
|
+
}
|
|
3
226
|
/**
|
|
4
227
|
* Type definition for segment test functions.
|
|
5
228
|
* These functions can be called directly with a segment, or curried for later use.
|
|
6
229
|
*/
|
|
7
|
-
interface SegmentTestFunction {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
230
|
+
export interface SegmentTestFunction {
|
|
231
|
+
(route: State | string): (segment: string) => boolean;
|
|
232
|
+
(route: State | string, segment: string): boolean;
|
|
233
|
+
(route: State | string, segment: null): false;
|
|
234
|
+
(route: State | string, segment?: string | null): boolean | ((segment: string) => boolean);
|
|
12
235
|
}
|
|
13
|
-
|
|
14
236
|
/**
|
|
15
237
|
* Tests if a route name starts with the given segment.
|
|
16
238
|
*
|
|
@@ -70,7 +292,7 @@ interface SegmentTestFunction {
|
|
|
70
292
|
* @see endsWithSegment for suffix matching
|
|
71
293
|
* @see includesSegment for anywhere matching
|
|
72
294
|
*/
|
|
73
|
-
declare const startsWithSegment: SegmentTestFunction;
|
|
295
|
+
export declare const startsWithSegment: SegmentTestFunction;
|
|
74
296
|
/**
|
|
75
297
|
* Tests if a route name ends with the given segment.
|
|
76
298
|
*
|
|
@@ -111,7 +333,7 @@ declare const startsWithSegment: SegmentTestFunction;
|
|
|
111
333
|
* @see startsWithSegment for prefix matching
|
|
112
334
|
* @see includesSegment for anywhere matching
|
|
113
335
|
*/
|
|
114
|
-
declare const endsWithSegment: SegmentTestFunction;
|
|
336
|
+
export declare const endsWithSegment: SegmentTestFunction;
|
|
115
337
|
/**
|
|
116
338
|
* Tests if a route name includes the given segment anywhere in its path.
|
|
117
339
|
*
|
|
@@ -156,8 +378,7 @@ declare const endsWithSegment: SegmentTestFunction;
|
|
|
156
378
|
* @see startsWithSegment for prefix matching
|
|
157
379
|
* @see endsWithSegment for suffix matching
|
|
158
380
|
*/
|
|
159
|
-
declare const includesSegment: SegmentTestFunction;
|
|
160
|
-
|
|
381
|
+
export declare const includesSegment: SegmentTestFunction;
|
|
161
382
|
/**
|
|
162
383
|
* Checks if two routes are related in the hierarchy.
|
|
163
384
|
*
|
|
@@ -177,6 +398,6 @@ declare const includesSegment: SegmentTestFunction;
|
|
|
177
398
|
* areRoutesRelated("users", "admin"); // false (different branches)
|
|
178
399
|
* areRoutesRelated("users.list", "users.view"); // false (siblings)
|
|
179
400
|
*/
|
|
180
|
-
declare function areRoutesRelated(route1: string, route2: string): boolean;
|
|
401
|
+
export declare function areRoutesRelated(route1: string, route2: string): boolean;
|
|
181
402
|
|
|
182
|
-
export {
|
|
403
|
+
export {};
|
package/dist/esm/index.d.mts
CHANGED
|
@@ -1,16 +1,238 @@
|
|
|
1
|
-
|
|
1
|
+
// Generated by dts-bundle-generator v9.5.1
|
|
2
2
|
|
|
3
|
+
export interface State<P extends Params = Params, MP extends Params = Params> {
|
|
4
|
+
name: string;
|
|
5
|
+
params: P;
|
|
6
|
+
path: string;
|
|
7
|
+
meta?: StateMeta<MP> | undefined;
|
|
8
|
+
}
|
|
9
|
+
export interface StateMeta<P extends Params = Params> {
|
|
10
|
+
id: number;
|
|
11
|
+
params: P;
|
|
12
|
+
options: NavigationOptions;
|
|
13
|
+
redirected: boolean;
|
|
14
|
+
source?: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Configuration options that control navigation transition behavior.
|
|
18
|
+
*
|
|
19
|
+
* @description
|
|
20
|
+
* NavigationOptions provides fine-grained control over how the router performs navigation
|
|
21
|
+
* transitions. These options affect history management, transition lifecycle execution,
|
|
22
|
+
* guard enforcement, and state comparison logic.
|
|
23
|
+
*
|
|
24
|
+
* All options are optional and have sensible defaults. Options can be combined to achieve
|
|
25
|
+
* complex navigation behaviors. The options object is stored in state.meta.options and is
|
|
26
|
+
* available to middleware, guards, and event listeners.
|
|
27
|
+
*
|
|
28
|
+
* @see {@link Router.navigate} for navigation method that accepts these options
|
|
29
|
+
* @see {@link State.meta} for where options are stored after navigation
|
|
30
|
+
*/
|
|
31
|
+
export interface NavigationOptions {
|
|
32
|
+
[key: string]: string | number | boolean | Record<string, unknown> | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Replace the current history entry instead of pushing a new one.
|
|
35
|
+
*
|
|
36
|
+
* @description
|
|
37
|
+
* When `true`, the navigation will replace the current entry in browser history instead
|
|
38
|
+
* of adding a new entry. This is typically used by history plugins (browser plugin) to
|
|
39
|
+
* control how navigation affects the browser's back/forward buttons.
|
|
40
|
+
*
|
|
41
|
+
* @default false
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Redirect after login - prevent back button to login page
|
|
45
|
+
* router.navigate('dashboard', {}, { replace: true });
|
|
46
|
+
*
|
|
47
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState}
|
|
48
|
+
*/
|
|
49
|
+
replace?: boolean | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Force reload of the current route even if states are equal.
|
|
52
|
+
*
|
|
53
|
+
* @description
|
|
54
|
+
* When `true`, bypasses the "same state" check that normally prevents navigation when
|
|
55
|
+
* the target state equals the current state. This forces a full transition lifecycle
|
|
56
|
+
* execution, allowing route components to reload with the same parameters.
|
|
57
|
+
*
|
|
58
|
+
* Without `reload`:
|
|
59
|
+
* - Navigation to current route throws SAME_STATES error
|
|
60
|
+
* - No lifecycle hooks or middleware execute
|
|
61
|
+
* - No events are fired
|
|
62
|
+
*
|
|
63
|
+
* With `reload`:
|
|
64
|
+
* - Full transition executes (deactivate → activate → middleware)
|
|
65
|
+
* - All lifecycle hooks run again
|
|
66
|
+
* - TRANSITION_SUCCESS event fires with same state
|
|
67
|
+
* - State object is recreated (new reference)
|
|
68
|
+
*
|
|
69
|
+
* @default false
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* // Refresh current page data
|
|
73
|
+
* router.navigate(currentRoute.name, currentRoute.params, { reload: true });
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Force re-fetch on same route with different query params
|
|
77
|
+
* // Note: query params are in path, not checked for equality
|
|
78
|
+
* router.navigate('search', { term: 'react' }, { reload: true });
|
|
79
|
+
*
|
|
80
|
+
* @see {@link force} for alternative that forces transition
|
|
81
|
+
* @see {@link Router.areStatesEqual} for state comparison logic
|
|
82
|
+
*/
|
|
83
|
+
reload?: boolean | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Preview navigation without any side effects (dry-run mode).
|
|
86
|
+
*
|
|
87
|
+
* @description
|
|
88
|
+
* When `true`, returns the would-be target state via callback WITHOUT:
|
|
89
|
+
* - Executing canDeactivate/canActivate guards
|
|
90
|
+
* - Executing middleware
|
|
91
|
+
* - Updating router state (`router.getState()` remains unchanged)
|
|
92
|
+
* - Emitting any transition events (TRANSITION_START, TRANSITION_SUCCESS, etc.)
|
|
93
|
+
*
|
|
94
|
+
* The callback receives `(undefined, toState)` where `toState` is the computed
|
|
95
|
+
* target state that WOULD result from this navigation.
|
|
96
|
+
*
|
|
97
|
+
* @default false
|
|
98
|
+
*
|
|
99
|
+
* @remarks
|
|
100
|
+
* This option is useful for:
|
|
101
|
+
* - Validating that a route exists and params are correct
|
|
102
|
+
* - SSR: previewing state for pre-rendering without side effects
|
|
103
|
+
* - Dry-run before actual navigation
|
|
104
|
+
*
|
|
105
|
+
* @deprecated Consider using `router.buildState()` + `router.makeState()` instead
|
|
106
|
+
* for clearer intent. This option may be removed in a future major version.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* // Preview navigation - router.getState() is NOT changed
|
|
110
|
+
* router.navigate('users.view', { id: 123 }, { skipTransition: true }, (err, previewState) => {
|
|
111
|
+
* console.log(previewState); // { name: 'users.view', params: { id: 123 }, path: '/users/view/123', ... }
|
|
112
|
+
* console.log(router.getState()); // Still the previous state!
|
|
113
|
+
* });
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* // Recommended alternative (clearer intent)
|
|
117
|
+
* const route = router.buildState('users.view', { id: 123 });
|
|
118
|
+
* if (route) {
|
|
119
|
+
* const path = router.buildPath(route.name, route.params);
|
|
120
|
+
* const previewState = router.makeState(route.name, route.params, path, { params: route.meta });
|
|
121
|
+
* }
|
|
122
|
+
*
|
|
123
|
+
* @see {@link forceDeactivate} for skipping only canDeactivate guards
|
|
124
|
+
* @see {@link force} for forcing navigation while preserving lifecycle
|
|
125
|
+
*/
|
|
126
|
+
skipTransition?: boolean | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* Force navigation even if target state equals current state.
|
|
129
|
+
*
|
|
130
|
+
* @description
|
|
131
|
+
* When `true`, bypasses the "same state" equality check but still executes the full
|
|
132
|
+
* transition lifecycle (unlike `skipTransition`). Similar to `reload` but can be used
|
|
133
|
+
* for any forced navigation scenario.
|
|
134
|
+
*
|
|
135
|
+
* Difference from `reload`:
|
|
136
|
+
* - `reload`: semantic meaning is "refresh current route"
|
|
137
|
+
* - `force`: general-purpose bypass of equality check
|
|
138
|
+
* - Both have identical implementation effect
|
|
139
|
+
*
|
|
140
|
+
* The equality check compares:
|
|
141
|
+
* - state.name (route name)
|
|
142
|
+
* - state.params (route parameters, shallow comparison)
|
|
143
|
+
*
|
|
144
|
+
* @default false
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* // Force transition for tracking even if params didn't change
|
|
148
|
+
* router.navigate('analytics', { event: 'pageview' }, { force: true });
|
|
149
|
+
*
|
|
150
|
+
* @see {@link reload} for semantic equivalent (preferred for refresh scenarios)
|
|
151
|
+
* @see {@link skipTransition} for bypassing entire lifecycle
|
|
152
|
+
*/
|
|
153
|
+
force?: boolean | undefined;
|
|
154
|
+
/**
|
|
155
|
+
* Skip canDeactivate guards during transition.
|
|
156
|
+
*
|
|
157
|
+
* @description
|
|
158
|
+
* When `true`, bypasses only the canDeactivate lifecycle hooks for segments being
|
|
159
|
+
* deactivated. canActivate guards and middleware still execute normally. This allows
|
|
160
|
+
* forcing navigation away from routes with confirmation dialogs or unsaved changes.
|
|
161
|
+
*
|
|
162
|
+
* Skipped vs executed:
|
|
163
|
+
* ```
|
|
164
|
+
* // Normal transition
|
|
165
|
+
* deactivate(fromSegments) → activate(toSegments) → middleware → success
|
|
166
|
+
*
|
|
167
|
+
* // With forceDeactivate: true
|
|
168
|
+
* [skip deactivate] → activate(toSegments) → middleware → success
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* ⚠️ Data loss risk: Bypassing canDeactivate means unsaved changes will be lost
|
|
172
|
+
*
|
|
173
|
+
* @default false
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* // Force logout even with unsaved changes
|
|
177
|
+
* function forceLogout() {
|
|
178
|
+
* router.navigate('login', {}, {
|
|
179
|
+
* forceDeactivate: true,
|
|
180
|
+
* replace: true
|
|
181
|
+
* });
|
|
182
|
+
* }
|
|
183
|
+
*
|
|
184
|
+
* @see {@link skipTransition} for bypassing all guards and middleware
|
|
185
|
+
* @see {@link Router.clearCanDeactivate} for programmatically clearing guards
|
|
186
|
+
*/
|
|
187
|
+
forceDeactivate?: boolean | undefined;
|
|
188
|
+
/**
|
|
189
|
+
* Internal flag indicating navigation is result of a redirect.
|
|
190
|
+
*
|
|
191
|
+
* @internal
|
|
192
|
+
*
|
|
193
|
+
* @description
|
|
194
|
+
* Automatically set by the router when a navigation is triggered by a redirect from
|
|
195
|
+
* middleware or lifecycle hooks. This flag is used internally to track redirect chains
|
|
196
|
+
* and is stored in state.meta.redirected.
|
|
197
|
+
*
|
|
198
|
+
* @default false (auto-set by router during redirects)
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* // Middleware triggers automatic redirect
|
|
202
|
+
* router.useMiddleware((toState, fromState, opts) => {
|
|
203
|
+
* if (!isAuthenticated && toState.name !== 'login') {
|
|
204
|
+
* // Router will automatically set redirected: true
|
|
205
|
+
* return { name: 'login', params: { next: toState.path } };
|
|
206
|
+
* }
|
|
207
|
+
* });
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* // Accessing redirect flag in lifecycle
|
|
211
|
+
* router.canActivate('dashboard', (toState, fromState) => {
|
|
212
|
+
* if (toState.meta?.redirected) {
|
|
213
|
+
* console.log('This navigation is from a redirect');
|
|
214
|
+
* }
|
|
215
|
+
* return true;
|
|
216
|
+
* });
|
|
217
|
+
*
|
|
218
|
+
* @see {@link Router.navigate} for redirect handling implementation
|
|
219
|
+
* @see {@link State.meta.redirected} for redirect flag in state
|
|
220
|
+
*/
|
|
221
|
+
redirected?: boolean | undefined;
|
|
222
|
+
}
|
|
223
|
+
export interface Params {
|
|
224
|
+
[key: string]: string | string[] | number | number[] | boolean | boolean[] | Params | Params[] | Record<string, string | number | boolean> | null | undefined;
|
|
225
|
+
}
|
|
3
226
|
/**
|
|
4
227
|
* Type definition for segment test functions.
|
|
5
228
|
* These functions can be called directly with a segment, or curried for later use.
|
|
6
229
|
*/
|
|
7
|
-
interface SegmentTestFunction {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
230
|
+
export interface SegmentTestFunction {
|
|
231
|
+
(route: State | string): (segment: string) => boolean;
|
|
232
|
+
(route: State | string, segment: string): boolean;
|
|
233
|
+
(route: State | string, segment: null): false;
|
|
234
|
+
(route: State | string, segment?: string | null): boolean | ((segment: string) => boolean);
|
|
12
235
|
}
|
|
13
|
-
|
|
14
236
|
/**
|
|
15
237
|
* Tests if a route name starts with the given segment.
|
|
16
238
|
*
|
|
@@ -70,7 +292,7 @@ interface SegmentTestFunction {
|
|
|
70
292
|
* @see endsWithSegment for suffix matching
|
|
71
293
|
* @see includesSegment for anywhere matching
|
|
72
294
|
*/
|
|
73
|
-
declare const startsWithSegment: SegmentTestFunction;
|
|
295
|
+
export declare const startsWithSegment: SegmentTestFunction;
|
|
74
296
|
/**
|
|
75
297
|
* Tests if a route name ends with the given segment.
|
|
76
298
|
*
|
|
@@ -111,7 +333,7 @@ declare const startsWithSegment: SegmentTestFunction;
|
|
|
111
333
|
* @see startsWithSegment for prefix matching
|
|
112
334
|
* @see includesSegment for anywhere matching
|
|
113
335
|
*/
|
|
114
|
-
declare const endsWithSegment: SegmentTestFunction;
|
|
336
|
+
export declare const endsWithSegment: SegmentTestFunction;
|
|
115
337
|
/**
|
|
116
338
|
* Tests if a route name includes the given segment anywhere in its path.
|
|
117
339
|
*
|
|
@@ -156,8 +378,7 @@ declare const endsWithSegment: SegmentTestFunction;
|
|
|
156
378
|
* @see startsWithSegment for prefix matching
|
|
157
379
|
* @see endsWithSegment for suffix matching
|
|
158
380
|
*/
|
|
159
|
-
declare const includesSegment: SegmentTestFunction;
|
|
160
|
-
|
|
381
|
+
export declare const includesSegment: SegmentTestFunction;
|
|
161
382
|
/**
|
|
162
383
|
* Checks if two routes are related in the hierarchy.
|
|
163
384
|
*
|
|
@@ -177,6 +398,6 @@ declare const includesSegment: SegmentTestFunction;
|
|
|
177
398
|
* areRoutesRelated("users", "admin"); // false (different branches)
|
|
178
399
|
* areRoutesRelated("users.list", "users.view"); // false (siblings)
|
|
179
400
|
*/
|
|
180
|
-
declare function areRoutesRelated(route1: string, route2: string): boolean;
|
|
401
|
+
export declare function areRoutesRelated(route1: string, route2: string): boolean;
|
|
181
402
|
|
|
182
|
-
export {
|
|
403
|
+
export {};
|