navgo 5.0.1 → 6.0.0-beta.0
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/index.d.ts +21 -16
- package/index.js +76 -29
- package/package.json +2 -2
- package/readme.md +56 -55
package/index.d.ts
CHANGED
|
@@ -3,23 +3,31 @@
|
|
|
3
3
|
* For string patterns, missing optional params are `null`.
|
|
4
4
|
* For RegExp named groups, missing groups may be `undefined`.
|
|
5
5
|
*/
|
|
6
|
-
export type
|
|
6
|
+
export type RawParam = string | null | undefined
|
|
7
|
+
export type Params = Record<string, any>
|
|
7
8
|
|
|
8
9
|
/** Built-in validator helpers shape. */
|
|
9
10
|
export interface ValidatorHelpers {
|
|
10
|
-
int(opts?: {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
int(opts?: { min?: number | null; max?: number | null }): (value: RawParam) => boolean
|
|
12
|
+
one_of(values: Iterable<string>): (value: RawParam) => boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ParamRule =
|
|
16
|
+
| ((value: RawParam) => boolean)
|
|
17
|
+
| { validator?: (value: RawParam) => boolean; coercer?: (value: RawParam) => any }
|
|
18
|
+
|
|
19
|
+
export interface LoaderArgs {
|
|
20
|
+
route_entry: RouteTuple
|
|
21
|
+
url: URL
|
|
22
|
+
params: Params
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
/** Optional per-route hooks recognized by Navgo. */
|
|
18
26
|
export interface Hooks {
|
|
19
|
-
/** Validate
|
|
20
|
-
|
|
27
|
+
/** Validate and/or coerce params (validator runs before coercer). */
|
|
28
|
+
param_rules?: Record<string, ParamRule>
|
|
21
29
|
/** Load data for a route before navigation. May return a Promise or an array of values/promises. */
|
|
22
|
-
loader?(
|
|
30
|
+
loader?(args: LoaderArgs): unknown | Promise<unknown> | Array<unknown | Promise<unknown>>
|
|
23
31
|
/** Predicate used during match(); may be async. If it returns `false`, the route is skipped. */
|
|
24
32
|
validate?(params: Params): boolean | Promise<boolean>
|
|
25
33
|
/** Route-level navigation guard, called on the current route when leaving it. Synchronous only; call `nav.cancel()` to prevent navigation. */
|
|
@@ -46,8 +54,8 @@ export interface Navigation {
|
|
|
46
54
|
cancel(): void
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
/** A route tuple: [pattern, data?]. The `data` may include {@link Hooks}. */
|
|
50
|
-
export type RouteTuple<T = unknown> = [pattern: string | RegExp, data
|
|
57
|
+
/** A route tuple: [pattern, data?, extra?]. The `data`/`extra` may include {@link Hooks}. */
|
|
58
|
+
export type RouteTuple<T = unknown, U = unknown> = [pattern: string | RegExp, data?: T, extra?: U]
|
|
51
59
|
|
|
52
60
|
/** Result of calling `router.match(url)` */
|
|
53
61
|
export interface MatchResult<T = unknown> {
|
|
@@ -88,11 +96,8 @@ export interface Options {
|
|
|
88
96
|
tick?: () => void | Promise<void>
|
|
89
97
|
/** When `false`, do not scroll to top on non-hash navigations. Default true. */
|
|
90
98
|
scroll_to_top?: boolean
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
* Receives the router's current snapshot (eg `{ url: URL, route: RouteTuple|null, params: Params }`).
|
|
94
|
-
*/
|
|
95
|
-
url_changed?(payload: any): void
|
|
99
|
+
/** When `true`, sets `aria-current="page"` on active in-app links. Default false. */
|
|
100
|
+
aria_current?: boolean
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
/** Navgo default export: class-based router. */
|
package/index.js
CHANGED
|
@@ -13,9 +13,9 @@ export default class Navgo {
|
|
|
13
13
|
preload_on_hover: true,
|
|
14
14
|
before_navigate: undefined,
|
|
15
15
|
after_navigate: undefined,
|
|
16
|
-
url_changed: undefined,
|
|
17
16
|
tick,
|
|
18
17
|
scroll_to_top: true,
|
|
18
|
+
aria_current: false,
|
|
19
19
|
attach_to_window: true,
|
|
20
20
|
}
|
|
21
21
|
/** @type {Array<{ pattern: RegExp, keys: string[]|null, data: RouteTuple }>} */
|
|
@@ -107,6 +107,7 @@ export default class Navgo {
|
|
|
107
107
|
ℹ(' - [🧭 event:popstate]', 'same path+search; skip loader')
|
|
108
108
|
this.#apply_scroll(ev)
|
|
109
109
|
this.route.set(this.#current)
|
|
110
|
+
this.#update_active_links()
|
|
110
111
|
return
|
|
111
112
|
}
|
|
112
113
|
// Explicit shallow entries (pushState/replaceState) regardless of path
|
|
@@ -115,6 +116,7 @@ export default class Navgo {
|
|
|
115
116
|
ℹ(' - [🧭 event:popstate]', 'shallow entry; skip loader')
|
|
116
117
|
this.#apply_scroll(ev)
|
|
117
118
|
this.route.set(this.#current)
|
|
119
|
+
this.#update_active_links()
|
|
118
120
|
return
|
|
119
121
|
}
|
|
120
122
|
|
|
@@ -150,6 +152,7 @@ export default class Navgo {
|
|
|
150
152
|
// update current URL snapshot and notify
|
|
151
153
|
this.#current.url = new URL(location.href)
|
|
152
154
|
this.route.set(this.#current)
|
|
155
|
+
this.#update_active_links()
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
/** @type {any} */
|
|
@@ -202,7 +205,7 @@ export default class Navgo {
|
|
|
202
205
|
will_unload: true,
|
|
203
206
|
event: ev,
|
|
204
207
|
})
|
|
205
|
-
this.#current.route?.
|
|
208
|
+
this.#get_hooks(this.#current.route)?.before_route_leave?.(nav)
|
|
206
209
|
if (nav.cancelled) {
|
|
207
210
|
ℹ('[🧭 navigate]', 'cancelled by before_route_leave during unload')
|
|
208
211
|
ev.preventDefault()
|
|
@@ -247,17 +250,39 @@ export default class Navgo {
|
|
|
247
250
|
: null
|
|
248
251
|
}
|
|
249
252
|
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
#update_active_links() {
|
|
254
|
+
if (!this.#opts.aria_current) return
|
|
255
|
+
const cur = this.format(this.#current.url?.pathname)
|
|
256
|
+
if (!cur) return
|
|
257
|
+
for (const a of document.querySelectorAll('a[href]')) {
|
|
258
|
+
const href = a.getAttribute('href')
|
|
259
|
+
if (href[0] === '#') continue
|
|
260
|
+
const link_path = href && this.#resolve_url_and_path(href)?.path
|
|
261
|
+
if (link_path === cur) a.setAttribute('aria-current', 'page')
|
|
262
|
+
else if (a.getAttribute('aria-current') === 'page') a.removeAttribute('aria-current')
|
|
255
263
|
}
|
|
256
|
-
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#get_hooks(route) {
|
|
267
|
+
if (!route) return {}
|
|
268
|
+
const a = route[1]
|
|
269
|
+
const b = route[2]
|
|
270
|
+
if (!b) return a || {}
|
|
271
|
+
const hooks = { ...(a || {}), ...b }
|
|
272
|
+
const ar = a?.param_rules
|
|
273
|
+
const br = b?.param_rules
|
|
274
|
+
if (ar || br) {
|
|
275
|
+
const out = {}
|
|
276
|
+
const norm = r => (typeof r === 'function' ? { validator: r } : r || {})
|
|
277
|
+
for (const k in ar || {}) out[k] = norm(ar[k])
|
|
278
|
+
for (const k in br || {}) out[k] = { ...out[k], ...norm(br[k]) }
|
|
279
|
+
hooks.param_rules = out
|
|
280
|
+
}
|
|
281
|
+
return hooks
|
|
257
282
|
}
|
|
258
283
|
|
|
259
284
|
async #run_loader(route, url, params) {
|
|
260
|
-
const ret_val = route
|
|
285
|
+
const ret_val = this.#get_hooks(route)?.loader?.({ route_entry: route, url, params })
|
|
261
286
|
return Array.isArray(ret_val) ? Promise.all(ret_val) : ret_val
|
|
262
287
|
}
|
|
263
288
|
|
|
@@ -318,7 +343,7 @@ export default class Navgo {
|
|
|
318
343
|
//
|
|
319
344
|
// before_route_leave
|
|
320
345
|
//
|
|
321
|
-
this.#current.route?.
|
|
346
|
+
this.#get_hooks(this.#current.route)?.before_route_leave?.(nav)
|
|
322
347
|
if (nav.cancelled) {
|
|
323
348
|
// use history.go to cancel the nav, and jump back to where we are
|
|
324
349
|
if (is_popstate) {
|
|
@@ -355,11 +380,23 @@ export default class Navgo {
|
|
|
355
380
|
let data
|
|
356
381
|
if (hit) {
|
|
357
382
|
const pre = this.#preloads.get(path)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
383
|
+
try {
|
|
384
|
+
data =
|
|
385
|
+
pre?.data ??
|
|
386
|
+
(await (pre?.promise || this.#run_loader(hit.route, url, hit.params)))
|
|
387
|
+
} catch (e) {
|
|
388
|
+
this.#preloads.delete(path)
|
|
389
|
+
ℹ('[🧭 loader]', 'error; abort', { path, error: e })
|
|
390
|
+
if (is_popstate) {
|
|
391
|
+
const new_idx = ev_param?.state?.__navgo?.idx
|
|
392
|
+
if (new_idx != null) {
|
|
393
|
+
const delta = new_idx - this.#route_idx
|
|
394
|
+
if (delta) history.go(-delta)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (nav_id === this.#nav_active) this.is_navigating.set(false)
|
|
398
|
+
return
|
|
399
|
+
}
|
|
363
400
|
this.#preloads.delete(path)
|
|
364
401
|
ℹ('[🧭 loader]', pre ? 'using preloaded data' : 'loaded', {
|
|
365
402
|
path,
|
|
@@ -427,6 +464,7 @@ export default class Navgo {
|
|
|
427
464
|
// allow frameworks to flush DOM before scrolling
|
|
428
465
|
await this.#opts.tick?.()
|
|
429
466
|
|
|
467
|
+
this.#update_active_links()
|
|
430
468
|
this.#apply_scroll(nav)
|
|
431
469
|
this.is_navigating.set(false)
|
|
432
470
|
}
|
|
@@ -467,6 +505,7 @@ export default class Navgo {
|
|
|
467
505
|
// update current URL snapshot and notify
|
|
468
506
|
this.#current.url = u
|
|
469
507
|
this.route.set(this.#current)
|
|
508
|
+
this.#update_active_links()
|
|
470
509
|
}
|
|
471
510
|
|
|
472
511
|
/** @param {string|URL} [url] @param {any} [state] */
|
|
@@ -537,15 +576,28 @@ export default class Navgo {
|
|
|
537
576
|
}
|
|
538
577
|
|
|
539
578
|
// per-route validators and optional async validate()
|
|
540
|
-
const hooks = obj.data
|
|
541
|
-
if (
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
579
|
+
const hooks = this.#get_hooks(obj.data)
|
|
580
|
+
if (hooks.param_rules) {
|
|
581
|
+
let ok = true
|
|
582
|
+
for (const k in hooks.param_rules) {
|
|
583
|
+
const param_rule = hooks.param_rules[k]
|
|
584
|
+
const param_validator =
|
|
585
|
+
typeof param_rule === 'function' ? param_rule : param_rule?.validator
|
|
586
|
+
if (typeof param_validator === 'function' && !param_validator(params[k])) {
|
|
587
|
+
ok = false
|
|
588
|
+
break
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (!ok) {
|
|
592
|
+
ℹ('[🧭 match]', 'skip: param_rules', { pattern: obj.data?.[0] })
|
|
593
|
+
continue
|
|
594
|
+
}
|
|
595
|
+
for (const k in hooks.param_rules) {
|
|
596
|
+
const param_rule = hooks.param_rules[k]
|
|
597
|
+
const param_coercer =
|
|
598
|
+
typeof param_rule === 'function' ? null : param_rule?.coercer
|
|
599
|
+
if (typeof param_coercer === 'function') params[k] = param_coercer(params[k])
|
|
600
|
+
}
|
|
549
601
|
}
|
|
550
602
|
if (hooks.validate && !(await hooks.validate(params))) {
|
|
551
603
|
ℹ('[🧭 match]', 'skip: validate', { pattern: obj.data?.[0] })
|
|
@@ -574,11 +626,6 @@ export default class Navgo {
|
|
|
574
626
|
return pat
|
|
575
627
|
})
|
|
576
628
|
|
|
577
|
-
// TODO: deprecated, remove later
|
|
578
|
-
this.route.subscribe(() => {
|
|
579
|
-
this.#opts.url_changed?.(this.#current)
|
|
580
|
-
})
|
|
581
|
-
|
|
582
629
|
ℹ('[🧭 init]', {
|
|
583
630
|
base: this.#base,
|
|
584
631
|
routes: this.#routes.length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "navgo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0-beta.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/mustafa0x/navgo.git"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"build": "perl -0777 -i -pe 's/console.debug\\(...args\\)/{}/g' index.js",
|
|
19
19
|
"prepublishOnly": "pnpm run build",
|
|
20
20
|
"test": "vitest run index.test.js",
|
|
21
|
-
"test:e2e": "playwright test",
|
|
21
|
+
"test:e2e": "playwright test --project=chromium",
|
|
22
22
|
"start:testsite": "pnpm vite dev test/site --port 5714",
|
|
23
23
|
"types": "tsc -p test/types"
|
|
24
24
|
},
|
package/readme.md
CHANGED
|
@@ -8,62 +8,64 @@ $ pnpm install --dev navgo
|
|
|
8
8
|
|
|
9
9
|
```js
|
|
10
10
|
import Navgo from 'navgo'
|
|
11
|
+
import {mount} from 'svelte'
|
|
12
|
+
|
|
13
|
+
import App from './App.svelte'
|
|
14
|
+
import * as HomeRoute from './routes/Home.svelte'
|
|
15
|
+
import * as ReaderRoute from './routes/Reader.svelte'
|
|
16
|
+
import * as AccountRoute from './routes/Account.svelte'
|
|
17
|
+
import * as AdminRoute from './routes/Admin.svelte'
|
|
18
|
+
import * as DebugRoute from './routes/Debug.svelte'
|
|
11
19
|
|
|
12
|
-
// Define routes up front (strings or RegExp)
|
|
13
20
|
const routes = [
|
|
14
|
-
['/',
|
|
15
|
-
['
|
|
16
|
-
['/
|
|
17
|
-
[/articles\/(?<year>[0-9]{4})/, {}],
|
|
18
|
-
[/privacy|privacy-policy/, {}],
|
|
21
|
+
['/', HomeRoute],
|
|
22
|
+
['/:book_id', ReaderRoute],
|
|
23
|
+
['/account', AccountRoute],
|
|
19
24
|
[
|
|
20
|
-
'/admin',
|
|
25
|
+
'/admin/:id',
|
|
26
|
+
AdminRoute,
|
|
21
27
|
{
|
|
22
|
-
// constrain params
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
// constrain/coerce params
|
|
29
|
+
param_rules: {
|
|
30
|
+
id: { validator: Navgo.validators.int({ min: 1 }), coercer: Number },
|
|
25
31
|
},
|
|
26
32
|
// load data before URL changes; result goes to after_navigate(...)
|
|
27
|
-
loader: params => fetch(
|
|
33
|
+
loader: ({ params }) => fetch(`/api/admin/${params.id}`).then(r => r.json()),
|
|
28
34
|
// per-route guard; cancel synchronously to block nav
|
|
29
35
|
before_route_leave(nav) {
|
|
30
|
-
if ((nav.type === 'link' || nav.type === '
|
|
36
|
+
if ((nav.type === 'link' || nav.type === 'goto') && !confirm('Enter admin?')) {
|
|
31
37
|
nav.cancel()
|
|
32
38
|
}
|
|
33
39
|
},
|
|
34
40
|
},
|
|
35
41
|
],
|
|
36
42
|
]
|
|
43
|
+
if (window.__DEBUG__) routes.push(['/debug', DebugRoute])
|
|
44
|
+
|
|
45
|
+
const props = $state({
|
|
46
|
+
component: null,
|
|
47
|
+
route_data: null,
|
|
48
|
+
is_404: false,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
function after_navigate(nav) {
|
|
52
|
+
props.is_404 = nav.to?.data?.__error?.status === 404
|
|
53
|
+
props.route_data = nav.to?.data ?? null
|
|
54
|
+
props.component = nav.to?.route?.[1]?.default || null
|
|
55
|
+
}
|
|
37
56
|
|
|
38
|
-
// Create router with options + callbacks
|
|
39
57
|
const router = new Navgo(routes, {
|
|
40
|
-
base: '/',
|
|
41
58
|
before_navigate(nav) {
|
|
42
59
|
// app-level hook before loader/URL update; may cancel
|
|
43
60
|
console.log('before_navigate', nav.type, '→', nav.to?.url.pathname)
|
|
44
61
|
},
|
|
45
|
-
after_navigate
|
|
46
|
-
// called after routing completes; nav.to.data holds loader result
|
|
47
|
-
if (nav.to?.data?.__error?.status === 404) {
|
|
48
|
-
console.log('404 for', nav.to.url.pathname)
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
console.log('after_navigate', nav.to?.url.pathname, nav.to?.data)
|
|
53
|
-
},
|
|
54
|
-
// let your framework flush DOM before scroll
|
|
55
|
-
// e.g. in Svelte: `import { tick } from 'svelte'`
|
|
56
|
-
tick: tick,
|
|
57
|
-
url_changed(cur) {
|
|
58
|
-
// fires on shallow/hash/popstate-shallow/404 and full navigations
|
|
59
|
-
// `cur` is the router snapshot: { url: URL, route, params }
|
|
60
|
-
console.log('url_changed', cur.url.href)
|
|
61
|
-
},
|
|
62
|
+
after_navigate,
|
|
62
63
|
})
|
|
63
64
|
|
|
64
65
|
// Long-lived router: history + <a> bindings
|
|
65
66
|
// Also immediately processes the current location
|
|
66
67
|
router.init()
|
|
68
|
+
mount(App, {target: document.body, props})
|
|
67
69
|
```
|
|
68
70
|
|
|
69
71
|
## API
|
|
@@ -74,9 +76,9 @@ Returns: `Router`
|
|
|
74
76
|
|
|
75
77
|
#### `routes`
|
|
76
78
|
|
|
77
|
-
Type: `Array<[pattern: string | RegExp, data
|
|
79
|
+
Type: `Array<[pattern: string | RegExp, data?: any, extra?: any]>`
|
|
78
80
|
|
|
79
|
-
Each route is a tuple whose first item is the pattern and whose second item is hooks (see “Route Hooks”).
|
|
81
|
+
Each route is a tuple whose first item is the pattern and whose second item is hooks (see “Route Hooks”). The optional third item is extra hooks and is merged with the second item (third wins; `param_rules` are merged by key).
|
|
80
82
|
|
|
81
83
|
Supported pattern types:
|
|
82
84
|
|
|
@@ -104,10 +106,8 @@ Notes:
|
|
|
104
106
|
- Awaited after `after_navigate` and before scroll handling; useful for frameworks to flush DOM so anchor/top scrolling lands correctly.
|
|
105
107
|
- `scroll_to_top`: `boolean` (default `true`)
|
|
106
108
|
- When `false`, skips the default top scroll for non-hash navigations.
|
|
107
|
-
- `
|
|
108
|
-
-
|
|
109
|
-
- Receives the router's current snapshot: an object like `{ url: URL, route: RouteTuple|null, params: Params }`.
|
|
110
|
-
- The snapshot type is intentionally `any` and may evolve without a breaking change.
|
|
109
|
+
- `aria_current`: `boolean` (default `false`)
|
|
110
|
+
- When `true`, sets `aria-current="page"` on active in-app links.
|
|
111
111
|
- `preload_delay`: `number` (default `20`)
|
|
112
112
|
- Delay in ms before hover preloading triggers.
|
|
113
113
|
- `preload_on_hover`: `boolean` (default `true`)
|
|
@@ -139,10 +139,11 @@ const {route, is_navigating} = router
|
|
|
139
139
|
|
|
140
140
|
### Route Hooks
|
|
141
141
|
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
|
|
142
|
+
- param_rules?: `Record<string, ((value: string|null|undefined) => boolean) | { validator?: (value: string|null|undefined) => boolean; coercer?: (value: string|null|undefined) => any }>`
|
|
143
|
+
- Single place for param rules. If the value is a function, it is treated as a validator.
|
|
144
|
+
- Validators run on raw params; coercers run after validators and may transform params before `validate(...)`/`loader`.
|
|
145
|
+
- loader?({ params }): `unknown | Promise | Array<unknown|Promise>`
|
|
146
|
+
- Run before URL changes on `link`/`goto`. Results are cached per formatted path and forwarded to `after_navigate`.
|
|
146
147
|
- validate?(params): `boolean | Promise<boolean>`
|
|
147
148
|
- Predicate called during matching. If it returns or resolves to `false`, the route is skipped.
|
|
148
149
|
- before_route_leave?(nav): `(nav: Navigation) => void`
|
|
@@ -152,7 +153,7 @@ The `Navigation` object contains:
|
|
|
152
153
|
|
|
153
154
|
```ts
|
|
154
155
|
{
|
|
155
|
-
type: 'link' | '
|
|
156
|
+
type: 'link' | 'goto' | 'popstate' | 'leave',
|
|
156
157
|
from: { url, params, route } | null,
|
|
157
158
|
to: { url, params, route } | null,
|
|
158
159
|
will_unload: boolean,
|
|
@@ -166,7 +167,7 @@ The `Navigation` object contains:
|
|
|
166
167
|
|
|
167
168
|
- Router calls `before_navigate` on the current route (leave).
|
|
168
169
|
- Call `nav.cancel()` synchronously to cancel.
|
|
169
|
-
- For `link`/`
|
|
170
|
+
- For `link`/`goto`, it stops before URL change.
|
|
170
171
|
- For `popstate`, cancellation causes an automatic `history.go(...)` to revert to the previous index.
|
|
171
172
|
- For `leave`, cancellation triggers the native “Leave site?” dialog (behavior is browser-controlled).
|
|
172
173
|
|
|
@@ -175,15 +176,15 @@ Example:
|
|
|
175
176
|
```js
|
|
176
177
|
const routes = [
|
|
177
178
|
[
|
|
178
|
-
'/
|
|
179
|
+
'/account/:account_id',
|
|
179
180
|
{
|
|
180
|
-
|
|
181
|
-
|
|
181
|
+
param_rules: {
|
|
182
|
+
account_id: {validator: Navgo.validators.int({min: 1}), coercer: Number},
|
|
182
183
|
},
|
|
183
|
-
loader: params => fetch(
|
|
184
|
+
loader: ({params}) => fetch(`/api/account/${params.account_id}`).then(r => r.json()),
|
|
184
185
|
before_route_leave(nav) {
|
|
185
|
-
if (nav.type === 'link' || nav.type === '
|
|
186
|
-
if (!confirm('
|
|
186
|
+
if (nav.type === 'link' || nav.type === 'goto') {
|
|
187
|
+
if (!confirm('Leave account settings?')) nav.cancel()
|
|
187
188
|
}
|
|
188
189
|
},
|
|
189
190
|
},
|
|
@@ -325,7 +326,7 @@ The router passes the type to your route-level `before_route_leave(nav)` hook.
|
|
|
325
326
|
- Named params from string patterns populate `params` with `string` values; optional params that do not appear are `null`.
|
|
326
327
|
- Wildcards use the `'*'` key.
|
|
327
328
|
- RegExp named groups also populate `params`; omitted groups can be `undefined`.
|
|
328
|
-
- If `data.
|
|
329
|
+
- If `data.param_rules` is present, each `params[k]` validator runs first, then coercers run to transform params.
|
|
329
330
|
- If `data.validate(params)` returns or resolves to `false`, the route is also skipped.
|
|
330
331
|
|
|
331
332
|
### Data Flow
|
|
@@ -337,7 +338,7 @@ For `link` and `goto` navigations that match a route:
|
|
|
337
338
|
→ before_route_leave({ type }) // per-route guard
|
|
338
339
|
→ before_navigate(nav) // app-level start
|
|
339
340
|
→ cancelled? yes → stop
|
|
340
|
-
→ no → run loader(params) // may be value, Promise, or Promise[]
|
|
341
|
+
→ no → run loader({ params }) // may be value, Promise, or Promise[]
|
|
341
342
|
→ cache data by formatted path
|
|
342
343
|
→ history.push/replaceState(new URL)
|
|
343
344
|
→ after_navigate(nav)
|
|
@@ -345,8 +346,8 @@ For `link` and `goto` navigations that match a route:
|
|
|
345
346
|
→ scroll restore/hash/top
|
|
346
347
|
```
|
|
347
348
|
|
|
348
|
-
- If a loader throws/rejects, navigation
|
|
349
|
-
- For `popstate`, the route's `loader` runs before completion so content matches the target entry; this improves scroll restoration.
|
|
349
|
+
- If a loader throws/rejects, navigation aborts (no URL/history change, no `after_navigate`).
|
|
350
|
+
- For `popstate`, the route's `loader` runs before completion so content matches the target entry; this improves scroll restoration. If the loader throws, the popstate is reverted.
|
|
350
351
|
|
|
351
352
|
### Shallow Routing
|
|
352
353
|
|
|
@@ -369,7 +370,7 @@ To enable `popstate` cancellation, Navgo stores a monotonic `idx` in `history.st
|
|
|
369
370
|
Navgo manages scroll manually (sets `history.scrollRestoration = 'manual'`) and applies SvelteKit-like behavior:
|
|
370
371
|
|
|
371
372
|
- Saves the current scroll position for the active history index.
|
|
372
|
-
- On `link`/`
|
|
373
|
+
- On `link`/`goto` (after route commit):
|
|
373
374
|
- If the URL has a `#hash`, scroll to the matching element `id` or `[name="..."]`.
|
|
374
375
|
- Otherwise, scroll to the top `(0, 0)`.
|
|
375
376
|
- On `popstate`: restore the saved position for the target history index; if not found but there is a `#hash`, scroll to the anchor instead.
|
|
@@ -398,7 +399,7 @@ scroll flow
|
|
|
398
399
|
- `Navgo.validators.int({ min?, max? })` -- `true` iff the value is an integer within optional bounds.
|
|
399
400
|
- `Navgo.validators.one_of(iterable)` -- `true` iff the value is in the provided set.
|
|
400
401
|
|
|
401
|
-
Attach validators via a route tuple's `data.
|
|
402
|
+
Attach validators via a route tuple's `data.param_rules` rules.
|
|
402
403
|
|
|
403
404
|
# Credits
|
|
404
405
|
|