@inglorious/web 4.0.0 → 4.0.1
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 +29 -30
- package/package.json +1 -1
- package/src/router/index.js +93 -101
- package/types/router.d.ts +13 -9
package/README.md
CHANGED
|
@@ -367,12 +367,12 @@ No additional configuration is needed.
|
|
|
367
367
|
|
|
368
368
|
### 1. Setup the Router
|
|
369
369
|
|
|
370
|
-
To enable the router, add it to your store's types and create a `router` entity.
|
|
370
|
+
To enable the router, add it to your store's types and create a `router` entity. Register route patterns using the router module helpers (`setRoutes`, `addRoute`) — routes are configured at module level and not stored on the router entity itself.
|
|
371
371
|
|
|
372
372
|
```javascript
|
|
373
373
|
// store.js
|
|
374
374
|
import { createStore, html } from "@inglorious/web"
|
|
375
|
-
import { router } from "@inglorious/web/router"
|
|
375
|
+
import { router, setRoutes } from "@inglorious/web/router"
|
|
376
376
|
|
|
377
377
|
const types = {
|
|
378
378
|
// 1. Add the router type to your store's types
|
|
@@ -395,14 +395,9 @@ const types = {
|
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
const entities = {
|
|
398
|
-
// 3. Create the router entity
|
|
398
|
+
// 3. Create the router entity (no `routes` here)
|
|
399
399
|
router: {
|
|
400
400
|
type: "router",
|
|
401
|
-
routes: {
|
|
402
|
-
"/": "homePage",
|
|
403
|
-
"/users/:id": "userPage",
|
|
404
|
-
"*": "notFoundPage", // Fallback for unmatched routes
|
|
405
|
-
},
|
|
406
401
|
},
|
|
407
402
|
userPage: {
|
|
408
403
|
type: "userPage",
|
|
@@ -411,6 +406,13 @@ const entities = {
|
|
|
411
406
|
}
|
|
412
407
|
|
|
413
408
|
export const store = createStore({ types, entities })
|
|
409
|
+
|
|
410
|
+
// Register routes at module level
|
|
411
|
+
setRoutes({
|
|
412
|
+
"/": "homePage",
|
|
413
|
+
"/users/:id": "userPage",
|
|
414
|
+
"*": "notFoundPage",
|
|
415
|
+
})
|
|
414
416
|
```
|
|
415
417
|
|
|
416
418
|
### 2. Render the Current Route
|
|
@@ -456,22 +458,23 @@ api.notify("navigate", {
|
|
|
456
458
|
|
|
457
459
|
### 4. Lazy Loading Routes
|
|
458
460
|
|
|
459
|
-
You can improve performance by lazy-loading routes.
|
|
461
|
+
You can improve performance by lazy-loading routes. Use a loader function that returns a dynamic import when registering the route via `setRoutes`.
|
|
460
462
|
|
|
461
463
|
**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.
|
|
462
464
|
|
|
463
465
|
```javascript
|
|
464
466
|
// store.js
|
|
465
467
|
const entities = {
|
|
466
|
-
router: {
|
|
467
|
-
type: "router",
|
|
468
|
-
routes: {
|
|
469
|
-
"/": "homePage",
|
|
470
|
-
// Lazy load: returns a Promise resolving to a module
|
|
471
|
-
"/admin": () => import("./pages/admin.js"),
|
|
472
|
-
},
|
|
473
|
-
},
|
|
468
|
+
router: { type: "router" },
|
|
474
469
|
}
|
|
470
|
+
|
|
471
|
+
export const store = createStore({ types, entities })
|
|
472
|
+
|
|
473
|
+
setRoutes({
|
|
474
|
+
"/": "homePage",
|
|
475
|
+
// Lazy load: returns a Promise resolving to a module
|
|
476
|
+
"/admin": () => import("./pages/admin.js"),
|
|
477
|
+
})
|
|
475
478
|
```
|
|
476
479
|
|
|
477
480
|
```javascript
|
|
@@ -538,22 +541,18 @@ const types = {
|
|
|
538
541
|
}
|
|
539
542
|
|
|
540
543
|
const entities = {
|
|
541
|
-
router: {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
"/login": "loginPage",
|
|
545
|
-
"/admin": "adminPage",
|
|
546
|
-
},
|
|
547
|
-
},
|
|
548
|
-
adminPage: {
|
|
549
|
-
type: "adminPage",
|
|
550
|
-
},
|
|
551
|
-
loginPage: {
|
|
552
|
-
type: "loginPage",
|
|
553
|
-
},
|
|
544
|
+
router: { type: "router" },
|
|
545
|
+
adminPage: { type: "adminPage" },
|
|
546
|
+
loginPage: { type: "loginPage" },
|
|
554
547
|
}
|
|
555
548
|
|
|
556
549
|
export const store = createStore({ types, entities })
|
|
550
|
+
|
|
551
|
+
// Register routes via the router module API
|
|
552
|
+
setRoutes({
|
|
553
|
+
"/login": "loginPage",
|
|
554
|
+
"/admin": "adminPage",
|
|
555
|
+
})
|
|
557
556
|
```
|
|
558
557
|
|
|
559
558
|
#### How Type Composition Works
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/web",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1",
|
|
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",
|
package/src/router/index.js
CHANGED
|
@@ -7,7 +7,18 @@
|
|
|
7
7
|
const SKIP_FULL_MATCH_GROUP = 1 // .match() result at index 0 is the full string
|
|
8
8
|
const REMOVE_COLON_PREFIX = 1
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Route configuration map. Keys are route patterns (e.g. `/users/:id`) and
|
|
12
|
+
* values are either a string type name or a loader function that returns a
|
|
13
|
+
* module exporting a type.
|
|
14
|
+
* @type {Record<string, string|function>}
|
|
15
|
+
*/
|
|
10
16
|
const routeConfig = {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Guard to ensure global listeners are only attached once.
|
|
20
|
+
* @type {boolean}
|
|
21
|
+
*/
|
|
11
22
|
let areListenersInitialized = false
|
|
12
23
|
|
|
13
24
|
/**
|
|
@@ -41,18 +52,9 @@ export const router = {
|
|
|
41
52
|
areListenersInitialized = true
|
|
42
53
|
|
|
43
54
|
// Listen for browser back/forward
|
|
44
|
-
window.addEventListener("popstate", () =>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (route) {
|
|
49
|
-
api.notify(`#${entityId}:routeSync`, {
|
|
50
|
-
entityType: route.entityType,
|
|
51
|
-
path,
|
|
52
|
-
params: route.params,
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
})
|
|
55
|
+
window.addEventListener("popstate", () =>
|
|
56
|
+
api.notify(`#${entityId}:popstate`, payload),
|
|
57
|
+
)
|
|
56
58
|
|
|
57
59
|
// Intercept link clicks
|
|
58
60
|
document.addEventListener("click", (event) => {
|
|
@@ -79,10 +81,44 @@ export const router = {
|
|
|
79
81
|
event.preventDefault()
|
|
80
82
|
|
|
81
83
|
const path = link.pathname + link.search + link.hash
|
|
82
|
-
api.notify(
|
|
84
|
+
api.notify(`#${entityId}:navigate`, path)
|
|
83
85
|
})
|
|
84
86
|
},
|
|
85
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Handles browser `popstate` events.
|
|
90
|
+
* Attempts to match the current location to a route and updates the router
|
|
91
|
+
* entity state. If the matched route is lazy (a loader function) it will
|
|
92
|
+
* attempt to load the module first.
|
|
93
|
+
* @param {RouterEntity} entity
|
|
94
|
+
* @param {Object} payload
|
|
95
|
+
* @param {Api} api
|
|
96
|
+
*/
|
|
97
|
+
async popstate(entity, payload, api) {
|
|
98
|
+
const path = window.location.pathname + window.location.search
|
|
99
|
+
const route = findRoute(routeConfig, path)
|
|
100
|
+
const entityId = entity.id
|
|
101
|
+
|
|
102
|
+
if (route) {
|
|
103
|
+
if (typeof route.entityType === "function") {
|
|
104
|
+
entity.isLoading = true
|
|
105
|
+
entity.error = null
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const module = await route.entityType()
|
|
109
|
+
api.notify(`#${entityId}:routeLoadSuccess`, { module, route })
|
|
110
|
+
api.notify(`#${entityId}:popstate`, payload)
|
|
111
|
+
} catch (error) {
|
|
112
|
+
api.notify(`#${entityId}:routeLoadError`, { error, path })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
updateRouter(entity, route)
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
86
122
|
/**
|
|
87
123
|
* Handles navigation to a new route.
|
|
88
124
|
* @param {RouterEntity} entity - The router entity.
|
|
@@ -113,6 +149,7 @@ export const router = {
|
|
|
113
149
|
// The router will match it against patterns in routeConfig
|
|
114
150
|
|
|
115
151
|
const route = findRoute(routeConfig, path)
|
|
152
|
+
const entityId = entity.id
|
|
116
153
|
|
|
117
154
|
if (!route) {
|
|
118
155
|
console.warn(`No route matches path: ${path}`)
|
|
@@ -132,64 +169,55 @@ export const router = {
|
|
|
132
169
|
if (typeof route.entityType === "function") {
|
|
133
170
|
entity.isLoading = true
|
|
134
171
|
entity.error = null
|
|
135
|
-
const entityId = entity.id
|
|
136
172
|
|
|
137
173
|
try {
|
|
138
174
|
const module = await route.entityType()
|
|
139
|
-
api.notify(`#${entityId}:
|
|
140
|
-
|
|
141
|
-
route,
|
|
142
|
-
path,
|
|
143
|
-
replace,
|
|
144
|
-
state,
|
|
145
|
-
})
|
|
175
|
+
api.notify(`#${entityId}:routeLoadSuccess`, { module, route })
|
|
176
|
+
api.notify(`#${entityId}:navigate`, payload)
|
|
146
177
|
} catch (error) {
|
|
147
|
-
api.notify(`#${entityId}:
|
|
178
|
+
api.notify(`#${entityId}:routeLoadError`, { error, path })
|
|
148
179
|
}
|
|
149
180
|
|
|
150
181
|
return
|
|
151
182
|
}
|
|
152
183
|
|
|
153
|
-
|
|
184
|
+
updateRouter(entity, route)
|
|
154
185
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
186
|
+
// Prepare history state
|
|
187
|
+
const historyState = {
|
|
188
|
+
...state,
|
|
189
|
+
route: entity.route,
|
|
190
|
+
params: entity.params,
|
|
191
|
+
query: entity.query,
|
|
192
|
+
path: entity.path,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Navigate
|
|
196
|
+
const method = replace ? "replaceState" : "pushState"
|
|
197
|
+
history[method](historyState, "", path)
|
|
198
|
+
|
|
199
|
+
api.notify("routeChange", historyState)
|
|
166
200
|
},
|
|
167
201
|
|
|
168
202
|
/**
|
|
169
|
-
* Handles
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* @param {
|
|
203
|
+
* Handles successful loading of a lazily-loaded route module.
|
|
204
|
+
* Registers the loaded type in the runtime type registry via `api.setType`
|
|
205
|
+
* and updates the `routeConfig` entry for the pattern.
|
|
206
|
+
* @param {RouterEntity} entity
|
|
207
|
+
* @param {{module: object, route: {pattern: string, entityType: string}}} payload
|
|
208
|
+
* @param {Api} api
|
|
173
209
|
*/
|
|
174
|
-
|
|
175
|
-
const { module, route
|
|
210
|
+
routeLoadSuccess(entity, payload, api) {
|
|
211
|
+
const { module, route } = payload
|
|
176
212
|
|
|
177
213
|
const [typeName, type] = Object.entries(module).find(
|
|
178
214
|
([, type]) => type?.render,
|
|
179
215
|
)
|
|
180
216
|
|
|
181
217
|
api.setType(typeName, type)
|
|
182
|
-
|
|
183
218
|
routeConfig[route.pattern] = typeName
|
|
184
219
|
|
|
185
|
-
// Complete the navigation
|
|
186
220
|
entity.isLoading = false
|
|
187
|
-
|
|
188
|
-
doNavigate(
|
|
189
|
-
entity,
|
|
190
|
-
{ entityType: typeName, path, params: route.params, replace, state },
|
|
191
|
-
api,
|
|
192
|
-
)
|
|
193
221
|
},
|
|
194
222
|
|
|
195
223
|
/**
|
|
@@ -197,22 +225,13 @@ export const router = {
|
|
|
197
225
|
* @param {RouterEntity} entity - The router entity.
|
|
198
226
|
* @param {{error: Error, path: string}} payload - The error payload.
|
|
199
227
|
*/
|
|
200
|
-
|
|
228
|
+
routeLoadError(entity, payload) {
|
|
201
229
|
const { error, path } = payload
|
|
202
230
|
console.error(`Failed to load route ${path}:`, error)
|
|
203
231
|
entity.path = path
|
|
204
232
|
entity.isLoading = false
|
|
205
233
|
entity.error = error
|
|
206
234
|
},
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Synchronizes the router state with the browser's history (e.g., on popstate).
|
|
210
|
-
* @param {RouterEntity} entity - The router entity.
|
|
211
|
-
* @param {{entityType: string, path: string, params: object}} payload - The sync payload.
|
|
212
|
-
*/
|
|
213
|
-
routeSync(entity, payload) {
|
|
214
|
-
updateRouter(entity, payload)
|
|
215
|
-
},
|
|
216
235
|
}
|
|
217
236
|
|
|
218
237
|
/**
|
|
@@ -278,24 +297,29 @@ function buildPath(pattern, params = {}) {
|
|
|
278
297
|
* Finds a matching route configuration for a given URL path.
|
|
279
298
|
* It supports parameterized routes and a fallback "*" route.
|
|
280
299
|
* @param {Record<string, string>} routeConfig - The routes configuration map.
|
|
281
|
-
* @param {string}
|
|
300
|
+
* @param {string} pathname - The URL path to match.
|
|
282
301
|
* @returns {{pattern: string, entityType: string, params: Record<string, string>, path: string}|null}
|
|
283
302
|
* The matched route object or null if no match is found.
|
|
284
303
|
*/
|
|
285
|
-
function findRoute(routeConfig,
|
|
286
|
-
const [
|
|
304
|
+
function findRoute(routeConfig, pathname) {
|
|
305
|
+
const [path, search] = pathname.split("?")
|
|
287
306
|
let fallbackRoute = null
|
|
288
307
|
|
|
289
308
|
for (const [pattern, entityType] of Object.entries(routeConfig)) {
|
|
290
309
|
if (pattern === "*") {
|
|
291
|
-
fallbackRoute = { pattern, entityType, params: {}, path
|
|
310
|
+
fallbackRoute = { pattern, entityType, params: {}, path }
|
|
292
311
|
continue
|
|
293
312
|
}
|
|
294
|
-
|
|
313
|
+
|
|
314
|
+
const params = matchRoute(pattern, path)
|
|
295
315
|
if (params !== null) {
|
|
296
|
-
|
|
316
|
+
const query = search
|
|
317
|
+
? Object.fromEntries(new URLSearchParams(search))
|
|
318
|
+
: {}
|
|
319
|
+
return { pattern, entityType, params, path, query }
|
|
297
320
|
}
|
|
298
321
|
}
|
|
322
|
+
|
|
299
323
|
return fallbackRoute
|
|
300
324
|
}
|
|
301
325
|
|
|
@@ -345,50 +369,18 @@ function patternToRegex(pattern) {
|
|
|
345
369
|
return new RegExp(`^${regexPattern}$`)
|
|
346
370
|
}
|
|
347
371
|
|
|
348
|
-
/**
|
|
349
|
-
* Performs the actual navigation by updating entity state and browser history.
|
|
350
|
-
* @param {RouterEntity} entity - The router entity.
|
|
351
|
-
* @param {object} options - Navigation options.
|
|
352
|
-
* @param {string} options.entityType - The type of the entity to render.
|
|
353
|
-
* @param {string} options.path - The full path.
|
|
354
|
-
* @param {object} options.params - The route parameters.
|
|
355
|
-
* @param {boolean} [options.replace] - Whether to replace the current history entry.
|
|
356
|
-
* @param {object} [options.state] - Additional state to save in history.
|
|
357
|
-
* @param {Api} api - The application API.
|
|
358
|
-
*/
|
|
359
|
-
function doNavigate(entity, { entityType, path, params, replace, state }, api) {
|
|
360
|
-
updateRouter(entity, { entityType, path, params })
|
|
361
|
-
|
|
362
|
-
// Prepare history state
|
|
363
|
-
const historyState = {
|
|
364
|
-
...state,
|
|
365
|
-
route: entity.route,
|
|
366
|
-
params: entity.params,
|
|
367
|
-
query: entity.query,
|
|
368
|
-
path: entity.path,
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Navigate
|
|
372
|
-
const method = replace ? "replaceState" : "pushState"
|
|
373
|
-
history[method](historyState, "", path)
|
|
374
|
-
|
|
375
|
-
api.notify("routeChange", historyState)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
372
|
/**
|
|
379
373
|
* Updates the router entity's internal state.
|
|
380
374
|
* @param {RouterEntity} entity - The router entity.
|
|
381
375
|
* @param {object} options - The update options.
|
|
382
376
|
* @param {string} options.entityType - The matched entity type.
|
|
383
|
-
* @param {string} options.path - The full path.
|
|
384
|
-
* @param {object} options.params - The extracted parameters.
|
|
377
|
+
* @param {string} options.path - The full path (pathname only, no query).
|
|
378
|
+
* @param {object} options.params - The extracted route parameters.
|
|
379
|
+
* @param {object} [options.query] - The parsed query parameters.
|
|
385
380
|
*/
|
|
386
|
-
function updateRouter(entity, { entityType, path, params }) {
|
|
387
|
-
const [pathname, search] = path.split("?")
|
|
388
|
-
const query = search ? Object.fromEntries(new URLSearchParams(search)) : {}
|
|
389
|
-
|
|
390
|
-
entity.path = pathname
|
|
381
|
+
function updateRouter(entity, { entityType, path, params, query }) {
|
|
391
382
|
entity.route = entityType
|
|
383
|
+
entity.path = path
|
|
392
384
|
entity.params = params
|
|
393
385
|
entity.query = query
|
|
394
386
|
entity.hash = window.location.hash
|
package/types/router.d.ts
CHANGED
|
@@ -114,21 +114,25 @@ export interface RouterType {
|
|
|
114
114
|
payload: RouteSyncPayload,
|
|
115
115
|
api: StoreApi,
|
|
116
116
|
): void
|
|
117
|
+
/**
|
|
118
|
+
* Handles browser `popstate` events. May perform async loading for lazy routes.
|
|
119
|
+
*/
|
|
120
|
+
popstate(
|
|
121
|
+
entity: RouterEntity,
|
|
122
|
+
payload: any,
|
|
123
|
+
api: StoreApi,
|
|
124
|
+
): void | Promise<void>
|
|
117
125
|
|
|
118
126
|
/**
|
|
119
|
-
* Handles successful async route loading.
|
|
120
|
-
*
|
|
121
|
-
* @param {Object} payload - The load success payload.
|
|
122
|
-
* @param {StoreApi} api - The store API.
|
|
127
|
+
* Handles successful async route loading for a pattern.
|
|
128
|
+
* Registers the loaded type and updates runtime route config.
|
|
123
129
|
*/
|
|
124
|
-
|
|
130
|
+
routeLoadSuccess(entity: RouterEntity, payload: any, api: StoreApi): void
|
|
125
131
|
|
|
126
132
|
/**
|
|
127
|
-
* Handles
|
|
128
|
-
* @param {RouterEntity} entity - The router entity.
|
|
129
|
-
* @param {Object} payload - The error payload.
|
|
133
|
+
* Handles errors that occurred while loading a lazy route.
|
|
130
134
|
*/
|
|
131
|
-
|
|
135
|
+
routeLoadError(entity: RouterEntity, payload: any): void
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
/**
|