@inglorious/web 4.0.4 → 4.0.5
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 +1115 -1115
- package/package.json +5 -5
- package/src/select/base.css +52 -52
- package/src/select/theme.css +133 -133
- package/src/table/base.css +34 -34
- package/src/table/theme.css +83 -83
package/README.md
CHANGED
|
@@ -1,1115 +1,1115 @@
|
|
|
1
|
-
# @inglorious/web
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@inglorious/web)
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
|
|
6
|
-
A lightweight, reactive-enough web framework built on **pure JavaScript**, the entity-based state management provided by **@inglorious/store**, and the DOM-diffing efficiency of **lit-html**.
|
|
7
|
-
|
|
8
|
-
Unlike modern frameworks that invent their own languages or rely on signals, proxies, or compilers, **@inglorious/web embraces plain JavaScript** and a transparent architecture.
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Features
|
|
13
|
-
|
|
14
|
-
- **Full-tree Re-rendering with DOM Diffing**
|
|
15
|
-
Your entire template tree re-renders on every state change, while **lit-html updates only the minimal DOM parts**.
|
|
16
|
-
No VDOM, no signals, no hidden dependencies.
|
|
17
|
-
|
|
18
|
-
- **Entity-Based Rendering Model**
|
|
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.
|
|
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
|
-
|
|
25
|
-
- **Simple and Predictable API**
|
|
26
|
-
Zero magic, zero reactivity graphs, zero compiler.
|
|
27
|
-
Just JavaScript functions and store events.
|
|
28
|
-
|
|
29
|
-
- **Router, Forms, Tables, Virtual Lists**
|
|
30
|
-
High-level primitives built on the same predictable model.
|
|
31
|
-
|
|
32
|
-
- **Zero Component State**
|
|
33
|
-
All state lives in the store — never inside components.
|
|
34
|
-
|
|
35
|
-
- **No Signals, No Subscriptions, No Memory Leaks**
|
|
36
|
-
Because every render is triggered by the store, and lit-html handles the rest.
|
|
37
|
-
|
|
38
|
-
- **No compilation required**
|
|
39
|
-
Apps can run directly in the browser — no build/compile step is strictly necessary (though you may use bundlers or Vite for convenience in larger projects).
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## Create App (scaffolding)
|
|
44
|
-
|
|
45
|
-
To help bootstrap projects quickly, there's an official scaffolding tool: **[`@inglorious/create-app`](https://www.npmjs.com/package/@inglorious/create-app)**. It generates opinionated boilerplates so you can start coding right away.
|
|
46
|
-
|
|
47
|
-
Available templates:
|
|
48
|
-
|
|
49
|
-
- **minimal** — plain HTML, CSS, and JS (no build step)
|
|
50
|
-
- **js** — Vite-based JavaScript project
|
|
51
|
-
- **ts** — Vite + TypeScript project
|
|
52
|
-
- **ssx-js** — Static Site Xecution (SSX) project using JavaScript
|
|
53
|
-
- **ssx-ts** — Static Site Xecution (SSX) project using TypeScript
|
|
54
|
-
|
|
55
|
-
Use the scaffolder to create a starter app tailored to your workflow.
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## Key Architectural Insight
|
|
60
|
-
|
|
61
|
-
### ✨ **Inglorious Web re-renders the whole template tree on each state change.**
|
|
62
|
-
|
|
63
|
-
Thanks to lit-html's optimized diffing, this is fast, predictable, and surprisingly efficient.
|
|
64
|
-
|
|
65
|
-
This means:
|
|
66
|
-
|
|
67
|
-
- **You do NOT need fine-grained reactivity**
|
|
68
|
-
- **You do NOT need selectors/signals/memos**
|
|
69
|
-
- **You do NOT track dependencies between UI fragments**
|
|
70
|
-
- **You cannot accidentally create memory leaks through subscriptions**
|
|
71
|
-
|
|
72
|
-
You get Svelte-like ergonomic simplicity, but with no compiler and no magic.
|
|
73
|
-
|
|
74
|
-
> "Re-render everything → let lit-html update only what changed."
|
|
75
|
-
|
|
76
|
-
It's that simple — and surprisingly fast in practice.
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## When to Use Inglorious Web
|
|
81
|
-
|
|
82
|
-
- You want predictable behavior
|
|
83
|
-
- You prefer explicit state transitions
|
|
84
|
-
- You want to avoid complex reactive graphs
|
|
85
|
-
- You want UI to be fully controlled by your entity-based store
|
|
86
|
-
- You want to stay entirely in **JavaScript**, without DSLs or compilers
|
|
87
|
-
- You want **React-like declarative UI** but without the cost and overhead of React
|
|
88
|
-
- You want to build **static sites with SSX** — same entity patterns, pre-rendered HTML, and client hydration
|
|
89
|
-
|
|
90
|
-
This framework is ideal for both small apps and large business UIs.
|
|
91
|
-
|
|
92
|
-
--
|
|
93
|
-
|
|
94
|
-
## When NOT to Use Inglorious Web
|
|
95
|
-
|
|
96
|
-
- You need fine-grained reactivity for very large datasets (1000+ items per view)
|
|
97
|
-
- You're building a library that needs to be framework-agnostic
|
|
98
|
-
- Your team is already deeply invested in React/Vue/Angular
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## Why Inglorious Web Avoids Signals
|
|
103
|
-
|
|
104
|
-
Other modern frameworks use:
|
|
105
|
-
|
|
106
|
-
- Proxies (Vue)
|
|
107
|
-
- Observables (MobX)
|
|
108
|
-
- Fine-grained signals (Solid, Angular v17+)
|
|
109
|
-
- Compiler-generated reactivity (Svelte)
|
|
110
|
-
- Fiber or granular subscriptions (React, Preact, Qwik, etc.)
|
|
111
|
-
|
|
112
|
-
These systems are powerful but introduce:
|
|
113
|
-
|
|
114
|
-
- hidden dependencies
|
|
115
|
-
- memory retention risks
|
|
116
|
-
- unpredictable update ordering
|
|
117
|
-
- steep learning curves
|
|
118
|
-
- framework-specific languages
|
|
119
|
-
- need for cleanup, teardown, and special lifecycle APIs
|
|
120
|
-
- challenges when mixing with game engines, workers, or non-UI code
|
|
121
|
-
|
|
122
|
-
### Inglorious Web takes a different stance:
|
|
123
|
-
|
|
124
|
-
✔ **Every entity update is explicit**
|
|
125
|
-
✔ **Every UI update is a full diff pass**
|
|
126
|
-
✔ **Every part of the system is just JavaScript**
|
|
127
|
-
✔ **No special lifecycle**
|
|
128
|
-
✔ **No subscriptions needed**
|
|
129
|
-
✔ **No signals**
|
|
130
|
-
✔ **No cleanup**
|
|
131
|
-
✔ **No surprises**
|
|
132
|
-
|
|
133
|
-
This makes it especially suitable for:
|
|
134
|
-
|
|
135
|
-
- realtime applications
|
|
136
|
-
- hybrid UI/game engine contexts
|
|
137
|
-
- large enterprise apps where predictability matters
|
|
138
|
-
- developers who prefer simplicity over magic
|
|
139
|
-
|
|
140
|
-
---
|
|
141
|
-
|
|
142
|
-
# Comparison with Other Frameworks
|
|
143
|
-
|
|
144
|
-
Here's how @inglorious/web compares to the major players:
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
## **React**
|
|
149
|
-
|
|
150
|
-
| Feature | React | Inglorious Web |
|
|
151
|
-
| ------------------------- | ----------------------------- | ---------------------------------- |
|
|
152
|
-
| Rendering model | VDOM diff + effects | Full tree template + lit-html diff |
|
|
153
|
-
| Language | JSX (non-JS) | Pure JavaScript |
|
|
154
|
-
| Component state | Yes | No — store only |
|
|
155
|
-
| Refs & lifecycles | Many | None needed |
|
|
156
|
-
| Signals / fine reactivity | No (but heavy reconciliation) | No (rely on lit-html diff) |
|
|
157
|
-
| Reconciliation overhead | High (full VDOM diff) | Low (template string diff) |
|
|
158
|
-
| Bundle size | Large | Tiny |
|
|
159
|
-
| Learning curve | Medium/High | Very low |
|
|
160
|
-
|
|
161
|
-
React is powerful but complicated. Inglorious Web is simpler, lighter, and closer to native JS.
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## **Vue (3)**
|
|
166
|
-
|
|
167
|
-
| Feature | Vue | Inglorious Web |
|
|
168
|
-
| --------------- | -------------------------- | ----------------------------------- |
|
|
169
|
-
| Reactivity | Proxy-based, deep tracking | Event-based updates + lit-html diff |
|
|
170
|
-
| Templates | DSL | JavaScript templates |
|
|
171
|
-
| Component state | Yes | No |
|
|
172
|
-
| Lifecycle | Many | None |
|
|
173
|
-
| Compiler | Required for SFC | None |
|
|
174
|
-
|
|
175
|
-
Vue reactivity is elegant but complex. Inglorious Web avoids proxies and keeps everything explicit.
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## **Svelte**
|
|
180
|
-
|
|
181
|
-
| Feature | Svelte | Inglorious Web |
|
|
182
|
-
| -------------- | --------------------------- | ------------------ |
|
|
183
|
-
| Compiler | Required | None |
|
|
184
|
-
| Reactivity | Compiler transforms $labels | Transparent JS |
|
|
185
|
-
| Granularity | Fine-grained | Full-tree rerender |
|
|
186
|
-
| Learning curve | Medium | Low |
|
|
187
|
-
|
|
188
|
-
Svelte is magic; Inglorious Web is explicit.
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
|
|
192
|
-
## **SolidJS**
|
|
193
|
-
|
|
194
|
-
| Feature | Solid | Inglorious Web |
|
|
195
|
-
| ---------- | -------------------- | ------------------ |
|
|
196
|
-
| Reactivity | Fine-grained signals | No signals |
|
|
197
|
-
| Components | Run once | Rerun always |
|
|
198
|
-
| Cleanup | Required | None |
|
|
199
|
-
| Behavior | Highly optimized | Highly predictable |
|
|
200
|
-
|
|
201
|
-
Solid is extremely fast but requires a mental model.
|
|
202
|
-
Inglorious Web trades peak performance for simplicity and zero overhead.
|
|
203
|
-
|
|
204
|
-
---
|
|
205
|
-
|
|
206
|
-
## **Qwik**
|
|
207
|
-
|
|
208
|
-
| Feature | Qwik | Inglorious Web |
|
|
209
|
-
| -------------------- | -------------------- | -------------- |
|
|
210
|
-
| Execution model | Resumable | Plain JS |
|
|
211
|
-
| Framework complexity | Very high | Very low |
|
|
212
|
-
| Reactivity | Fine-grained signals | None |
|
|
213
|
-
|
|
214
|
-
Qwik targets extreme performance at extreme complexity.
|
|
215
|
-
Inglorious Web is minimal, predictable, and tiny.
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
## **HTMX / Alpine / Vanilla DOM**
|
|
220
|
-
|
|
221
|
-
Inglorious Web is closer philosophically to **HTMX** and **vanilla JS**, but with a declarative rendering model and entity-based state.
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
# Why Choose Inglorious Web
|
|
226
|
-
|
|
227
|
-
- Minimalistic
|
|
228
|
-
- Pure JavaScript
|
|
229
|
-
- Entity-based and predictable
|
|
230
|
-
- Extremely easy to reason about
|
|
231
|
-
- One render path, no hidden rules
|
|
232
|
-
- No reactivity graphs
|
|
233
|
-
- No per-component subscriptions
|
|
234
|
-
- No memory leaks
|
|
235
|
-
- No build step required (apps can run in the browser)
|
|
236
|
-
- Works perfectly in hybrid UI/game engine contexts
|
|
237
|
-
- Uses native ES modules and standards
|
|
238
|
-
|
|
239
|
-
If you want a framework that **does not fight JavaScript**, this is the one.
|
|
240
|
-
|
|
241
|
-
---
|
|
242
|
-
|
|
243
|
-
## Installation
|
|
244
|
-
|
|
245
|
-
```bash
|
|
246
|
-
npm install @inglorious/web
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
---
|
|
250
|
-
|
|
251
|
-
## Quick Start
|
|
252
|
-
|
|
253
|
-
### 1. Define Your Store and Entity Renders
|
|
254
|
-
|
|
255
|
-
First, set up your store with entity types. For each type you want to render, add a render method that returns a `lit-html` template.
|
|
256
|
-
|
|
257
|
-
```javascript
|
|
258
|
-
// store.js
|
|
259
|
-
import { createStore, html } from "@inglorious/web"
|
|
260
|
-
|
|
261
|
-
const types = {
|
|
262
|
-
counter: {
|
|
263
|
-
increment(entity, id) {
|
|
264
|
-
if (entity.id !== id) return
|
|
265
|
-
entity.value++
|
|
266
|
-
},
|
|
267
|
-
|
|
268
|
-
// Define how a 'counter' entity should be rendered
|
|
269
|
-
render(entity, api) {
|
|
270
|
-
return html`
|
|
271
|
-
<div>
|
|
272
|
-
<span>Count: ${entity.value}</span>
|
|
273
|
-
<button @click=${() => api.notify("increment", entity.id)}>+1</button>
|
|
274
|
-
</div>
|
|
275
|
-
`
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const entities = {
|
|
281
|
-
counter1: { type: "counter", value: 0 },
|
|
282
|
-
counter2: { type: "counter", value: 10 },
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export const store = createStore({ types, entities })
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### 2. Create Your Root Template and Mount
|
|
289
|
-
|
|
290
|
-
Write a root rendering function that uses the provided api to compose the UI, then use `mount` to attach it to the DOM.
|
|
291
|
-
|
|
292
|
-
```javascript
|
|
293
|
-
// main.js
|
|
294
|
-
import { mount, html } from "@inglorious/web"
|
|
295
|
-
import { store } from "./store.js"
|
|
296
|
-
|
|
297
|
-
// This function receives the API and returns a lit-html template
|
|
298
|
-
const renderApp = (api) => {
|
|
299
|
-
const entities = Object.values(api.getEntities())
|
|
300
|
-
|
|
301
|
-
return html`
|
|
302
|
-
<h1>Counters</h1>
|
|
303
|
-
${entities.map((entity) => api.render(entity.id))}
|
|
304
|
-
`
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Mount the app to the DOM
|
|
308
|
-
mount(store, renderApp, document.getElementById("root"))
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
The `mount` function subscribes to the store and automatically re-renders your template whenever the state changes.
|
|
312
|
-
|
|
313
|
-
---
|
|
314
|
-
|
|
315
|
-
## JSX Support
|
|
316
|
-
|
|
317
|
-
If you prefer JSX syntax over template literals, you can use **[`@inglorious/vite-plugin-jsx`](https://www.npmjs.com/package/@inglorious/vite-plugin-jsx)**.
|
|
318
|
-
|
|
319
|
-
This Vite plugin transforms standard JSX/TSX into optimized `lit-html` templates at compile time. You get the familiar developer experience of JSX without React's runtime, hooks, or VDOM overhead.
|
|
320
|
-
|
|
321
|
-
To use it:
|
|
322
|
-
|
|
323
|
-
1. Install the plugin: `npm install -D @inglorious/vite-plugin-jsx`
|
|
324
|
-
2. Add it to your `vite.config.js`
|
|
325
|
-
3. Write your render functions using JSX
|
|
326
|
-
|
|
327
|
-
```jsx
|
|
328
|
-
export const counter = {
|
|
329
|
-
render(entity, api) {
|
|
330
|
-
return (
|
|
331
|
-
<div className="counter">
|
|
332
|
-
<span>Count: {entity.value}</span>
|
|
333
|
-
<button onClick={() => api.notify(`#${entity.id}:increment`)}>
|
|
334
|
-
+1
|
|
335
|
-
</button>
|
|
336
|
-
</div>
|
|
337
|
-
)
|
|
338
|
-
},
|
|
339
|
-
}
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
See the plugin documentation for full details on control flow, attributes, and engine components.
|
|
343
|
-
|
|
344
|
-
---
|
|
345
|
-
|
|
346
|
-
## Redux DevTools Integration
|
|
347
|
-
|
|
348
|
-
`@inglorious/web` ships with first-class support for the **Redux DevTools Extension**, allowing you to:
|
|
349
|
-
|
|
350
|
-
- inspect all store events
|
|
351
|
-
- time-travel through state changes
|
|
352
|
-
- restore previous states
|
|
353
|
-
- debug your entity-based logic visually
|
|
354
|
-
|
|
355
|
-
To enable DevTools, add the middleware provided by `createDevtools()`.
|
|
356
|
-
|
|
357
|
-
### 1. Create a `middlewares.js` file
|
|
358
|
-
|
|
359
|
-
```javascript
|
|
360
|
-
// middlewares.js
|
|
361
|
-
import { createDevtools } from "@inglorious/web"
|
|
362
|
-
|
|
363
|
-
export const middlewares = []
|
|
364
|
-
|
|
365
|
-
// Enable DevTools only in development mode
|
|
366
|
-
if (import.meta.env.DEV) {
|
|
367
|
-
middlewares.push(createDevtools().middleware)
|
|
368
|
-
}
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
### 2. Pass middlewares when creating the store
|
|
372
|
-
|
|
373
|
-
```javascript
|
|
374
|
-
// store.js
|
|
375
|
-
import { createStore } from "@inglorious/web"
|
|
376
|
-
import { middlewares } from "./middlewares.js"
|
|
377
|
-
|
|
378
|
-
export const store = createStore({
|
|
379
|
-
types,
|
|
380
|
-
entities,
|
|
381
|
-
middlewares,
|
|
382
|
-
})
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
Now your application state is fully visible in the Redux DevTools browser extension.
|
|
386
|
-
|
|
387
|
-
### What You'll See in DevTools
|
|
388
|
-
|
|
389
|
-
- Each event you dispatch via `api.notify(event, payload)` will appear as an action in the DevTools timeline.
|
|
390
|
-
- The entire store is visible under the _State_ tab.
|
|
391
|
-
- You can time-travel or replay events exactly like in Redux.
|
|
392
|
-
|
|
393
|
-
No additional configuration is needed.
|
|
394
|
-
|
|
395
|
-
---
|
|
396
|
-
|
|
397
|
-
## Client-Side Router
|
|
398
|
-
|
|
399
|
-
`@inglorious/web` includes a lightweight, entity-based client-side router. It integrates directly into your `@inglorious/store` state, allowing your components to reactively update based on the current URL.
|
|
400
|
-
|
|
401
|
-
### 1. Setup the Router
|
|
402
|
-
|
|
403
|
-
To enable the router, add it to your store's types and create a `router` entity. Register route patterns using the router module helpers (`setRoutes`, `addRoute`) — routes are configured at module level and not stored on the router entity itself.
|
|
404
|
-
|
|
405
|
-
```javascript
|
|
406
|
-
// store.js
|
|
407
|
-
import { createStore, html } from "@inglorious/web"
|
|
408
|
-
import { router, setRoutes } from "@inglorious/web/router"
|
|
409
|
-
|
|
410
|
-
const types = {
|
|
411
|
-
// 1. Add the router type to your store's types
|
|
412
|
-
router,
|
|
413
|
-
|
|
414
|
-
// 2. Define types for your pages
|
|
415
|
-
homePage: {
|
|
416
|
-
render: () => html`<h1>Welcome Home!</h1>`,
|
|
417
|
-
},
|
|
418
|
-
userPage: {
|
|
419
|
-
render: (entity, api) => {
|
|
420
|
-
// Access route params from the router entity
|
|
421
|
-
const { params } = api.getEntity("router")
|
|
422
|
-
return html`<h1>User ${params?.id ?? "Unknown"} - ${entity.username}</h1>`
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
notFoundPage: {
|
|
426
|
-
render: () => html`<h1>404 - Page Not Found</h1>`,
|
|
427
|
-
},
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const entities = {
|
|
431
|
-
// 3. Create the router entity (no `routes` here)
|
|
432
|
-
router: {
|
|
433
|
-
type: "router",
|
|
434
|
-
},
|
|
435
|
-
userPage: {
|
|
436
|
-
type: "userPage",
|
|
437
|
-
username: "Alice",
|
|
438
|
-
},
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
export const store = createStore({ types, entities })
|
|
442
|
-
|
|
443
|
-
// Register routes at module level
|
|
444
|
-
setRoutes({
|
|
445
|
-
"/": "homePage",
|
|
446
|
-
"/users/:id": "userPage",
|
|
447
|
-
"*": "notFoundPage",
|
|
448
|
-
})
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
### 2. Render the Current Route
|
|
452
|
-
|
|
453
|
-
In your root template, read the `route` property from the router entity and use `api.render()` to display the correct page.
|
|
454
|
-
|
|
455
|
-
```javascript
|
|
456
|
-
// main.js
|
|
457
|
-
import { mount, html } from "@inglorious/web"
|
|
458
|
-
import { store } from "./store.js"
|
|
459
|
-
|
|
460
|
-
const renderApp = (api) => {
|
|
461
|
-
const { route } = api.getEntity("router") // e.g., "homePage" or "userPage"
|
|
462
|
-
|
|
463
|
-
return html`
|
|
464
|
-
<nav><a href="/">Home</a> | <a href="/users/123">User 123</a></nav>
|
|
465
|
-
<main>${route ? api.render(route) : ""}</main>
|
|
466
|
-
`
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
mount(store, renderApp, document.getElementById("root"))
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
The router automatically intercepts clicks on local `<a>` tags and handles browser back/forward events, keeping your UI in sync with the URL.
|
|
473
|
-
|
|
474
|
-
### 3. Programmatic Navigation
|
|
475
|
-
|
|
476
|
-
To navigate from your JavaScript code, dispatch a `navigate` event.
|
|
477
|
-
|
|
478
|
-
```javascript
|
|
479
|
-
api.notify("navigate", "/users/456")
|
|
480
|
-
|
|
481
|
-
// Or navigate back in history
|
|
482
|
-
api.notify("navigate", -1)
|
|
483
|
-
|
|
484
|
-
// With options
|
|
485
|
-
api.notify("navigate", {
|
|
486
|
-
to: "/users/456",
|
|
487
|
-
replace: true, // Replace current history entry
|
|
488
|
-
force: true, // Force navigation even if path is identical (useful after logout)
|
|
489
|
-
})
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
### 4. Lazy Loading Routes
|
|
493
|
-
|
|
494
|
-
You can improve performance by lazy-loading routes. Use a loader function that returns a dynamic import when registering the route via `setRoutes`.
|
|
495
|
-
|
|
496
|
-
**Note:** The imported module must use a named export for the entity type (not `export default`), so the router can register it with a unique name in the store.
|
|
497
|
-
|
|
498
|
-
```javascript
|
|
499
|
-
// store.js
|
|
500
|
-
const entities = {
|
|
501
|
-
router: { type: "router" },
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
export const store = createStore({ types, entities })
|
|
505
|
-
|
|
506
|
-
setRoutes({
|
|
507
|
-
"/": "homePage",
|
|
508
|
-
// Lazy load: returns a Promise resolving to a module
|
|
509
|
-
"/admin": () => import("./pages/admin.js"),
|
|
510
|
-
})
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
```javascript
|
|
514
|
-
// pages/admin.js
|
|
515
|
-
import { html } from "@inglorious/web"
|
|
516
|
-
|
|
517
|
-
// Must be a named export matching the type name you want to use
|
|
518
|
-
export const adminPage = {
|
|
519
|
-
render: () => html`<h1>Admin Area</h1>`,
|
|
520
|
-
}
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
### 5. Route Guards (Type Composition)
|
|
524
|
-
|
|
525
|
-
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.
|
|
526
|
-
|
|
527
|
-
Guards are simply behaviors that intercept events (like `routeChange`) and can prevent navigation, redirect, or pass through to the protected page.
|
|
528
|
-
|
|
529
|
-
#### Example: Authentication Guard
|
|
530
|
-
|
|
531
|
-
```javascript
|
|
532
|
-
// guards/require-auth.js
|
|
533
|
-
export const requireAuth = (type) => ({
|
|
534
|
-
routeChange(entity, payload, api) {
|
|
535
|
-
// Only act when navigating to this specific route
|
|
536
|
-
if (payload.route !== entity.type) return
|
|
537
|
-
|
|
538
|
-
// Check authentication
|
|
539
|
-
const user = localStorage.getItem("user")
|
|
540
|
-
if (!user) {
|
|
541
|
-
// Redirect to login, preserving the intended destination
|
|
542
|
-
api.notify("navigate", {
|
|
543
|
-
to: "/login",
|
|
544
|
-
redirectTo: window.location.pathname,
|
|
545
|
-
replace: true,
|
|
546
|
-
})
|
|
547
|
-
return
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// User is authenticated - pass through to the actual page handler
|
|
551
|
-
type.routeChange?.(entity, payload, api)
|
|
552
|
-
},
|
|
553
|
-
})
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
#### Using Guards with Type Composition
|
|
557
|
-
|
|
558
|
-
```javascript
|
|
559
|
-
// store.js
|
|
560
|
-
import { createStore } from "@inglorious/web"
|
|
561
|
-
import { router } from "@inglorious/web/router"
|
|
562
|
-
import { requireAuth } from "./guards/require-auth.js"
|
|
563
|
-
import { adminPage } from "./pages/admin.js"
|
|
564
|
-
import { loginPage } from "./pages/login.js"
|
|
565
|
-
|
|
566
|
-
const types = {
|
|
567
|
-
router,
|
|
568
|
-
|
|
569
|
-
// Public page - no guard
|
|
570
|
-
loginPage,
|
|
571
|
-
|
|
572
|
-
// Protected page - composed with requireAuth guard
|
|
573
|
-
adminPage: [adminPage, requireAuth],
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const entities = {
|
|
577
|
-
router: { type: "router" },
|
|
578
|
-
adminPage: { type: "adminPage" },
|
|
579
|
-
loginPage: { type: "loginPage" },
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
export const store = createStore({ types, entities })
|
|
583
|
-
|
|
584
|
-
// Register routes via the router module API
|
|
585
|
-
setRoutes({
|
|
586
|
-
"/login": "loginPage",
|
|
587
|
-
"/admin": "adminPage",
|
|
588
|
-
})
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
#### How Type Composition Works
|
|
592
|
-
|
|
593
|
-
When you define a type as an array like `[adminPage, requireAuth]`:
|
|
594
|
-
|
|
595
|
-
1. The behaviors compose in order (left to right)
|
|
596
|
-
2. Each behavior can intercept events before they reach the next behavior
|
|
597
|
-
3. Guards can choose to:
|
|
598
|
-
- **Block** by returning early (not calling the next handler)
|
|
599
|
-
- **Redirect** by triggering navigation to a different route
|
|
600
|
-
- **Pass through** by calling the next behavior's handler
|
|
601
|
-
|
|
602
|
-
This pattern is extremely flexible and can be used for:
|
|
603
|
-
|
|
604
|
-
- **Authentication** - Check if user is logged in
|
|
605
|
-
- **Authorization** - Check user roles or permissions
|
|
606
|
-
- **Analytics** - Log page views
|
|
607
|
-
- **Redirects** - Redirect logged-in users away from login page
|
|
608
|
-
- **Loading states** - Show loading UI while checking async permissions
|
|
609
|
-
- **Any cross-cutting concern** you can think of
|
|
610
|
-
|
|
611
|
-
#### Multiple Guards
|
|
612
|
-
|
|
613
|
-
You can compose multiple guards for fine-grained control:
|
|
614
|
-
|
|
615
|
-
```javascript
|
|
616
|
-
const types = {
|
|
617
|
-
// Require authentication AND admin role
|
|
618
|
-
adminPage: [adminPage, requireAuth, requireAdmin],
|
|
619
|
-
|
|
620
|
-
// Require authentication AND resource ownership
|
|
621
|
-
userProfile: [userProfile, requireAuth, requireOwnership],
|
|
622
|
-
}
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
Guards execute in order, so earlier guards can block navigation before later guards even run.
|
|
626
|
-
|
|
627
|
-
---
|
|
628
|
-
|
|
629
|
-
## Type Composition
|
|
630
|
-
|
|
631
|
-
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.
|
|
632
|
-
|
|
633
|
-
### Basic Composition
|
|
634
|
-
|
|
635
|
-
```javascript
|
|
636
|
-
const logging = (type) => ({
|
|
637
|
-
// Intercept the render method
|
|
638
|
-
render(entity, api) {
|
|
639
|
-
console.log(`Rendering ${entity.id}`)
|
|
640
|
-
return type.render(entity, api)
|
|
641
|
-
},
|
|
642
|
-
|
|
643
|
-
// Intercept any event
|
|
644
|
-
someEvent(entity, payload, api) {
|
|
645
|
-
console.log(`Event triggered on ${entity.id}`)
|
|
646
|
-
type.someEvent?.(entity, payload, api)
|
|
647
|
-
},
|
|
648
|
-
})
|
|
649
|
-
|
|
650
|
-
const types = {
|
|
651
|
-
// Compose the counter type with logging
|
|
652
|
-
counter: [counterBase, logging],
|
|
653
|
-
}
|
|
654
|
-
```
|
|
655
|
-
|
|
656
|
-
### Use Cases
|
|
657
|
-
|
|
658
|
-
Type composition enables elegant solutions for:
|
|
659
|
-
|
|
660
|
-
- **Route guards** - Authentication, authorization, redirects
|
|
661
|
-
- **Logging/debugging** - Trace renders and events
|
|
662
|
-
- **Analytics** - Track user interactions
|
|
663
|
-
- **Error boundaries** - Catch and handle render errors gracefully
|
|
664
|
-
- **Loading states** - Show spinners during async operations
|
|
665
|
-
- **Caching/memoization** - Cache expensive computations
|
|
666
|
-
- **Validation** - Validate entity state before operations
|
|
667
|
-
- **Any cross-cutting concern**
|
|
668
|
-
|
|
669
|
-
The composition pattern keeps your code modular and reusable without introducing framework magic.
|
|
670
|
-
|
|
671
|
-
---
|
|
672
|
-
|
|
673
|
-
## Table
|
|
674
|
-
|
|
675
|
-
`@inglorious/web` includes a `table` type for displaying data in a tabular format. It's designed to be flexible and customizable.
|
|
676
|
-
|
|
677
|
-
### 1. Add the `table` type
|
|
678
|
-
|
|
679
|
-
To use it, import the `table` type and its CSS, then create an entity for your table. You must define the `data` to be displayed and can optionally provide `columns` definitions.
|
|
680
|
-
|
|
681
|
-
```javascript
|
|
682
|
-
// In your entity definition file
|
|
683
|
-
import { table } from "@inglorious/web/table"
|
|
684
|
-
|
|
685
|
-
// Import base styles and a theme. You can create your own theme.
|
|
686
|
-
import "@inglorious/web/table/base.css"
|
|
687
|
-
import "@inglorious/web/table/theme.css"
|
|
688
|
-
|
|
689
|
-
export default {
|
|
690
|
-
...table,
|
|
691
|
-
data: [
|
|
692
|
-
{ id: 1, name: "Product A", price: 100 },
|
|
693
|
-
{ id: 2, name: "Product B", price: 150 },
|
|
694
|
-
],
|
|
695
|
-
columns: [
|
|
696
|
-
{ id: "id", label: "ID" },
|
|
697
|
-
{ id: "name", label: "Product Name" },
|
|
698
|
-
{ id: "price", label: "Price" },
|
|
699
|
-
],
|
|
700
|
-
}
|
|
701
|
-
```
|
|
702
|
-
|
|
703
|
-
### 2. Custom Rendering
|
|
704
|
-
|
|
705
|
-
You can customize how data is rendered in the table cells by overriding the `renderValue` method. This is useful for formatting values or displaying custom content.
|
|
706
|
-
|
|
707
|
-
The example below from `examples/apps/web-table/src/product-table/product-table.js` shows how to format values based on a `formatter` property in the column definition.
|
|
708
|
-
|
|
709
|
-
```javascript
|
|
710
|
-
import { table } from "@inglorious/web/table"
|
|
711
|
-
import { format } from "date-fns"
|
|
712
|
-
|
|
713
|
-
const formatters = {
|
|
714
|
-
isAvailable: (val) => (val ? "✔️" : "❌"),
|
|
715
|
-
createdAt: (val) => format(val, "dd/MM/yyyy HH:mm"),
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
export const productTable = {
|
|
719
|
-
...table,
|
|
720
|
-
|
|
721
|
-
renderValue(value, column) {
|
|
722
|
-
return formatters[column.formatter]?.(value) ?? value
|
|
723
|
-
},
|
|
724
|
-
}
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
### 3. Theming
|
|
728
|
-
|
|
729
|
-
The table comes with a base stylesheet (`@inglorious/web/table/base.css`) and a default theme (`@inglorious/web/table/theme.css`). You can create your own theme by creating a new CSS file and styling the table elements to match your application's design.
|
|
730
|
-
|
|
731
|
-
---
|
|
732
|
-
|
|
733
|
-
## Select
|
|
734
|
-
|
|
735
|
-
`@inglorious/web` includes a robust `select` type for handling dropdowns, supporting single/multi-select, filtering, and keyboard navigation.
|
|
736
|
-
|
|
737
|
-
### 1. Add the `select` type
|
|
738
|
-
|
|
739
|
-
Import the `select` type and its CSS, then create an entity.
|
|
740
|
-
|
|
741
|
-
```javascript
|
|
742
|
-
import { createStore } from "@inglorious/web"
|
|
743
|
-
import { select } from "@inglorious/web/select"
|
|
744
|
-
// Import base styles and theme
|
|
745
|
-
import "@inglorious/web/select/base.css"
|
|
746
|
-
import "@inglorious/web/select/theme.css"
|
|
747
|
-
|
|
748
|
-
const types = { select }
|
|
749
|
-
|
|
750
|
-
const entities = {
|
|
751
|
-
countrySelect: {
|
|
752
|
-
type: "select",
|
|
753
|
-
options: [
|
|
754
|
-
{ value: "us", label: "United States" },
|
|
755
|
-
{ value: "ca", label: "Canada" },
|
|
756
|
-
{ value: "fr", label: "France" },
|
|
757
|
-
],
|
|
758
|
-
// Configuration
|
|
759
|
-
isMulti: false,
|
|
760
|
-
isSearchable: true,
|
|
761
|
-
placeholder: "Select a country...",
|
|
762
|
-
},
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const store = createStore({ types, entities })
|
|
766
|
-
```
|
|
767
|
-
|
|
768
|
-
### 2. Render
|
|
769
|
-
|
|
770
|
-
Render it like any other entity.
|
|
771
|
-
|
|
772
|
-
```javascript
|
|
773
|
-
const renderApp = (api) => {
|
|
774
|
-
return html` <div class="my-form">${api.render("countrySelect")}</div> `
|
|
775
|
-
}
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
### 3. State & Events
|
|
779
|
-
|
|
780
|
-
The `select` entity maintains its own state:
|
|
781
|
-
|
|
782
|
-
- `selectedValue`: The current value (single value or array if `isMulti: true`).
|
|
783
|
-
- `isOpen`: Whether the dropdown is open.
|
|
784
|
-
- `searchTerm`: Current search input.
|
|
785
|
-
|
|
786
|
-
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.
|
|
787
|
-
|
|
788
|
-
---
|
|
789
|
-
|
|
790
|
-
## Forms
|
|
791
|
-
|
|
792
|
-
`@inglorious/web` includes a small but powerful `form` type for managing form state inside your entity store. It offers:
|
|
793
|
-
|
|
794
|
-
- Declarative form state held on an entity (`initialValues`, `values`, `errors`, `touched`)
|
|
795
|
-
- Convenient helpers for reading field value/error/touched state (`getFieldValue`, `getFieldError`, `isFieldTouched`)
|
|
796
|
-
- Built-in handlers for field changes, blurs, array fields, sync/async validation and submission
|
|
797
|
-
|
|
798
|
-
### Add the `form` type
|
|
799
|
-
|
|
800
|
-
Include `form` in your `types` and create an entity for the form (use any id you like — `form` is used below for clarity):
|
|
801
|
-
|
|
802
|
-
```javascript
|
|
803
|
-
import { createStore } from "@inglorious/web"
|
|
804
|
-
import { form } from "@inglorious/web/form"
|
|
805
|
-
|
|
806
|
-
const types = { form }
|
|
807
|
-
|
|
808
|
-
const entities = {
|
|
809
|
-
form: {
|
|
810
|
-
type: "form",
|
|
811
|
-
initialValues: {
|
|
812
|
-
name: "",
|
|
813
|
-
email: "",
|
|
814
|
-
addresses: [],
|
|
815
|
-
},
|
|
816
|
-
},
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const store = createStore({ types, entities })
|
|
820
|
-
```
|
|
821
|
-
|
|
822
|
-
### How it works (events & helpers)
|
|
823
|
-
|
|
824
|
-
The `form` type listens for a simple set of events (target the specific entity id with `#<id>:<event>`):
|
|
825
|
-
|
|
826
|
-
- `#<id>:fieldChange` — payload { path, value, validate? } — set a field value and optionally run a single-field validator
|
|
827
|
-
- `#<id>:fieldBlur` — payload { path, validate? } — mark field touched and optionally validate on blur
|
|
828
|
-
- `#<id>:fieldArrayAppend|fieldArrayRemove|fieldArrayInsert|fieldArrayMove` — manipulate array fields
|
|
829
|
-
- `#<id>:reset` — reset the form to `initialValues`
|
|
830
|
-
- `#<id>:validate` — synchronous whole-form validation; payload { validate }
|
|
831
|
-
- `#<id>:validateAsync` — async whole-form validation; payload { validate }
|
|
832
|
-
- `#<id>:submit` — typically handled by your `form` type's `submit` method (implement custom behavior there)
|
|
833
|
-
|
|
834
|
-
Helpers available from the package let you read state from templates and field helper components:
|
|
835
|
-
|
|
836
|
-
- `getFieldValue(formEntity, path)` — read a nested field value
|
|
837
|
-
- `getFieldError(formEntity, path)` — read a nested field's error message
|
|
838
|
-
- `isFieldTouched(formEntity, path)` — check if a field has been touched
|
|
839
|
-
|
|
840
|
-
Form state includes helpful flags:
|
|
841
|
-
|
|
842
|
-
- `isPristine` — whether the form has changed from initial values
|
|
843
|
-
- `isValid` — whether the current form has no validation errors
|
|
844
|
-
- `isValidating` — whether async validation is in progress
|
|
845
|
-
- `isSubmitting` — whether submission is in progress
|
|
846
|
-
- `submitError` — an optional submission-level error message
|
|
847
|
-
|
|
848
|
-
### Simple example (from examples/apps/web-form)
|
|
849
|
-
|
|
850
|
-
Field components typically call `api.notify` and the `form` entity reacts accordingly. Example input field usage:
|
|
851
|
-
|
|
852
|
-
```javascript
|
|
853
|
-
// inside a field component render
|
|
854
|
-
@input=${(e) => api.notify(`#${entity.id}:fieldChange`, { path: 'name', value: e.target.value, validate: validateName })}
|
|
855
|
-
@blur=${() => api.notify(`#${entity.id}:fieldBlur`, { path: 'name', validate: validateName })}
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
Submissions and whole-form validation can be triggered from a `form` render:
|
|
859
|
-
|
|
860
|
-
```javascript
|
|
861
|
-
<form @submit=${() => { api.notify(`#form:validate`, { validate: validateForm }); api.notify(`#form:submit`) }}>
|
|
862
|
-
<!-- inputs / buttons -->
|
|
863
|
-
</form>
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
For a complete, working demo and helper components look at `examples/apps/web-form` which ships with the repository.
|
|
867
|
-
|
|
868
|
-
---
|
|
869
|
-
|
|
870
|
-
## Virtualized lists
|
|
871
|
-
|
|
872
|
-
`@inglorious/web` provides a small virtualized `list` type to efficiently render very long lists by only keeping visible items in the DOM. The `list` type is useful when you need to display large datasets without paying the full cost of mounting every element at once.
|
|
873
|
-
|
|
874
|
-
Key features:
|
|
875
|
-
|
|
876
|
-
- Renders only the visible slice of items and positions them absolutely inside a scrolling container.
|
|
877
|
-
- Automatically measures the first visible item height when not provided.
|
|
878
|
-
- Efficient scroll handling with simple buffer controls to avoid visual gaps.
|
|
879
|
-
|
|
880
|
-
### Typical entity shape
|
|
881
|
-
|
|
882
|
-
When you add the `list` type to your store the entity can include these properties (the type will provide sensible defaults). Only `items` is required — all other properties are optional:
|
|
883
|
-
|
|
884
|
-
- `items` (Array) — the dataset to render.
|
|
885
|
-
- `visibleRange` ({ start, end }) — current visible slice indices.
|
|
886
|
-
- `viewportHeight` (number) — height of the scrolling viewport in pixels.
|
|
887
|
-
- `itemHeight` (number | null) — fixed height for each item (when null, the type will measure the first item and use an estimated height).
|
|
888
|
-
- `estimatedHeight` (number) — fallback height used before measurement.
|
|
889
|
-
- `bufferSize` (number) — extra items to render before/after the visible range to reduce flicker during scrolling.
|
|
890
|
-
|
|
891
|
-
### Events & methods
|
|
892
|
-
|
|
893
|
-
The `list` type listens for the following events on the target entity:
|
|
894
|
-
|
|
895
|
-
- `#<id>:scroll` — payload is the scrolling container; updates `visibleRange` based on scroll position.
|
|
896
|
-
- `#<id>:measureHeight` — payload is the container element; used internally to measure the first item and compute `itemHeight`.
|
|
897
|
-
|
|
898
|
-
It also expects the item type to export `renderItem(item, index, api)` so each visible item can be rendered using the project's entity-based render approach.
|
|
899
|
-
|
|
900
|
-
### Example
|
|
901
|
-
|
|
902
|
-
Minimal example showing how to extend the `list` type to create a domain-specific list (e.g. `productList`) and provide a `renderItem(item, index, api)` helper.
|
|
903
|
-
|
|
904
|
-
```javascript
|
|
905
|
-
import { createStore, html } from "@inglorious/web"
|
|
906
|
-
import { list } from "@inglorious/web/list"
|
|
907
|
-
|
|
908
|
-
// Extend the built-in list type to render product items
|
|
909
|
-
const productList = {
|
|
910
|
-
...list,
|
|
911
|
-
|
|
912
|
-
renderItem(item, index) {
|
|
913
|
-
return html`<div class="product">
|
|
914
|
-
${index}: <strong>${item.name}</strong> — ${item.price}
|
|
915
|
-
</div>`
|
|
916
|
-
},
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
const types = { list: productList }
|
|
920
|
-
|
|
921
|
-
const entities = {
|
|
922
|
-
products: {
|
|
923
|
-
type: "list",
|
|
924
|
-
items: Array.from({ length: 10000 }, (_, i) => ({
|
|
925
|
-
name: `Product ${i}`,
|
|
926
|
-
price: `$${i}`,
|
|
927
|
-
})),
|
|
928
|
-
viewportHeight: 400,
|
|
929
|
-
estimatedHeight: 40,
|
|
930
|
-
bufferSize: 5,
|
|
931
|
-
},
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
const store = createStore({ types, entities })
|
|
935
|
-
|
|
936
|
-
// Render with api.render(entity.id) as usual — the list will call productList.renderItem for each visible item.
|
|
937
|
-
```
|
|
938
|
-
|
|
939
|
-
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`.
|
|
940
|
-
|
|
941
|
-
---
|
|
942
|
-
|
|
943
|
-
## API Reference
|
|
944
|
-
|
|
945
|
-
**`mount(store, renderFn, element)`**
|
|
946
|
-
|
|
947
|
-
Connects a store to a `lit-html` template and renders it into a DOM element. It automatically handles re-rendering on state changes.
|
|
948
|
-
|
|
949
|
-
**Parameters:**
|
|
950
|
-
|
|
951
|
-
- `store` (required): An instance of `@inglorious/store`.
|
|
952
|
-
- `renderFn(api)` (required): A function that takes an `api` object and returns a `lit-html` `TemplateResult` or `null`.
|
|
953
|
-
- `element` (required): The `HTMLElement` or `DocumentFragment` to render the template into.
|
|
954
|
-
|
|
955
|
-
**Returns:**
|
|
956
|
-
|
|
957
|
-
- `() => void`: An `unsubscribe` function to stop listening to store updates and clean up.
|
|
958
|
-
|
|
959
|
-
### The `api` Object
|
|
960
|
-
|
|
961
|
-
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.
|
|
962
|
-
|
|
963
|
-
**`api.render(id, options?)`**
|
|
964
|
-
|
|
965
|
-
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.
|
|
966
|
-
|
|
967
|
-
### Re-exported `lit-html` Utilities
|
|
968
|
-
|
|
969
|
-
For convenience, `@inglorious/web` re-exports the most common utilities from `@inglorious/store` and `lit-html`, so you only need one import.
|
|
970
|
-
|
|
971
|
-
```javascript
|
|
972
|
-
import {
|
|
973
|
-
// from @inglorious/store
|
|
974
|
-
createStore,
|
|
975
|
-
createDevtools,
|
|
976
|
-
createSelector,
|
|
977
|
-
// from @inglorious/store/test
|
|
978
|
-
trigger,
|
|
979
|
-
// from lit-html
|
|
980
|
-
mount,
|
|
981
|
-
html,
|
|
982
|
-
render,
|
|
983
|
-
svg,
|
|
984
|
-
// lit-html directives
|
|
985
|
-
choose,
|
|
986
|
-
classMap,
|
|
987
|
-
ref,
|
|
988
|
-
repeat,
|
|
989
|
-
styleMap,
|
|
990
|
-
unsafeHTML,
|
|
991
|
-
when,
|
|
992
|
-
} from "@inglorious/web"
|
|
993
|
-
|
|
994
|
-
// Subpath imports for tree-shaking
|
|
995
|
-
import {
|
|
996
|
-
form,
|
|
997
|
-
getFieldError,
|
|
998
|
-
getFieldValue,
|
|
999
|
-
isFieldTouched,
|
|
1000
|
-
} from "@inglorious/web/form"
|
|
1001
|
-
import { list } from "@inglorious/web/list"
|
|
1002
|
-
import { router } from "@inglorious/web/router"
|
|
1003
|
-
import { select } from "@inglorious/web/select"
|
|
1004
|
-
import { table } from "@inglorious/web/table"
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
---
|
|
1008
|
-
|
|
1009
|
-
## Error Handling
|
|
1010
|
-
|
|
1011
|
-
When an entity's `render()` method throws an error, it can crash your entire app since the whole tree re-renders.
|
|
1012
|
-
|
|
1013
|
-
**Best practice:** Wrap your render logic in try-catch at the entity level:
|
|
1014
|
-
|
|
1015
|
-
```javascript
|
|
1016
|
-
const myType = {
|
|
1017
|
-
render(entity, api) {
|
|
1018
|
-
try {
|
|
1019
|
-
// Your render logic
|
|
1020
|
-
return html`<div>...</div>`
|
|
1021
|
-
} catch (error) {
|
|
1022
|
-
console.error("Render error:", error)
|
|
1023
|
-
return html`<div class="error">Failed to render ${entity.id}</div>`
|
|
1024
|
-
}
|
|
1025
|
-
},
|
|
1026
|
-
}
|
|
1027
|
-
```
|
|
1028
|
-
|
|
1029
|
-
---
|
|
1030
|
-
|
|
1031
|
-
## Performance Tips
|
|
1032
|
-
|
|
1033
|
-
1. **Keep render() pure** - No side effects, no API calls
|
|
1034
|
-
2. **Avoid creating new objects in render** - Use entity properties, not inline `{}`
|
|
1035
|
-
3. **Use `repeat()` directive for lists** - Helps lit-html track item identity
|
|
1036
|
-
4. **Profile with browser DevTools** - Look for slow renders (>16ms)
|
|
1037
|
-
5. **Consider virtualization** - Use `list` type for 1000+ items
|
|
1038
|
-
|
|
1039
|
-
If renders are slow:
|
|
1040
|
-
|
|
1041
|
-
- Move expensive computations to event handlers
|
|
1042
|
-
- Cache derived values on the entity
|
|
1043
|
-
- ...Or memoize them!
|
|
1044
|
-
|
|
1045
|
-
---
|
|
1046
|
-
|
|
1047
|
-
## Relationship to Inglorious Engine
|
|
1048
|
-
|
|
1049
|
-
`@inglorious/web` shares its architectural philosophy with [Inglorious Engine](https://www.npmjs.com/package/@inglorious/engine):
|
|
1050
|
-
|
|
1051
|
-
- **Same state management** - Both use `@inglorious/store`
|
|
1052
|
-
- **Same event system** - Entity behaviors respond to events
|
|
1053
|
-
- **Same rendering model** - Full-state render on every update
|
|
1054
|
-
|
|
1055
|
-
The key difference:
|
|
1056
|
-
|
|
1057
|
-
- **@inglorious/engine** targets game loops (60fps, Canvas/WebGL rendering)
|
|
1058
|
-
- **@inglorious/web** targets web UIs (DOM rendering, user interactions)
|
|
1059
|
-
|
|
1060
|
-
You can even mix them in the same app!
|
|
1061
|
-
|
|
1062
|
-
---
|
|
1063
|
-
|
|
1064
|
-
## Static Site Generation with SSX
|
|
1065
|
-
|
|
1066
|
-
For building **static HTML sites** with full pre-rendering, client-side hydration, and automatic sitemap/RSS generation, use [**@inglorious/ssx**](https://www.npmjs.com/package/@inglorious/ssx).
|
|
1067
|
-
|
|
1068
|
-
SSX is built entirely on **@inglorious/web** and lets you use the same entity-based patterns for both interactive apps and static sites, with:
|
|
1069
|
-
|
|
1070
|
-
- Pre-rendered HTML at build time
|
|
1071
|
-
- Automatic code splitting and lazy loading
|
|
1072
|
-
- Client-side hydration with lit-html
|
|
1073
|
-
- File-based routing
|
|
1074
|
-
- Sitemap and RSS feed generation
|
|
1075
|
-
- Incremental builds
|
|
1076
|
-
|
|
1077
|
-
It's the perfect companion to @inglorious/web for building blazing-fast static sites, blogs, documentation, and marketing pages.
|
|
1078
|
-
|
|
1079
|
-
---
|
|
1080
|
-
|
|
1081
|
-
## Examples
|
|
1082
|
-
|
|
1083
|
-
Check out these demos to see `@inglorious/web` in action:
|
|
1084
|
-
|
|
1085
|
-
- **[Web TodoMVC](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-todomvc)** - A client-only TodoMVC implementation, a good starting point for learning the framework.
|
|
1086
|
-
- **[Web TodoMVC-CS](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-todomvc-cs)** - A client-server version with JSON server, showing async event handlers and API integration with component organization (render/handlers modules).
|
|
1087
|
-
- **[Web Form](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-form)** - Form handling with validation, arrays, and field helpers.
|
|
1088
|
-
- **[Web List](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-list)** - Virtualized list with `renderItem` helper for efficient rendering of large datasets.
|
|
1089
|
-
- **[Web Table](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-table)** - Table component with complex data display patterns.
|
|
1090
|
-
- **[Web Router](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-router)** - Entity-based client-side routing with hash navigation.
|
|
1091
|
-
|
|
1092
|
-
---
|
|
1093
|
-
|
|
1094
|
-
## Related Packages
|
|
1095
|
-
|
|
1096
|
-
- [**@inglorious/ssx**](https://www.npmjs.com/package/@inglorious/ssx) - Static site generation with pre-rendering and client hydration
|
|
1097
|
-
- [**@inglorious/store**](https://www.npmjs.com/package/@inglorious/store) - Entity-based state management (used by @inglorious/web)
|
|
1098
|
-
- [**@inglorious/engine**](https://www.npmjs.com/package/@inglorious/engine) - Game engine with the same entity architecture
|
|
1099
|
-
- [**@inglorious/create-app**](https://www.npmjs.com/package/@inglorious/create-app) - Scaffolding tool for quick project setup
|
|
1100
|
-
|
|
1101
|
-
---
|
|
1102
|
-
|
|
1103
|
-
## License
|
|
1104
|
-
|
|
1105
|
-
**MIT License - Free and open source**
|
|
1106
|
-
|
|
1107
|
-
Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
1108
|
-
|
|
1109
|
-
You're free to use, modify, and distribute this software. See [LICENSE](./LICENSE) for details.
|
|
1110
|
-
|
|
1111
|
-
---
|
|
1112
|
-
|
|
1113
|
-
## Contributing
|
|
1114
|
-
|
|
1115
|
-
Contributions welcome! Please read our [Contributing Guidelines](../../CONTRIBUTING.md) first.
|
|
1
|
+
# @inglorious/web
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@inglorious/web)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
A lightweight, reactive-enough web framework built on **pure JavaScript**, the entity-based state management provided by **@inglorious/store**, and the DOM-diffing efficiency of **lit-html**.
|
|
7
|
+
|
|
8
|
+
Unlike modern frameworks that invent their own languages or rely on signals, proxies, or compilers, **@inglorious/web embraces plain JavaScript** and a transparent architecture.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Full-tree Re-rendering with DOM Diffing**
|
|
15
|
+
Your entire template tree re-renders on every state change, while **lit-html updates only the minimal DOM parts**.
|
|
16
|
+
No VDOM, no signals, no hidden dependencies.
|
|
17
|
+
|
|
18
|
+
- **Entity-Based Rendering Model**
|
|
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.
|
|
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
|
+
|
|
25
|
+
- **Simple and Predictable API**
|
|
26
|
+
Zero magic, zero reactivity graphs, zero compiler.
|
|
27
|
+
Just JavaScript functions and store events.
|
|
28
|
+
|
|
29
|
+
- **Router, Forms, Tables, Virtual Lists**
|
|
30
|
+
High-level primitives built on the same predictable model.
|
|
31
|
+
|
|
32
|
+
- **Zero Component State**
|
|
33
|
+
All state lives in the store — never inside components.
|
|
34
|
+
|
|
35
|
+
- **No Signals, No Subscriptions, No Memory Leaks**
|
|
36
|
+
Because every render is triggered by the store, and lit-html handles the rest.
|
|
37
|
+
|
|
38
|
+
- **No compilation required**
|
|
39
|
+
Apps can run directly in the browser — no build/compile step is strictly necessary (though you may use bundlers or Vite for convenience in larger projects).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Create App (scaffolding)
|
|
44
|
+
|
|
45
|
+
To help bootstrap projects quickly, there's an official scaffolding tool: **[`@inglorious/create-app`](https://www.npmjs.com/package/@inglorious/create-app)**. It generates opinionated boilerplates so you can start coding right away.
|
|
46
|
+
|
|
47
|
+
Available templates:
|
|
48
|
+
|
|
49
|
+
- **minimal** — plain HTML, CSS, and JS (no build step)
|
|
50
|
+
- **js** — Vite-based JavaScript project
|
|
51
|
+
- **ts** — Vite + TypeScript project
|
|
52
|
+
- **ssx-js** — Static Site Xecution (SSX) project using JavaScript
|
|
53
|
+
- **ssx-ts** — Static Site Xecution (SSX) project using TypeScript
|
|
54
|
+
|
|
55
|
+
Use the scaffolder to create a starter app tailored to your workflow.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Key Architectural Insight
|
|
60
|
+
|
|
61
|
+
### ✨ **Inglorious Web re-renders the whole template tree on each state change.**
|
|
62
|
+
|
|
63
|
+
Thanks to lit-html's optimized diffing, this is fast, predictable, and surprisingly efficient.
|
|
64
|
+
|
|
65
|
+
This means:
|
|
66
|
+
|
|
67
|
+
- **You do NOT need fine-grained reactivity**
|
|
68
|
+
- **You do NOT need selectors/signals/memos**
|
|
69
|
+
- **You do NOT track dependencies between UI fragments**
|
|
70
|
+
- **You cannot accidentally create memory leaks through subscriptions**
|
|
71
|
+
|
|
72
|
+
You get Svelte-like ergonomic simplicity, but with no compiler and no magic.
|
|
73
|
+
|
|
74
|
+
> "Re-render everything → let lit-html update only what changed."
|
|
75
|
+
|
|
76
|
+
It's that simple — and surprisingly fast in practice.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## When to Use Inglorious Web
|
|
81
|
+
|
|
82
|
+
- You want predictable behavior
|
|
83
|
+
- You prefer explicit state transitions
|
|
84
|
+
- You want to avoid complex reactive graphs
|
|
85
|
+
- You want UI to be fully controlled by your entity-based store
|
|
86
|
+
- You want to stay entirely in **JavaScript**, without DSLs or compilers
|
|
87
|
+
- You want **React-like declarative UI** but without the cost and overhead of React
|
|
88
|
+
- You want to build **static sites with SSX** — same entity patterns, pre-rendered HTML, and client hydration
|
|
89
|
+
|
|
90
|
+
This framework is ideal for both small apps and large business UIs.
|
|
91
|
+
|
|
92
|
+
--
|
|
93
|
+
|
|
94
|
+
## When NOT to Use Inglorious Web
|
|
95
|
+
|
|
96
|
+
- You need fine-grained reactivity for very large datasets (1000+ items per view)
|
|
97
|
+
- You're building a library that needs to be framework-agnostic
|
|
98
|
+
- Your team is already deeply invested in React/Vue/Angular
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Why Inglorious Web Avoids Signals
|
|
103
|
+
|
|
104
|
+
Other modern frameworks use:
|
|
105
|
+
|
|
106
|
+
- Proxies (Vue)
|
|
107
|
+
- Observables (MobX)
|
|
108
|
+
- Fine-grained signals (Solid, Angular v17+)
|
|
109
|
+
- Compiler-generated reactivity (Svelte)
|
|
110
|
+
- Fiber or granular subscriptions (React, Preact, Qwik, etc.)
|
|
111
|
+
|
|
112
|
+
These systems are powerful but introduce:
|
|
113
|
+
|
|
114
|
+
- hidden dependencies
|
|
115
|
+
- memory retention risks
|
|
116
|
+
- unpredictable update ordering
|
|
117
|
+
- steep learning curves
|
|
118
|
+
- framework-specific languages
|
|
119
|
+
- need for cleanup, teardown, and special lifecycle APIs
|
|
120
|
+
- challenges when mixing with game engines, workers, or non-UI code
|
|
121
|
+
|
|
122
|
+
### Inglorious Web takes a different stance:
|
|
123
|
+
|
|
124
|
+
✔ **Every entity update is explicit**
|
|
125
|
+
✔ **Every UI update is a full diff pass**
|
|
126
|
+
✔ **Every part of the system is just JavaScript**
|
|
127
|
+
✔ **No special lifecycle**
|
|
128
|
+
✔ **No subscriptions needed**
|
|
129
|
+
✔ **No signals**
|
|
130
|
+
✔ **No cleanup**
|
|
131
|
+
✔ **No surprises**
|
|
132
|
+
|
|
133
|
+
This makes it especially suitable for:
|
|
134
|
+
|
|
135
|
+
- realtime applications
|
|
136
|
+
- hybrid UI/game engine contexts
|
|
137
|
+
- large enterprise apps where predictability matters
|
|
138
|
+
- developers who prefer simplicity over magic
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
# Comparison with Other Frameworks
|
|
143
|
+
|
|
144
|
+
Here's how @inglorious/web compares to the major players:
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## **React**
|
|
149
|
+
|
|
150
|
+
| Feature | React | Inglorious Web |
|
|
151
|
+
| ------------------------- | ----------------------------- | ---------------------------------- |
|
|
152
|
+
| Rendering model | VDOM diff + effects | Full tree template + lit-html diff |
|
|
153
|
+
| Language | JSX (non-JS) | Pure JavaScript |
|
|
154
|
+
| Component state | Yes | No — store only |
|
|
155
|
+
| Refs & lifecycles | Many | None needed |
|
|
156
|
+
| Signals / fine reactivity | No (but heavy reconciliation) | No (rely on lit-html diff) |
|
|
157
|
+
| Reconciliation overhead | High (full VDOM diff) | Low (template string diff) |
|
|
158
|
+
| Bundle size | Large | Tiny |
|
|
159
|
+
| Learning curve | Medium/High | Very low |
|
|
160
|
+
|
|
161
|
+
React is powerful but complicated. Inglorious Web is simpler, lighter, and closer to native JS.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## **Vue (3)**
|
|
166
|
+
|
|
167
|
+
| Feature | Vue | Inglorious Web |
|
|
168
|
+
| --------------- | -------------------------- | ----------------------------------- |
|
|
169
|
+
| Reactivity | Proxy-based, deep tracking | Event-based updates + lit-html diff |
|
|
170
|
+
| Templates | DSL | JavaScript templates |
|
|
171
|
+
| Component state | Yes | No |
|
|
172
|
+
| Lifecycle | Many | None |
|
|
173
|
+
| Compiler | Required for SFC | None |
|
|
174
|
+
|
|
175
|
+
Vue reactivity is elegant but complex. Inglorious Web avoids proxies and keeps everything explicit.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## **Svelte**
|
|
180
|
+
|
|
181
|
+
| Feature | Svelte | Inglorious Web |
|
|
182
|
+
| -------------- | --------------------------- | ------------------ |
|
|
183
|
+
| Compiler | Required | None |
|
|
184
|
+
| Reactivity | Compiler transforms $labels | Transparent JS |
|
|
185
|
+
| Granularity | Fine-grained | Full-tree rerender |
|
|
186
|
+
| Learning curve | Medium | Low |
|
|
187
|
+
|
|
188
|
+
Svelte is magic; Inglorious Web is explicit.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## **SolidJS**
|
|
193
|
+
|
|
194
|
+
| Feature | Solid | Inglorious Web |
|
|
195
|
+
| ---------- | -------------------- | ------------------ |
|
|
196
|
+
| Reactivity | Fine-grained signals | No signals |
|
|
197
|
+
| Components | Run once | Rerun always |
|
|
198
|
+
| Cleanup | Required | None |
|
|
199
|
+
| Behavior | Highly optimized | Highly predictable |
|
|
200
|
+
|
|
201
|
+
Solid is extremely fast but requires a mental model.
|
|
202
|
+
Inglorious Web trades peak performance for simplicity and zero overhead.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## **Qwik**
|
|
207
|
+
|
|
208
|
+
| Feature | Qwik | Inglorious Web |
|
|
209
|
+
| -------------------- | -------------------- | -------------- |
|
|
210
|
+
| Execution model | Resumable | Plain JS |
|
|
211
|
+
| Framework complexity | Very high | Very low |
|
|
212
|
+
| Reactivity | Fine-grained signals | None |
|
|
213
|
+
|
|
214
|
+
Qwik targets extreme performance at extreme complexity.
|
|
215
|
+
Inglorious Web is minimal, predictable, and tiny.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## **HTMX / Alpine / Vanilla DOM**
|
|
220
|
+
|
|
221
|
+
Inglorious Web is closer philosophically to **HTMX** and **vanilla JS**, but with a declarative rendering model and entity-based state.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
# Why Choose Inglorious Web
|
|
226
|
+
|
|
227
|
+
- Minimalistic
|
|
228
|
+
- Pure JavaScript
|
|
229
|
+
- Entity-based and predictable
|
|
230
|
+
- Extremely easy to reason about
|
|
231
|
+
- One render path, no hidden rules
|
|
232
|
+
- No reactivity graphs
|
|
233
|
+
- No per-component subscriptions
|
|
234
|
+
- No memory leaks
|
|
235
|
+
- No build step required (apps can run in the browser)
|
|
236
|
+
- Works perfectly in hybrid UI/game engine contexts
|
|
237
|
+
- Uses native ES modules and standards
|
|
238
|
+
|
|
239
|
+
If you want a framework that **does not fight JavaScript**, this is the one.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Installation
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
npm install @inglorious/web
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Quick Start
|
|
252
|
+
|
|
253
|
+
### 1. Define Your Store and Entity Renders
|
|
254
|
+
|
|
255
|
+
First, set up your store with entity types. For each type you want to render, add a render method that returns a `lit-html` template.
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
// store.js
|
|
259
|
+
import { createStore, html } from "@inglorious/web"
|
|
260
|
+
|
|
261
|
+
const types = {
|
|
262
|
+
counter: {
|
|
263
|
+
increment(entity, id) {
|
|
264
|
+
if (entity.id !== id) return
|
|
265
|
+
entity.value++
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
// Define how a 'counter' entity should be rendered
|
|
269
|
+
render(entity, api) {
|
|
270
|
+
return html`
|
|
271
|
+
<div>
|
|
272
|
+
<span>Count: ${entity.value}</span>
|
|
273
|
+
<button @click=${() => api.notify("increment", entity.id)}>+1</button>
|
|
274
|
+
</div>
|
|
275
|
+
`
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const entities = {
|
|
281
|
+
counter1: { type: "counter", value: 0 },
|
|
282
|
+
counter2: { type: "counter", value: 10 },
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export const store = createStore({ types, entities })
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### 2. Create Your Root Template and Mount
|
|
289
|
+
|
|
290
|
+
Write a root rendering function that uses the provided api to compose the UI, then use `mount` to attach it to the DOM.
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
// main.js
|
|
294
|
+
import { mount, html } from "@inglorious/web"
|
|
295
|
+
import { store } from "./store.js"
|
|
296
|
+
|
|
297
|
+
// This function receives the API and returns a lit-html template
|
|
298
|
+
const renderApp = (api) => {
|
|
299
|
+
const entities = Object.values(api.getEntities())
|
|
300
|
+
|
|
301
|
+
return html`
|
|
302
|
+
<h1>Counters</h1>
|
|
303
|
+
${entities.map((entity) => api.render(entity.id))}
|
|
304
|
+
`
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Mount the app to the DOM
|
|
308
|
+
mount(store, renderApp, document.getElementById("root"))
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The `mount` function subscribes to the store and automatically re-renders your template whenever the state changes.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## JSX Support
|
|
316
|
+
|
|
317
|
+
If you prefer JSX syntax over template literals, you can use **[`@inglorious/vite-plugin-jsx`](https://www.npmjs.com/package/@inglorious/vite-plugin-jsx)**.
|
|
318
|
+
|
|
319
|
+
This Vite plugin transforms standard JSX/TSX into optimized `lit-html` templates at compile time. You get the familiar developer experience of JSX without React's runtime, hooks, or VDOM overhead.
|
|
320
|
+
|
|
321
|
+
To use it:
|
|
322
|
+
|
|
323
|
+
1. Install the plugin: `npm install -D @inglorious/vite-plugin-jsx`
|
|
324
|
+
2. Add it to your `vite.config.js`
|
|
325
|
+
3. Write your render functions using JSX
|
|
326
|
+
|
|
327
|
+
```jsx
|
|
328
|
+
export const counter = {
|
|
329
|
+
render(entity, api) {
|
|
330
|
+
return (
|
|
331
|
+
<div className="counter">
|
|
332
|
+
<span>Count: {entity.value}</span>
|
|
333
|
+
<button onClick={() => api.notify(`#${entity.id}:increment`)}>
|
|
334
|
+
+1
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
)
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
See the plugin documentation for full details on control flow, attributes, and engine components.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Redux DevTools Integration
|
|
347
|
+
|
|
348
|
+
`@inglorious/web` ships with first-class support for the **Redux DevTools Extension**, allowing you to:
|
|
349
|
+
|
|
350
|
+
- inspect all store events
|
|
351
|
+
- time-travel through state changes
|
|
352
|
+
- restore previous states
|
|
353
|
+
- debug your entity-based logic visually
|
|
354
|
+
|
|
355
|
+
To enable DevTools, add the middleware provided by `createDevtools()`.
|
|
356
|
+
|
|
357
|
+
### 1. Create a `middlewares.js` file
|
|
358
|
+
|
|
359
|
+
```javascript
|
|
360
|
+
// middlewares.js
|
|
361
|
+
import { createDevtools } from "@inglorious/web"
|
|
362
|
+
|
|
363
|
+
export const middlewares = []
|
|
364
|
+
|
|
365
|
+
// Enable DevTools only in development mode
|
|
366
|
+
if (import.meta.env.DEV) {
|
|
367
|
+
middlewares.push(createDevtools().middleware)
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### 2. Pass middlewares when creating the store
|
|
372
|
+
|
|
373
|
+
```javascript
|
|
374
|
+
// store.js
|
|
375
|
+
import { createStore } from "@inglorious/web"
|
|
376
|
+
import { middlewares } from "./middlewares.js"
|
|
377
|
+
|
|
378
|
+
export const store = createStore({
|
|
379
|
+
types,
|
|
380
|
+
entities,
|
|
381
|
+
middlewares,
|
|
382
|
+
})
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Now your application state is fully visible in the Redux DevTools browser extension.
|
|
386
|
+
|
|
387
|
+
### What You'll See in DevTools
|
|
388
|
+
|
|
389
|
+
- Each event you dispatch via `api.notify(event, payload)` will appear as an action in the DevTools timeline.
|
|
390
|
+
- The entire store is visible under the _State_ tab.
|
|
391
|
+
- You can time-travel or replay events exactly like in Redux.
|
|
392
|
+
|
|
393
|
+
No additional configuration is needed.
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Client-Side Router
|
|
398
|
+
|
|
399
|
+
`@inglorious/web` includes a lightweight, entity-based client-side router. It integrates directly into your `@inglorious/store` state, allowing your components to reactively update based on the current URL.
|
|
400
|
+
|
|
401
|
+
### 1. Setup the Router
|
|
402
|
+
|
|
403
|
+
To enable the router, add it to your store's types and create a `router` entity. Register route patterns using the router module helpers (`setRoutes`, `addRoute`) — routes are configured at module level and not stored on the router entity itself.
|
|
404
|
+
|
|
405
|
+
```javascript
|
|
406
|
+
// store.js
|
|
407
|
+
import { createStore, html } from "@inglorious/web"
|
|
408
|
+
import { router, setRoutes } from "@inglorious/web/router"
|
|
409
|
+
|
|
410
|
+
const types = {
|
|
411
|
+
// 1. Add the router type to your store's types
|
|
412
|
+
router,
|
|
413
|
+
|
|
414
|
+
// 2. Define types for your pages
|
|
415
|
+
homePage: {
|
|
416
|
+
render: () => html`<h1>Welcome Home!</h1>`,
|
|
417
|
+
},
|
|
418
|
+
userPage: {
|
|
419
|
+
render: (entity, api) => {
|
|
420
|
+
// Access route params from the router entity
|
|
421
|
+
const { params } = api.getEntity("router")
|
|
422
|
+
return html`<h1>User ${params?.id ?? "Unknown"} - ${entity.username}</h1>`
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
notFoundPage: {
|
|
426
|
+
render: () => html`<h1>404 - Page Not Found</h1>`,
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const entities = {
|
|
431
|
+
// 3. Create the router entity (no `routes` here)
|
|
432
|
+
router: {
|
|
433
|
+
type: "router",
|
|
434
|
+
},
|
|
435
|
+
userPage: {
|
|
436
|
+
type: "userPage",
|
|
437
|
+
username: "Alice",
|
|
438
|
+
},
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export const store = createStore({ types, entities })
|
|
442
|
+
|
|
443
|
+
// Register routes at module level
|
|
444
|
+
setRoutes({
|
|
445
|
+
"/": "homePage",
|
|
446
|
+
"/users/:id": "userPage",
|
|
447
|
+
"*": "notFoundPage",
|
|
448
|
+
})
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### 2. Render the Current Route
|
|
452
|
+
|
|
453
|
+
In your root template, read the `route` property from the router entity and use `api.render()` to display the correct page.
|
|
454
|
+
|
|
455
|
+
```javascript
|
|
456
|
+
// main.js
|
|
457
|
+
import { mount, html } from "@inglorious/web"
|
|
458
|
+
import { store } from "./store.js"
|
|
459
|
+
|
|
460
|
+
const renderApp = (api) => {
|
|
461
|
+
const { route } = api.getEntity("router") // e.g., "homePage" or "userPage"
|
|
462
|
+
|
|
463
|
+
return html`
|
|
464
|
+
<nav><a href="/">Home</a> | <a href="/users/123">User 123</a></nav>
|
|
465
|
+
<main>${route ? api.render(route) : ""}</main>
|
|
466
|
+
`
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
mount(store, renderApp, document.getElementById("root"))
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
The router automatically intercepts clicks on local `<a>` tags and handles browser back/forward events, keeping your UI in sync with the URL.
|
|
473
|
+
|
|
474
|
+
### 3. Programmatic Navigation
|
|
475
|
+
|
|
476
|
+
To navigate from your JavaScript code, dispatch a `navigate` event.
|
|
477
|
+
|
|
478
|
+
```javascript
|
|
479
|
+
api.notify("navigate", "/users/456")
|
|
480
|
+
|
|
481
|
+
// Or navigate back in history
|
|
482
|
+
api.notify("navigate", -1)
|
|
483
|
+
|
|
484
|
+
// With options
|
|
485
|
+
api.notify("navigate", {
|
|
486
|
+
to: "/users/456",
|
|
487
|
+
replace: true, // Replace current history entry
|
|
488
|
+
force: true, // Force navigation even if path is identical (useful after logout)
|
|
489
|
+
})
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 4. Lazy Loading Routes
|
|
493
|
+
|
|
494
|
+
You can improve performance by lazy-loading routes. Use a loader function that returns a dynamic import when registering the route via `setRoutes`.
|
|
495
|
+
|
|
496
|
+
**Note:** The imported module must use a named export for the entity type (not `export default`), so the router can register it with a unique name in the store.
|
|
497
|
+
|
|
498
|
+
```javascript
|
|
499
|
+
// store.js
|
|
500
|
+
const entities = {
|
|
501
|
+
router: { type: "router" },
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export const store = createStore({ types, entities })
|
|
505
|
+
|
|
506
|
+
setRoutes({
|
|
507
|
+
"/": "homePage",
|
|
508
|
+
// Lazy load: returns a Promise resolving to a module
|
|
509
|
+
"/admin": () => import("./pages/admin.js"),
|
|
510
|
+
})
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
```javascript
|
|
514
|
+
// pages/admin.js
|
|
515
|
+
import { html } from "@inglorious/web"
|
|
516
|
+
|
|
517
|
+
// Must be a named export matching the type name you want to use
|
|
518
|
+
export const adminPage = {
|
|
519
|
+
render: () => html`<h1>Admin Area</h1>`,
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### 5. Route Guards (Type Composition)
|
|
524
|
+
|
|
525
|
+
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.
|
|
526
|
+
|
|
527
|
+
Guards are simply behaviors that intercept events (like `routeChange`) and can prevent navigation, redirect, or pass through to the protected page.
|
|
528
|
+
|
|
529
|
+
#### Example: Authentication Guard
|
|
530
|
+
|
|
531
|
+
```javascript
|
|
532
|
+
// guards/require-auth.js
|
|
533
|
+
export const requireAuth = (type) => ({
|
|
534
|
+
routeChange(entity, payload, api) {
|
|
535
|
+
// Only act when navigating to this specific route
|
|
536
|
+
if (payload.route !== entity.type) return
|
|
537
|
+
|
|
538
|
+
// Check authentication
|
|
539
|
+
const user = localStorage.getItem("user")
|
|
540
|
+
if (!user) {
|
|
541
|
+
// Redirect to login, preserving the intended destination
|
|
542
|
+
api.notify("navigate", {
|
|
543
|
+
to: "/login",
|
|
544
|
+
redirectTo: window.location.pathname,
|
|
545
|
+
replace: true,
|
|
546
|
+
})
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// User is authenticated - pass through to the actual page handler
|
|
551
|
+
type.routeChange?.(entity, payload, api)
|
|
552
|
+
},
|
|
553
|
+
})
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
#### Using Guards with Type Composition
|
|
557
|
+
|
|
558
|
+
```javascript
|
|
559
|
+
// store.js
|
|
560
|
+
import { createStore } from "@inglorious/web"
|
|
561
|
+
import { router } from "@inglorious/web/router"
|
|
562
|
+
import { requireAuth } from "./guards/require-auth.js"
|
|
563
|
+
import { adminPage } from "./pages/admin.js"
|
|
564
|
+
import { loginPage } from "./pages/login.js"
|
|
565
|
+
|
|
566
|
+
const types = {
|
|
567
|
+
router,
|
|
568
|
+
|
|
569
|
+
// Public page - no guard
|
|
570
|
+
loginPage,
|
|
571
|
+
|
|
572
|
+
// Protected page - composed with requireAuth guard
|
|
573
|
+
adminPage: [adminPage, requireAuth],
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const entities = {
|
|
577
|
+
router: { type: "router" },
|
|
578
|
+
adminPage: { type: "adminPage" },
|
|
579
|
+
loginPage: { type: "loginPage" },
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export const store = createStore({ types, entities })
|
|
583
|
+
|
|
584
|
+
// Register routes via the router module API
|
|
585
|
+
setRoutes({
|
|
586
|
+
"/login": "loginPage",
|
|
587
|
+
"/admin": "adminPage",
|
|
588
|
+
})
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
#### How Type Composition Works
|
|
592
|
+
|
|
593
|
+
When you define a type as an array like `[adminPage, requireAuth]`:
|
|
594
|
+
|
|
595
|
+
1. The behaviors compose in order (left to right)
|
|
596
|
+
2. Each behavior can intercept events before they reach the next behavior
|
|
597
|
+
3. Guards can choose to:
|
|
598
|
+
- **Block** by returning early (not calling the next handler)
|
|
599
|
+
- **Redirect** by triggering navigation to a different route
|
|
600
|
+
- **Pass through** by calling the next behavior's handler
|
|
601
|
+
|
|
602
|
+
This pattern is extremely flexible and can be used for:
|
|
603
|
+
|
|
604
|
+
- **Authentication** - Check if user is logged in
|
|
605
|
+
- **Authorization** - Check user roles or permissions
|
|
606
|
+
- **Analytics** - Log page views
|
|
607
|
+
- **Redirects** - Redirect logged-in users away from login page
|
|
608
|
+
- **Loading states** - Show loading UI while checking async permissions
|
|
609
|
+
- **Any cross-cutting concern** you can think of
|
|
610
|
+
|
|
611
|
+
#### Multiple Guards
|
|
612
|
+
|
|
613
|
+
You can compose multiple guards for fine-grained control:
|
|
614
|
+
|
|
615
|
+
```javascript
|
|
616
|
+
const types = {
|
|
617
|
+
// Require authentication AND admin role
|
|
618
|
+
adminPage: [adminPage, requireAuth, requireAdmin],
|
|
619
|
+
|
|
620
|
+
// Require authentication AND resource ownership
|
|
621
|
+
userProfile: [userProfile, requireAuth, requireOwnership],
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
Guards execute in order, so earlier guards can block navigation before later guards even run.
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## Type Composition
|
|
630
|
+
|
|
631
|
+
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.
|
|
632
|
+
|
|
633
|
+
### Basic Composition
|
|
634
|
+
|
|
635
|
+
```javascript
|
|
636
|
+
const logging = (type) => ({
|
|
637
|
+
// Intercept the render method
|
|
638
|
+
render(entity, api) {
|
|
639
|
+
console.log(`Rendering ${entity.id}`)
|
|
640
|
+
return type.render(entity, api)
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
// Intercept any event
|
|
644
|
+
someEvent(entity, payload, api) {
|
|
645
|
+
console.log(`Event triggered on ${entity.id}`)
|
|
646
|
+
type.someEvent?.(entity, payload, api)
|
|
647
|
+
},
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
const types = {
|
|
651
|
+
// Compose the counter type with logging
|
|
652
|
+
counter: [counterBase, logging],
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Use Cases
|
|
657
|
+
|
|
658
|
+
Type composition enables elegant solutions for:
|
|
659
|
+
|
|
660
|
+
- **Route guards** - Authentication, authorization, redirects
|
|
661
|
+
- **Logging/debugging** - Trace renders and events
|
|
662
|
+
- **Analytics** - Track user interactions
|
|
663
|
+
- **Error boundaries** - Catch and handle render errors gracefully
|
|
664
|
+
- **Loading states** - Show spinners during async operations
|
|
665
|
+
- **Caching/memoization** - Cache expensive computations
|
|
666
|
+
- **Validation** - Validate entity state before operations
|
|
667
|
+
- **Any cross-cutting concern**
|
|
668
|
+
|
|
669
|
+
The composition pattern keeps your code modular and reusable without introducing framework magic.
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## Table
|
|
674
|
+
|
|
675
|
+
`@inglorious/web` includes a `table` type for displaying data in a tabular format. It's designed to be flexible and customizable.
|
|
676
|
+
|
|
677
|
+
### 1. Add the `table` type
|
|
678
|
+
|
|
679
|
+
To use it, import the `table` type and its CSS, then create an entity for your table. You must define the `data` to be displayed and can optionally provide `columns` definitions.
|
|
680
|
+
|
|
681
|
+
```javascript
|
|
682
|
+
// In your entity definition file
|
|
683
|
+
import { table } from "@inglorious/web/table"
|
|
684
|
+
|
|
685
|
+
// Import base styles and a theme. You can create your own theme.
|
|
686
|
+
import "@inglorious/web/table/base.css"
|
|
687
|
+
import "@inglorious/web/table/theme.css"
|
|
688
|
+
|
|
689
|
+
export default {
|
|
690
|
+
...table,
|
|
691
|
+
data: [
|
|
692
|
+
{ id: 1, name: "Product A", price: 100 },
|
|
693
|
+
{ id: 2, name: "Product B", price: 150 },
|
|
694
|
+
],
|
|
695
|
+
columns: [
|
|
696
|
+
{ id: "id", label: "ID" },
|
|
697
|
+
{ id: "name", label: "Product Name" },
|
|
698
|
+
{ id: "price", label: "Price" },
|
|
699
|
+
],
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### 2. Custom Rendering
|
|
704
|
+
|
|
705
|
+
You can customize how data is rendered in the table cells by overriding the `renderValue` method. This is useful for formatting values or displaying custom content.
|
|
706
|
+
|
|
707
|
+
The example below from `examples/apps/web-table/src/product-table/product-table.js` shows how to format values based on a `formatter` property in the column definition.
|
|
708
|
+
|
|
709
|
+
```javascript
|
|
710
|
+
import { table } from "@inglorious/web/table"
|
|
711
|
+
import { format } from "date-fns"
|
|
712
|
+
|
|
713
|
+
const formatters = {
|
|
714
|
+
isAvailable: (val) => (val ? "✔️" : "❌"),
|
|
715
|
+
createdAt: (val) => format(val, "dd/MM/yyyy HH:mm"),
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export const productTable = {
|
|
719
|
+
...table,
|
|
720
|
+
|
|
721
|
+
renderValue(value, column) {
|
|
722
|
+
return formatters[column.formatter]?.(value) ?? value
|
|
723
|
+
},
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### 3. Theming
|
|
728
|
+
|
|
729
|
+
The table comes with a base stylesheet (`@inglorious/web/table/base.css`) and a default theme (`@inglorious/web/table/theme.css`). You can create your own theme by creating a new CSS file and styling the table elements to match your application's design.
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## Select
|
|
734
|
+
|
|
735
|
+
`@inglorious/web` includes a robust `select` type for handling dropdowns, supporting single/multi-select, filtering, and keyboard navigation.
|
|
736
|
+
|
|
737
|
+
### 1. Add the `select` type
|
|
738
|
+
|
|
739
|
+
Import the `select` type and its CSS, then create an entity.
|
|
740
|
+
|
|
741
|
+
```javascript
|
|
742
|
+
import { createStore } from "@inglorious/web"
|
|
743
|
+
import { select } from "@inglorious/web/select"
|
|
744
|
+
// Import base styles and theme
|
|
745
|
+
import "@inglorious/web/select/base.css"
|
|
746
|
+
import "@inglorious/web/select/theme.css"
|
|
747
|
+
|
|
748
|
+
const types = { select }
|
|
749
|
+
|
|
750
|
+
const entities = {
|
|
751
|
+
countrySelect: {
|
|
752
|
+
type: "select",
|
|
753
|
+
options: [
|
|
754
|
+
{ value: "us", label: "United States" },
|
|
755
|
+
{ value: "ca", label: "Canada" },
|
|
756
|
+
{ value: "fr", label: "France" },
|
|
757
|
+
],
|
|
758
|
+
// Configuration
|
|
759
|
+
isMulti: false,
|
|
760
|
+
isSearchable: true,
|
|
761
|
+
placeholder: "Select a country...",
|
|
762
|
+
},
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const store = createStore({ types, entities })
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### 2. Render
|
|
769
|
+
|
|
770
|
+
Render it like any other entity.
|
|
771
|
+
|
|
772
|
+
```javascript
|
|
773
|
+
const renderApp = (api) => {
|
|
774
|
+
return html` <div class="my-form">${api.render("countrySelect")}</div> `
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
### 3. State & Events
|
|
779
|
+
|
|
780
|
+
The `select` entity maintains its own state:
|
|
781
|
+
|
|
782
|
+
- `selectedValue`: The current value (single value or array if `isMulti: true`).
|
|
783
|
+
- `isOpen`: Whether the dropdown is open.
|
|
784
|
+
- `searchTerm`: Current search input.
|
|
785
|
+
|
|
786
|
+
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.
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
## Forms
|
|
791
|
+
|
|
792
|
+
`@inglorious/web` includes a small but powerful `form` type for managing form state inside your entity store. It offers:
|
|
793
|
+
|
|
794
|
+
- Declarative form state held on an entity (`initialValues`, `values`, `errors`, `touched`)
|
|
795
|
+
- Convenient helpers for reading field value/error/touched state (`getFieldValue`, `getFieldError`, `isFieldTouched`)
|
|
796
|
+
- Built-in handlers for field changes, blurs, array fields, sync/async validation and submission
|
|
797
|
+
|
|
798
|
+
### Add the `form` type
|
|
799
|
+
|
|
800
|
+
Include `form` in your `types` and create an entity for the form (use any id you like — `form` is used below for clarity):
|
|
801
|
+
|
|
802
|
+
```javascript
|
|
803
|
+
import { createStore } from "@inglorious/web"
|
|
804
|
+
import { form } from "@inglorious/web/form"
|
|
805
|
+
|
|
806
|
+
const types = { form }
|
|
807
|
+
|
|
808
|
+
const entities = {
|
|
809
|
+
form: {
|
|
810
|
+
type: "form",
|
|
811
|
+
initialValues: {
|
|
812
|
+
name: "",
|
|
813
|
+
email: "",
|
|
814
|
+
addresses: [],
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const store = createStore({ types, entities })
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
### How it works (events & helpers)
|
|
823
|
+
|
|
824
|
+
The `form` type listens for a simple set of events (target the specific entity id with `#<id>:<event>`):
|
|
825
|
+
|
|
826
|
+
- `#<id>:fieldChange` — payload { path, value, validate? } — set a field value and optionally run a single-field validator
|
|
827
|
+
- `#<id>:fieldBlur` — payload { path, validate? } — mark field touched and optionally validate on blur
|
|
828
|
+
- `#<id>:fieldArrayAppend|fieldArrayRemove|fieldArrayInsert|fieldArrayMove` — manipulate array fields
|
|
829
|
+
- `#<id>:reset` — reset the form to `initialValues`
|
|
830
|
+
- `#<id>:validate` — synchronous whole-form validation; payload { validate }
|
|
831
|
+
- `#<id>:validateAsync` — async whole-form validation; payload { validate }
|
|
832
|
+
- `#<id>:submit` — typically handled by your `form` type's `submit` method (implement custom behavior there)
|
|
833
|
+
|
|
834
|
+
Helpers available from the package let you read state from templates and field helper components:
|
|
835
|
+
|
|
836
|
+
- `getFieldValue(formEntity, path)` — read a nested field value
|
|
837
|
+
- `getFieldError(formEntity, path)` — read a nested field's error message
|
|
838
|
+
- `isFieldTouched(formEntity, path)` — check if a field has been touched
|
|
839
|
+
|
|
840
|
+
Form state includes helpful flags:
|
|
841
|
+
|
|
842
|
+
- `isPristine` — whether the form has changed from initial values
|
|
843
|
+
- `isValid` — whether the current form has no validation errors
|
|
844
|
+
- `isValidating` — whether async validation is in progress
|
|
845
|
+
- `isSubmitting` — whether submission is in progress
|
|
846
|
+
- `submitError` — an optional submission-level error message
|
|
847
|
+
|
|
848
|
+
### Simple example (from examples/apps/web-form)
|
|
849
|
+
|
|
850
|
+
Field components typically call `api.notify` and the `form` entity reacts accordingly. Example input field usage:
|
|
851
|
+
|
|
852
|
+
```javascript
|
|
853
|
+
// inside a field component render
|
|
854
|
+
@input=${(e) => api.notify(`#${entity.id}:fieldChange`, { path: 'name', value: e.target.value, validate: validateName })}
|
|
855
|
+
@blur=${() => api.notify(`#${entity.id}:fieldBlur`, { path: 'name', validate: validateName })}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
Submissions and whole-form validation can be triggered from a `form` render:
|
|
859
|
+
|
|
860
|
+
```javascript
|
|
861
|
+
<form @submit=${() => { api.notify(`#form:validate`, { validate: validateForm }); api.notify(`#form:submit`) }}>
|
|
862
|
+
<!-- inputs / buttons -->
|
|
863
|
+
</form>
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
For a complete, working demo and helper components look at `examples/apps/web-form` which ships with the repository.
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
## Virtualized lists
|
|
871
|
+
|
|
872
|
+
`@inglorious/web` provides a small virtualized `list` type to efficiently render very long lists by only keeping visible items in the DOM. The `list` type is useful when you need to display large datasets without paying the full cost of mounting every element at once.
|
|
873
|
+
|
|
874
|
+
Key features:
|
|
875
|
+
|
|
876
|
+
- Renders only the visible slice of items and positions them absolutely inside a scrolling container.
|
|
877
|
+
- Automatically measures the first visible item height when not provided.
|
|
878
|
+
- Efficient scroll handling with simple buffer controls to avoid visual gaps.
|
|
879
|
+
|
|
880
|
+
### Typical entity shape
|
|
881
|
+
|
|
882
|
+
When you add the `list` type to your store the entity can include these properties (the type will provide sensible defaults). Only `items` is required — all other properties are optional:
|
|
883
|
+
|
|
884
|
+
- `items` (Array) — the dataset to render.
|
|
885
|
+
- `visibleRange` ({ start, end }) — current visible slice indices.
|
|
886
|
+
- `viewportHeight` (number) — height of the scrolling viewport in pixels.
|
|
887
|
+
- `itemHeight` (number | null) — fixed height for each item (when null, the type will measure the first item and use an estimated height).
|
|
888
|
+
- `estimatedHeight` (number) — fallback height used before measurement.
|
|
889
|
+
- `bufferSize` (number) — extra items to render before/after the visible range to reduce flicker during scrolling.
|
|
890
|
+
|
|
891
|
+
### Events & methods
|
|
892
|
+
|
|
893
|
+
The `list` type listens for the following events on the target entity:
|
|
894
|
+
|
|
895
|
+
- `#<id>:scroll` — payload is the scrolling container; updates `visibleRange` based on scroll position.
|
|
896
|
+
- `#<id>:measureHeight` — payload is the container element; used internally to measure the first item and compute `itemHeight`.
|
|
897
|
+
|
|
898
|
+
It also expects the item type to export `renderItem(item, index, api)` so each visible item can be rendered using the project's entity-based render approach.
|
|
899
|
+
|
|
900
|
+
### Example
|
|
901
|
+
|
|
902
|
+
Minimal example showing how to extend the `list` type to create a domain-specific list (e.g. `productList`) and provide a `renderItem(item, index, api)` helper.
|
|
903
|
+
|
|
904
|
+
```javascript
|
|
905
|
+
import { createStore, html } from "@inglorious/web"
|
|
906
|
+
import { list } from "@inglorious/web/list"
|
|
907
|
+
|
|
908
|
+
// Extend the built-in list type to render product items
|
|
909
|
+
const productList = {
|
|
910
|
+
...list,
|
|
911
|
+
|
|
912
|
+
renderItem(item, index) {
|
|
913
|
+
return html`<div class="product">
|
|
914
|
+
${index}: <strong>${item.name}</strong> — ${item.price}
|
|
915
|
+
</div>`
|
|
916
|
+
},
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const types = { list: productList }
|
|
920
|
+
|
|
921
|
+
const entities = {
|
|
922
|
+
products: {
|
|
923
|
+
type: "list",
|
|
924
|
+
items: Array.from({ length: 10000 }, (_, i) => ({
|
|
925
|
+
name: `Product ${i}`,
|
|
926
|
+
price: `$${i}`,
|
|
927
|
+
})),
|
|
928
|
+
viewportHeight: 400,
|
|
929
|
+
estimatedHeight: 40,
|
|
930
|
+
bufferSize: 5,
|
|
931
|
+
},
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const store = createStore({ types, entities })
|
|
935
|
+
|
|
936
|
+
// Render with api.render(entity.id) as usual — the list will call productList.renderItem for each visible item.
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
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`.
|
|
940
|
+
|
|
941
|
+
---
|
|
942
|
+
|
|
943
|
+
## API Reference
|
|
944
|
+
|
|
945
|
+
**`mount(store, renderFn, element)`**
|
|
946
|
+
|
|
947
|
+
Connects a store to a `lit-html` template and renders it into a DOM element. It automatically handles re-rendering on state changes.
|
|
948
|
+
|
|
949
|
+
**Parameters:**
|
|
950
|
+
|
|
951
|
+
- `store` (required): An instance of `@inglorious/store`.
|
|
952
|
+
- `renderFn(api)` (required): A function that takes an `api` object and returns a `lit-html` `TemplateResult` or `null`.
|
|
953
|
+
- `element` (required): The `HTMLElement` or `DocumentFragment` to render the template into.
|
|
954
|
+
|
|
955
|
+
**Returns:**
|
|
956
|
+
|
|
957
|
+
- `() => void`: An `unsubscribe` function to stop listening to store updates and clean up.
|
|
958
|
+
|
|
959
|
+
### The `api` Object
|
|
960
|
+
|
|
961
|
+
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.
|
|
962
|
+
|
|
963
|
+
**`api.render(id, options?)`**
|
|
964
|
+
|
|
965
|
+
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.
|
|
966
|
+
|
|
967
|
+
### Re-exported `lit-html` Utilities
|
|
968
|
+
|
|
969
|
+
For convenience, `@inglorious/web` re-exports the most common utilities from `@inglorious/store` and `lit-html`, so you only need one import.
|
|
970
|
+
|
|
971
|
+
```javascript
|
|
972
|
+
import {
|
|
973
|
+
// from @inglorious/store
|
|
974
|
+
createStore,
|
|
975
|
+
createDevtools,
|
|
976
|
+
createSelector,
|
|
977
|
+
// from @inglorious/store/test
|
|
978
|
+
trigger,
|
|
979
|
+
// from lit-html
|
|
980
|
+
mount,
|
|
981
|
+
html,
|
|
982
|
+
render,
|
|
983
|
+
svg,
|
|
984
|
+
// lit-html directives
|
|
985
|
+
choose,
|
|
986
|
+
classMap,
|
|
987
|
+
ref,
|
|
988
|
+
repeat,
|
|
989
|
+
styleMap,
|
|
990
|
+
unsafeHTML,
|
|
991
|
+
when,
|
|
992
|
+
} from "@inglorious/web"
|
|
993
|
+
|
|
994
|
+
// Subpath imports for tree-shaking
|
|
995
|
+
import {
|
|
996
|
+
form,
|
|
997
|
+
getFieldError,
|
|
998
|
+
getFieldValue,
|
|
999
|
+
isFieldTouched,
|
|
1000
|
+
} from "@inglorious/web/form"
|
|
1001
|
+
import { list } from "@inglorious/web/list"
|
|
1002
|
+
import { router } from "@inglorious/web/router"
|
|
1003
|
+
import { select } from "@inglorious/web/select"
|
|
1004
|
+
import { table } from "@inglorious/web/table"
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
---
|
|
1008
|
+
|
|
1009
|
+
## Error Handling
|
|
1010
|
+
|
|
1011
|
+
When an entity's `render()` method throws an error, it can crash your entire app since the whole tree re-renders.
|
|
1012
|
+
|
|
1013
|
+
**Best practice:** Wrap your render logic in try-catch at the entity level:
|
|
1014
|
+
|
|
1015
|
+
```javascript
|
|
1016
|
+
const myType = {
|
|
1017
|
+
render(entity, api) {
|
|
1018
|
+
try {
|
|
1019
|
+
// Your render logic
|
|
1020
|
+
return html`<div>...</div>`
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
console.error("Render error:", error)
|
|
1023
|
+
return html`<div class="error">Failed to render ${entity.id}</div>`
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
}
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
---
|
|
1030
|
+
|
|
1031
|
+
## Performance Tips
|
|
1032
|
+
|
|
1033
|
+
1. **Keep render() pure** - No side effects, no API calls
|
|
1034
|
+
2. **Avoid creating new objects in render** - Use entity properties, not inline `{}`
|
|
1035
|
+
3. **Use `repeat()` directive for lists** - Helps lit-html track item identity
|
|
1036
|
+
4. **Profile with browser DevTools** - Look for slow renders (>16ms)
|
|
1037
|
+
5. **Consider virtualization** - Use `list` type for 1000+ items
|
|
1038
|
+
|
|
1039
|
+
If renders are slow:
|
|
1040
|
+
|
|
1041
|
+
- Move expensive computations to event handlers
|
|
1042
|
+
- Cache derived values on the entity
|
|
1043
|
+
- ...Or memoize them!
|
|
1044
|
+
|
|
1045
|
+
---
|
|
1046
|
+
|
|
1047
|
+
## Relationship to Inglorious Engine
|
|
1048
|
+
|
|
1049
|
+
`@inglorious/web` shares its architectural philosophy with [Inglorious Engine](https://www.npmjs.com/package/@inglorious/engine):
|
|
1050
|
+
|
|
1051
|
+
- **Same state management** - Both use `@inglorious/store`
|
|
1052
|
+
- **Same event system** - Entity behaviors respond to events
|
|
1053
|
+
- **Same rendering model** - Full-state render on every update
|
|
1054
|
+
|
|
1055
|
+
The key difference:
|
|
1056
|
+
|
|
1057
|
+
- **@inglorious/engine** targets game loops (60fps, Canvas/WebGL rendering)
|
|
1058
|
+
- **@inglorious/web** targets web UIs (DOM rendering, user interactions)
|
|
1059
|
+
|
|
1060
|
+
You can even mix them in the same app!
|
|
1061
|
+
|
|
1062
|
+
---
|
|
1063
|
+
|
|
1064
|
+
## Static Site Generation with SSX
|
|
1065
|
+
|
|
1066
|
+
For building **static HTML sites** with full pre-rendering, client-side hydration, and automatic sitemap/RSS generation, use [**@inglorious/ssx**](https://www.npmjs.com/package/@inglorious/ssx).
|
|
1067
|
+
|
|
1068
|
+
SSX is built entirely on **@inglorious/web** and lets you use the same entity-based patterns for both interactive apps and static sites, with:
|
|
1069
|
+
|
|
1070
|
+
- Pre-rendered HTML at build time
|
|
1071
|
+
- Automatic code splitting and lazy loading
|
|
1072
|
+
- Client-side hydration with lit-html
|
|
1073
|
+
- File-based routing
|
|
1074
|
+
- Sitemap and RSS feed generation
|
|
1075
|
+
- Incremental builds
|
|
1076
|
+
|
|
1077
|
+
It's the perfect companion to @inglorious/web for building blazing-fast static sites, blogs, documentation, and marketing pages.
|
|
1078
|
+
|
|
1079
|
+
---
|
|
1080
|
+
|
|
1081
|
+
## Examples
|
|
1082
|
+
|
|
1083
|
+
Check out these demos to see `@inglorious/web` in action:
|
|
1084
|
+
|
|
1085
|
+
- **[Web TodoMVC](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-todomvc)** - A client-only TodoMVC implementation, a good starting point for learning the framework.
|
|
1086
|
+
- **[Web TodoMVC-CS](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-todomvc-cs)** - A client-server version with JSON server, showing async event handlers and API integration with component organization (render/handlers modules).
|
|
1087
|
+
- **[Web Form](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-form)** - Form handling with validation, arrays, and field helpers.
|
|
1088
|
+
- **[Web List](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-list)** - Virtualized list with `renderItem` helper for efficient rendering of large datasets.
|
|
1089
|
+
- **[Web Table](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-table)** - Table component with complex data display patterns.
|
|
1090
|
+
- **[Web Router](https://github.com/IngloriousCoderz/inglorious-forge/tree/main/examples/apps/web-router)** - Entity-based client-side routing with hash navigation.
|
|
1091
|
+
|
|
1092
|
+
---
|
|
1093
|
+
|
|
1094
|
+
## Related Packages
|
|
1095
|
+
|
|
1096
|
+
- [**@inglorious/ssx**](https://www.npmjs.com/package/@inglorious/ssx) - Static site generation with pre-rendering and client hydration
|
|
1097
|
+
- [**@inglorious/store**](https://www.npmjs.com/package/@inglorious/store) - Entity-based state management (used by @inglorious/web)
|
|
1098
|
+
- [**@inglorious/engine**](https://www.npmjs.com/package/@inglorious/engine) - Game engine with the same entity architecture
|
|
1099
|
+
- [**@inglorious/create-app**](https://www.npmjs.com/package/@inglorious/create-app) - Scaffolding tool for quick project setup
|
|
1100
|
+
|
|
1101
|
+
---
|
|
1102
|
+
|
|
1103
|
+
## License
|
|
1104
|
+
|
|
1105
|
+
**MIT License - Free and open source**
|
|
1106
|
+
|
|
1107
|
+
Created by [Matteo Antony Mistretta](https://github.com/IngloriousCoderz)
|
|
1108
|
+
|
|
1109
|
+
You're free to use, modify, and distribute this software. See [LICENSE](./LICENSE) for details.
|
|
1110
|
+
|
|
1111
|
+
---
|
|
1112
|
+
|
|
1113
|
+
## Contributing
|
|
1114
|
+
|
|
1115
|
+
Contributions welcome! Please read our [Contributing Guidelines](../../CONTRIBUTING.md) first.
|