navgo 3.0.3 → 3.0.5
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 +37 -37
- package/index.js +27 -26
- package/package.json +27 -17
- package/readme.md +90 -91
package/index.d.ts
CHANGED
|
@@ -7,23 +7,23 @@ export type Params = Record<string, string | null | undefined>
|
|
|
7
7
|
|
|
8
8
|
/** Built-in validator helpers shape. */
|
|
9
9
|
export interface ValidatorHelpers {
|
|
10
|
-
int(
|
|
10
|
+
int(_opts?: {
|
|
11
11
|
min?: number | null
|
|
12
12
|
max?: number | null
|
|
13
|
-
}): (
|
|
14
|
-
|
|
13
|
+
}): (_value: string | null | undefined) => boolean
|
|
14
|
+
one_of(_values: Iterable<string>): (_value: string | null | undefined) => boolean
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/** Optional per-route hooks recognized by
|
|
17
|
+
/** Optional per-route hooks recognized by Navgo. */
|
|
18
18
|
export interface Hooks {
|
|
19
19
|
/** Validate params with custom per-param validators. Return `false` to skip a match. */
|
|
20
|
-
param_validators?: Record<string, (
|
|
20
|
+
param_validators?: Record<string, (_value: string | null | undefined) => boolean>
|
|
21
21
|
/** Load data for a route before navigation. May return a Promise or an array of values/promises. */
|
|
22
|
-
loaders?(
|
|
22
|
+
loaders?(_params: Params): unknown | Promise<unknown> | Array<unknown | Promise<unknown>>
|
|
23
23
|
/** Predicate used during match(); may be async. If it returns `false`, the route is skipped. */
|
|
24
|
-
validate?(
|
|
24
|
+
validate?(_params: Params): boolean | Promise<boolean>
|
|
25
25
|
/** Route-level navigation guard, called on the current route when leaving it. Synchronous only; call `nav.cancel()` to prevent navigation. */
|
|
26
|
-
|
|
26
|
+
before_route_leave?(_nav: Navigation): void
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface NavigationTarget {
|
|
@@ -39,7 +39,7 @@ export interface Navigation {
|
|
|
39
39
|
type: 'link' | 'goto' | 'popstate' | 'leave'
|
|
40
40
|
from: NavigationTarget | null
|
|
41
41
|
to: NavigationTarget | null
|
|
42
|
-
|
|
42
|
+
will_unload: boolean
|
|
43
43
|
cancelled: boolean
|
|
44
44
|
/** The original browser event that initiated navigation, when available. */
|
|
45
45
|
event?: Event
|
|
@@ -55,30 +55,14 @@ export interface MatchResult<T = unknown> {
|
|
|
55
55
|
params: Params
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
format(url: string): string | false
|
|
61
|
-
/** SvelteKit-like navigation that runs loaders before updating the URL. */
|
|
62
|
-
goto(url: string, opts?: { replace?: boolean }): Promise<void>
|
|
63
|
-
/** Shallow push — updates URL/state without triggering handlers. */
|
|
64
|
-
pushState(url?: string | URL, state?: any): void
|
|
65
|
-
/** Shallow replace — updates URL/state without triggering handlers. */
|
|
66
|
-
replaceState(url?: string | URL, state?: any): void
|
|
67
|
-
/** Manually preload loaders for a URL (deduped). */
|
|
68
|
-
preload(url: string): Promise<unknown | void>
|
|
69
|
-
/** Try to match `url`; returns route tuple and params or `null`. Supports async `validate`. */
|
|
70
|
-
match(url: string): Promise<MatchResult<T> | null>
|
|
71
|
-
/** Attach history + click listeners and immediately process current location. */
|
|
72
|
-
init(): Promise<void>
|
|
73
|
-
/** Remove listeners installed by `init()`. */
|
|
74
|
-
destroy(): void
|
|
75
|
-
}
|
|
58
|
+
// For convenience in docs/types, alias the class instance type
|
|
59
|
+
export type Router<T = unknown> = Navgo<T>
|
|
76
60
|
|
|
77
|
-
/** Router metadata stored under `history.state.
|
|
78
|
-
export interface
|
|
61
|
+
/** Router metadata stored under `history.state.__navgo`. */
|
|
62
|
+
export interface NavgoHistoryMeta {
|
|
79
63
|
/** Monotonic index of the current history entry for scroll restoration. */
|
|
80
64
|
idx: number
|
|
81
|
-
/** Present when the entry was created via shallow `
|
|
65
|
+
/** Present when the entry was created via shallow `push_state`/`replace_state`. */
|
|
82
66
|
shallow?: boolean
|
|
83
67
|
/** Origin of the navigation that created this entry. */
|
|
84
68
|
type?: 'link' | 'goto' | 'popstate'
|
|
@@ -91,20 +75,36 @@ export interface Options {
|
|
|
91
75
|
preload_delay?: number
|
|
92
76
|
/** Disable hover/touch preloading when `false`. Default true. */
|
|
93
77
|
preload_on_hover?: boolean
|
|
94
|
-
/** Global hook fired after per-route `
|
|
95
|
-
before_navigate?(
|
|
78
|
+
/** Global hook fired after per-route `before_route_leave`, before loaders/history change. Can cancel. */
|
|
79
|
+
before_navigate?(_nav: Navigation): void
|
|
96
80
|
/** Global hook fired after routing completes (data loaded, URL updated, handlers run). */
|
|
97
|
-
after_navigate?(
|
|
81
|
+
after_navigate?(_nav: Navigation): void
|
|
98
82
|
/** Global hook fired whenever the URL changes.
|
|
99
83
|
* Triggers for shallow pushes/replaces, hash changes, popstate-shallow, 404s, and full navigations.
|
|
100
84
|
* Receives the router's current snapshot (eg `{ url: URL, route: RouteTuple|null, params: Params }`).
|
|
101
85
|
*/
|
|
102
|
-
url_changed?(
|
|
86
|
+
url_changed?(_payload: any): void
|
|
103
87
|
}
|
|
104
88
|
|
|
105
|
-
/**
|
|
106
|
-
export default class Navgo<T = unknown>
|
|
107
|
-
constructor(
|
|
89
|
+
/** Navgo default export: class-based router. */
|
|
90
|
+
export default class Navgo<T = unknown> {
|
|
91
|
+
constructor(_routes?: Array<RouteTuple<T>>, _opts?: Options)
|
|
92
|
+
/** Format `url` relative to the configured base. */
|
|
93
|
+
format(_url: string): string | false
|
|
94
|
+
/** SvelteKit-like navigation that runs loaders before updating the URL. */
|
|
95
|
+
goto(_url: string, _opts?: { replace?: boolean }): Promise<void>
|
|
96
|
+
/** Shallow push — updates URL/state without triggering handlers. */
|
|
97
|
+
push_state(_url?: string | URL, _state?: any): void
|
|
98
|
+
/** Shallow replace — updates URL/state without triggering handlers. */
|
|
99
|
+
replace_state(_url?: string | URL, _state?: any): void
|
|
100
|
+
/** Manually preload loaders for a URL (deduped). */
|
|
101
|
+
preload(_url: string): Promise<unknown | void>
|
|
102
|
+
/** Try to match `url`; returns route tuple and params or `null`. Supports async `validate`. */
|
|
103
|
+
match(_url: string): Promise<MatchResult<T> | null>
|
|
104
|
+
/** Attach history + click listeners and immediately process current location. */
|
|
105
|
+
init(): Promise<void>
|
|
106
|
+
/** Remove listeners installed by `init()`. */
|
|
107
|
+
destroy(): void
|
|
108
108
|
/** Built-in validator helpers (namespaced). */
|
|
109
109
|
static validators: ValidatorHelpers
|
|
110
110
|
}
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parse } from 'regexparam'
|
|
2
2
|
|
|
3
|
-
const ℹ = (...args) =>
|
|
3
|
+
const ℹ = (...args) => {}
|
|
4
4
|
|
|
5
5
|
export default class Navgo {
|
|
6
6
|
#opts = {
|
|
@@ -67,7 +67,7 @@ export default class Navgo {
|
|
|
67
67
|
// ignore popstate while a hash-originating nav is in flight
|
|
68
68
|
if (this.#hash_navigating) return
|
|
69
69
|
|
|
70
|
-
const st = ev?.state?.
|
|
70
|
+
const st = ev?.state?.__navgo
|
|
71
71
|
ℹ('[🧭 event:popstate]', st)
|
|
72
72
|
// Hash-only or state-only change: pathname+search unchanged -> skip loaders
|
|
73
73
|
const cur = this.#current.url
|
|
@@ -97,7 +97,7 @@ export default class Navgo {
|
|
|
97
97
|
this.#hash_navigating = false
|
|
98
98
|
const prev = history.state && typeof history.state == 'object' ? history.state : {}
|
|
99
99
|
const next_idx = this.#route_idx + 1
|
|
100
|
-
const next_state = { ...prev,
|
|
100
|
+
const next_state = { ...prev, __navgo: { ...prev.__navgo, idx: next_idx } }
|
|
101
101
|
history.replaceState(next_state, '', location.href)
|
|
102
102
|
this.#route_idx = next_idx
|
|
103
103
|
ℹ('[🧭 event:hashchange]', { idx: next_idx, href: location.href })
|
|
@@ -125,7 +125,7 @@ export default class Navgo {
|
|
|
125
125
|
// persist scroll for refresh / session restore
|
|
126
126
|
try {
|
|
127
127
|
sessionStorage.setItem(
|
|
128
|
-
`
|
|
128
|
+
`__navgo_scroll:${location.href}`,
|
|
129
129
|
JSON.stringify({ x: scrollX, y: scrollY }),
|
|
130
130
|
)
|
|
131
131
|
} catch {}
|
|
@@ -135,12 +135,12 @@ export default class Navgo {
|
|
|
135
135
|
const nav = this.#make_nav({
|
|
136
136
|
type: 'leave',
|
|
137
137
|
to: null,
|
|
138
|
-
|
|
138
|
+
will_unload: true,
|
|
139
139
|
event: ev,
|
|
140
140
|
})
|
|
141
|
-
this.#current.route?.[1]?.
|
|
141
|
+
this.#current.route?.[1]?.before_route_leave?.(nav)
|
|
142
142
|
if (nav.cancelled) {
|
|
143
|
-
ℹ('[🧭 navigate]', 'cancelled by
|
|
143
|
+
ℹ('[🧭 navigate]', 'cancelled by before_route_leave during unload')
|
|
144
144
|
ev.preventDefault()
|
|
145
145
|
ev.returnValue = ''
|
|
146
146
|
}
|
|
@@ -163,7 +163,7 @@ export default class Navgo {
|
|
|
163
163
|
#resolve_url_and_path(url_raw) {
|
|
164
164
|
if (url_raw[0] == '/' && !this.#base_rgx.test(url_raw)) url_raw = this.#base + url_raw
|
|
165
165
|
const url = new URL(url_raw, location.href)
|
|
166
|
-
const path = this.format(url.pathname)?.
|
|
166
|
+
const path = this.format(url.pathname).match?.(/[^?#]*/)?.[0]
|
|
167
167
|
ℹ('[🧭 resolve]', { url_in: url_raw, url: url.href, path })
|
|
168
168
|
return path ? { url, path } : null
|
|
169
169
|
}
|
|
@@ -178,7 +178,8 @@ export default class Navgo {
|
|
|
178
178
|
!a.target &&
|
|
179
179
|
!a.download &&
|
|
180
180
|
a.host === location.host &&
|
|
181
|
-
|
|
181
|
+
href[0] != '#' &&
|
|
182
|
+
this.#base_rgx.test(a.pathname)
|
|
182
183
|
? { a, href }
|
|
183
184
|
: null
|
|
184
185
|
}
|
|
@@ -200,7 +201,7 @@ export default class Navgo {
|
|
|
200
201
|
/**
|
|
201
202
|
* @returns {Navigation}
|
|
202
203
|
*/
|
|
203
|
-
#make_nav({ type, from = undefined, to = undefined,
|
|
204
|
+
#make_nav({ type, from = undefined, to = undefined, will_unload = false, event = undefined }) {
|
|
204
205
|
const from_obj =
|
|
205
206
|
from !== undefined
|
|
206
207
|
? from
|
|
@@ -215,7 +216,7 @@ export default class Navgo {
|
|
|
215
216
|
type, // 'link' | 'goto' | 'popstate' | 'leave'
|
|
216
217
|
from: from_obj,
|
|
217
218
|
to,
|
|
218
|
-
|
|
219
|
+
will_unload,
|
|
219
220
|
cancelled: false,
|
|
220
221
|
event,
|
|
221
222
|
cancel() {
|
|
@@ -253,13 +254,13 @@ export default class Navgo {
|
|
|
253
254
|
})
|
|
254
255
|
|
|
255
256
|
//
|
|
256
|
-
//
|
|
257
|
+
// before_route_leave
|
|
257
258
|
//
|
|
258
|
-
this.#current.route?.[1]?.
|
|
259
|
+
this.#current.route?.[1]?.before_route_leave?.(nav)
|
|
259
260
|
if (nav.cancelled) {
|
|
260
261
|
// use history.go to cancel the nav, and jump back to where we are
|
|
261
262
|
if (is_popstate) {
|
|
262
|
-
const new_idx = ev_param?.state?.
|
|
263
|
+
const new_idx = ev_param?.state?.__navgo?.idx
|
|
263
264
|
if (new_idx != null) {
|
|
264
265
|
const delta = new_idx - this.#route_idx
|
|
265
266
|
if (delta) {
|
|
@@ -270,7 +271,7 @@ export default class Navgo {
|
|
|
270
271
|
}
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
|
-
ℹ('[🧭 goto]', 'cancelled by
|
|
274
|
+
ℹ('[🧭 goto]', 'cancelled by before_route_leave')
|
|
274
275
|
return
|
|
275
276
|
}
|
|
276
277
|
|
|
@@ -315,7 +316,7 @@ export default class Navgo {
|
|
|
315
316
|
history.state && typeof history.state == 'object' ? history.state : {}
|
|
316
317
|
const next_state = {
|
|
317
318
|
...prev_state,
|
|
318
|
-
|
|
319
|
+
__navgo: { ...prev_state.__navgo, idx: next_idx, type: nav_type },
|
|
319
320
|
}
|
|
320
321
|
history[(opts.replace ? 'replace' : 'push') + 'State'](next_state, null, url.href)
|
|
321
322
|
ℹ('[🧭 history]', opts.replace ? 'replaceState' : 'pushState', {
|
|
@@ -370,13 +371,13 @@ export default class Navgo {
|
|
|
370
371
|
// save scroll for current index before shallow change
|
|
371
372
|
this.#save_scroll()
|
|
372
373
|
const idx = this.#route_idx + (replace ? 0 : 1)
|
|
373
|
-
const st = { ...state,
|
|
374
|
+
const st = { ...state, __navgo: { ...state?.__navgo, shallow: true, idx } }
|
|
374
375
|
history[(replace ? 'replace' : 'push') + 'State'](st, '', u.href)
|
|
375
|
-
ℹ('[🧭 history]', replace ? '
|
|
376
|
+
ℹ('[🧭 history]', replace ? 'replace_state(shallow)' : 'push_state(shallow)', {
|
|
376
377
|
idx,
|
|
377
378
|
href: u.href,
|
|
378
379
|
})
|
|
379
|
-
// Popstate handler checks state.
|
|
380
|
+
// Popstate handler checks state.__navgo.shallow and skips router processing
|
|
380
381
|
this.#route_idx = idx
|
|
381
382
|
// carry forward current scroll position for the shallow entry so Forward restores correctly
|
|
382
383
|
this.#scroll.set(idx, { x: scrollX, y: scrollY })
|
|
@@ -387,11 +388,11 @@ export default class Navgo {
|
|
|
387
388
|
}
|
|
388
389
|
|
|
389
390
|
/** @param {string|URL} [url] @param {any} [state] */
|
|
390
|
-
|
|
391
|
+
push_state(url, state) {
|
|
391
392
|
this.#commit_shallow(url, state, false)
|
|
392
393
|
}
|
|
393
394
|
/** @param {string|URL} [url] @param {any} [state] */
|
|
394
|
-
|
|
395
|
+
replace_state(url, state) {
|
|
395
396
|
this.#commit_shallow(url, state, true)
|
|
396
397
|
}
|
|
397
398
|
|
|
@@ -522,10 +523,10 @@ export default class Navgo {
|
|
|
522
523
|
}
|
|
523
524
|
|
|
524
525
|
// ensure current history state carries our index
|
|
525
|
-
const cur_idx = history.state?.
|
|
526
|
+
const cur_idx = history.state?.__navgo?.idx
|
|
526
527
|
if (cur_idx == null) {
|
|
527
528
|
const prev = history.state && typeof history.state == 'object' ? history.state : {}
|
|
528
|
-
const next_state = { ...prev,
|
|
529
|
+
const next_state = { ...prev, __navgo: { ...prev.__navgo, idx: this.#route_idx } }
|
|
529
530
|
history.replaceState(next_state, '', location.href)
|
|
530
531
|
ℹ('[🧭 history]', 'init idx', { idx: this.#route_idx })
|
|
531
532
|
} else {
|
|
@@ -567,7 +568,7 @@ export default class Navgo {
|
|
|
567
568
|
const is_initial = ctx && 'from' in ctx ? ctx.from == null : !t
|
|
568
569
|
if (is_initial) {
|
|
569
570
|
try {
|
|
570
|
-
const k = `
|
|
571
|
+
const k = `__navgo_scroll:${location.href}`
|
|
571
572
|
const { x, y } = JSON.parse(sessionStorage.getItem(k))
|
|
572
573
|
sessionStorage.removeItem(k)
|
|
573
574
|
scrollTo(x, y)
|
|
@@ -578,7 +579,7 @@ export default class Navgo {
|
|
|
578
579
|
// 1) On back/forward, restore saved position if available
|
|
579
580
|
if (t === 'popstate') {
|
|
580
581
|
const ev_state = ctx?.state ?? ctx?.event?.state
|
|
581
|
-
const idx = ev_state?.
|
|
582
|
+
const idx = ev_state?.__navgo?.idx
|
|
582
583
|
const target_idx = typeof idx === 'number' ? idx : this.#route_idx - 1
|
|
583
584
|
this.#route_idx = target_idx
|
|
584
585
|
const pos = this.#scroll.get(target_idx)
|
|
@@ -627,7 +628,7 @@ export default class Navgo {
|
|
|
627
628
|
return true
|
|
628
629
|
}
|
|
629
630
|
},
|
|
630
|
-
|
|
631
|
+
one_of(values) {
|
|
631
632
|
const set = new Set(values)
|
|
632
633
|
return v => set.has(v)
|
|
633
634
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "navgo",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/mustafa0x/navgo.git"
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"name": "mustafa j"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"build": "perl -0777 -i -pe 's/console.debug\\(...args\\)/{}/g' index.js",
|
|
19
|
+
"prepublishOnly": "pnpm run build",
|
|
20
|
+
"test": "vitest run index.test.js",
|
|
20
21
|
"test:e2e": "playwright test",
|
|
21
|
-
"
|
|
22
|
-
"types": "pnpm --package=typescript@5.6.3 dlx tsc -p test/types"
|
|
22
|
+
"types": "tsc -p test/types"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"*.d.ts",
|
|
@@ -34,26 +34,27 @@
|
|
|
34
34
|
"regexparam": "^3.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@eslint/js": "^9.
|
|
38
|
-
"@playwright/test": "^1.
|
|
37
|
+
"@eslint/js": "^9.37.0",
|
|
38
|
+
"@playwright/test": "^1.56.0",
|
|
39
39
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
40
|
-
"@tailwindcss/vite": "^4.1.
|
|
40
|
+
"@tailwindcss/vite": "^4.1.14",
|
|
41
41
|
"@tsconfig/svelte": "^5.0.5",
|
|
42
|
-
"@types/node": "^24.
|
|
43
|
-
"eslint": "^9.
|
|
42
|
+
"@types/node": "^24.7.2",
|
|
43
|
+
"eslint": "^9.37.0",
|
|
44
44
|
"eslint-plugin-svelte": "^3.12.4",
|
|
45
45
|
"globals": "^16.4.0",
|
|
46
|
-
"jiti": "^2.6.
|
|
46
|
+
"jiti": "^2.6.1",
|
|
47
47
|
"lightningcss": "^1.30.2",
|
|
48
48
|
"prettier": "^3.6.2",
|
|
49
49
|
"prettier-plugin-svelte": "^3.4.0",
|
|
50
50
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
|
51
|
-
"svelte": "^5.39.
|
|
52
|
-
"tailwindcss": "^4.1.
|
|
51
|
+
"svelte": "^5.39.11",
|
|
52
|
+
"tailwindcss": "^4.1.14",
|
|
53
53
|
"terser": "5.44.0",
|
|
54
|
-
"typescript
|
|
55
|
-
"
|
|
56
|
-
"
|
|
54
|
+
"typescript": "5.9.3",
|
|
55
|
+
"typescript-eslint": "^8.46.0",
|
|
56
|
+
"vite": "^7.1.9",
|
|
57
|
+
"vitest": "^3.2.4"
|
|
57
58
|
},
|
|
58
59
|
"pnpm": {
|
|
59
60
|
"onlyBuiltDependencies": [
|
|
@@ -69,6 +70,15 @@
|
|
|
69
70
|
"quoteProps": "as-needed",
|
|
70
71
|
"bracketSpacing": true,
|
|
71
72
|
"arrowParens": "avoid",
|
|
72
|
-
"useTabs": true
|
|
73
|
+
"useTabs": true,
|
|
74
|
+
"overrides": [
|
|
75
|
+
{
|
|
76
|
+
"files": "*.md",
|
|
77
|
+
"options": {
|
|
78
|
+
"tabWidth": 2,
|
|
79
|
+
"useTabs": false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
]
|
|
73
83
|
}
|
|
74
84
|
}
|
package/readme.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
## Install
|
|
2
2
|
|
|
3
3
|
```
|
|
4
|
-
$ pnpm install --dev
|
|
4
|
+
$ pnpm install --dev navgo
|
|
5
5
|
```
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
@@ -11,51 +11,51 @@ import Navgo from 'navgo'
|
|
|
11
11
|
|
|
12
12
|
// Define routes up front (strings or RegExp)
|
|
13
13
|
const routes = [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
14
|
+
['/', {}],
|
|
15
|
+
['/users/:username', {}],
|
|
16
|
+
['/books/*', {}],
|
|
17
|
+
[/articles\/(?<year>[0-9]{4})/, {}],
|
|
18
|
+
[/privacy|privacy-policy/, {}],
|
|
19
|
+
[
|
|
20
|
+
'/admin',
|
|
21
|
+
{
|
|
22
|
+
// constrain params with built-ins or your own
|
|
23
|
+
param_validators: {
|
|
24
|
+
/* id: Navgo.validators.int({ min: 1 }) */
|
|
25
|
+
},
|
|
26
|
+
// load data before URL changes; result goes to after_navigate(...)
|
|
27
|
+
loaders: params => fetch('/api/admin').then(r => r.json()),
|
|
28
|
+
// per-route guard; cancel synchronously to block nav
|
|
29
|
+
before_route_leave(nav) {
|
|
30
|
+
if ((nav.type === 'link' || nav.type === 'nav') && !confirm('Enter admin?')) {
|
|
31
|
+
nav.cancel()
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
// Create router with options + callbacks
|
|
39
39
|
const router = new Navgo(routes, {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
40
|
+
base: '/',
|
|
41
|
+
before_navigate(nav) {
|
|
42
|
+
// app-level hook before loaders/URL update; may cancel
|
|
43
|
+
console.log('before_navigate', nav.type, '→', nav.to?.url.pathname)
|
|
44
|
+
},
|
|
45
|
+
after_navigate(nav) {
|
|
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
|
+
url_changed(cur) {
|
|
55
|
+
// fires on shallow/hash/popstate-shallow/404 and full navigations
|
|
56
|
+
// `cur` is the router snapshot: { url: URL, route, params }
|
|
57
|
+
console.log('url_changed', cur.url.href)
|
|
58
|
+
},
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
// Long-lived router: history + <a> bindings
|
|
@@ -92,32 +92,32 @@ Notes:
|
|
|
92
92
|
#### `options`
|
|
93
93
|
|
|
94
94
|
- `base`: `string` (default `'/'`)
|
|
95
|
-
|
|
95
|
+
- App base pathname. With or without leading/trailing slashes is accepted.
|
|
96
96
|
- `before_navigate`: `(nav: Navigation) => void`
|
|
97
|
-
|
|
97
|
+
- App-level hook called once per navigation attempt after the per-route guard and before loaders/URL update. May call `nav.cancel()` synchronously to prevent navigation.
|
|
98
98
|
- `after_navigate`: `(nav: Navigation) => void`
|
|
99
|
-
|
|
99
|
+
- App-level hook called after routing completes (URL updated, data loaded). `nav.to.data` holds any loader data.
|
|
100
100
|
- `url_changed`: `(snapshot: any) => void`
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
- Fires on every URL change — shallow `push_state`/`replace_state`, hash changes, `popstate` shallow entries, 404s, and full navigations.
|
|
102
|
+
- Receives the router's current snapshot: an object like `{ url: URL, route: RouteTuple|null, params: Params }`.
|
|
103
|
+
- The snapshot type is intentionally `any` and may evolve without a breaking change.
|
|
104
104
|
- `preload_delay`: `number` (default `20`)
|
|
105
|
-
|
|
105
|
+
- Delay in ms before hover preloading triggers.
|
|
106
106
|
- `preload_on_hover`: `boolean` (default `true`)
|
|
107
|
-
|
|
107
|
+
- When `false`, disables hover/touch preloading.
|
|
108
108
|
|
|
109
|
-
Important:
|
|
109
|
+
Important: Navgo only processes routes that match your `base` path.
|
|
110
110
|
|
|
111
111
|
### Route Hooks
|
|
112
112
|
|
|
113
113
|
- param_validators?: `Record<string, (value: string|null|undefined) => boolean>`
|
|
114
|
-
|
|
114
|
+
- Validate params (e.g., `id: Navgo.validators.int({ min: 1 })`). Any `false` result skips the route.
|
|
115
115
|
- loaders?(params): `unknown | Promise | Array<unknown|Promise>`
|
|
116
|
-
|
|
116
|
+
- Run before URL changes on `link`/`nav`. Results are cached per formatted path and forwarded to `after_navigate`.
|
|
117
117
|
- validate?(params): `boolean | Promise<boolean>`
|
|
118
|
-
|
|
119
|
-
-
|
|
120
|
-
|
|
118
|
+
- Predicate called during matching. If it returns or resolves to `false`, the route is skipped.
|
|
119
|
+
- before_route_leave?(nav): `(nav: Navigation) => void`
|
|
120
|
+
- Guard called once per navigation attempt on the current route (leave). Call `nav.cancel()` synchronously to prevent navigation. For `popstate`, cancellation auto-reverts the history jump.
|
|
121
121
|
|
|
122
122
|
The `Navigation` object contains:
|
|
123
123
|
|
|
@@ -126,7 +126,7 @@ The `Navigation` object contains:
|
|
|
126
126
|
type: 'link' | 'nav' | 'popstate' | 'leave',
|
|
127
127
|
from: { url, params, route } | null,
|
|
128
128
|
to: { url, params, route } | null,
|
|
129
|
-
|
|
129
|
+
will_unload: boolean,
|
|
130
130
|
cancelled: boolean,
|
|
131
131
|
event?: Event,
|
|
132
132
|
cancel(): void
|
|
@@ -137,29 +137,29 @@ The `Navigation` object contains:
|
|
|
137
137
|
|
|
138
138
|
- Router calls `before_navigate` on the current route (leave).
|
|
139
139
|
- Call `nav.cancel()` synchronously to cancel.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
- For `link`/`nav`, it stops before URL change.
|
|
141
|
+
- For `popstate`, cancellation causes an automatic `history.go(...)` to revert to the previous index.
|
|
142
|
+
- For `leave`, cancellation triggers the native “Leave site?” dialog (behavior is browser-controlled).
|
|
143
143
|
|
|
144
144
|
Example:
|
|
145
145
|
|
|
146
146
|
```js
|
|
147
147
|
const routes = [
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
148
|
+
[
|
|
149
|
+
'/admin',
|
|
150
|
+
{
|
|
151
|
+
param_validators: {
|
|
152
|
+
/* ... */
|
|
153
|
+
},
|
|
154
|
+
loaders: params => fetch('/api/admin/stats').then(r => r.json()),
|
|
155
|
+
before_route_leave(nav) {
|
|
156
|
+
if (nav.type === 'link' || nav.type === 'nav') {
|
|
157
|
+
if (!confirm('Enter admin area?')) nav.cancel()
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
['/', {}],
|
|
163
163
|
]
|
|
164
164
|
|
|
165
165
|
const router = new Navgo(routes, { base: '/app' })
|
|
@@ -204,7 +204,7 @@ The desired path to navigate. If it begins with `/` and does not match the confi
|
|
|
204
204
|
Type: `Object`
|
|
205
205
|
|
|
206
206
|
- replace: `Boolean` (default `false`)
|
|
207
|
-
|
|
207
|
+
- When `true`, uses `history.replaceState`; otherwise `history.pushState`.
|
|
208
208
|
|
|
209
209
|
### init()
|
|
210
210
|
|
|
@@ -240,13 +240,13 @@ Returns: `Promise<unknown | void>`
|
|
|
240
240
|
Preload a route's `loaders` data for a given `uri` without navigating. Concurrent calls for the same path are deduped.
|
|
241
241
|
Note: Resolves to `undefined` when the matched route has no `loaders`.
|
|
242
242
|
|
|
243
|
-
###
|
|
243
|
+
### push_state(url?, state?)
|
|
244
244
|
|
|
245
245
|
Returns: `void`
|
|
246
246
|
|
|
247
247
|
Perform a shallow history push: updates the URL/state without triggering route processing.
|
|
248
248
|
|
|
249
|
-
###
|
|
249
|
+
### replace_state(url?, state?)
|
|
250
250
|
|
|
251
251
|
Returns: `void`
|
|
252
252
|
|
|
@@ -266,9 +266,8 @@ This section explains, in detail, how navigation is processed: matching, hooks,
|
|
|
266
266
|
- `goto` — programmatic navigation via `router.goto(...)`.
|
|
267
267
|
- `popstate` — browser back/forward.
|
|
268
268
|
- `leave` — page is unloading (refresh, external navigation, tab close) via `beforeunload`.
|
|
269
|
-
- `pushState` (shallow)?
|
|
270
269
|
|
|
271
|
-
The router passes the type to your route-level `
|
|
270
|
+
The router passes the type to your route-level `before_route_leave(nav)` hook.
|
|
272
271
|
|
|
273
272
|
### Matching and Params
|
|
274
273
|
|
|
@@ -286,7 +285,7 @@ For `link` and `goto` navigations that match a route:
|
|
|
286
285
|
|
|
287
286
|
```
|
|
288
287
|
[click <a>] or [router.goto()]
|
|
289
|
-
→
|
|
288
|
+
→ before_route_leave({ type }) // per-route guard
|
|
290
289
|
→ before_navigate(nav) // app-level start
|
|
291
290
|
→ cancelled? yes → stop
|
|
292
291
|
→ no → run loaders(params) // may be value, Promise, or Promise[]
|
|
@@ -300,10 +299,10 @@ For `link` and `goto` navigations that match a route:
|
|
|
300
299
|
|
|
301
300
|
### Shallow Routing
|
|
302
301
|
|
|
303
|
-
Use `
|
|
302
|
+
Use `push_state(url, state?)` or `replace_state(url, state?)` to update the URL/state without re-running routing logic.
|
|
304
303
|
|
|
305
304
|
```
|
|
306
|
-
|
|
305
|
+
push_state/replace_state (shallow)
|
|
307
306
|
→ updates history.state and URL
|
|
308
307
|
→ router does not process routing on shallow operations
|
|
309
308
|
```
|
|
@@ -312,7 +311,7 @@ This lets you reflect UI state in the URL while deferring route transitions unti
|
|
|
312
311
|
|
|
313
312
|
### History Index & popstate Cancellation
|
|
314
313
|
|
|
315
|
-
To enable `popstate` cancellation,
|
|
314
|
+
To enable `popstate` cancellation, Navgo stores a monotonic `idx` in `history.state.__navgo.idx`. On `popstate`, a cancelled navigation computes the delta between the target and current `idx` and calls `history.go(-delta)` to return to the prior entry.
|
|
316
315
|
|
|
317
316
|
### Scroll Restoration
|
|
318
317
|
|
|
@@ -320,10 +319,10 @@ Navgo manages scroll manually (sets `history.scrollRestoration = 'manual'`) and
|
|
|
320
319
|
|
|
321
320
|
- Saves the current scroll position for the active history index.
|
|
322
321
|
- On `link`/`nav` (after route commit):
|
|
323
|
-
|
|
324
|
-
|
|
322
|
+
- If the URL has a `#hash`, scroll to the matching element `id` or `[name="..."]`.
|
|
323
|
+
- Otherwise, scroll to the top `(0, 0)`.
|
|
325
324
|
- On `popstate`: restore the saved position for the target history index; if not found but there is a `#hash`, scroll to the anchor instead.
|
|
326
|
-
- Shallow `
|
|
325
|
+
- Shallow `push_state`/`replace_state` never adjust scroll (routing is skipped).
|
|
327
326
|
|
|
328
327
|
```
|
|
329
328
|
scroll flow
|
|
@@ -336,16 +335,16 @@ scroll flow
|
|
|
336
335
|
|
|
337
336
|
- `format(uri)` — normalizes a path relative to `base`. Returns `false` when `uri` is outside of `base`.
|
|
338
337
|
- `match(uri)` — returns a Promise of `{ route, params } | null` using string/RegExp patterns and validators. Awaits an async `validate(params)` if provided.
|
|
339
|
-
- `goto(uri, { replace? })` — fires route-level `
|
|
338
|
+
- `goto(uri, { replace? })` — fires route-level `before_route_leave('goto')`, calls global `before_navigate`, saves scroll, runs loaders, pushes/replaces, and completes via `after_navigate`.
|
|
340
339
|
- `init()` — wires global listeners (`popstate`, `pushstate`, `replacestate`, click) and optional hover/tap preloading; immediately processes the current location.
|
|
341
340
|
- `destroy()` — removes listeners added by `init()`.
|
|
342
341
|
- `preload(uri)` — pre-executes a route's `loaders` for a path and caches the result; concurrent calls are deduped.
|
|
343
|
-
- `
|
|
344
|
-
- `
|
|
342
|
+
- `push_state(url?, state?)` — shallow push that updates the URL and `history.state` without route processing.
|
|
343
|
+
- `replace_state(url?, state?)` — shallow replace that updates the URL and `history.state` without route processing.
|
|
345
344
|
|
|
346
345
|
### Built-in Validators
|
|
347
346
|
|
|
348
347
|
- `Navgo.validators.int({ min?, max? })` — `true` iff the value is an integer within optional bounds.
|
|
349
|
-
- `Navgo.validators.
|
|
348
|
+
- `Navgo.validators.one_of(iterable)` — `true` iff the value is in the provided set.
|
|
350
349
|
|
|
351
350
|
Attach validators via a route tuple's `data.param_validators` to constrain matches.
|