@inglorious/web 4.0.9 → 4.0.11
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 +99 -10
- package/package.json +3 -3
- package/src/list/index.js +4 -165
- package/src/list/logic.js +75 -0
- package/src/list/rendering.js +105 -0
- package/src/table/rendering.js +98 -10
- package/types/list.d.ts +39 -24
- package/types/table.d.ts +36 -20
package/README.md
CHANGED
|
@@ -96,8 +96,15 @@ This framework is ideal for both small apps and large business UIs.
|
|
|
96
96
|
## When NOT to Use Inglorious Web
|
|
97
97
|
|
|
98
98
|
- You're frequently mutating thousands of items without virtualization (though our `list` component handles this elegantly)
|
|
99
|
-
- You
|
|
100
|
-
- Your team is already deeply invested in React/Vue/Angular
|
|
99
|
+
- You need framework-agnostic components for users who might not use Inglorious (use Web Components instead)
|
|
100
|
+
- Your team is already deeply invested in React/Vue/Angular and migration costs outweigh benefits
|
|
101
|
+
|
|
102
|
+
## When Inglorious Web EXCELS
|
|
103
|
+
|
|
104
|
+
- Building **internal component libraries** - Types are more customizable than React components
|
|
105
|
+
- Creating **design systems** - Spread, override, and compose behaviors freely
|
|
106
|
+
- Building **pattern libraries** - Ship pre-configured entity types that users can adapt
|
|
107
|
+
- Apps where **predictable state flow** matters more than ecosystem size
|
|
101
108
|
|
|
102
109
|
---
|
|
103
110
|
|
|
@@ -145,14 +152,16 @@ This makes it especially suitable for:
|
|
|
145
152
|
|
|
146
153
|
### TL;DR Quick Comparison
|
|
147
154
|
|
|
148
|
-
| Framework | Reactivity | Compiler | Component State | Bundle Size | Learning Curve |
|
|
149
|
-
| -------------- | -------------- |
|
|
150
|
-
| Inglorious Web | Event-based | None
|
|
151
|
-
| React | VDOM diffing |
|
|
152
|
-
| Vue | Proxy-based |
|
|
153
|
-
| Svelte | Compiler magic | Required | Yes | Small | Medium |
|
|
154
|
-
| SolidJS | Fine signals |
|
|
155
|
-
| Qwik | Resumable | Required | Yes | Small | Very High |
|
|
155
|
+
| Framework | Reactivity | Build Transform | Runtime Compiler | Component State | Bundle Size | Learning Curve |
|
|
156
|
+
| -------------- | -------------- | ---------------- | ---------------- | --------------- | ----------- | -------------- |
|
|
157
|
+
| Inglorious Web | Event-based | None required | No | No (store only) | Tiny | Very Low |
|
|
158
|
+
| React | VDOM diffing | JSX + optional\* | No | Yes | Large | Medium/High |
|
|
159
|
+
| Vue | Proxy-based | SFC (optional) | Available | Yes | Medium | Medium |
|
|
160
|
+
| Svelte | Compiler magic | Required | No | Yes | Small | Medium |
|
|
161
|
+
| SolidJS | Fine signals | JSX only | No | No (runs once) | Tiny | Medium/High |
|
|
162
|
+
| Qwik | Resumable | Required | No | Yes | Small | Very High |
|
|
163
|
+
|
|
164
|
+
\*React Compiler (stable 2024+) provides automatic memoization
|
|
156
165
|
|
|
157
166
|
<details>
|
|
158
167
|
<summary><strong>Click to expand detailed framework comparisons</strong></summary>
|
|
@@ -1299,6 +1308,86 @@ See `src/list.js` in the package for the implementation details and the `example
|
|
|
1299
1308
|
|
|
1300
1309
|
---
|
|
1301
1310
|
|
|
1311
|
+
## Building Component Libraries with Inglorious Web
|
|
1312
|
+
|
|
1313
|
+
**Inglorious types are uniquely suited for component libraries** because they're fully customizable through JavaScript's spread operator.
|
|
1314
|
+
|
|
1315
|
+
### The Pattern
|
|
1316
|
+
|
|
1317
|
+
**Library publishes types:**
|
|
1318
|
+
|
|
1319
|
+
```javascript
|
|
1320
|
+
// @acme/design-system/table.js
|
|
1321
|
+
export const table = {
|
|
1322
|
+
render(entity, api) {
|
|
1323
|
+
const type = api.getType(entity.type)
|
|
1324
|
+
|
|
1325
|
+
return html`
|
|
1326
|
+
<table>
|
|
1327
|
+
${entity.data.map((row) => type.renderRow(entity, row, api))}
|
|
1328
|
+
</table>
|
|
1329
|
+
`
|
|
1330
|
+
},
|
|
1331
|
+
|
|
1332
|
+
renderRow(entity, row, api) {
|
|
1333
|
+
return html`<tr>
|
|
1334
|
+
${row.name}
|
|
1335
|
+
</tr>`
|
|
1336
|
+
},
|
|
1337
|
+
|
|
1338
|
+
rowClick(entity, row) {
|
|
1339
|
+
entity.selectedRow = row
|
|
1340
|
+
},
|
|
1341
|
+
}
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
**Users customize freely:**
|
|
1345
|
+
|
|
1346
|
+
```javascript
|
|
1347
|
+
import { table } from "@acme/design-system/table"
|
|
1348
|
+
|
|
1349
|
+
// Use as-is
|
|
1350
|
+
const simpleTable = { ...table }
|
|
1351
|
+
|
|
1352
|
+
// Override methods
|
|
1353
|
+
const customTable = {
|
|
1354
|
+
...table,
|
|
1355
|
+
renderRow(entity, row, api) {
|
|
1356
|
+
return html`<tr class="custom">
|
|
1357
|
+
${row.name} - ${row.email}
|
|
1358
|
+
</tr>`
|
|
1359
|
+
},
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Compose with behaviors
|
|
1363
|
+
import { sortable, exportable } from "@acme/design-system/behaviors"
|
|
1364
|
+
|
|
1365
|
+
const advancedTable = [
|
|
1366
|
+
table,
|
|
1367
|
+
sortable,
|
|
1368
|
+
exportable,
|
|
1369
|
+
{
|
|
1370
|
+
rowClick(entity, row) {
|
|
1371
|
+
console.log("Custom click handler", row)
|
|
1372
|
+
},
|
|
1373
|
+
},
|
|
1374
|
+
]
|
|
1375
|
+
```
|
|
1376
|
+
|
|
1377
|
+
### Why This Is Better Than React Components
|
|
1378
|
+
|
|
1379
|
+
| Feature | React Components | Inglorious Types |
|
|
1380
|
+
| -------------------------- | ------------------------------ | ----------------------------------------- |
|
|
1381
|
+
| Customize rendering | Only if `render` prop exposed | Override `render()` method directly |
|
|
1382
|
+
| Customize behavior | Only if callback props exposed | Override any method |
|
|
1383
|
+
| Compose multiple libraries | Wrapper hell / HOCs | Array composition `[type1, type2, {...}]` |
|
|
1384
|
+
| Access internals | Private - must fork | Public - spread and override |
|
|
1385
|
+
| Type safety | Props interface | Entity schema + method signatures |
|
|
1386
|
+
|
|
1387
|
+
**Users get complete control** without forking your library. They can override rendering, behavior, or both—down to individual methods.
|
|
1388
|
+
|
|
1389
|
+
---
|
|
1390
|
+
|
|
1302
1391
|
## Using Third-Party Web Components
|
|
1303
1392
|
|
|
1304
1393
|
Inglorious Web works seamlessly with any Web Component library, such as Shoelace, Material Web Components, or Lion. Thanks to lit-html's efficient diffing algorithm, Web Components maintain their internal state even when the full tree re-renders - the component only updates when its properties change.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/web",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.11",
|
|
4
4
|
"description": "A new web framework that leverages the power of the Inglorious Store combined with the performance and simplicity of lit-html.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@lit-labs/ssr-client": "^1.1.8",
|
|
70
70
|
"lit-html": "^3.3.1",
|
|
71
|
-
"@inglorious/
|
|
72
|
-
"@inglorious/
|
|
71
|
+
"@inglorious/store": "9.0.1",
|
|
72
|
+
"@inglorious/utils": "3.7.2"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"prettier": "^3.6.2",
|
package/src/list/index.js
CHANGED
|
@@ -1,168 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { repeat } from "lit-html/directives/repeat.js"
|
|
4
|
-
import { styleMap } from "lit-html/directives/style-map.js"
|
|
5
|
-
|
|
6
|
-
const LIST_START = 0
|
|
7
|
-
const PRETTY_INDEX = 1
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {import('../../types/list').ListEntity} ListEntity
|
|
11
|
-
* @typedef {import('../../types/mount').Api} Api
|
|
12
|
-
* @typedef {import('lit-html').TemplateResult} TemplateResult
|
|
13
|
-
*/
|
|
1
|
+
import { logic } from "./logic.js"
|
|
2
|
+
import { rendering } from "./rendering.js"
|
|
14
3
|
|
|
15
4
|
export const list = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* @param {ListEntity} entity
|
|
19
|
-
*/
|
|
20
|
-
create(entity) {
|
|
21
|
-
resetList(entity)
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Handles the scroll event to update the visible range.
|
|
26
|
-
* @param {ListEntity} entity
|
|
27
|
-
* @param {HTMLElement} containerEl
|
|
28
|
-
*/
|
|
29
|
-
scroll(entity, containerEl) {
|
|
30
|
-
const scrollTop = containerEl.scrollTop
|
|
31
|
-
const { items, bufferSize, itemHeight, estimatedHeight, viewportHeight } =
|
|
32
|
-
entity
|
|
33
|
-
const height = itemHeight || estimatedHeight
|
|
34
|
-
|
|
35
|
-
const start = Math.max(
|
|
36
|
-
LIST_START,
|
|
37
|
-
Math.floor(scrollTop / height) - bufferSize,
|
|
38
|
-
)
|
|
39
|
-
const visibleCount = Math.ceil(viewportHeight / height)
|
|
40
|
-
const end = Math.min(start + visibleCount + bufferSize, items.length)
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
entity.visibleRange.start === start &&
|
|
44
|
-
entity.visibleRange.end === end
|
|
45
|
-
) {
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
entity.scrollTop = scrollTop
|
|
50
|
-
entity.visibleRange = { start, end }
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Mounts the list, measuring the first item to determine item height.
|
|
55
|
-
* @param {ListEntity} entity
|
|
56
|
-
* @param {HTMLElement} containerEl
|
|
57
|
-
*/
|
|
58
|
-
mount(entity, containerEl) {
|
|
59
|
-
const firstItem = containerEl.querySelector("[data-index]")
|
|
60
|
-
if (!firstItem) return
|
|
61
|
-
|
|
62
|
-
entity.itemHeight = firstItem.offsetHeight
|
|
63
|
-
entity.visibleRange = {
|
|
64
|
-
start: 0,
|
|
65
|
-
end: Math.ceil(entity.viewportHeight / entity.itemHeight),
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Renders the virtualized list component.
|
|
71
|
-
* @param {ListEntity} entity
|
|
72
|
-
* @param {Api} api
|
|
73
|
-
* @returns {TemplateResult}
|
|
74
|
-
*/
|
|
75
|
-
render(entity, api) {
|
|
76
|
-
const { items, visibleRange, viewportHeight, itemHeight, estimatedHeight } =
|
|
77
|
-
entity
|
|
78
|
-
const type = api.getType(entity.type)
|
|
79
|
-
|
|
80
|
-
if (!items) {
|
|
81
|
-
console.warn(`list entity ${entity.id} needs 'items'`)
|
|
82
|
-
return html``
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!type.renderItem) {
|
|
86
|
-
console.warn(`type ${entity.type} needs 'renderItem' method`)
|
|
87
|
-
return html``
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const visibleItems = items.slice(visibleRange.start, visibleRange.end)
|
|
91
|
-
const height = itemHeight || estimatedHeight
|
|
92
|
-
const totalHeight = items.length * height
|
|
93
|
-
|
|
94
|
-
return html`
|
|
95
|
-
<div
|
|
96
|
-
style=${styleMap({ height: `${viewportHeight}px`, overflow: "auto" })}
|
|
97
|
-
@scroll=${(e) => api.notify(`#${entity.id}:scroll`, e.target)}
|
|
98
|
-
${ref((el) => {
|
|
99
|
-
if (el && !itemHeight) {
|
|
100
|
-
queueMicrotask(() => {
|
|
101
|
-
api.notify(`#${entity.id}:mount`, el)
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
})}
|
|
105
|
-
>
|
|
106
|
-
<div
|
|
107
|
-
style=${styleMap({
|
|
108
|
-
height: `${totalHeight}px`,
|
|
109
|
-
position: "relative",
|
|
110
|
-
})}
|
|
111
|
-
>
|
|
112
|
-
${repeat(
|
|
113
|
-
visibleItems,
|
|
114
|
-
(item) => item.id,
|
|
115
|
-
(item, index) => {
|
|
116
|
-
const absoluteIndex = visibleRange.start + index
|
|
117
|
-
const top = absoluteIndex * height
|
|
118
|
-
|
|
119
|
-
return html`
|
|
120
|
-
<div
|
|
121
|
-
style=${styleMap({
|
|
122
|
-
position: "absolute",
|
|
123
|
-
top: `${top}px`,
|
|
124
|
-
width: "100%",
|
|
125
|
-
})}
|
|
126
|
-
data-index=${absoluteIndex}
|
|
127
|
-
>
|
|
128
|
-
${type.renderItem(
|
|
129
|
-
entity,
|
|
130
|
-
{ item, index: absoluteIndex },
|
|
131
|
-
api,
|
|
132
|
-
)}
|
|
133
|
-
</div>
|
|
134
|
-
`
|
|
135
|
-
},
|
|
136
|
-
)}
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
139
|
-
`
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Default item renderer.
|
|
144
|
-
* @param {any} item
|
|
145
|
-
* @param {number} index
|
|
146
|
-
* @param {Api} api
|
|
147
|
-
* @returns {TemplateResult}
|
|
148
|
-
*/
|
|
149
|
-
// eslint-disable-next-line no-unused-vars
|
|
150
|
-
renderItem(entity, { item, index }, api) {
|
|
151
|
-
return html`<div class="iw-list-item">
|
|
152
|
-
${index + PRETTY_INDEX}. ${JSON.stringify(item)}
|
|
153
|
-
</div>`
|
|
154
|
-
},
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Resets the list entity state.
|
|
159
|
-
* @param {ListEntity} entity
|
|
160
|
-
*/
|
|
161
|
-
function resetList(entity) {
|
|
162
|
-
entity.scrollTop = 0
|
|
163
|
-
entity.visibleRange ??= { start: 0, end: 20 }
|
|
164
|
-
entity.viewportHeight ??= 600
|
|
165
|
-
entity.bufferSize ??= 5
|
|
166
|
-
entity.itemHeight ??= null
|
|
167
|
-
entity.estimatedHeight ??= 50
|
|
5
|
+
...logic,
|
|
6
|
+
...rendering,
|
|
168
7
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../types/list').ListEntity} ListEntity
|
|
3
|
+
* @typedef {import('../../types/mount').Api} Api
|
|
4
|
+
* @typedef {import('lit-html').TemplateResult} TemplateResult
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const LIST_START = 0
|
|
8
|
+
|
|
9
|
+
export const logic = {
|
|
10
|
+
/**
|
|
11
|
+
* Initializes the list entity with default state.
|
|
12
|
+
* @param {ListEntity} entity
|
|
13
|
+
*/
|
|
14
|
+
create(entity) {
|
|
15
|
+
resetList(entity)
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handles the scroll event to update the visible range.
|
|
20
|
+
* @param {ListEntity} entity
|
|
21
|
+
* @param {HTMLElement} containerEl
|
|
22
|
+
*/
|
|
23
|
+
scroll(entity, containerEl) {
|
|
24
|
+
const scrollTop = containerEl.scrollTop
|
|
25
|
+
const { items, bufferSize, itemHeight, estimatedHeight, viewportHeight } =
|
|
26
|
+
entity
|
|
27
|
+
const height = itemHeight || estimatedHeight
|
|
28
|
+
|
|
29
|
+
const start = Math.max(
|
|
30
|
+
LIST_START,
|
|
31
|
+
Math.floor(scrollTop / height) - bufferSize,
|
|
32
|
+
)
|
|
33
|
+
const visibleCount = Math.ceil(viewportHeight / height)
|
|
34
|
+
const end = Math.min(start + visibleCount + bufferSize, items.length)
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
entity.visibleRange.start === start &&
|
|
38
|
+
entity.visibleRange.end === end
|
|
39
|
+
) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
entity.scrollTop = scrollTop
|
|
44
|
+
entity.visibleRange = { start, end }
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Mounts the list, measuring the first item to determine item height.
|
|
49
|
+
* @param {ListEntity} entity
|
|
50
|
+
* @param {HTMLElement} containerEl
|
|
51
|
+
*/
|
|
52
|
+
mount(entity, containerEl) {
|
|
53
|
+
const firstItem = containerEl.querySelector("[data-index]")
|
|
54
|
+
if (!firstItem) return
|
|
55
|
+
|
|
56
|
+
entity.itemHeight = firstItem.offsetHeight
|
|
57
|
+
entity.visibleRange = {
|
|
58
|
+
start: 0,
|
|
59
|
+
end: Math.ceil(entity.viewportHeight / entity.itemHeight),
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resets the list entity state.
|
|
66
|
+
* @param {ListEntity} entity
|
|
67
|
+
*/
|
|
68
|
+
function resetList(entity) {
|
|
69
|
+
entity.scrollTop = 0
|
|
70
|
+
entity.visibleRange ??= { start: 0, end: 20 }
|
|
71
|
+
entity.viewportHeight ??= 600
|
|
72
|
+
entity.bufferSize ??= 5
|
|
73
|
+
entity.itemHeight ??= null
|
|
74
|
+
entity.estimatedHeight ??= 50
|
|
75
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../types/mount').Api} Api
|
|
3
|
+
* @typedef {import('../../types/list').ListEntity} ListEntity
|
|
4
|
+
* @typedef {import('lit-html').TemplateResult} TemplateResult
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { html } from "lit-html"
|
|
8
|
+
import { ref } from "lit-html/directives/ref.js"
|
|
9
|
+
import { repeat } from "lit-html/directives/repeat.js"
|
|
10
|
+
import { styleMap } from "lit-html/directives/style-map.js"
|
|
11
|
+
|
|
12
|
+
const PRETTY_INDEX = 1
|
|
13
|
+
/**
|
|
14
|
+
* Rendering logic for the virtualized list component.
|
|
15
|
+
*/
|
|
16
|
+
export const rendering = {
|
|
17
|
+
/**
|
|
18
|
+
* Renders the virtualized list component.
|
|
19
|
+
* @param {ListEntity} entity The list entity state.
|
|
20
|
+
* @param {Api} api The API object.
|
|
21
|
+
* @returns {TemplateResult} The rendered list.
|
|
22
|
+
*/
|
|
23
|
+
render(entity, api) {
|
|
24
|
+
const { items, visibleRange, viewportHeight, itemHeight, estimatedHeight } =
|
|
25
|
+
entity
|
|
26
|
+
const type = api.getType(entity.type)
|
|
27
|
+
|
|
28
|
+
if (!items) {
|
|
29
|
+
console.warn(`list entity ${entity.id} needs 'items'`)
|
|
30
|
+
return html``
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!type.renderItem) {
|
|
34
|
+
console.warn(`type ${entity.type} needs 'renderItem' method`)
|
|
35
|
+
return html``
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const visibleItems = items.slice(visibleRange.start, visibleRange.end)
|
|
39
|
+
const height = itemHeight || estimatedHeight
|
|
40
|
+
const totalHeight = items.length * height
|
|
41
|
+
|
|
42
|
+
return html`
|
|
43
|
+
<div
|
|
44
|
+
style=${styleMap({ height: `${viewportHeight}px`, overflow: "auto" })}
|
|
45
|
+
@scroll=${(e) => api.notify(`#${entity.id}:scroll`, e.target)}
|
|
46
|
+
${ref((el) => {
|
|
47
|
+
if (el && !itemHeight) {
|
|
48
|
+
queueMicrotask(() => {
|
|
49
|
+
api.notify(`#${entity.id}:mount`, el)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
})}
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
style=${styleMap({
|
|
56
|
+
height: `${totalHeight}px`,
|
|
57
|
+
position: "relative",
|
|
58
|
+
})}
|
|
59
|
+
>
|
|
60
|
+
${repeat(
|
|
61
|
+
visibleItems,
|
|
62
|
+
(item) => item.id,
|
|
63
|
+
(item, index) => {
|
|
64
|
+
const absoluteIndex = visibleRange.start + index
|
|
65
|
+
const top = absoluteIndex * height
|
|
66
|
+
|
|
67
|
+
return html`
|
|
68
|
+
<div
|
|
69
|
+
style=${styleMap({
|
|
70
|
+
position: "absolute",
|
|
71
|
+
top: `${top}px`,
|
|
72
|
+
width: "100%",
|
|
73
|
+
})}
|
|
74
|
+
data-index=${absoluteIndex}
|
|
75
|
+
>
|
|
76
|
+
${type.renderItem(
|
|
77
|
+
entity,
|
|
78
|
+
{ item, index: absoluteIndex },
|
|
79
|
+
api,
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
`
|
|
83
|
+
},
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
`
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Default item renderer.
|
|
92
|
+
* @param {ListEntity} entity The list entity.
|
|
93
|
+
* @param {object} payload The payload for rendering the item.
|
|
94
|
+
* @param {any} payload.item The item data.
|
|
95
|
+
* @param {number} payload.index The item's absolute index in the full list.
|
|
96
|
+
* @param {Api} api The API object.
|
|
97
|
+
* @returns {TemplateResult} The rendered list item.
|
|
98
|
+
*/
|
|
99
|
+
// eslint-disable-next-line no-unused-vars
|
|
100
|
+
renderItem(entity, { item, index }, api) {
|
|
101
|
+
return html`<div class="iw-list-item">
|
|
102
|
+
${index + PRETTY_INDEX}. ${JSON.stringify(item)}
|
|
103
|
+
</div>`
|
|
104
|
+
},
|
|
105
|
+
}
|
package/src/table/rendering.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../types/api').API} API
|
|
3
|
+
* @typedef {import('../../types/table').TableColumn} TableColumn
|
|
4
|
+
* @typedef {import('../../types/table').TableEntity} TableEntity
|
|
5
|
+
* @typedef {import('../../types/table').TableRow} TableRow
|
|
6
|
+
* @typedef {import('lit-html').TemplateResult} TemplateResult
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { html } from "lit-html"
|
|
2
10
|
import { classMap } from "lit-html/directives/class-map.js"
|
|
3
11
|
import { ref } from "lit-html/directives/ref.js"
|
|
@@ -12,14 +20,28 @@ const LAST_PAGE = 1
|
|
|
12
20
|
const PRETTY_PAGE = 1
|
|
13
21
|
const PERCENTAGE_TO_FLEX = 0.01
|
|
14
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Rendering logic for the table component.
|
|
25
|
+
*/
|
|
15
26
|
export const rendering = {
|
|
27
|
+
/**
|
|
28
|
+
* Measures and sets initial column widths.
|
|
29
|
+
* This is called after the initial render to capture the "auto" widths of columns.
|
|
30
|
+
* @param {TableEntity} entity The table entity.
|
|
31
|
+
* @param {HTMLElement} containerEl The header row element containing the columns.
|
|
32
|
+
*/
|
|
16
33
|
mount(entity, containerEl) {
|
|
17
34
|
const columns = containerEl.querySelectorAll(":scope > *")
|
|
18
35
|
;[...columns].forEach((column, index) => {
|
|
19
36
|
entity.columns[index].width = column.offsetWidth
|
|
20
37
|
})
|
|
21
38
|
},
|
|
22
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Renders the main table component.
|
|
41
|
+
* @param {TableEntity} entity The table entity.
|
|
42
|
+
* @param {API} api The API object.
|
|
43
|
+
* @returns {TemplateResult} The rendered table.
|
|
44
|
+
*/
|
|
23
45
|
render(entity, api) {
|
|
24
46
|
const type = api.getType(entity.type)
|
|
25
47
|
|
|
@@ -28,7 +50,12 @@ export const rendering = {
|
|
|
28
50
|
${type.renderFooter(entity, api)}
|
|
29
51
|
</div> `
|
|
30
52
|
},
|
|
31
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Renders the table header.
|
|
55
|
+
* @param {TableEntity} entity The table entity.
|
|
56
|
+
* @param {API} api The API object.
|
|
57
|
+
* @returns {TemplateResult} The rendered header.
|
|
58
|
+
*/
|
|
32
59
|
renderHeader(entity, api) {
|
|
33
60
|
const type = api.getType(entity.type)
|
|
34
61
|
|
|
@@ -57,7 +84,14 @@ export const rendering = {
|
|
|
57
84
|
${entity.search && type.renderSearchbar(entity, api)}
|
|
58
85
|
</div>`
|
|
59
86
|
},
|
|
60
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Renders a single column in the header.
|
|
89
|
+
* @param {TableEntity} entity The table entity.
|
|
90
|
+
* @param {object} payload The payload.
|
|
91
|
+
* @param {TableColumn} payload.column The column definition.
|
|
92
|
+
* @param {API} api The API object.
|
|
93
|
+
* @returns {TemplateResult} The rendered header column.
|
|
94
|
+
*/
|
|
61
95
|
renderHeaderColumn(entity, { column }, api) {
|
|
62
96
|
return html`<div
|
|
63
97
|
class="iw-table-header-column"
|
|
@@ -75,7 +109,12 @@ export const rendering = {
|
|
|
75
109
|
${column.isFilterable && filters.render(entity, column, api)}
|
|
76
110
|
</div>`
|
|
77
111
|
},
|
|
78
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Renders the search bar.
|
|
114
|
+
* @param {TableEntity} entity The table entity.
|
|
115
|
+
* @param {API} api The API object.
|
|
116
|
+
* @returns {TemplateResult} The rendered search bar.
|
|
117
|
+
*/
|
|
79
118
|
renderSearchbar(entity, api) {
|
|
80
119
|
return html`<input
|
|
81
120
|
name="search"
|
|
@@ -87,7 +126,12 @@ export const rendering = {
|
|
|
87
126
|
class="iw-table-searchbar"
|
|
88
127
|
/>`
|
|
89
128
|
},
|
|
90
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Renders the table body with rows.
|
|
131
|
+
* @param {TableEntity} entity The table entity.
|
|
132
|
+
* @param {API} api The API object.
|
|
133
|
+
* @returns {TemplateResult} The rendered table body.
|
|
134
|
+
*/
|
|
91
135
|
renderBody(entity, api) {
|
|
92
136
|
const type = api.getType(entity.type)
|
|
93
137
|
|
|
@@ -99,7 +143,15 @@ export const rendering = {
|
|
|
99
143
|
)}
|
|
100
144
|
</div>`
|
|
101
145
|
},
|
|
102
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Renders a single row in the table body.
|
|
148
|
+
* @param {TableEntity} entity The table entity.
|
|
149
|
+
* @param {object} payload The payload.
|
|
150
|
+
* @param {TableRow} payload.row The row data.
|
|
151
|
+
* @param {number} payload.index The row index.
|
|
152
|
+
* @param {API} api The API object.
|
|
153
|
+
* @returns {TemplateResult} The rendered row.
|
|
154
|
+
*/
|
|
103
155
|
renderRow(entity, { row, index }, api) {
|
|
104
156
|
const type = api.getType(entity.type)
|
|
105
157
|
const rowId = row[entity.rowId ?? "id"]
|
|
@@ -119,7 +171,15 @@ export const rendering = {
|
|
|
119
171
|
)}
|
|
120
172
|
</div>`
|
|
121
173
|
},
|
|
122
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Renders a single cell within a row.
|
|
176
|
+
* @param {TableEntity} entity The table entity.
|
|
177
|
+
* @param {object} payload The payload.
|
|
178
|
+
* @param {any} payload.cell The cell data.
|
|
179
|
+
* @param {number} payload.index The column index.
|
|
180
|
+
* @param {API} api The API object.
|
|
181
|
+
* @returns {TemplateResult} The rendered cell.
|
|
182
|
+
*/
|
|
123
183
|
renderCell(entity, { cell, index }, api) {
|
|
124
184
|
const type = api.getType(entity.type)
|
|
125
185
|
const column = entity.columns[index]
|
|
@@ -136,10 +196,22 @@ export const rendering = {
|
|
|
136
196
|
</div>`
|
|
137
197
|
},
|
|
138
198
|
|
|
139
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Renders the value within a cell. This can be overridden for custom formatting.
|
|
201
|
+
* @param {TableEntity} _entity The table entity (ignored).
|
|
202
|
+
* @param {object} payload The payload.
|
|
203
|
+
* @param {any} payload.value The value to render.
|
|
204
|
+
* @returns {any} The value to be rendered.
|
|
205
|
+
*/
|
|
206
|
+
renderValue(_entity, { value }) {
|
|
140
207
|
return value
|
|
141
208
|
},
|
|
142
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Renders the table footer.
|
|
211
|
+
* @param {TableEntity} entity The table entity.
|
|
212
|
+
* @param {API} api The API object.
|
|
213
|
+
* @returns {TemplateResult} The rendered footer.
|
|
214
|
+
*/
|
|
143
215
|
renderFooter(entity, api) {
|
|
144
216
|
const type = api.getType(entity.type)
|
|
145
217
|
const pagination = getPaginationInfo(entity)
|
|
@@ -172,7 +244,13 @@ export const rendering = {
|
|
|
172
244
|
</div>
|
|
173
245
|
</div>`
|
|
174
246
|
},
|
|
175
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Renders the pagination controls.
|
|
249
|
+
* @param {TableEntity} entity The table entity.
|
|
250
|
+
* @param {object} pagination The pagination info object from `getPaginationInfo`.
|
|
251
|
+
* @param {API} api The API object.
|
|
252
|
+
* @returns {TemplateResult} The rendered pagination controls.
|
|
253
|
+
*/
|
|
176
254
|
renderPagination(entity, pagination, api) {
|
|
177
255
|
return html`<div class="iw-table-row">
|
|
178
256
|
<button
|
|
@@ -226,6 +304,11 @@ export const rendering = {
|
|
|
226
304
|
},
|
|
227
305
|
}
|
|
228
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Generates the style string for a column.
|
|
309
|
+
* @param {TableColumn} column The column definition.
|
|
310
|
+
* @returns {string} The style string.
|
|
311
|
+
*/
|
|
229
312
|
function getColumnStyle(column) {
|
|
230
313
|
if (typeof column.width === "string") {
|
|
231
314
|
if (column.width?.endsWith("%")) {
|
|
@@ -240,6 +323,11 @@ function getColumnStyle(column) {
|
|
|
240
323
|
return `width: ${column.width}px`
|
|
241
324
|
}
|
|
242
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Gets the appropriate sort icon for a given direction.
|
|
328
|
+
* @param {"asc"|"desc"|null} direction The sort direction.
|
|
329
|
+
* @returns {string} The sort icon.
|
|
330
|
+
*/
|
|
243
331
|
function getSortIcon(direction) {
|
|
244
332
|
switch (direction) {
|
|
245
333
|
case "asc":
|
package/types/list.d.ts
CHANGED
|
@@ -2,58 +2,66 @@ import type { TemplateResult } from "lit-html"
|
|
|
2
2
|
import type { Api } from "./mount"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Represents
|
|
5
|
+
* Represents a single item in the list's data array.
|
|
6
|
+
* It's a generic record but should ideally have a unique `id`.
|
|
6
7
|
*/
|
|
7
|
-
export
|
|
8
|
-
start: number
|
|
9
|
-
end: number
|
|
10
|
-
}
|
|
8
|
+
export type ListItem = Record<string, any> & { id: string | number }
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
|
-
* Represents the state of a list entity.
|
|
11
|
+
* Represents the state of a virtualized list entity.
|
|
14
12
|
*/
|
|
15
|
-
export interface ListEntity<T =
|
|
13
|
+
export interface ListEntity<T extends ListItem = ListItem> {
|
|
16
14
|
/** A unique identifier for the list entity. */
|
|
17
15
|
id: string | number
|
|
18
|
-
|
|
16
|
+
|
|
17
|
+
/** The entity type, used to look up rendering logic. */
|
|
19
18
|
type: string
|
|
20
|
-
|
|
19
|
+
|
|
20
|
+
/** The full array of items for the list. */
|
|
21
21
|
items: T[]
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
/** The current scroll position from the top in pixels. */
|
|
23
24
|
scrollTop: number
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
|
|
26
|
+
/** The start and end indices of the currently visible items (including buffer). */
|
|
27
|
+
visibleRange: { start: number; end: number }
|
|
28
|
+
|
|
29
|
+
/** The height of the visible viewport in pixels. */
|
|
27
30
|
viewportHeight: number
|
|
28
|
-
|
|
31
|
+
|
|
32
|
+
/** The number of items to render outside the viewport (above and below). */
|
|
29
33
|
bufferSize: number
|
|
30
|
-
|
|
34
|
+
|
|
35
|
+
/** The measured height of a single item in pixels. Null until measured on mount. */
|
|
31
36
|
itemHeight: number | null
|
|
32
|
-
|
|
37
|
+
|
|
38
|
+
/** An estimated height for items used for calculations before they are measured. */
|
|
33
39
|
estimatedHeight: number
|
|
40
|
+
|
|
34
41
|
/** Any other custom properties. */
|
|
35
42
|
[key: string]: any
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/**
|
|
39
|
-
* The list type implementation.
|
|
46
|
+
* The list type implementation, combining logic and rendering.
|
|
40
47
|
*/
|
|
41
48
|
export declare const list: {
|
|
42
49
|
/**
|
|
43
50
|
* Initializes the list entity with default state.
|
|
44
|
-
* @param entity The list entity.
|
|
51
|
+
* @param entity The list entity to initialize.
|
|
45
52
|
*/
|
|
46
53
|
create(entity: ListEntity): void
|
|
47
54
|
|
|
48
55
|
/**
|
|
49
|
-
* Handles the scroll event to update the visible range.
|
|
56
|
+
* Handles the scroll event to update the visible range of items.
|
|
50
57
|
* @param entity The list entity.
|
|
51
58
|
* @param containerEl The scrolling container element.
|
|
52
59
|
*/
|
|
53
60
|
scroll(entity: ListEntity, containerEl: HTMLElement): void
|
|
54
61
|
|
|
55
62
|
/**
|
|
56
|
-
* Mounts the list, measuring the first item to determine
|
|
63
|
+
* Mounts the list, measuring the first item to determine the `itemHeight`
|
|
64
|
+
* for accurate virtualization calculations.
|
|
57
65
|
* @param entity The list entity.
|
|
58
66
|
* @param containerEl The scrolling container element.
|
|
59
67
|
*/
|
|
@@ -67,10 +75,17 @@ export declare const list: {
|
|
|
67
75
|
render(entity: ListEntity, api: Api): TemplateResult
|
|
68
76
|
|
|
69
77
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* @param
|
|
78
|
+
* Renders a single item in the list. This is a default implementation
|
|
79
|
+
* and is expected to be overridden by a specific list type.
|
|
80
|
+
* @param entity The list entity.
|
|
81
|
+
* @param payload The payload for rendering the item.
|
|
82
|
+
* @param payload.item The item data.
|
|
83
|
+
* @param payload.index The item's absolute index in the full list.
|
|
73
84
|
* @param api The store API.
|
|
74
85
|
*/
|
|
75
|
-
renderItem(
|
|
86
|
+
renderItem(
|
|
87
|
+
entity: ListEntity,
|
|
88
|
+
payload: { item: ListItem; index: number },
|
|
89
|
+
api: Api,
|
|
90
|
+
): TemplateResult
|
|
76
91
|
}
|
package/types/table.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { TemplateResult } from "lit-html"
|
|
2
2
|
import type { Api } from "./mount"
|
|
3
|
+
import type { SelectOption } from "./select"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a single row of data in the table.
|
|
7
|
+
*/
|
|
8
|
+
export type TableRow = Record<string, any>
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
11
|
* Represents a column definition for the table.
|
|
@@ -18,17 +24,17 @@ export interface TableColumn {
|
|
|
18
24
|
/** Filter configuration. */
|
|
19
25
|
filter?: {
|
|
20
26
|
type: "text" | "number" | "range" | "select" | "date" | "time" | "datetime"
|
|
21
|
-
options?:
|
|
27
|
+
options?: SelectOption[]
|
|
22
28
|
[key: string]: any
|
|
23
29
|
}
|
|
24
|
-
/** The width of the column. */
|
|
25
|
-
width?: number
|
|
30
|
+
/** The width of the column, in pixels or as a string (e.g., '10%'). */
|
|
31
|
+
width?: number | string
|
|
26
32
|
/** Optional formatter key or function. */
|
|
27
33
|
formatter?: string | ((value: any) => any)
|
|
28
|
-
/** Custom sort function. */
|
|
29
|
-
sortFn?: (a:
|
|
30
|
-
/** Custom filter function. */
|
|
31
|
-
filterFn?: (row:
|
|
34
|
+
/** Custom sort function for a column, operating on two rows. */
|
|
35
|
+
sortFn?: (a: TableRow, b: TableRow) => number
|
|
36
|
+
/** Custom filter function for a column, operating on a single row. */
|
|
37
|
+
filterFn?: (row: TableRow, filterValue: any) => boolean
|
|
32
38
|
/** Custom format function. */
|
|
33
39
|
format?: (value: any) => string
|
|
34
40
|
/** Any other custom properties. */
|
|
@@ -38,7 +44,7 @@ export interface TableColumn {
|
|
|
38
44
|
/**
|
|
39
45
|
* Represents the state of a table entity.
|
|
40
46
|
*/
|
|
41
|
-
export interface TableEntity<T =
|
|
47
|
+
export interface TableEntity<T extends TableRow = TableRow> {
|
|
42
48
|
/** A unique identifier for the table entity. */
|
|
43
49
|
id: string | number
|
|
44
50
|
/** The entity type. */
|
|
@@ -52,11 +58,13 @@ export interface TableEntity<T = any> {
|
|
|
52
58
|
/** Filtering state. */
|
|
53
59
|
filters: Record<string, any>
|
|
54
60
|
/** Search state. */
|
|
55
|
-
search: { value: string } | null
|
|
61
|
+
search: { value: string; placeholder?: string } | null
|
|
56
62
|
/** Selected row IDs. */
|
|
57
63
|
selection: (string | number)[]
|
|
58
64
|
/** Pagination state. */
|
|
59
65
|
pagination: { page: number; pageSize: number } | null
|
|
66
|
+
/** The property on a row object that uniquely identifies it. Defaults to 'id'. */
|
|
67
|
+
rowId?: string
|
|
60
68
|
/** Whether multi-select is enabled. */
|
|
61
69
|
isMultiSelect: boolean
|
|
62
70
|
/** Any other custom properties. */
|
|
@@ -198,35 +206,43 @@ export declare const table: {
|
|
|
198
206
|
/**
|
|
199
207
|
* Renders a single row.
|
|
200
208
|
* @param entity The table entity.
|
|
201
|
-
* @param
|
|
202
|
-
* @param
|
|
209
|
+
* @param payload The payload for rendering the row.
|
|
210
|
+
* @param payload.row The data item for the row.
|
|
211
|
+
* @param payload.index The index of the row.
|
|
203
212
|
* @param api The store API.
|
|
204
213
|
*/
|
|
205
214
|
renderRow(
|
|
206
215
|
entity: TableEntity,
|
|
207
|
-
|
|
208
|
-
index: number,
|
|
216
|
+
payload: { row: TableRow; index: number },
|
|
209
217
|
api: Api,
|
|
210
218
|
): TemplateResult
|
|
211
219
|
|
|
212
220
|
/**
|
|
213
221
|
* Renders a single cell.
|
|
214
222
|
* @param entity The table entity.
|
|
215
|
-
* @param
|
|
216
|
-
* @param
|
|
223
|
+
* @param payload The payload for rendering the cell.
|
|
224
|
+
* @param payload.cell The data for the cell.
|
|
225
|
+
* @param payload.index The column index of the cell.
|
|
217
226
|
* @param api The store API.
|
|
218
227
|
*/
|
|
219
228
|
renderCell(
|
|
220
229
|
entity: TableEntity,
|
|
221
|
-
|
|
222
|
-
column: TableColumn,
|
|
230
|
+
payload: { cell: any; index: number },
|
|
223
231
|
api: Api,
|
|
224
232
|
): TemplateResult
|
|
225
233
|
|
|
226
234
|
/**
|
|
227
235
|
* Renders the value of a cell.
|
|
228
|
-
* @param
|
|
229
|
-
* @param
|
|
236
|
+
* @param entity The table entity.
|
|
237
|
+
* @param payload The payload for rendering the value.
|
|
238
|
+
* @param payload.value The raw value from the data item.
|
|
239
|
+
* @param payload.column The column definition.
|
|
240
|
+
* @param payload.index The column index.
|
|
241
|
+
* @param api The store API.
|
|
230
242
|
*/
|
|
231
|
-
renderValue(
|
|
243
|
+
renderValue(
|
|
244
|
+
entity: TableEntity,
|
|
245
|
+
payload: { value: any; column: TableColumn; index: number },
|
|
246
|
+
api: Api,
|
|
247
|
+
): any
|
|
232
248
|
}
|