@inglorious/web 2.2.3 → 2.4.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 +46 -0
- package/package.json +4 -4
- package/src/index.js +2 -0
- package/src/mount.js +12 -11
- package/src/router.js +166 -91
- package/src/router.test.js +52 -8
- package/types/router.d.ts +41 -22
package/README.md
CHANGED
|
@@ -443,6 +443,36 @@ api.notify("navigate", "/users/456")
|
|
|
443
443
|
api.notify("navigate", -1)
|
|
444
444
|
```
|
|
445
445
|
|
|
446
|
+
### 4. Lazy Loading Routes
|
|
447
|
+
|
|
448
|
+
You can improve performance by lazy-loading routes. Instead of a string, provide a function that returns a dynamic import.
|
|
449
|
+
|
|
450
|
+
**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.
|
|
451
|
+
|
|
452
|
+
```javascript
|
|
453
|
+
// store.js
|
|
454
|
+
const entities = {
|
|
455
|
+
router: {
|
|
456
|
+
type: "router",
|
|
457
|
+
routes: {
|
|
458
|
+
"/": "homePage",
|
|
459
|
+
// Lazy load: returns a Promise resolving to a module
|
|
460
|
+
"/admin": () => import("./pages/admin.js"),
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
```javascript
|
|
467
|
+
// pages/admin.js
|
|
468
|
+
import { html } from "@inglorious/web"
|
|
469
|
+
|
|
470
|
+
// Must be a named export matching the type name you want to use
|
|
471
|
+
export const adminPage = {
|
|
472
|
+
render: () => html`<h1>Admin Area</h1>`,
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
446
476
|
---
|
|
447
477
|
|
|
448
478
|
## Table
|
|
@@ -688,6 +718,8 @@ import {
|
|
|
688
718
|
createStore,
|
|
689
719
|
createDevtools,
|
|
690
720
|
createSelector,
|
|
721
|
+
// from @inglorious/store/test
|
|
722
|
+
trigger,
|
|
691
723
|
// from lit-html
|
|
692
724
|
mount,
|
|
693
725
|
html,
|
|
@@ -699,6 +731,7 @@ import {
|
|
|
699
731
|
ref,
|
|
700
732
|
repeat,
|
|
701
733
|
styleMap,
|
|
734
|
+
unsafeHTML,
|
|
702
735
|
when,
|
|
703
736
|
// router stuff
|
|
704
737
|
router,
|
|
@@ -771,6 +804,19 @@ You can even mix them in the same app!
|
|
|
771
804
|
|
|
772
805
|
---
|
|
773
806
|
|
|
807
|
+
## Examples
|
|
808
|
+
|
|
809
|
+
Check out these demos to see `@inglorious/web` in action:
|
|
810
|
+
|
|
811
|
+
- **[Web TodoMVC](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-todomvc)** - A client-only TodoMVC implementation, a good starting point for learning the framework.
|
|
812
|
+
- **[Web TodoMVC-CS](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-todomvc-cs)** - A client-server version with JSON server, showing async event handlers and API integration with component organization (render/handlers modules).
|
|
813
|
+
- **[Web Form](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-form)** - Form handling with validation, arrays, and field helpers.
|
|
814
|
+
- **[Web List](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-list)** - Virtualized list with `renderItem` helper for efficient rendering of large datasets.
|
|
815
|
+
- **[Web Table](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-table)** - Table component with complex data display patterns.
|
|
816
|
+
- **[Web Router](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-router)** - Entity-based client-side routing with hash navigation.
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
774
820
|
## License
|
|
775
821
|
|
|
776
822
|
**MIT License - Free and open source**
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/web",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/IngloriousCoderz/inglorious-
|
|
9
|
+
"url": "git+https://github.com/IngloriousCoderz/inglorious-forge.git",
|
|
10
10
|
"directory": "packages/web"
|
|
11
11
|
},
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/IngloriousCoderz/inglorious-
|
|
13
|
+
"url": "https://github.com/IngloriousCoderz/inglorious-forge/issues"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
16
|
"lit-html",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"lit-html": "^3.3.1",
|
|
41
41
|
"@inglorious/utils": "3.7.0",
|
|
42
|
-
"@inglorious/store": "7.1.
|
|
42
|
+
"@inglorious/store": "7.1.4"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"prettier": "^3.6.2",
|
package/src/index.js
CHANGED
|
@@ -6,10 +6,12 @@ export { table } from "./table.js"
|
|
|
6
6
|
export { createStore } from "@inglorious/store"
|
|
7
7
|
export { createDevtools } from "@inglorious/store/client/devtools.js"
|
|
8
8
|
export { createSelector } from "@inglorious/store/select.js"
|
|
9
|
+
export { trigger } from "@inglorious/store/test"
|
|
9
10
|
export { html, render, svg } from "lit-html"
|
|
10
11
|
export { choose } from "lit-html/directives/choose.js"
|
|
11
12
|
export { classMap } from "lit-html/directives/class-map.js"
|
|
12
13
|
export { ref } from "lit-html/directives/ref.js"
|
|
13
14
|
export { repeat } from "lit-html/directives/repeat.js"
|
|
14
15
|
export { styleMap } from "lit-html/directives/style-map.js"
|
|
16
|
+
export { unsafeHTML } from "lit-html/directives/unsafe-html.js"
|
|
15
17
|
export { when } from "lit-html/directives/when.js"
|
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, state = {} } = options
|
|
111
92
|
|
|
112
93
|
// Numeric navigation (back/forward)
|
|
113
94
|
if (typeof to === "number") {
|
|
@@ -140,54 +121,98 @@ export const router = {
|
|
|
140
121
|
return
|
|
141
122
|
}
|
|
142
123
|
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
124
|
+
// Asynchronous navigation
|
|
125
|
+
if (typeof route.entityType === "function") {
|
|
126
|
+
entity.loading = true
|
|
127
|
+
entity.error = null
|
|
128
|
+
const entityId = entity.id
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const module = await route.entityType()
|
|
132
|
+
api.notify(`#${entityId}:loadSuccess`, {
|
|
133
|
+
module,
|
|
134
|
+
route,
|
|
135
|
+
path,
|
|
136
|
+
replace,
|
|
137
|
+
state,
|
|
138
|
+
})
|
|
139
|
+
} catch (error) {
|
|
140
|
+
api.notify(`#${entityId}:loadError`, { error, path })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return
|
|
161
144
|
}
|
|
162
145
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
146
|
+
// Synchronous navigation
|
|
147
|
+
|
|
148
|
+
doNavigate(
|
|
149
|
+
entity,
|
|
150
|
+
{
|
|
151
|
+
entityType: route.entityType,
|
|
152
|
+
path,
|
|
153
|
+
params: route.params,
|
|
154
|
+
replace,
|
|
155
|
+
state,
|
|
156
|
+
},
|
|
157
|
+
api,
|
|
158
|
+
)
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Handles the successful loading of a lazy route module.
|
|
163
|
+
* @param {RouterEntity} entity - The router entity.
|
|
164
|
+
* @param {{module: object, route: object, path: string, replace: boolean, state: object}} payload - The success payload.
|
|
165
|
+
* @param {Api} api - The application API.
|
|
166
|
+
*/
|
|
167
|
+
loadSuccess(entity, payload, api) {
|
|
168
|
+
const { module, route, path, replace, state } = payload
|
|
169
|
+
|
|
170
|
+
const [[typeName, type]] = Object.entries(module)
|
|
171
|
+
|
|
172
|
+
api.notify("morph", { name: typeName, type })
|
|
166
173
|
|
|
167
|
-
|
|
174
|
+
entity.routes[route.pattern] = typeName
|
|
175
|
+
|
|
176
|
+
// Complete the navigation
|
|
177
|
+
entity.loading = false
|
|
178
|
+
|
|
179
|
+
doNavigate(
|
|
180
|
+
entity,
|
|
181
|
+
{ entityType: typeName, path, params: route.params, replace, state },
|
|
182
|
+
api,
|
|
183
|
+
)
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Handles errors during lazy route loading.
|
|
188
|
+
* @param {RouterEntity} entity - The router entity.
|
|
189
|
+
* @param {{error: Error, path: string}} payload - The error payload.
|
|
190
|
+
*/
|
|
191
|
+
loadError(entity, payload) {
|
|
192
|
+
const { error, path } = payload
|
|
193
|
+
console.error(`Failed to load route ${path}:`, error)
|
|
194
|
+
entity.path = path
|
|
195
|
+
entity.loading = false
|
|
196
|
+
entity.error = error
|
|
168
197
|
},
|
|
169
198
|
|
|
170
199
|
/**
|
|
171
|
-
* Synchronizes the router
|
|
172
|
-
*
|
|
173
|
-
* @param {
|
|
174
|
-
* @param {import('../types/router.js').RouteSyncPayload} payload - The new route state.
|
|
200
|
+
* Synchronizes the router state with the browser's history (e.g., on popstate).
|
|
201
|
+
* @param {RouterEntity} entity - The router entity.
|
|
202
|
+
* @param {{entityType: string, path: string, params: object}} payload - The sync payload.
|
|
175
203
|
*/
|
|
176
204
|
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
|
|
205
|
+
updateRouter(entity, payload)
|
|
182
206
|
},
|
|
183
207
|
}
|
|
184
208
|
|
|
185
209
|
/**
|
|
186
210
|
* Builds a URL path by substituting parameters into a route pattern.
|
|
187
|
-
* Example: `buildPath("/users/:userId", { userId: "123" })` returns `"/users/123"`.
|
|
188
211
|
* @param {string} pattern - The route pattern (e.g., "/users/:userId").
|
|
189
|
-
* @param {
|
|
212
|
+
* @param {Record<string, string>} [params={}] - The parameters to substitute.
|
|
190
213
|
* @returns {string} The constructed path.
|
|
214
|
+
* @example
|
|
215
|
+
* buildPath("/users/:userId", { userId: "123" }) // returns "/users/123"
|
|
191
216
|
*/
|
|
192
217
|
function buildPath(pattern, params = {}) {
|
|
193
218
|
let path = pattern
|
|
@@ -200,9 +225,9 @@ function buildPath(pattern, params = {}) {
|
|
|
200
225
|
/**
|
|
201
226
|
* Finds a matching route configuration for a given URL path.
|
|
202
227
|
* It supports parameterized routes and a fallback "*" route.
|
|
203
|
-
* @param {
|
|
228
|
+
* @param {Record<string, string>} routes - The routes configuration map.
|
|
204
229
|
* @param {string} path - The URL path to match.
|
|
205
|
-
* @returns {{pattern: string, entityType: string, params:
|
|
230
|
+
* @returns {{pattern: string, entityType: string, params: Record<string, string>, path: string}|null}
|
|
206
231
|
* The matched route object or null if no match is found.
|
|
207
232
|
*/
|
|
208
233
|
function findRoute(routes, path) {
|
|
@@ -226,7 +251,7 @@ function findRoute(routes, path) {
|
|
|
226
251
|
* Matches a URL path against a route pattern and extracts any parameters.
|
|
227
252
|
* @param {string} pattern - The route pattern (e.g., "/users/:userId").
|
|
228
253
|
* @param {string} path - The URL path to match (e.g., "/users/123").
|
|
229
|
-
* @returns {
|
|
254
|
+
* @returns {Record<string, string>|null} An object of extracted parameters,
|
|
230
255
|
* or null if the path does not match the pattern.
|
|
231
256
|
*/
|
|
232
257
|
function matchRoute(pattern, path) {
|
|
@@ -246,9 +271,10 @@ function matchRoute(pattern, path) {
|
|
|
246
271
|
|
|
247
272
|
/**
|
|
248
273
|
* Parses a route pattern and extracts the names of its parameters.
|
|
249
|
-
* Example: `getParamNames("/users/:userId/posts/:postId")` returns `["userId", "postId"]`.
|
|
250
274
|
* @param {string} pattern - The route pattern.
|
|
251
275
|
* @returns {string[]} An array of parameter names.
|
|
276
|
+
* @example
|
|
277
|
+
* getParamNames("/users/:userId/posts/:postId") // returns ["userId", "postId"]
|
|
252
278
|
*/
|
|
253
279
|
function getParamNames(pattern) {
|
|
254
280
|
const matches = pattern.match(/:(\w+)/g)
|
|
@@ -266,3 +292,52 @@ function patternToRegex(pattern) {
|
|
|
266
292
|
.replace(/:(\w+)/g, "([^\\/]+)")
|
|
267
293
|
return new RegExp(`^${regexPattern}$`)
|
|
268
294
|
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Performs the actual navigation by updating entity state and browser history.
|
|
298
|
+
* @param {RouterEntity} entity - The router entity.
|
|
299
|
+
* @param {object} options - Navigation options.
|
|
300
|
+
* @param {string} options.entityType - The type of the entity to render.
|
|
301
|
+
* @param {string} options.path - The full path.
|
|
302
|
+
* @param {object} options.params - The route parameters.
|
|
303
|
+
* @param {boolean} [options.replace] - Whether to replace the current history entry.
|
|
304
|
+
* @param {object} [options.state] - Additional state to save in history.
|
|
305
|
+
* @param {Api} api - The application API.
|
|
306
|
+
*/
|
|
307
|
+
function doNavigate(entity, { entityType, path, params, replace, state }, api) {
|
|
308
|
+
updateRouter(entity, { entityType, path, params })
|
|
309
|
+
|
|
310
|
+
// Prepare history state
|
|
311
|
+
const historyState = {
|
|
312
|
+
...state,
|
|
313
|
+
route: entity.route,
|
|
314
|
+
params: entity.params,
|
|
315
|
+
query: entity.query,
|
|
316
|
+
path: entity.path,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Navigate
|
|
320
|
+
const method = replace ? "replaceState" : "pushState"
|
|
321
|
+
history[method](historyState, "", path)
|
|
322
|
+
|
|
323
|
+
api.notify("routeChange", historyState)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Updates the router entity's internal state.
|
|
328
|
+
* @param {RouterEntity} entity - The router entity.
|
|
329
|
+
* @param {object} options - The update options.
|
|
330
|
+
* @param {string} options.entityType - The matched entity type.
|
|
331
|
+
* @param {string} options.path - The full path.
|
|
332
|
+
* @param {object} options.params - The extracted parameters.
|
|
333
|
+
*/
|
|
334
|
+
function updateRouter(entity, { entityType, path, params }) {
|
|
335
|
+
const [pathname, search] = path.split("?")
|
|
336
|
+
const query = search ? Object.fromEntries(new URLSearchParams(search)) : {}
|
|
337
|
+
|
|
338
|
+
entity.path = pathname
|
|
339
|
+
entity.route = entityType
|
|
340
|
+
entity.params = params
|
|
341
|
+
entity.query = query
|
|
342
|
+
entity.hash = window.location.hash
|
|
343
|
+
}
|
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", () => {
|
|
@@ -143,16 +143,60 @@ describe("router", () => {
|
|
|
143
143
|
describe("routeSync()", () => {
|
|
144
144
|
it("should update the entity state from a payload", () => {
|
|
145
145
|
const payload = {
|
|
146
|
-
path: "/new",
|
|
146
|
+
path: "/new?a=1",
|
|
147
147
|
entityType: "newPage",
|
|
148
148
|
params: {},
|
|
149
|
-
query: { a: "1" },
|
|
150
|
-
hash: "#section",
|
|
151
149
|
}
|
|
150
|
+
|
|
151
|
+
vi.spyOn(window, "location", "get").mockReturnValue({ hash: "#section" })
|
|
152
|
+
|
|
152
153
|
router.routeSync(entity, payload)
|
|
154
|
+
|
|
153
155
|
expect(entity.path).toBe("/new")
|
|
154
156
|
expect(entity.route).toBe("newPage")
|
|
155
157
|
expect(entity.query).toEqual({ a: "1" })
|
|
158
|
+
expect(entity.hash).toBe("#section")
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe("loadSuccess()", () => {
|
|
163
|
+
it("should handle lazy loaded modules", () => {
|
|
164
|
+
const module = { myPage: { render: () => {} } }
|
|
165
|
+
const route = { pattern: "/lazy", params: {} }
|
|
166
|
+
const payload = {
|
|
167
|
+
module,
|
|
168
|
+
route,
|
|
169
|
+
path: "/lazy",
|
|
170
|
+
replace: false,
|
|
171
|
+
state: {},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
router.loadSuccess(entity, payload, api)
|
|
175
|
+
|
|
176
|
+
expect(api.notify).toHaveBeenCalledWith("morph", {
|
|
177
|
+
name: "myPage",
|
|
178
|
+
type: module.myPage,
|
|
179
|
+
})
|
|
180
|
+
expect(entity.routes["/lazy"]).toBe("myPage")
|
|
181
|
+
expect(entity.loading).toBe(false)
|
|
182
|
+
expect(entity.route).toBe("myPage")
|
|
183
|
+
expect(history.pushState).toHaveBeenCalled()
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe("loadError()", () => {
|
|
188
|
+
it("should handle load errors", () => {
|
|
189
|
+
const error = new Error("Failed")
|
|
190
|
+
const payload = { error, path: "/lazy" }
|
|
191
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
|
192
|
+
|
|
193
|
+
router.loadError(entity, payload)
|
|
194
|
+
|
|
195
|
+
expect(entity.path).toBe("/lazy")
|
|
196
|
+
expect(entity.loading).toBe(false)
|
|
197
|
+
expect(entity.error).toBe(error)
|
|
198
|
+
|
|
199
|
+
consoleSpy.mockRestore()
|
|
156
200
|
})
|
|
157
201
|
})
|
|
158
202
|
})
|
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
|
}
|