@inglorious/web 2.3.0 → 2.5.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/README.md +37 -0
- package/package.json +2 -2
- package/src/mount.js +12 -11
- package/src/router.js +173 -96
- package/src/router.test.js +66 -8
- package/types/router.d.ts +41 -22
package/README.md
CHANGED
|
@@ -441,6 +441,43 @@ api.notify("navigate", "/users/456")
|
|
|
441
441
|
|
|
442
442
|
// Or navigate back in history
|
|
443
443
|
api.notify("navigate", -1)
|
|
444
|
+
|
|
445
|
+
// With options
|
|
446
|
+
api.notify("navigate", {
|
|
447
|
+
to: "/users/456",
|
|
448
|
+
replace: true, // Replace current history entry
|
|
449
|
+
force: true, // Force navigation even if path is identical
|
|
450
|
+
})
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### 4. Lazy Loading Routes
|
|
454
|
+
|
|
455
|
+
You can improve performance by lazy-loading routes. Instead of a string, provide a function that returns a dynamic import.
|
|
456
|
+
|
|
457
|
+
**Note:** The imported module must use a named export for the entity type (not `export default`), so the router can register it with a unique name in the store.
|
|
458
|
+
|
|
459
|
+
```javascript
|
|
460
|
+
// store.js
|
|
461
|
+
const entities = {
|
|
462
|
+
router: {
|
|
463
|
+
type: "router",
|
|
464
|
+
routes: {
|
|
465
|
+
"/": "homePage",
|
|
466
|
+
// Lazy load: returns a Promise resolving to a module
|
|
467
|
+
"/admin": () => import("./pages/admin.js"),
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
```javascript
|
|
474
|
+
// pages/admin.js
|
|
475
|
+
import { html } from "@inglorious/web"
|
|
476
|
+
|
|
477
|
+
// Must be a named export matching the type name you want to use
|
|
478
|
+
export const adminPage = {
|
|
479
|
+
render: () => html`<h1>Admin Area</h1>`,
|
|
480
|
+
}
|
|
444
481
|
```
|
|
445
482
|
|
|
446
483
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/web",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "A new web framework that leverages the power of the Inglorious Store combined with the performance and simplicity of lit-html.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"lit-html": "^3.3.1",
|
|
41
|
-
"@inglorious/store": "7.1.
|
|
41
|
+
"@inglorious/store": "7.1.4",
|
|
42
42
|
"@inglorious/utils": "3.7.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
package/src/mount.js
CHANGED
|
@@ -11,19 +11,20 @@ export function mount(store, renderFn, element) {
|
|
|
11
11
|
const api = { ...store._api }
|
|
12
12
|
api.render = createRender(api)
|
|
13
13
|
|
|
14
|
-
let renderScheduled = false
|
|
14
|
+
// let renderScheduled = false
|
|
15
15
|
|
|
16
|
-
const scheduleRender = () => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
16
|
+
// const scheduleRender = () => {
|
|
17
|
+
// if (!renderScheduled) {
|
|
18
|
+
// renderScheduled = true
|
|
19
|
+
// requestAnimationFrame(() => {
|
|
20
|
+
// renderScheduled = false
|
|
21
|
+
// })
|
|
22
|
+
// render(renderFn(api), element)
|
|
23
|
+
// }
|
|
24
|
+
// }
|
|
25
25
|
|
|
26
|
-
const unsubscribe = store.subscribe(scheduleRender)
|
|
26
|
+
// const unsubscribe = store.subscribe(scheduleRender)
|
|
27
|
+
const unsubscribe = store.subscribe(() => render(renderFn(api), element))
|
|
27
28
|
store.notify("init")
|
|
28
29
|
|
|
29
30
|
return unsubscribe
|
package/src/router.js
CHANGED
|
@@ -1,71 +1,58 @@
|
|
|
1
|
-
const SKIP_FULL_MATCH_GROUP = 1 // .match() result at index 0 is the full string
|
|
2
|
-
const REMOVE_COLON_PREFIX = 1
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
* @typedef {
|
|
6
|
-
* @
|
|
7
|
-
* @
|
|
8
|
-
* @property {string} [path] - The current URL path.
|
|
9
|
-
* @property {string} [route] - The entity type for the current route.
|
|
10
|
-
* @property {Object.<string, string>} [params] - The parameters extracted from the URL.
|
|
11
|
-
* @property {Object.<string, string>} [query] - The query parameters from the URL.
|
|
12
|
-
* @property {string} [hash] - The URL hash.
|
|
2
|
+
* @typedef {import("../types/router").RouterType} RouterType
|
|
3
|
+
* @typedef {import("../types/router").RouterEntity} RouterEntity
|
|
4
|
+
* @typedef {import("../types/router").Api} Api
|
|
13
5
|
*/
|
|
14
6
|
|
|
7
|
+
const SKIP_FULL_MATCH_GROUP = 1 // .match() result at index 0 is the full string
|
|
8
|
+
const REMOVE_COLON_PREFIX = 1
|
|
9
|
+
|
|
15
10
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @type {import('@inglorious/engine/types/type.js').Type}
|
|
11
|
+
* Client-side router for entity-based systems. Handles URL changes, link interception, and browser history management.
|
|
12
|
+
* @type {RouterType}
|
|
19
13
|
*/
|
|
20
14
|
export const router = {
|
|
21
15
|
/**
|
|
22
|
-
* Initializes the router.
|
|
23
|
-
*
|
|
16
|
+
* Initializes the router entity.
|
|
17
|
+
* Sets up the initial route and event listeners for navigation.
|
|
24
18
|
* @param {RouterEntity} entity - The router entity.
|
|
25
|
-
* @param {
|
|
26
|
-
* @param {
|
|
19
|
+
* @param {void} payload - Unused payload.
|
|
20
|
+
* @param {Api} api - The application API.
|
|
27
21
|
*/
|
|
28
22
|
init(entity, payload, api) {
|
|
29
23
|
// Handle initial route
|
|
30
|
-
const
|
|
24
|
+
const { pathname, search } = window.location
|
|
25
|
+
const initialPath = pathname + search
|
|
31
26
|
const route = findRoute(entity.routes, initialPath)
|
|
27
|
+
const entityId = entity.id
|
|
32
28
|
|
|
33
29
|
if (route) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
new URLSearchParams(window.location.search),
|
|
40
|
-
)
|
|
41
|
-
entity.query = query
|
|
42
|
-
entity.hash = window.location.hash
|
|
30
|
+
api.notify(`#${entityId}:navigate`, {
|
|
31
|
+
to: initialPath,
|
|
32
|
+
params: route.params,
|
|
33
|
+
replace: true,
|
|
34
|
+
})
|
|
43
35
|
}
|
|
44
36
|
|
|
45
|
-
const id = entity.id
|
|
46
37
|
// Listen for browser back/forward
|
|
47
38
|
window.addEventListener("popstate", () => {
|
|
48
|
-
const path = window.location.pathname
|
|
49
|
-
const { routes } = api.getEntity(
|
|
39
|
+
const path = window.location.pathname + window.location.search
|
|
40
|
+
const { routes } = api.getEntity(entityId)
|
|
50
41
|
const route = findRoute(routes, path)
|
|
51
42
|
|
|
52
43
|
if (route) {
|
|
53
|
-
api.notify(
|
|
54
|
-
path: route.path,
|
|
44
|
+
api.notify(`#${entityId}:routeSync`, {
|
|
55
45
|
entityType: route.entityType,
|
|
46
|
+
path,
|
|
56
47
|
params: route.params,
|
|
57
|
-
query: Object.fromEntries(
|
|
58
|
-
new URLSearchParams(window.location.search),
|
|
59
|
-
),
|
|
60
|
-
hash: window.location.hash,
|
|
61
48
|
})
|
|
62
49
|
}
|
|
63
50
|
})
|
|
64
51
|
|
|
65
52
|
// Intercept link clicks
|
|
66
|
-
document.addEventListener("click", (
|
|
53
|
+
document.addEventListener("click", (event) => {
|
|
67
54
|
// Find the closest <a> tag (handles clicks on children)
|
|
68
|
-
const link =
|
|
55
|
+
const link = event.target.closest("a")
|
|
69
56
|
|
|
70
57
|
if (!link) return
|
|
71
58
|
|
|
@@ -84,7 +71,7 @@ export const router = {
|
|
|
84
71
|
if (!["http:", "https:"].includes(link.protocol)) return
|
|
85
72
|
|
|
86
73
|
// Prevent default and use router
|
|
87
|
-
|
|
74
|
+
event.preventDefault()
|
|
88
75
|
|
|
89
76
|
const path = link.pathname + link.search + link.hash
|
|
90
77
|
api.notify("navigate", path)
|
|
@@ -92,22 +79,16 @@ export const router = {
|
|
|
92
79
|
},
|
|
93
80
|
|
|
94
81
|
/**
|
|
95
|
-
*
|
|
82
|
+
* Handles navigation to a new route.
|
|
96
83
|
* @param {RouterEntity} entity - The router entity.
|
|
97
|
-
* @param {string|number|
|
|
98
|
-
*
|
|
99
|
-
* @param {string|number} payload.to - The destination path or history offset.
|
|
100
|
-
* @param {Object} [payload.params] - Route parameters to build the path from a pattern.
|
|
101
|
-
* @param {boolean} [payload.replace] - If true, uses `history.replaceState` instead of `pushState`.
|
|
102
|
-
* @param {Object} [payload.state] - Additional state to store in the browser's history.
|
|
103
|
-
* @param {import('../types/router.js').RouterApi} api - The router API.
|
|
84
|
+
* @param {string|number|{to: string, params?: object, replace?: boolean, state?: object}} payload - The navigation target or options.
|
|
85
|
+
* @param {Api} api - The application API.
|
|
104
86
|
*/
|
|
105
|
-
navigate(entity, payload, api) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const { to, params, replace, state = {} } = payload
|
|
87
|
+
async navigate(entity, payload, api) {
|
|
88
|
+
const options = ["number", "string"].includes(typeof payload)
|
|
89
|
+
? { to: payload }
|
|
90
|
+
: payload
|
|
91
|
+
const { to, params, replace, force, state = {} } = options
|
|
111
92
|
|
|
112
93
|
// Numeric navigation (back/forward)
|
|
113
94
|
if (typeof to === "number") {
|
|
@@ -133,61 +114,107 @@ export const router = {
|
|
|
133
114
|
return
|
|
134
115
|
}
|
|
135
116
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
117
|
+
if (!force) {
|
|
118
|
+
// Prevent navigation if the full path (including query/hash) is identical.
|
|
119
|
+
const currentFullPath =
|
|
120
|
+
entity.path + window.location.search + window.location.hash
|
|
121
|
+
if (path === currentFullPath) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
141
124
|
}
|
|
142
125
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
126
|
+
// Asynchronous navigation
|
|
127
|
+
if (typeof route.entityType === "function") {
|
|
128
|
+
entity.loading = true
|
|
129
|
+
entity.error = null
|
|
130
|
+
const entityId = entity.id
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const module = await route.entityType()
|
|
134
|
+
api.notify(`#${entityId}:loadSuccess`, {
|
|
135
|
+
module,
|
|
136
|
+
route,
|
|
137
|
+
path,
|
|
138
|
+
replace,
|
|
139
|
+
state,
|
|
140
|
+
})
|
|
141
|
+
} catch (error) {
|
|
142
|
+
api.notify(`#${entityId}:loadError`, { error, path })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return
|
|
161
146
|
}
|
|
162
147
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
148
|
+
// Synchronous navigation
|
|
149
|
+
|
|
150
|
+
doNavigate(
|
|
151
|
+
entity,
|
|
152
|
+
{
|
|
153
|
+
entityType: route.entityType,
|
|
154
|
+
path,
|
|
155
|
+
params: route.params,
|
|
156
|
+
replace,
|
|
157
|
+
state,
|
|
158
|
+
},
|
|
159
|
+
api,
|
|
160
|
+
)
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Handles the successful loading of a lazy route module.
|
|
165
|
+
* @param {RouterEntity} entity - The router entity.
|
|
166
|
+
* @param {{module: object, route: object, path: string, replace: boolean, state: object}} payload - The success payload.
|
|
167
|
+
* @param {Api} api - The application API.
|
|
168
|
+
*/
|
|
169
|
+
loadSuccess(entity, payload, api) {
|
|
170
|
+
const { module, route, path, replace, state } = payload
|
|
171
|
+
|
|
172
|
+
const [[typeName, type]] = Object.entries(module)
|
|
166
173
|
|
|
167
|
-
api.notify("
|
|
174
|
+
api.notify("morph", { name: typeName, type })
|
|
175
|
+
|
|
176
|
+
entity.routes[route.pattern] = typeName
|
|
177
|
+
|
|
178
|
+
// Complete the navigation
|
|
179
|
+
entity.loading = false
|
|
180
|
+
|
|
181
|
+
doNavigate(
|
|
182
|
+
entity,
|
|
183
|
+
{ entityType: typeName, path, params: route.params, replace, state },
|
|
184
|
+
api,
|
|
185
|
+
)
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Handles errors during lazy route loading.
|
|
190
|
+
* @param {RouterEntity} entity - The router entity.
|
|
191
|
+
* @param {{error: Error, path: string}} payload - The error payload.
|
|
192
|
+
*/
|
|
193
|
+
loadError(entity, payload) {
|
|
194
|
+
const { error, path } = payload
|
|
195
|
+
console.error(`Failed to load route ${path}:`, error)
|
|
196
|
+
entity.path = path
|
|
197
|
+
entity.loading = false
|
|
198
|
+
entity.error = error
|
|
168
199
|
},
|
|
169
200
|
|
|
170
201
|
/**
|
|
171
|
-
* Synchronizes the router
|
|
172
|
-
*
|
|
173
|
-
* @param {
|
|
174
|
-
* @param {import('../types/router.js').RouteSyncPayload} payload - The new route state.
|
|
202
|
+
* Synchronizes the router state with the browser's history (e.g., on popstate).
|
|
203
|
+
* @param {RouterEntity} entity - The router entity.
|
|
204
|
+
* @param {{entityType: string, path: string, params: object}} payload - The sync payload.
|
|
175
205
|
*/
|
|
176
206
|
routeSync(entity, payload) {
|
|
177
|
-
entity
|
|
178
|
-
entity.route = payload.entityType
|
|
179
|
-
entity.params = payload.params
|
|
180
|
-
entity.query = payload.query
|
|
181
|
-
entity.hash = payload.hash
|
|
207
|
+
updateRouter(entity, payload)
|
|
182
208
|
},
|
|
183
209
|
}
|
|
184
210
|
|
|
185
211
|
/**
|
|
186
212
|
* Builds a URL path by substituting parameters into a route pattern.
|
|
187
|
-
* Example: `buildPath("/users/:userId", { userId: "123" })` returns `"/users/123"`.
|
|
188
213
|
* @param {string} pattern - The route pattern (e.g., "/users/:userId").
|
|
189
|
-
* @param {
|
|
214
|
+
* @param {Record<string, string>} [params={}] - The parameters to substitute.
|
|
190
215
|
* @returns {string} The constructed path.
|
|
216
|
+
* @example
|
|
217
|
+
* buildPath("/users/:userId", { userId: "123" }) // returns "/users/123"
|
|
191
218
|
*/
|
|
192
219
|
function buildPath(pattern, params = {}) {
|
|
193
220
|
let path = pattern
|
|
@@ -200,9 +227,9 @@ function buildPath(pattern, params = {}) {
|
|
|
200
227
|
/**
|
|
201
228
|
* Finds a matching route configuration for a given URL path.
|
|
202
229
|
* It supports parameterized routes and a fallback "*" route.
|
|
203
|
-
* @param {
|
|
230
|
+
* @param {Record<string, string>} routes - The routes configuration map.
|
|
204
231
|
* @param {string} path - The URL path to match.
|
|
205
|
-
* @returns {{pattern: string, entityType: string, params:
|
|
232
|
+
* @returns {{pattern: string, entityType: string, params: Record<string, string>, path: string}|null}
|
|
206
233
|
* The matched route object or null if no match is found.
|
|
207
234
|
*/
|
|
208
235
|
function findRoute(routes, path) {
|
|
@@ -226,7 +253,7 @@ function findRoute(routes, path) {
|
|
|
226
253
|
* Matches a URL path against a route pattern and extracts any parameters.
|
|
227
254
|
* @param {string} pattern - The route pattern (e.g., "/users/:userId").
|
|
228
255
|
* @param {string} path - The URL path to match (e.g., "/users/123").
|
|
229
|
-
* @returns {
|
|
256
|
+
* @returns {Record<string, string>|null} An object of extracted parameters,
|
|
230
257
|
* or null if the path does not match the pattern.
|
|
231
258
|
*/
|
|
232
259
|
function matchRoute(pattern, path) {
|
|
@@ -246,9 +273,10 @@ function matchRoute(pattern, path) {
|
|
|
246
273
|
|
|
247
274
|
/**
|
|
248
275
|
* Parses a route pattern and extracts the names of its parameters.
|
|
249
|
-
* Example: `getParamNames("/users/:userId/posts/:postId")` returns `["userId", "postId"]`.
|
|
250
276
|
* @param {string} pattern - The route pattern.
|
|
251
277
|
* @returns {string[]} An array of parameter names.
|
|
278
|
+
* @example
|
|
279
|
+
* getParamNames("/users/:userId/posts/:postId") // returns ["userId", "postId"]
|
|
252
280
|
*/
|
|
253
281
|
function getParamNames(pattern) {
|
|
254
282
|
const matches = pattern.match(/:(\w+)/g)
|
|
@@ -266,3 +294,52 @@ function patternToRegex(pattern) {
|
|
|
266
294
|
.replace(/:(\w+)/g, "([^\\/]+)")
|
|
267
295
|
return new RegExp(`^${regexPattern}$`)
|
|
268
296
|
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Performs the actual navigation by updating entity state and browser history.
|
|
300
|
+
* @param {RouterEntity} entity - The router entity.
|
|
301
|
+
* @param {object} options - Navigation options.
|
|
302
|
+
* @param {string} options.entityType - The type of the entity to render.
|
|
303
|
+
* @param {string} options.path - The full path.
|
|
304
|
+
* @param {object} options.params - The route parameters.
|
|
305
|
+
* @param {boolean} [options.replace] - Whether to replace the current history entry.
|
|
306
|
+
* @param {object} [options.state] - Additional state to save in history.
|
|
307
|
+
* @param {Api} api - The application API.
|
|
308
|
+
*/
|
|
309
|
+
function doNavigate(entity, { entityType, path, params, replace, state }, api) {
|
|
310
|
+
updateRouter(entity, { entityType, path, params })
|
|
311
|
+
|
|
312
|
+
// Prepare history state
|
|
313
|
+
const historyState = {
|
|
314
|
+
...state,
|
|
315
|
+
route: entity.route,
|
|
316
|
+
params: entity.params,
|
|
317
|
+
query: entity.query,
|
|
318
|
+
path: entity.path,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Navigate
|
|
322
|
+
const method = replace ? "replaceState" : "pushState"
|
|
323
|
+
history[method](historyState, "", path)
|
|
324
|
+
|
|
325
|
+
api.notify("routeChange", historyState)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Updates the router entity's internal state.
|
|
330
|
+
* @param {RouterEntity} entity - The router entity.
|
|
331
|
+
* @param {object} options - The update options.
|
|
332
|
+
* @param {string} options.entityType - The matched entity type.
|
|
333
|
+
* @param {string} options.path - The full path.
|
|
334
|
+
* @param {object} options.params - The extracted parameters.
|
|
335
|
+
*/
|
|
336
|
+
function updateRouter(entity, { entityType, path, params }) {
|
|
337
|
+
const [pathname, search] = path.split("?")
|
|
338
|
+
const query = search ? Object.fromEntries(new URLSearchParams(search)) : {}
|
|
339
|
+
|
|
340
|
+
entity.path = pathname
|
|
341
|
+
entity.route = entityType
|
|
342
|
+
entity.params = params
|
|
343
|
+
entity.query = query
|
|
344
|
+
entity.hash = window.location.hash
|
|
345
|
+
}
|
package/src/router.test.js
CHANGED
|
@@ -54,11 +54,11 @@ describe("router", () => {
|
|
|
54
54
|
|
|
55
55
|
router.init(entity, {}, api)
|
|
56
56
|
|
|
57
|
-
expect(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
expect(api.notify).toHaveBeenCalledWith("#router:navigate", {
|
|
58
|
+
to: "/users/123?sort=asc",
|
|
59
|
+
params: { id: "123" },
|
|
60
|
+
replace: true,
|
|
61
|
+
})
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
it("should set up a popstate listener", () => {
|
|
@@ -138,21 +138,79 @@ describe("router", () => {
|
|
|
138
138
|
expect.any(Object),
|
|
139
139
|
)
|
|
140
140
|
})
|
|
141
|
+
|
|
142
|
+
it("should navigate if the path is identical but force is true", () => {
|
|
143
|
+
entity.path = "/users"
|
|
144
|
+
vi.spyOn(window, "location", "get").mockReturnValue({
|
|
145
|
+
pathname: "/users",
|
|
146
|
+
search: "",
|
|
147
|
+
hash: "",
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
router.navigate(entity, { to: "/users", force: true }, api)
|
|
151
|
+
|
|
152
|
+
expect(history.pushState).toHaveBeenCalled()
|
|
153
|
+
expect(api.notify).toHaveBeenCalledWith("routeChange", expect.any(Object))
|
|
154
|
+
})
|
|
141
155
|
})
|
|
142
156
|
|
|
143
157
|
describe("routeSync()", () => {
|
|
144
158
|
it("should update the entity state from a payload", () => {
|
|
145
159
|
const payload = {
|
|
146
|
-
path: "/new",
|
|
160
|
+
path: "/new?a=1",
|
|
147
161
|
entityType: "newPage",
|
|
148
162
|
params: {},
|
|
149
|
-
query: { a: "1" },
|
|
150
|
-
hash: "#section",
|
|
151
163
|
}
|
|
164
|
+
|
|
165
|
+
vi.spyOn(window, "location", "get").mockReturnValue({ hash: "#section" })
|
|
166
|
+
|
|
152
167
|
router.routeSync(entity, payload)
|
|
168
|
+
|
|
153
169
|
expect(entity.path).toBe("/new")
|
|
154
170
|
expect(entity.route).toBe("newPage")
|
|
155
171
|
expect(entity.query).toEqual({ a: "1" })
|
|
172
|
+
expect(entity.hash).toBe("#section")
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe("loadSuccess()", () => {
|
|
177
|
+
it("should handle lazy loaded modules", () => {
|
|
178
|
+
const module = { myPage: { render: () => {} } }
|
|
179
|
+
const route = { pattern: "/lazy", params: {} }
|
|
180
|
+
const payload = {
|
|
181
|
+
module,
|
|
182
|
+
route,
|
|
183
|
+
path: "/lazy",
|
|
184
|
+
replace: false,
|
|
185
|
+
state: {},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
router.loadSuccess(entity, payload, api)
|
|
189
|
+
|
|
190
|
+
expect(api.notify).toHaveBeenCalledWith("morph", {
|
|
191
|
+
name: "myPage",
|
|
192
|
+
type: module.myPage,
|
|
193
|
+
})
|
|
194
|
+
expect(entity.routes["/lazy"]).toBe("myPage")
|
|
195
|
+
expect(entity.loading).toBe(false)
|
|
196
|
+
expect(entity.route).toBe("myPage")
|
|
197
|
+
expect(history.pushState).toHaveBeenCalled()
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe("loadError()", () => {
|
|
202
|
+
it("should handle load errors", () => {
|
|
203
|
+
const error = new Error("Failed")
|
|
204
|
+
const payload = { error, path: "/lazy" }
|
|
205
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
|
206
|
+
|
|
207
|
+
router.loadError(entity, payload)
|
|
208
|
+
|
|
209
|
+
expect(entity.path).toBe("/lazy")
|
|
210
|
+
expect(entity.loading).toBe(false)
|
|
211
|
+
expect(entity.error).toBe(error)
|
|
212
|
+
|
|
213
|
+
consoleSpy.mockRestore()
|
|
156
214
|
})
|
|
157
215
|
})
|
|
158
216
|
})
|
package/types/router.d.ts
CHANGED
|
@@ -66,46 +66,65 @@ export interface RouteSyncPayload {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* API from @inglorious/store
|
|
70
70
|
*/
|
|
71
|
-
export
|
|
72
|
-
/** Retrieves an entity by its ID. */
|
|
73
|
-
getEntity(id: string | number): { routes: RoutesConfig }
|
|
74
|
-
/** Dispatches an event to the system. */
|
|
75
|
-
notify(eventName: "routeSync", payload: RouteSyncPayload): void
|
|
76
|
-
notify(eventName: "navigate", payload: string): void
|
|
77
|
-
}
|
|
71
|
+
export type { Api as StoreApi } from "@inglorious/store"
|
|
78
72
|
|
|
79
73
|
/**
|
|
80
|
-
*
|
|
74
|
+
* Client-side router for entity-based systems. Handles URL changes, link interception, and browser history management.
|
|
81
75
|
*/
|
|
82
|
-
export
|
|
76
|
+
export interface RouterType {
|
|
83
77
|
/**
|
|
84
78
|
* Initializes the router, sets up a popstate listener to handle browser navigation,
|
|
85
79
|
* and intercepts clicks on local links.
|
|
86
80
|
* @param entity The router state entity.
|
|
87
81
|
* @param payload The initialization payload (currently unused).
|
|
88
|
-
* @param api The API for interacting with the
|
|
82
|
+
* @param api The store API for interacting with the system.
|
|
89
83
|
*/
|
|
90
|
-
init(entity: RouterEntity, payload: any, api:
|
|
84
|
+
init(entity: RouterEntity, payload: any, api: StoreApi): void
|
|
91
85
|
|
|
92
86
|
/**
|
|
93
|
-
* Navigates to a new route
|
|
94
|
-
* @param entity The router
|
|
95
|
-
* @param payload The navigation
|
|
96
|
-
*
|
|
87
|
+
* Navigates to a new route, updating the browser's history and the router entity state.
|
|
88
|
+
* @param {RouterEntity} entity - The router entity.
|
|
89
|
+
* @param {string|number|NavigatePayload} payload - The navigation payload.
|
|
90
|
+
* Can be a path string, a number for `history.go()`, or an object with navigation options.
|
|
91
|
+
* @param {string|number} payload.to - The destination path or history offset.
|
|
92
|
+
* @param {RouteParams} [payload.params] - Route parameters to build the path from a pattern.
|
|
93
|
+
* @param {boolean} [payload.replace] - If true, uses `history.replaceState` instead of `pushState`.
|
|
94
|
+
* @param {Record<string, any>} [payload.state] - Additional state to store in the browser's history.
|
|
95
|
+
* @param {StoreApi} api - The store API.
|
|
97
96
|
*/
|
|
98
97
|
navigate(
|
|
99
98
|
entity: RouterEntity,
|
|
100
99
|
payload: string | number | NavigatePayload,
|
|
101
|
-
api:
|
|
100
|
+
api: StoreApi,
|
|
102
101
|
): void
|
|
103
102
|
|
|
104
103
|
/**
|
|
105
|
-
* Synchronizes the router entity's state with
|
|
106
|
-
*
|
|
107
|
-
* @param entity The router
|
|
108
|
-
* @param payload The new route
|
|
104
|
+
* Synchronizes the router entity's state with data from a routing event,
|
|
105
|
+
* typically triggered by a `popstate` event (browser back/forward).
|
|
106
|
+
* @param {RouterEntity} entity - The router entity to update.
|
|
107
|
+
* @param {RouteSyncPayload} payload - The new route state.
|
|
108
|
+
* @param {StoreApi} api - The store API.
|
|
109
|
+
*/
|
|
110
|
+
routeSync(
|
|
111
|
+
entity: RouterEntity,
|
|
112
|
+
payload: RouteSyncPayload,
|
|
113
|
+
api: StoreApi,
|
|
114
|
+
): void
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handles successful async route loading.
|
|
118
|
+
* @param {RouterEntity} entity - The router entity to update.
|
|
119
|
+
* @param {Object} payload - The load success payload.
|
|
120
|
+
* @param {StoreApi} api - The store API.
|
|
121
|
+
*/
|
|
122
|
+
loadSuccess(entity: RouterEntity, payload: any, api: StoreApi): void
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Handles async route loading errors.
|
|
126
|
+
* @param {RouterEntity} entity - The router entity.
|
|
127
|
+
* @param {Object} payload - The error payload.
|
|
109
128
|
*/
|
|
110
|
-
|
|
129
|
+
loadError(entity: RouterEntity, payload: any): void
|
|
111
130
|
}
|