@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 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. The entity's `routes` property maps URL patterns to the entity types that represent your pages.
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. Instead of a string, provide a function that returns a dynamic import.
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
- type: "router",
543
- routes: {
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.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",
@@ -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
- const path = window.location.pathname + window.location.search
46
- const route = findRoute(routeConfig, path)
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("navigate", path)
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}:loadSuccess`, {
140
- module,
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}:loadError`, { error, path })
178
+ api.notify(`#${entityId}:routeLoadError`, { error, path })
148
179
  }
149
180
 
150
181
  return
151
182
  }
152
183
 
153
- // Synchronous navigation
184
+ updateRouter(entity, route)
154
185
 
155
- doNavigate(
156
- entity,
157
- {
158
- entityType: route.entityType,
159
- path,
160
- params: route.params,
161
- replace,
162
- state,
163
- },
164
- api,
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 the successful loading of a lazy route module.
170
- * @param {RouterEntity} entity - The router entity.
171
- * @param {{module: object, route: object, path: string, replace: boolean, state: object}} payload - The success payload.
172
- * @param {Api} api - The application API.
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
- loadSuccess(entity, payload, api) {
175
- const { module, route, path, replace, state } = payload
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
- loadError(entity, payload) {
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} path - The URL path to match.
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, path) {
286
- const [pathname] = path.split("?")
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: pathname }
310
+ fallbackRoute = { pattern, entityType, params: {}, path }
292
311
  continue
293
312
  }
294
- const params = matchRoute(pattern, pathname)
313
+
314
+ const params = matchRoute(pattern, path)
295
315
  if (params !== null) {
296
- return { pattern, entityType, params, path: pathname }
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
- * @param {RouterEntity} entity - The router entity to update.
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
- loadSuccess(entity: RouterEntity, payload: any, api: StoreApi): void
130
+ routeLoadSuccess(entity: RouterEntity, payload: any, api: StoreApi): void
125
131
 
126
132
  /**
127
- * Handles async route loading errors.
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
- loadError(entity: RouterEntity, payload: any): void
135
+ routeLoadError(entity: RouterEntity, payload: any): void
132
136
  }
133
137
 
134
138
  /**