@inglorious/web 4.5.1 → 5.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
@@ -17,7 +17,7 @@ Unlike modern frameworks that invent their own languages or rely on signals, pro
17
17
 
18
18
  - **Entity-Based Rendering Model**
19
19
  Each entity type defines its own `render(entity, api)` method.
20
- `api.render(id)` composes the UI by invoking the correct renderer for each entity.
20
+ `api.render(id, typeName?, Type?)` composes the UI by invoking the correct renderer for each entity and can lazily register a missing type.
21
21
 
22
22
  - **Type Composition**
23
23
  Types can be composed as arrays of behaviors, enabling reusable patterns like authentication guards, logging, or any cross-cutting concern.
@@ -306,8 +306,8 @@ const types = {
306
306
  }
307
307
 
308
308
  const entities = {
309
- counter1: { type: "counter", value: 0 },
310
- counter2: { type: "counter", value: 10 },
309
+ counter1: { type: "Counter", value: 0 },
310
+ counter2: { type: "Counter", value: 10 },
311
311
  }
312
312
 
313
313
  export const store = createStore({ types, entities })
@@ -358,7 +358,7 @@ import { counter } from "./types/counter.js"
358
358
 
359
359
  test("increment adds to value", () => {
360
360
  const { entity, events } = trigger(
361
- { type: "counter", id: "counter1", value: 10 },
361
+ { type: "Counter", id: "counter1", value: 10 },
362
362
  counter.increment,
363
363
  { amount: 5 },
364
364
  )
@@ -369,7 +369,7 @@ test("increment adds to value", () => {
369
369
 
370
370
  test("increment dispatches overflow event", () => {
371
371
  const { entity, events } = trigger(
372
- { type: "counter", id: "counter1", value: 99 },
372
+ { type: "Counter", id: "counter1", value: 99 },
373
373
  counter.increment,
374
374
  { amount: 5 },
375
375
  )
@@ -389,7 +389,7 @@ import { counter } from "./types/counter.js"
389
389
 
390
390
  test("counter renders correctly", () => {
391
391
  const entity = {
392
- type: "counter",
392
+ type: "Counter",
393
393
  id: "counter1",
394
394
  value: 42,
395
395
  }
@@ -406,7 +406,7 @@ test("counter renders correctly", () => {
406
406
  })
407
407
 
408
408
  test("counter button has click handler", () => {
409
- const entity = { type: "counter", id: "counter1", value: 10 }
409
+ const entity = { type: "Counter", id: "counter1", value: 10 }
410
410
  const mockApi = { notify: jest.fn() }
411
411
 
412
412
  const template = counter.render(entity, mockApi)
@@ -445,12 +445,12 @@ That's it. No `@testing-library/react`, no `renderHook`, no `act()` wrappers.
445
445
 
446
446
  ```javascript
447
447
  import { trigger } from "@inglorious/web/test"
448
- import { todo } from "./types/todo.js"
448
+ import { Todo } from "./types/todo.js"
449
449
 
450
450
  test("toggle changes completed status", () => {
451
451
  const { entity } = trigger(
452
- { type: "todo", id: "todo1", text: "Buy milk", completed: false },
453
- todo.toggle,
452
+ { type: "Todo", id: "todo1", text: "Buy milk", completed: false },
453
+ Todo.toggle,
454
454
  )
455
455
 
456
456
  expect(entity.completed).toBe(true)
@@ -458,8 +458,8 @@ test("toggle changes completed status", () => {
458
458
 
459
459
  test("delete dispatches remove event", () => {
460
460
  const { events } = trigger(
461
- { type: "todo", id: "todo1", text: "Buy milk" },
462
- todo.delete,
461
+ { type: "Todo", id: "todo1", text: "Buy milk" },
462
+ Todo.delete,
463
463
  )
464
464
 
465
465
  expect(events).toEqual([{ type: "#todoList:removeTodo", payload: "todo1" }])
@@ -470,17 +470,17 @@ test("delete dispatches remove event", () => {
470
470
 
471
471
  ```javascript
472
472
  import { render } from "@inglorious/web/test"
473
- import { todo } from "./types/todo.js"
473
+ import { Todo } from "./types/todo.js"
474
474
 
475
475
  test("todo renders text and status", () => {
476
476
  const entity = {
477
- type: "todo",
477
+ type: "Todo",
478
478
  id: "todo1",
479
479
  text: "Buy milk",
480
480
  completed: false,
481
481
  }
482
482
 
483
- const html = render(todo.render(entity, { notify: jest.fn() }))
483
+ const html = render(Todo.render(entity, { notify: jest.fn() }))
484
484
 
485
485
  expect(html).toContain("Buy milk")
486
486
  expect(html).not.toContain("completed")
@@ -488,13 +488,13 @@ test("todo renders text and status", () => {
488
488
 
489
489
  test("completed todo has completed class", () => {
490
490
  const entity = {
491
- type: "todo",
491
+ type: "Todo",
492
492
  id: "todo1",
493
493
  text: "Buy milk",
494
494
  completed: true,
495
495
  }
496
496
 
497
- const html = render(todo.render(entity, { notify: jest.fn() }))
497
+ const html = render(Todo.render(entity, { notify: jest.fn() }))
498
498
 
499
499
  expect(html).toContain("completed")
500
500
  })
@@ -504,13 +504,13 @@ test("completed todo has completed class", () => {
504
504
 
505
505
  ```javascript
506
506
  import { createStore } from "@inglorious/web"
507
- import { counter } from "./types/counter.js"
507
+ import { Counter } from "./types/counter.js"
508
508
 
509
509
  test("full user interaction flow", () => {
510
510
  const store = createStore({
511
- types: { counter },
511
+ types: { Counter },
512
512
  entities: {
513
- counter1: { type: "counter", id: "counter1", value: 0 },
513
+ counter1: { type: "Counter", id: "counter1", value: 0 },
514
514
  },
515
515
  })
516
516
 
@@ -595,11 +595,11 @@ Type composition (like route guards) is also easy to test:
595
595
  ```javascript
596
596
  import { trigger } from "@inglorious/web/test"
597
597
  import { requireAuth } from "./guards/require-auth.js"
598
- import { adminPage } from "./pages/admin.js"
598
+ import { AdminPage } from "./pages/admin.js"
599
599
 
600
600
  test("requireAuth blocks unauthenticated access", () => {
601
601
  // Compose the guard with the page type
602
- const guardedPage = requireAuth(adminPage)
602
+ const GuardedPage = requireAuth(AdminPage)
603
603
 
604
604
  // Mock localStorage.getItem to return null (not logged in)
605
605
  const mockApi = {
@@ -607,8 +607,8 @@ test("requireAuth blocks unauthenticated access", () => {
607
607
  }
608
608
 
609
609
  const { events } = trigger(
610
- { type: "adminPage", id: "admin" },
611
- guardedPage.routeChange,
610
+ { type: "GuardedPage", id: "admin" },
611
+ GuardedPage.routeChange,
612
612
  { route: "adminPage" },
613
613
  mockApi,
614
614
  )
@@ -626,12 +626,12 @@ test("requireAuth allows authenticated access", () => {
626
626
  // Mock localStorage.getItem to return user data
627
627
  localStorage.setItem("user", JSON.stringify({ id: 1 }))
628
628
 
629
- const guardedPage = requireAuth(adminPage)
629
+ const GuardedPage = requireAuth(AdminPage)
630
630
  const mockApi = { notify: jest.fn() }
631
631
 
632
632
  const { events } = trigger(
633
- { type: "adminPage", id: "admin" },
634
- guardedPage.routeChange,
633
+ { type: "GuardedPage", id: "admin" },
634
+ GuardedPage.routeChange,
635
635
  { route: "adminPage" },
636
636
  mockApi,
637
637
  )
@@ -694,7 +694,7 @@ To use it:
694
694
  3. Write your render functions using JSX
695
695
 
696
696
  ```jsx
697
- export const counter = {
697
+ export const Counter = {
698
698
  render(entity, api) {
699
699
  return (
700
700
  <div className="counter">
@@ -848,7 +848,7 @@ const title = "Todos"
848
848
  **Generated output (simplified):**
849
849
 
850
850
  ```js
851
- export const app = {
851
+ export const App = {
852
852
  create(entity) {
853
853
  entity.title = "Todos"
854
854
  },
@@ -963,24 +963,24 @@ To enable the router, add it to your store's types and create a `router` entity.
963
963
  ```javascript
964
964
  // store.js
965
965
  import { createStore, html } from "@inglorious/web"
966
- import { router, setRoutes } from "@inglorious/web/router"
966
+ import { Router, setRoutes } from "@inglorious/web/router"
967
967
 
968
968
  const types = {
969
969
  // 1. Add the router type to your store's types
970
- router,
970
+ Router,
971
971
 
972
972
  // 2. Define types for your pages
973
- homePage: {
973
+ HomePage: {
974
974
  render: () => html`<h1>Welcome Home!</h1>`,
975
975
  },
976
- userPage: {
976
+ UserPage: {
977
977
  render: (entity, api) => {
978
978
  // Access route params from the router entity
979
979
  const { params } = api.getEntity("router")
980
980
  return html`<h1>User ${params?.id ?? "Unknown"} - ${entity.username}</h1>`
981
981
  },
982
982
  },
983
- notFoundPage: {
983
+ NotFoundPage: {
984
984
  render: () => html`<h1>404 - Page Not Found</h1>`,
985
985
  },
986
986
  }
@@ -988,10 +988,10 @@ const types = {
988
988
  const entities = {
989
989
  // 3. Create the router entity (no `routes` here)
990
990
  router: {
991
- type: "router",
991
+ type: "Router",
992
992
  },
993
993
  userPage: {
994
- type: "userPage",
994
+ type: "UserPage",
995
995
  username: "Alice",
996
996
  },
997
997
  }
@@ -1056,7 +1056,7 @@ You can improve performance by lazy-loading routes. Use a loader function that r
1056
1056
  ```javascript
1057
1057
  // store.js
1058
1058
  const entities = {
1059
- router: { type: "router" },
1059
+ router: { type: "Router" },
1060
1060
  }
1061
1061
 
1062
1062
  export const store = createStore({ types, entities })
@@ -1073,7 +1073,7 @@ setRoutes({
1073
1073
  import { html } from "@inglorious/web"
1074
1074
 
1075
1075
  // Must be a named export matching the type name you want to use
1076
- export const adminPage = {
1076
+ export const AdminPage = {
1077
1077
  render: () => html`<h1>Admin Area</h1>`,
1078
1078
  }
1079
1079
  ```
@@ -1116,25 +1116,25 @@ export const requireAuth = (type) => ({
1116
1116
  ```javascript
1117
1117
  // store.js
1118
1118
  import { createStore } from "@inglorious/web"
1119
- import { router } from "@inglorious/web/router"
1119
+ import { Router } from "@inglorious/web/router"
1120
1120
  import { requireAuth } from "./guards/require-auth.js"
1121
- import { adminPage } from "./pages/admin.js"
1122
- import { loginPage } from "./pages/login.js"
1121
+ import { AdminPage } from "./pages/admin.js"
1122
+ import { LoginPage } from "./pages/login.js"
1123
1123
 
1124
1124
  const types = {
1125
- router,
1125
+ Router,
1126
1126
 
1127
1127
  // Public page - no guard
1128
- loginPage,
1128
+ LoginPage,
1129
1129
 
1130
1130
  // Protected page - composed with requireAuth guard
1131
- adminPage: [adminPage, requireAuth],
1131
+ AdminPage: [AdminPage, requireAuth],
1132
1132
  }
1133
1133
 
1134
1134
  const entities = {
1135
- router: { type: "router" },
1136
- adminPage: { type: "adminPage" },
1137
- loginPage: { type: "loginPage" },
1135
+ router: { type: "Router" },
1136
+ adminPage: { type: "AdminPage" },
1137
+ loginPage: { type: "LoginPage" },
1138
1138
  }
1139
1139
 
1140
1140
  export const store = createStore({ types, entities })
@@ -1173,10 +1173,10 @@ You can compose multiple guards for fine-grained control:
1173
1173
  ```javascript
1174
1174
  const types = {
1175
1175
  // Require authentication AND admin role
1176
- adminPage: [adminPage, requireAuth, requireAdmin],
1176
+ AdminPage: [AdminPage, requireAuth, requireAdmin],
1177
1177
 
1178
1178
  // Require authentication AND resource ownership
1179
- userProfile: [userProfile, requireAuth, requireOwnership],
1179
+ UserProfile: [UserProfile, requireAuth, requireOwnership],
1180
1180
  }
1181
1181
  ```
1182
1182
 
@@ -1207,7 +1207,7 @@ const logging = (type) => ({
1207
1207
 
1208
1208
  const types = {
1209
1209
  // Compose the counter type with logging
1210
- counter: [counterBase, logging],
1210
+ counter: [CounterBase, logging],
1211
1211
  }
1212
1212
  ```
1213
1213
 
@@ -1248,13 +1248,13 @@ Include `form` in your `types` and create an entity for the form (use any id you
1248
1248
 
1249
1249
  ```javascript
1250
1250
  import { createStore } from "@inglorious/web"
1251
- import { form } from "@inglorious/web/form"
1251
+ import { Form } from "@inglorious/web/form"
1252
1252
 
1253
- const types = { form }
1253
+ const types = { Form }
1254
1254
 
1255
1255
  const entities = {
1256
1256
  form: {
1257
- type: "form",
1257
+ type: "Form",
1258
1258
  initialValues: {
1259
1259
  name: "",
1260
1260
  email: "",
@@ -1324,7 +1324,7 @@ For a complete, working demo and helper components look at `examples/apps/web-fo
1324
1324
 
1325
1325
  ```javascript
1326
1326
  // @acme/design-system/data-grid.js
1327
- export const dataGrid = {
1327
+ export const DataGrid = {
1328
1328
  render(entity, api) {
1329
1329
  const type = api.getType(entity.type)
1330
1330
 
@@ -1348,14 +1348,14 @@ export const dataGrid = {
1348
1348
  **Users customize freely:**
1349
1349
 
1350
1350
  ```javascript
1351
- import { dataGrid } from "@acme/design-system/data-grid"
1351
+ import { DataGrid } from "@acme/design-system/data-grid"
1352
1352
 
1353
1353
  // Use as-is
1354
- const simpleGrid = { ...dataGrid }
1354
+ const SimpleGrid = { ...DataGrid }
1355
1355
 
1356
1356
  // Override methods
1357
- const customGrid = {
1358
- ...dataGrid,
1357
+ const CustomGrid = {
1358
+ ...DataGrid,
1359
1359
  renderRow(entity, row, api) {
1360
1360
  return html`<div class="data-grid-row custom">
1361
1361
  ${row.name} - ${row.email}
@@ -1366,8 +1366,8 @@ const customGrid = {
1366
1366
  // Compose with behaviors
1367
1367
  import { sortable, exportable } from "@acme/design-system/behaviors"
1368
1368
 
1369
- const advancedGrid = [
1370
- dataGrid,
1369
+ const AdvancedGrid = [
1370
+ DataGrid,
1371
1371
  sortable,
1372
1372
  exportable,
1373
1373
  {
@@ -1441,10 +1441,10 @@ import "@shoelace-style/shoelace/dist/components/color-picker/color-picker.js"
1441
1441
 
1442
1442
  const types = {
1443
1443
  // App-specific type - full store integration
1444
- productTable,
1444
+ ProductTable,
1445
1445
 
1446
1446
  // Web Component - for specialized UI
1447
- themeEditor: {
1447
+ ThemeEditor: {
1448
1448
  colorChange(entity, color) {
1449
1449
  entity.primaryColor = color
1450
1450
  },
@@ -1475,7 +1475,7 @@ Let's be real: in a web app you very rarely need to create more than one entity
1475
1475
 
1476
1476
  ```javascript
1477
1477
  const types = {
1478
- app: {
1478
+ App: {
1479
1479
  render(entity, api) {
1480
1480
  return html`
1481
1481
  <div class="app">${api.render("header")} ${api.render("content")}</div>
@@ -1483,13 +1483,13 @@ const types = {
1483
1483
  },
1484
1484
  },
1485
1485
 
1486
- header: {
1486
+ Header: {
1487
1487
  render(entity, api) {
1488
1488
  return html`<header>${entity.title}</header>`
1489
1489
  },
1490
1490
  },
1491
1491
 
1492
- content: {
1492
+ Content: {
1493
1493
  render(entity, api) {
1494
1494
  return html`<main>${entity.text}</main>`
1495
1495
  },
@@ -1498,9 +1498,9 @@ const types = {
1498
1498
 
1499
1499
  // Without autoCreateEntities - you need to define every entity
1500
1500
  const entities = {
1501
- app: { type: "app" },
1502
- header: { type: "header", title: "Welcome" },
1503
- content: { type: "content", text: "Hello World" },
1501
+ app: { type: "App" },
1502
+ header: { type: "Header", title: "Welcome" },
1503
+ content: { type: "Content", text: "Hello World" },
1504
1504
  }
1505
1505
 
1506
1506
  // With autoCreateEntities - entities are created automatically
@@ -1514,7 +1514,7 @@ const store = createStore({
1514
1514
  If you want to initialize an entity with specific data, you can use the `create()` event handler:
1515
1515
 
1516
1516
  ```javascript
1517
- const header = {
1517
+ const Header = {
1518
1518
  create(entity) {
1519
1519
  entity.title = "Welcome"
1520
1520
  },
@@ -1556,9 +1556,9 @@ Connects a store to a `lit-html` template and renders it into a DOM element. It
1556
1556
 
1557
1557
  The `renderFn` receives a powerful `api` object that contains all methods from the store's API (`getEntities`, `getEntity`, `notify`, etc.) plus special methods for the web package.
1558
1558
 
1559
- **`api.render(id, options?)`**
1559
+ **`api.render(id, typeName?, Type?)`**
1560
1560
 
1561
- This method is the cornerstone of entity-based rendering. It looks up an entity by its `id`, finds its corresponding type definition, and calls the `render(entity, api)` method on that type. This allows you to define rendering logic alongside an entity's other behaviors.
1561
+ This method is the cornerstone of entity-based rendering. It looks up an entity by its `id`, finds its corresponding type definition, and calls the `render(entity, api)` method on that type. When you pass `typeName` and `Type`, it will lazily register the type with `api.setType(typeName, Type)` if the type is not already present.
1562
1562
 
1563
1563
  ### Re-exported `lit-html` Utilities
1564
1564
 
@@ -1589,12 +1589,19 @@ import {
1589
1589
 
1590
1590
  // Subpath imports for tree-shaking
1591
1591
  import {
1592
- form,
1592
+ Form,
1593
1593
  getFieldError,
1594
1594
  getFieldValue,
1595
1595
  isFieldTouched,
1596
1596
  } from "@inglorious/web/form"
1597
- import { router } from "@inglorious/web/router"
1597
+ import {
1598
+ Router,
1599
+ getRoutes,
1600
+ getRoute,
1601
+ setRoutes,
1602
+ addRoute,
1603
+ removeRoute,
1604
+ } from "@inglorious/web/router"
1598
1605
  import { render, trigger } from "@inglorious/web/test"
1599
1606
  ```
1600
1607
 
@@ -1607,7 +1614,7 @@ When an entity's `render()` method throws an error, it can crash your entire app
1607
1614
  **Best practice:** Wrap your render logic in try-catch at the entity level:
1608
1615
 
1609
1616
  ```javascript
1610
- const myType = {
1617
+ const MyType = {
1611
1618
  render(entity, api) {
1612
1619
  try {
1613
1620
  // Your render logic
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/web",
3
- "version": "4.5.1",
3
+ "version": "5.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",
@@ -51,8 +51,8 @@
51
51
  "dependencies": {
52
52
  "@lit-labs/ssr-client": "^1.1.8",
53
53
  "lit-html": "^3.3.1",
54
- "@inglorious/store": "9.6.0",
55
- "@inglorious/utils": "3.7.3"
54
+ "@inglorious/utils": "3.8.0",
55
+ "@inglorious/store": "10.0.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "prettier": "^3.6.2",
package/src/form/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as handlers from "./handlers.js"
2
2
  import * as helpers from "./helpers.js"
3
3
 
4
- export const form = { ...handlers }
4
+ export const Form = { ...handlers }
5
5
 
6
6
  export const { getFieldError, getFieldValue, isFieldTouched } = helpers
package/src/mount.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /** @typedef {import('lit-html').TemplateResult} TemplateResult */
2
2
  /** @typedef {import('@inglorious/store').Store} Store */
3
+ /** @typedef {import('@inglorious/store').EntityType} EntityType */
3
4
  /** @typedef {import('../types/mount').Api} Api */
4
5
 
5
6
  import { html, render } from "lit-html"
@@ -9,20 +10,21 @@ import { html, render } from "lit-html"
9
10
  * @param {Store} store - The application state store.
10
11
  * @param {(api: Api) => TemplateResult | null} renderFn - The root render function.
11
12
  * @param {HTMLElement | DocumentFragment} element - The DOM element to mount the template to.
13
+ * @param {Object} [options] - Mount options.
14
+ * @param {boolean} [options.hydrate=true] - Whether to hydrate the existing DOM.
12
15
  * @returns {() => void} An unsubscribe function
13
16
  */
14
- export async function mount(store, renderFn, element) {
17
+ export async function mount(store, renderFn, element, options = {}) {
15
18
  const api = { ...store._api }
16
19
  api.render = createRender(api)
17
20
 
18
- let shouldHydrate = element.hasChildNodes()
21
+ const shouldHydrate = options.hydrate !== false && element.hasChildNodes()
19
22
 
20
23
  // If we need to hydrate, load the heavy lifting only now
21
24
  if (shouldHydrate) {
22
25
  const { hydrate } = await import("@lit-labs/ssr-client")
23
26
  const template = renderFn(api)
24
27
  hydrate(template, element)
25
- shouldHydrate = false
26
28
  }
27
29
 
28
30
  const unsubscribe = store.subscribe(() => {
@@ -42,19 +44,33 @@ export async function mount(store, renderFn, element) {
42
44
  * @private
43
45
  */
44
46
  function createRender(api) {
45
- return function (id) {
47
+ return function (id, typeName, type) {
48
+ registerTypeIfMissing(api, typeName, type)
49
+
46
50
  const entity = api.getEntity(id)
47
51
 
48
52
  if (!entity) {
49
53
  return ""
50
54
  }
51
55
 
52
- const type = api.getType(entity.type)
53
- if (!type?.render) {
56
+ const entityType = api.getType(entity.type)
57
+ if (!entityType?.render) {
54
58
  console.warn(`No render function for type: ${entity.type}`)
55
59
  return html`<div>No renderer for ${entity.type}</div>`
56
60
  }
57
61
 
58
- return type.render(entity, api)
62
+ return entityType.render(entity, api)
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Registers a type definition lazily when the caller provides one to render().
68
+ * @param {Api} api - The mount API.
69
+ * @param {string | undefined} typeName - The type name to register.
70
+ * @param {EntityType | undefined} type - The type definition to register.
71
+ */
72
+ function registerTypeIfMissing(api, typeName, type) {
73
+ if (typeName && type && !api.getType(typeName)) {
74
+ api.setType(typeName, type)
59
75
  }
60
76
  }
@@ -4,6 +4,8 @@
4
4
  * @typedef {import("../../types/router").Api} Api
5
5
  */
6
6
 
7
+ import { toCamelCase } from "@inglorious/utils/data-structures/string.js"
8
+
7
9
  const SKIP_FULL_MATCH_GROUP = 1 // .match() result at index 0 is the full string
8
10
  const REMOVE_COLON_PREFIX = 1
9
11
 
@@ -74,10 +76,6 @@ export function createHandlers(routeConfig) {
74
76
  })
75
77
  },
76
78
 
77
- create(entity) {
78
- entity.path ??= "/"
79
- },
80
-
81
79
  /**
82
80
  * Handles browser `popstate` events.
83
81
  * Attempts to match the current location to a route and updates the router
@@ -93,12 +91,12 @@ export function createHandlers(routeConfig) {
93
91
  const entityId = entity.id
94
92
 
95
93
  if (route) {
96
- if (typeof route.entityType === "function") {
94
+ if (typeof route.route === "function") {
97
95
  entity.isLoading = true
98
96
  entity.error = null
99
97
 
100
98
  try {
101
- const module = await route.entityType()
99
+ const module = await route.route()
102
100
  api.notify(`#${entityId}:routeLoadSuccess`, { module, route })
103
101
  api.notify(`#${entityId}:popstate`, payload)
104
102
  } catch (error) {
@@ -166,12 +164,12 @@ export function createHandlers(routeConfig) {
166
164
  }
167
165
 
168
166
  // Asynchronous navigation
169
- if (typeof route.entityType === "function") {
167
+ if (typeof route.route === "function") {
170
168
  entity.isLoading = true
171
169
  entity.error = null
172
170
 
173
171
  try {
174
- const module = await route.entityType()
172
+ const module = await route.route()
175
173
  api.notify(`#${entityId}:routeLoadSuccess`, { module, route })
176
174
  api.notify(`#${entityId}:navigate`, payload)
177
175
  } catch (error) {
@@ -204,7 +202,7 @@ export function createHandlers(routeConfig) {
204
202
  * Registers the loaded type in the runtime type registry via `api.setType`
205
203
  * and updates the `routeConfig` entry for the pattern.
206
204
  * @param {RouterEntity} entity
207
- * @param {{module: object, route: {pattern: string, entityType: string}}} payload
205
+ * @param {{module: object, route: {pattern: string, route: string}}} payload
208
206
  * @param {Api} api
209
207
  */
210
208
  routeLoadSuccess(entity, payload, api) {
@@ -215,7 +213,8 @@ export function createHandlers(routeConfig) {
215
213
  )
216
214
 
217
215
  api.setType(typeName, type)
218
- routeConfig[route.pattern] = typeName
216
+ const [routeEntity] = api.getEntities(typeName)
217
+ routeConfig[route.pattern] = routeEntity?.id ?? toCamelCase(typeName)
219
218
 
220
219
  entity.isLoading = false
221
220
  },
@@ -263,9 +262,9 @@ function findRoute(routeConfig, pathname) {
263
262
  const [path, search] = pathname.split("?")
264
263
  let fallbackRoute = null
265
264
 
266
- for (const [pattern, entityType] of Object.entries(routeConfig)) {
265
+ for (const [pattern, route] of Object.entries(routeConfig)) {
267
266
  if (pattern === "*") {
268
- fallbackRoute = { pattern, entityType, params: {}, path }
267
+ fallbackRoute = { pattern, route, params: {}, path }
269
268
  continue
270
269
  }
271
270
 
@@ -274,7 +273,7 @@ function findRoute(routeConfig, pathname) {
274
273
  const query = search
275
274
  ? Object.fromEntries(new URLSearchParams(search))
276
275
  : {}
277
- return { pattern, entityType, params, path, query }
276
+ return { pattern, route, params, path, query }
278
277
  }
279
278
  }
280
279
 
@@ -331,13 +330,13 @@ function patternToRegex(pattern) {
331
330
  * Updates the router entity's internal state.
332
331
  * @param {RouterEntity} entity - The router entity.
333
332
  * @param {object} options - The update options.
334
- * @param {string} options.entityType - The matched entity type.
333
+ * @param {string} options.route - The matched entity.
335
334
  * @param {string} options.path - The full path (pathname only, no query).
336
335
  * @param {object} options.params - The extracted route parameters.
337
336
  * @param {object} [options.query] - The parsed query parameters.
338
337
  */
339
- function updateRouter(entity, { entityType, path, params, query }) {
340
- entity.route = entityType
338
+ function updateRouter(entity, { route, path, params, query }) {
339
+ entity.route = route
341
340
  entity.path = path
342
341
  entity.params = params
343
342
  entity.query = query
@@ -9,7 +9,7 @@ import { createHelpers } from "./helpers.js"
9
9
  */
10
10
  const routeConfig = {}
11
11
 
12
- export const router = { ...createHandlers(routeConfig) }
12
+ export const Router = { ...createHandlers(routeConfig) }
13
13
 
14
14
  export const { getRoutes, getRoute, setRoutes, addRoute, removeRoute } =
15
15
  createHelpers(routeConfig)
package/types/form.d.ts CHANGED
@@ -189,7 +189,7 @@ export interface FormValidationErrorPayload {
189
189
  /**
190
190
  * The form type implementation for the entity-based state manager.
191
191
  */
192
- export declare const form: {
192
+ export interface FormType {
193
193
  /**
194
194
  * Initializes the form entity with default state.
195
195
  * @param entity The form entity.
@@ -310,6 +310,8 @@ export declare const form: {
310
310
  ): void
311
311
  }
312
312
 
313
+ export declare const Form: FormType
314
+
313
315
  /**
314
316
  * Gets the value of a specific field in the form.
315
317
  * @param form The form entity.
package/types/mount.d.ts CHANGED
@@ -1,13 +1,31 @@
1
1
  import type { TemplateResult } from "lit-html"
2
- import type { BaseEntity, Store, Api as StoreApi } from "@inglorious/store"
2
+ import type {
3
+ BaseEntity,
4
+ EntitiesState,
5
+ EntityType,
6
+ Store,
7
+ Api as StoreApi,
8
+ } from "@inglorious/store"
3
9
 
4
- export type Api = StoreApi & {
10
+ export type Api<
11
+ TEntity extends BaseEntity = BaseEntity,
12
+ TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
13
+ > = StoreApi<TEntity, TState> & {
5
14
  /**
6
15
  * Renders an entity or a type component by its ID.
7
16
  * @param id The ID of the entity or type to render.
17
+ * @param typeName Optional type name to lazy-register.
18
+ * @param type Optional type definition to register when typeName is provided.
8
19
  * @returns The rendered template or an empty string if not found.
9
20
  */
10
- render: (id: string) => TemplateResult | string
21
+ render: {
22
+ (id: string): TemplateResult | string
23
+ (
24
+ id: string,
25
+ typeName: string,
26
+ type: EntityType<TEntity>,
27
+ ): TemplateResult | string
28
+ }
11
29
  }
12
30
 
13
31
  /**
@@ -17,8 +35,11 @@ export type Api = StoreApi & {
17
35
  * @param element The DOM element to mount the template to.
18
36
  * @returns An unsubscribe function.
19
37
  */
20
- export function mount<Entity = BaseEntity, State = BaseState>(
21
- store: Store<Entity, State>,
22
- renderFn: (api: Api) => TemplateResult | null,
38
+ export function mount<
39
+ TEntity extends BaseEntity = BaseEntity,
40
+ TState extends EntitiesState<TEntity> = EntitiesState<TEntity>,
41
+ >(
42
+ store: Store<TEntity, TState>,
43
+ renderFn: (api: Api<TEntity, TState>) => TemplateResult | null,
23
44
  element: HTMLElement | DocumentFragment,
24
45
  ): () => void
package/types/router.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { Api } from "./mount"
2
+
1
3
  /**
2
4
  * A map of route patterns to entity types.
3
5
  * The order of routes matters: more specific routes (e.g., `/users/new`)
@@ -83,7 +85,7 @@ export interface RouterType {
83
85
  * @param payload The initialization payload (currently unused).
84
86
  * @param api The store API for interacting with the system.
85
87
  */
86
- init(entity: RouterEntity, payload: any, api: StoreApi): void
88
+ init(entity: RouterEntity, payload: any, api: Api): void
87
89
 
88
90
  /**
89
91
  * Navigates to a new route, updating the browser's history and the router entity state.
@@ -99,7 +101,7 @@ export interface RouterType {
99
101
  navigate(
100
102
  entity: RouterEntity,
101
103
  payload: string | number | NavigatePayload,
102
- api: StoreApi,
104
+ api: Api,
103
105
  ): void | Promise<void>
104
106
 
105
107
  /**
@@ -109,25 +111,17 @@ export interface RouterType {
109
111
  * @param {RouteSyncPayload} payload - The new route state.
110
112
  * @param {StoreApi} api - The store API.
111
113
  */
112
- routeSync(
113
- entity: RouterEntity,
114
- payload: RouteSyncPayload,
115
- api: StoreApi,
116
- ): void
114
+ routeSync(entity: RouterEntity, payload: RouteSyncPayload, api: Api): void
117
115
  /**
118
116
  * Handles browser `popstate` events. May perform async loading for lazy routes.
119
117
  */
120
- popstate(
121
- entity: RouterEntity,
122
- payload: any,
123
- api: StoreApi,
124
- ): void | Promise<void>
118
+ popstate(entity: RouterEntity, payload: any, api: Api): void | Promise<void>
125
119
 
126
120
  /**
127
121
  * Handles successful async route loading for a pattern.
128
122
  * Registers the loaded type and updates runtime route config.
129
123
  */
130
- routeLoadSuccess(entity: RouterEntity, payload: any, api: StoreApi): void
124
+ routeLoadSuccess(entity: RouterEntity, payload: any, api: Api): void
131
125
 
132
126
  /**
133
127
  * Handles errors that occurred while loading a lazy route.
@@ -135,6 +129,8 @@ export interface RouterType {
135
129
  routeLoadError(entity: RouterEntity, payload: any): void
136
130
  }
137
131
 
132
+ export declare const Router: RouterType
133
+
138
134
  /**
139
135
  * Returns the current route configuration.
140
136
  * @returns {Record<string, string|function>} The current route configuration.