@inglorious/web 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +217 -5
- package/package.json +4 -2
- package/src/form.js +4 -1
- package/src/form.test.js +9 -9
- package/src/index.js +1 -0
- package/src/list.js +2 -1
- package/src/list.test.js +1 -1
- package/src/select/base.css +52 -0
- package/src/select/logic.js +342 -0
- package/src/select/rendering.js +352 -0
- package/src/select/theme.css +133 -0
- package/src/select.js +7 -0
- package/src/select.test.js +415 -0
- package/src/table/logic.js +184 -2
- package/types/index.d.ts +3 -0
- package/types/select.d.ts +136 -0
- package/types/table.d.ts +239 -0
package/README.md
CHANGED
|
@@ -19,6 +19,9 @@ Unlike modern frameworks that invent their own languages or rely on signals, pro
|
|
|
19
19
|
Each entity type defines its own `render(entity, api)` method.
|
|
20
20
|
`api.render(id)` composes the UI by invoking the correct renderer for each entity.
|
|
21
21
|
|
|
22
|
+
- **Type Composition**
|
|
23
|
+
Types can be composed as arrays of behaviors, enabling reusable patterns like authentication guards, logging, or any cross-cutting concern.
|
|
24
|
+
|
|
22
25
|
- **Simple and Predictable API**
|
|
23
26
|
Zero magic, zero reactivity graphs, zero compiler.
|
|
24
27
|
Just JavaScript functions and store events.
|
|
@@ -55,7 +58,7 @@ Use the scaffolder to create a starter app tailored to your workflow.
|
|
|
55
58
|
|
|
56
59
|
### ✨ **Inglorious Web re-renders the whole template tree on each state change.**
|
|
57
60
|
|
|
58
|
-
Thanks to lit-html
|
|
61
|
+
Thanks to lit-html's optimized diffing, this is fast, predictable, and surprisingly efficient.
|
|
59
62
|
|
|
60
63
|
This means:
|
|
61
64
|
|
|
@@ -66,7 +69,7 @@ This means:
|
|
|
66
69
|
|
|
67
70
|
You get Svelte-like ergonomic simplicity, but with no compiler and no magic.
|
|
68
71
|
|
|
69
|
-
>
|
|
72
|
+
> "Re-render everything → let lit-html update only what changed."
|
|
70
73
|
|
|
71
74
|
It's that simple — and surprisingly fast in practice.
|
|
72
75
|
|
|
@@ -136,7 +139,7 @@ This makes it especially suitable for:
|
|
|
136
139
|
|
|
137
140
|
# Comparison with Other Frameworks
|
|
138
141
|
|
|
139
|
-
Here
|
|
142
|
+
Here's how @inglorious/web compares to the major players:
|
|
140
143
|
|
|
141
144
|
---
|
|
142
145
|
|
|
@@ -348,7 +351,7 @@ export const store = createStore({
|
|
|
348
351
|
|
|
349
352
|
Now your application state is fully visible in the Redux DevTools browser extension.
|
|
350
353
|
|
|
351
|
-
### What You
|
|
354
|
+
### What You'll See in DevTools
|
|
352
355
|
|
|
353
356
|
- Each event you dispatch via `api.notify(event, payload)` will appear as an action in the DevTools timeline.
|
|
354
357
|
- The entire store is visible under the _State_ tab.
|
|
@@ -446,7 +449,7 @@ api.notify("navigate", -1)
|
|
|
446
449
|
api.notify("navigate", {
|
|
447
450
|
to: "/users/456",
|
|
448
451
|
replace: true, // Replace current history entry
|
|
449
|
-
force: true, // Force navigation even if path is identical
|
|
452
|
+
force: true, // Force navigation even if path is identical (useful after logout)
|
|
450
453
|
})
|
|
451
454
|
```
|
|
452
455
|
|
|
@@ -480,6 +483,157 @@ export const adminPage = {
|
|
|
480
483
|
}
|
|
481
484
|
```
|
|
482
485
|
|
|
486
|
+
### 5. Route Guards (Type Composition)
|
|
487
|
+
|
|
488
|
+
Route guards are implemented using **type composition** — a powerful feature of `@inglorious/store` where types can be defined as arrays of behaviors that wrap and extend each other.
|
|
489
|
+
|
|
490
|
+
Guards are simply behaviors that intercept events (like `routeChange`) and can prevent navigation, redirect, or pass through to the protected page.
|
|
491
|
+
|
|
492
|
+
#### Example: Authentication Guard
|
|
493
|
+
|
|
494
|
+
```javascript
|
|
495
|
+
// guards/require-auth.js
|
|
496
|
+
export const requireAuth = (type) => ({
|
|
497
|
+
routeChange(entity, payload, api) {
|
|
498
|
+
// Only act when navigating to this specific route
|
|
499
|
+
if (payload.route !== entity.type) return
|
|
500
|
+
|
|
501
|
+
// Check authentication
|
|
502
|
+
const user = localStorage.getItem("user")
|
|
503
|
+
if (!user) {
|
|
504
|
+
// Redirect to login, preserving the intended destination
|
|
505
|
+
api.notify("navigate", {
|
|
506
|
+
to: "/login",
|
|
507
|
+
redirectTo: window.location.pathname,
|
|
508
|
+
replace: true,
|
|
509
|
+
})
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// User is authenticated - pass through to the actual page handler
|
|
514
|
+
type.routeChange?.(entity, payload, api)
|
|
515
|
+
},
|
|
516
|
+
})
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
#### Using Guards with Type Composition
|
|
520
|
+
|
|
521
|
+
```javascript
|
|
522
|
+
// store.js
|
|
523
|
+
import { createStore, router } from "@inglorious/web"
|
|
524
|
+
import { requireAuth } from "./guards/require-auth.js"
|
|
525
|
+
import { adminPage } from "./pages/admin.js"
|
|
526
|
+
import { loginPage } from "./pages/login.js"
|
|
527
|
+
|
|
528
|
+
const types = {
|
|
529
|
+
router,
|
|
530
|
+
|
|
531
|
+
// Public page - no guard
|
|
532
|
+
loginPage,
|
|
533
|
+
|
|
534
|
+
// Protected page - composed with requireAuth guard
|
|
535
|
+
adminPage: [adminPage, requireAuth],
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const entities = {
|
|
539
|
+
router: {
|
|
540
|
+
type: "router",
|
|
541
|
+
routes: {
|
|
542
|
+
"/login": "loginPage",
|
|
543
|
+
"/admin": "adminPage",
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
adminPage: {
|
|
547
|
+
type: "adminPage",
|
|
548
|
+
},
|
|
549
|
+
loginPage: {
|
|
550
|
+
type: "loginPage",
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export const store = createStore({ types, entities })
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
#### How Type Composition Works
|
|
558
|
+
|
|
559
|
+
When you define a type as an array like `[adminPage, requireAuth]`:
|
|
560
|
+
|
|
561
|
+
1. The behaviors compose in order (left to right)
|
|
562
|
+
2. Each behavior can intercept events before they reach the next behavior
|
|
563
|
+
3. Guards can choose to:
|
|
564
|
+
- **Block** by returning early (not calling the next handler)
|
|
565
|
+
- **Redirect** by triggering navigation to a different route
|
|
566
|
+
- **Pass through** by calling the next behavior's handler
|
|
567
|
+
|
|
568
|
+
This pattern is extremely flexible and can be used for:
|
|
569
|
+
|
|
570
|
+
- **Authentication** - Check if user is logged in
|
|
571
|
+
- **Authorization** - Check user roles or permissions
|
|
572
|
+
- **Analytics** - Log page views
|
|
573
|
+
- **Redirects** - Redirect logged-in users away from login page
|
|
574
|
+
- **Loading states** - Show loading UI while checking async permissions
|
|
575
|
+
- **Any cross-cutting concern** you can think of
|
|
576
|
+
|
|
577
|
+
#### Multiple Guards
|
|
578
|
+
|
|
579
|
+
You can compose multiple guards for fine-grained control:
|
|
580
|
+
|
|
581
|
+
```javascript
|
|
582
|
+
const types = {
|
|
583
|
+
// Require authentication AND admin role
|
|
584
|
+
adminPage: [adminPage, requireAuth, requireAdmin],
|
|
585
|
+
|
|
586
|
+
// Require authentication AND resource ownership
|
|
587
|
+
userProfile: [userProfile, requireAuth, requireOwnership],
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
Guards execute in order, so earlier guards can block navigation before later guards even run.
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## Type Composition
|
|
596
|
+
|
|
597
|
+
One of the most powerful features of `@inglorious/store` (and therefore `@inglorious/web`) is **type composition**. Types can be defined as arrays of behaviors that wrap each other, enabling elegant solutions to cross-cutting concerns.
|
|
598
|
+
|
|
599
|
+
### Basic Composition
|
|
600
|
+
|
|
601
|
+
```javascript
|
|
602
|
+
const logging = (type) => ({
|
|
603
|
+
// Intercept the render method
|
|
604
|
+
render(entity, api) {
|
|
605
|
+
console.log(`Rendering ${entity.id}`)
|
|
606
|
+
return type.render(entity, api)
|
|
607
|
+
},
|
|
608
|
+
|
|
609
|
+
// Intercept any event
|
|
610
|
+
someEvent(entity, payload, api) {
|
|
611
|
+
console.log(`Event triggered on ${entity.id}`)
|
|
612
|
+
type.someEvent?.(entity, payload, api)
|
|
613
|
+
},
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
const types = {
|
|
617
|
+
// Compose the counter type with logging
|
|
618
|
+
counter: [counterBase, logging],
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Use Cases
|
|
623
|
+
|
|
624
|
+
Type composition enables elegant solutions for:
|
|
625
|
+
|
|
626
|
+
- **Route guards** - Authentication, authorization, redirects
|
|
627
|
+
- **Logging/debugging** - Trace renders and events
|
|
628
|
+
- **Analytics** - Track user interactions
|
|
629
|
+
- **Error boundaries** - Catch and handle render errors gracefully
|
|
630
|
+
- **Loading states** - Show spinners during async operations
|
|
631
|
+
- **Caching/memoization** - Cache expensive computations
|
|
632
|
+
- **Validation** - Validate entity state before operations
|
|
633
|
+
- **Any cross-cutting concern**
|
|
634
|
+
|
|
635
|
+
The composition pattern keeps your code modular and reusable without introducing framework magic.
|
|
636
|
+
|
|
483
637
|
---
|
|
484
638
|
|
|
485
639
|
## Table
|
|
@@ -542,6 +696,62 @@ The table comes with a base stylesheet (`@inglorious/web/table/base.css`) and a
|
|
|
542
696
|
|
|
543
697
|
---
|
|
544
698
|
|
|
699
|
+
## Select
|
|
700
|
+
|
|
701
|
+
`@inglorious/web` includes a robust `select` type for handling dropdowns, supporting single/multi-select, filtering, and keyboard navigation.
|
|
702
|
+
|
|
703
|
+
### 1. Add the `select` type
|
|
704
|
+
|
|
705
|
+
Import the `select` type and its CSS, then create an entity.
|
|
706
|
+
|
|
707
|
+
```javascript
|
|
708
|
+
import { createStore, select } from "@inglorious/web"
|
|
709
|
+
// Import base styles and theme
|
|
710
|
+
import "@inglorious/web/select/base.css"
|
|
711
|
+
import "@inglorious/web/select/theme.css"
|
|
712
|
+
|
|
713
|
+
const types = { select }
|
|
714
|
+
|
|
715
|
+
const entities = {
|
|
716
|
+
countrySelect: {
|
|
717
|
+
type: "select",
|
|
718
|
+
options: [
|
|
719
|
+
{ value: "us", label: "United States" },
|
|
720
|
+
{ value: "ca", label: "Canada" },
|
|
721
|
+
{ value: "fr", label: "France" },
|
|
722
|
+
],
|
|
723
|
+
// Configuration
|
|
724
|
+
isMulti: false,
|
|
725
|
+
isSearchable: true,
|
|
726
|
+
placeholder: "Select a country...",
|
|
727
|
+
},
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const store = createStore({ types, entities })
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### 2. Render
|
|
734
|
+
|
|
735
|
+
Render it like any other entity.
|
|
736
|
+
|
|
737
|
+
```javascript
|
|
738
|
+
const renderApp = (api) => {
|
|
739
|
+
return html` <div class="my-form">${api.render("countrySelect")}</div> `
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### 3. State & Events
|
|
744
|
+
|
|
745
|
+
The `select` entity maintains its own state:
|
|
746
|
+
|
|
747
|
+
- `selectedValue`: The current value (single value or array if `isMulti: true`).
|
|
748
|
+
- `isOpen`: Whether the dropdown is open.
|
|
749
|
+
- `searchTerm`: Current search input.
|
|
750
|
+
|
|
751
|
+
It listens to internal events like `#<id>:toggle`, `#<id>:optionSelect`, etc. You typically don't need to manually dispatch these unless you are building custom controls around it.
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
545
755
|
## Forms
|
|
546
756
|
|
|
547
757
|
`@inglorious/web` includes a small but powerful `form` type for managing form state inside your entity store. It offers:
|
|
@@ -691,6 +901,8 @@ const store = createStore({ types, entities })
|
|
|
691
901
|
|
|
692
902
|
See `src/list.js` in the package for the implementation details and the `examples/apps/web-list` demo for a complete working example. In the demo the `productList` type extends the `list` type and provides `renderItem(item, index)` to render each visible item — see `examples/apps/web-list/src/product-list/product-list.js`.
|
|
693
903
|
|
|
904
|
+
---
|
|
905
|
+
|
|
694
906
|
## API Reference
|
|
695
907
|
|
|
696
908
|
**`mount(store, renderFn, element)`**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/web",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "A new web framework that leverages the power of the Inglorious Store combined with the performance and simplicity of lit-html.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
"import": "./src/index.js"
|
|
28
28
|
},
|
|
29
29
|
"./table/base.css": "./src/table/base.css",
|
|
30
|
-
"./table/theme.css": "./src/table/theme.css"
|
|
30
|
+
"./table/theme.css": "./src/table/theme.css",
|
|
31
|
+
"./select/base.css": "./src/select/base.css",
|
|
32
|
+
"./select/theme.css": "./src/select/theme.css"
|
|
31
33
|
},
|
|
32
34
|
"files": [
|
|
33
35
|
"src",
|
package/src/form.js
CHANGED
|
@@ -62,7 +62,8 @@ export const form = {
|
|
|
62
62
|
* @param {FormEntity} entity - The form entity.
|
|
63
63
|
* @param {string|number} entityId - The ID from the create event.
|
|
64
64
|
*/
|
|
65
|
-
create(entity) {
|
|
65
|
+
create(entity, id) {
|
|
66
|
+
if (id !== entity.id) return
|
|
66
67
|
resetForm(entity)
|
|
67
68
|
},
|
|
68
69
|
|
|
@@ -266,6 +267,8 @@ export const form = {
|
|
|
266
267
|
},
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
// Helper functions
|
|
271
|
+
|
|
269
272
|
/**
|
|
270
273
|
* Retrieves the validation error for a specific form field.
|
|
271
274
|
* @param {FormEntity} form - The form entity object.
|
package/src/form.test.js
CHANGED
|
@@ -57,14 +57,14 @@ describe("form", () => {
|
|
|
57
57
|
describe("create()", () => {
|
|
58
58
|
it("should reset the form", () => {
|
|
59
59
|
entity.values = {}
|
|
60
|
-
form.create(entity)
|
|
60
|
+
form.create(entity, "test-form")
|
|
61
61
|
expect(entity.values).toEqual(entity.initialValues)
|
|
62
62
|
})
|
|
63
63
|
})
|
|
64
64
|
|
|
65
65
|
describe("reset()", () => {
|
|
66
66
|
it("should reset the form to its initial state", () => {
|
|
67
|
-
form.create(entity)
|
|
67
|
+
form.create(entity, "test-form")
|
|
68
68
|
entity.values.name = "Jane Doe"
|
|
69
69
|
entity.isPristine = false
|
|
70
70
|
|
|
@@ -77,7 +77,7 @@ describe("form", () => {
|
|
|
77
77
|
|
|
78
78
|
describe("fieldChange()", () => {
|
|
79
79
|
beforeEach(() => {
|
|
80
|
-
form.create(entity)
|
|
80
|
+
form.create(entity, "test-form")
|
|
81
81
|
})
|
|
82
82
|
|
|
83
83
|
it("should update a field's value, mark it as touched, and set form to dirty", () => {
|
|
@@ -124,7 +124,7 @@ describe("form", () => {
|
|
|
124
124
|
|
|
125
125
|
describe("fieldBlur()", () => {
|
|
126
126
|
beforeEach(() => {
|
|
127
|
-
form.create(entity)
|
|
127
|
+
form.create(entity, "test-form")
|
|
128
128
|
})
|
|
129
129
|
|
|
130
130
|
it("should mark a field as touched", () => {
|
|
@@ -144,7 +144,7 @@ describe("form", () => {
|
|
|
144
144
|
|
|
145
145
|
describe("field array operations", () => {
|
|
146
146
|
beforeEach(() => {
|
|
147
|
-
form.create(entity)
|
|
147
|
+
form.create(entity, "test-form")
|
|
148
148
|
})
|
|
149
149
|
|
|
150
150
|
it("fieldArrayAppend: should append a value and metadata", () => {
|
|
@@ -198,7 +198,7 @@ describe("form", () => {
|
|
|
198
198
|
|
|
199
199
|
describe("validate()", () => {
|
|
200
200
|
beforeEach(() => {
|
|
201
|
-
form.create(entity)
|
|
201
|
+
form.create(entity, "test-form")
|
|
202
202
|
})
|
|
203
203
|
|
|
204
204
|
it("should set errors and isValid based on the validation function", () => {
|
|
@@ -231,7 +231,7 @@ describe("form", () => {
|
|
|
231
231
|
let api
|
|
232
232
|
|
|
233
233
|
beforeEach(() => {
|
|
234
|
-
form.create(entity)
|
|
234
|
+
form.create(entity, "test-form")
|
|
235
235
|
api = { notify: vi.fn() }
|
|
236
236
|
})
|
|
237
237
|
|
|
@@ -271,7 +271,7 @@ describe("form", () => {
|
|
|
271
271
|
|
|
272
272
|
describe("validationComplete()", () => {
|
|
273
273
|
it("should update form state after async validation", () => {
|
|
274
|
-
form.create(entity)
|
|
274
|
+
form.create(entity, "test-form")
|
|
275
275
|
entity.isValidating = true
|
|
276
276
|
const errors = { name: "Error" }
|
|
277
277
|
|
|
@@ -285,7 +285,7 @@ describe("form", () => {
|
|
|
285
285
|
|
|
286
286
|
describe("validationError()", () => {
|
|
287
287
|
it("should update form state after an async validation error", () => {
|
|
288
|
-
form.create(entity)
|
|
288
|
+
form.create(entity, "test-form")
|
|
289
289
|
entity.isValidating = true
|
|
290
290
|
|
|
291
291
|
form.validationError(entity, { error: "Network Error" })
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { form, getFieldError, getFieldValue, isFieldTouched } from "./form.js"
|
|
|
2
2
|
export { list } from "./list.js"
|
|
3
3
|
export { mount } from "./mount.js"
|
|
4
4
|
export { router } from "./router.js"
|
|
5
|
+
export { select } from "./select.js"
|
|
5
6
|
export { table } from "./table.js"
|
|
6
7
|
export { createStore } from "@inglorious/store"
|
|
7
8
|
export { createDevtools } from "@inglorious/store/client/devtools.js"
|
package/src/list.js
CHANGED
package/src/list.test.js
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
.iw-select {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: inline-block;
|
|
4
|
+
width: 100%;
|
|
5
|
+
|
|
6
|
+
.iw-select-control {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
user-select: none;
|
|
12
|
+
|
|
13
|
+
.iw-select-value,
|
|
14
|
+
.iw-select-placeholder,
|
|
15
|
+
.iw-select-multi-value {
|
|
16
|
+
flex: 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.iw-select-multi-value {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-wrap: wrap;
|
|
22
|
+
|
|
23
|
+
.iw-select-multi-value-tag {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.iw-select-dropdown {
|
|
31
|
+
position: absolute;
|
|
32
|
+
top: 100%;
|
|
33
|
+
left: 0;
|
|
34
|
+
right: 0;
|
|
35
|
+
z-index: 1000;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
|
|
39
|
+
.iw-select-dropdown-search {
|
|
40
|
+
width: 100%;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.iw-select-dropdown-options {
|
|
44
|
+
.iw-select-dropdown-options-option {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
user-select: none;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|