@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 +81 -74
- package/package.json +3 -3
- package/src/form/index.js +1 -1
- package/src/mount.js +23 -7
- package/src/router/handlers.js +15 -16
- package/src/router/index.js +1 -1
- package/types/form.d.ts +3 -1
- package/types/mount.d.ts +27 -6
- package/types/router.d.ts +9 -13
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: "
|
|
310
|
-
counter2: { type: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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 {
|
|
448
|
+
import { Todo } from "./types/todo.js"
|
|
449
449
|
|
|
450
450
|
test("toggle changes completed status", () => {
|
|
451
451
|
const { entity } = trigger(
|
|
452
|
-
{ type: "
|
|
453
|
-
|
|
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: "
|
|
462
|
-
|
|
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 {
|
|
473
|
+
import { Todo } from "./types/todo.js"
|
|
474
474
|
|
|
475
475
|
test("todo renders text and status", () => {
|
|
476
476
|
const entity = {
|
|
477
|
-
type: "
|
|
477
|
+
type: "Todo",
|
|
478
478
|
id: "todo1",
|
|
479
479
|
text: "Buy milk",
|
|
480
480
|
completed: false,
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
-
const html = render(
|
|
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: "
|
|
491
|
+
type: "Todo",
|
|
492
492
|
id: "todo1",
|
|
493
493
|
text: "Buy milk",
|
|
494
494
|
completed: true,
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
-
const html = render(
|
|
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 {
|
|
507
|
+
import { Counter } from "./types/counter.js"
|
|
508
508
|
|
|
509
509
|
test("full user interaction flow", () => {
|
|
510
510
|
const store = createStore({
|
|
511
|
-
types: {
|
|
511
|
+
types: { Counter },
|
|
512
512
|
entities: {
|
|
513
|
-
counter1: { type: "
|
|
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 {
|
|
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
|
|
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: "
|
|
611
|
-
|
|
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
|
|
629
|
+
const GuardedPage = requireAuth(AdminPage)
|
|
630
630
|
const mockApi = { notify: jest.fn() }
|
|
631
631
|
|
|
632
632
|
const { events } = trigger(
|
|
633
|
-
{ type: "
|
|
634
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
970
|
+
Router,
|
|
971
971
|
|
|
972
972
|
// 2. Define types for your pages
|
|
973
|
-
|
|
973
|
+
HomePage: {
|
|
974
974
|
render: () => html`<h1>Welcome Home!</h1>`,
|
|
975
975
|
},
|
|
976
|
-
|
|
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
|
-
|
|
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: "
|
|
991
|
+
type: "Router",
|
|
992
992
|
},
|
|
993
993
|
userPage: {
|
|
994
|
-
type: "
|
|
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: "
|
|
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
|
|
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 {
|
|
1119
|
+
import { Router } from "@inglorious/web/router"
|
|
1120
1120
|
import { requireAuth } from "./guards/require-auth.js"
|
|
1121
|
-
import {
|
|
1122
|
-
import {
|
|
1121
|
+
import { AdminPage } from "./pages/admin.js"
|
|
1122
|
+
import { LoginPage } from "./pages/login.js"
|
|
1123
1123
|
|
|
1124
1124
|
const types = {
|
|
1125
|
-
|
|
1125
|
+
Router,
|
|
1126
1126
|
|
|
1127
1127
|
// Public page - no guard
|
|
1128
|
-
|
|
1128
|
+
LoginPage,
|
|
1129
1129
|
|
|
1130
1130
|
// Protected page - composed with requireAuth guard
|
|
1131
|
-
|
|
1131
|
+
AdminPage: [AdminPage, requireAuth],
|
|
1132
1132
|
}
|
|
1133
1133
|
|
|
1134
1134
|
const entities = {
|
|
1135
|
-
router: { type: "
|
|
1136
|
-
adminPage: { type: "
|
|
1137
|
-
loginPage: { type: "
|
|
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
|
-
|
|
1176
|
+
AdminPage: [AdminPage, requireAuth, requireAdmin],
|
|
1177
1177
|
|
|
1178
1178
|
// Require authentication AND resource ownership
|
|
1179
|
-
|
|
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: [
|
|
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 {
|
|
1251
|
+
import { Form } from "@inglorious/web/form"
|
|
1252
1252
|
|
|
1253
|
-
const types = {
|
|
1253
|
+
const types = { Form }
|
|
1254
1254
|
|
|
1255
1255
|
const entities = {
|
|
1256
1256
|
form: {
|
|
1257
|
-
type: "
|
|
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
|
|
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 {
|
|
1351
|
+
import { DataGrid } from "@acme/design-system/data-grid"
|
|
1352
1352
|
|
|
1353
1353
|
// Use as-is
|
|
1354
|
-
const
|
|
1354
|
+
const SimpleGrid = { ...DataGrid }
|
|
1355
1355
|
|
|
1356
1356
|
// Override methods
|
|
1357
|
-
const
|
|
1358
|
-
...
|
|
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
|
|
1370
|
-
|
|
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
|
-
|
|
1444
|
+
ProductTable,
|
|
1445
1445
|
|
|
1446
1446
|
// Web Component - for specialized UI
|
|
1447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1486
|
+
Header: {
|
|
1487
1487
|
render(entity, api) {
|
|
1488
1488
|
return html`<header>${entity.title}</header>`
|
|
1489
1489
|
},
|
|
1490
1490
|
},
|
|
1491
1491
|
|
|
1492
|
-
|
|
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: "
|
|
1502
|
-
header: { type: "
|
|
1503
|
-
content: { type: "
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
1592
|
+
Form,
|
|
1593
1593
|
getFieldError,
|
|
1594
1594
|
getFieldValue,
|
|
1595
1595
|
isFieldTouched,
|
|
1596
1596
|
} from "@inglorious/web/form"
|
|
1597
|
-
import {
|
|
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
|
|
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": "
|
|
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/
|
|
55
|
-
"@inglorious/
|
|
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
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
|
-
|
|
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
|
|
53
|
-
if (!
|
|
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
|
|
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
|
}
|
package/src/router/handlers.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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,
|
|
265
|
+
for (const [pattern, route] of Object.entries(routeConfig)) {
|
|
267
266
|
if (pattern === "*") {
|
|
268
|
-
fallbackRoute = { pattern,
|
|
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,
|
|
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.
|
|
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, {
|
|
340
|
-
entity.route =
|
|
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
|
package/src/router/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createHelpers } from "./helpers.js"
|
|
|
9
9
|
*/
|
|
10
10
|
const routeConfig = {}
|
|
11
11
|
|
|
12
|
-
export const
|
|
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
|
|
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 {
|
|
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
|
|
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:
|
|
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<
|
|
21
|
-
|
|
22
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|