@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 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-htmls optimized diffing, this is fast, predictable, and surprisingly efficient.
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
- > Re-render everything → let lit-html update only what changed.”
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
- Heres how @inglorious/web compares to the major players:
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 Youll See in DevTools
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.5.0",
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
@@ -10,7 +10,8 @@ export const list = {
10
10
  resetList(entity)
11
11
  },
12
12
 
13
- create(entity) {
13
+ create(entity, id) {
14
+ if (id !== entity.id) return
14
15
  resetList(entity)
15
16
  },
16
17
 
package/src/list.test.js CHANGED
@@ -43,7 +43,7 @@ describe("list", () => {
43
43
  })
44
44
 
45
45
  it("should reset the list on create", () => {
46
- list.create(entity)
46
+ list.create(entity, "test-list")
47
47
  expect(entity.scrollTop).toBe(0)
48
48
  expect(entity.visibleRange).toEqual({ start: 0, end: 20 })
49
49
  })
@@ -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
+ }