apexgrid-skill 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/SKILL.md +410 -0
- package/index.js +38 -0
- package/package.json +50 -0
- package/references/columns-and-templates.md +191 -0
- package/references/data-pipeline.md +149 -0
- package/references/framework-integration.md +203 -0
- package/references/sort-and-filter.md +203 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 ApexCharts
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Apex Grid AI Skill
|
|
2
|
+
|
|
3
|
+
AI coding skill for building UIs with the [`apex-grid`](https://github.com/apexcharts/apexgrid) web component (the Lit-based ApexCharts data grid). Works with Claude Code, Cursor, GitHub Copilot, and any AI coding assistant.
|
|
4
|
+
|
|
5
|
+
> **Naming heads-up.** The npm package is `apex-grid` (with a hyphen), the custom-element tag is `<apex-grid>`, and the exported class is `ApexGrid<T>`.
|
|
6
|
+
|
|
7
|
+
## What This Does
|
|
8
|
+
|
|
9
|
+
AI models routinely get web-component grid code wrong: they treat `<apex-grid>` like a class with a constructor, forget to register the custom element, set `columns` as a stringified attribute, return strings (not Lit `TemplateResult`s) from `cellTemplate`, or expect events from programmatic `sort()` calls. This skill ships structured reference files so the assistant generates correct `apex-grid` code on the first try.
|
|
10
|
+
|
|
11
|
+
### Coverage
|
|
12
|
+
|
|
13
|
+
- **Custom-element registration** — `import 'apex-grid/define'` vs `ApexGrid.register()`
|
|
14
|
+
- **Generic `ColumnConfiguration<T>`** — `key`, `type`, templates, `sort`, `filter`
|
|
15
|
+
- **Lit cell & header templates** with the right context shapes
|
|
16
|
+
- **Sort & filter** — programmatic API, UI events, operands, multi-column
|
|
17
|
+
- **Server-side data** — `dataPipelineConfiguration` async hooks
|
|
18
|
+
- **Virtualization** — `grid.rows` vs `grid.dataView` vs `grid.totalItems`
|
|
19
|
+
- **Framework integration** — Lit, React, Vue, Angular
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### Claude Code
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
mkdir -p .claude/skills
|
|
27
|
+
cd .claude/skills
|
|
28
|
+
git clone https://github.com/apexcharts/apexgrid-skill.git
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Cursor / Windsurf
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
curl -o .cursorrules https://raw.githubusercontent.com/apexcharts/apexgrid-skill/main/.cursorrules
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### GitHub Copilot
|
|
38
|
+
|
|
39
|
+
Reference `SKILL.md` in Copilot Chat: `@workspace #file:SKILL.md`, or paste the contents of `.cursorrules` into Copilot's custom instructions.
|
|
40
|
+
|
|
41
|
+
### As an npm dependency
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install apexgrid-skill
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { skillFile, referencePath } from 'apexgrid-skill';
|
|
49
|
+
import { readFile } from 'node:fs/promises';
|
|
50
|
+
|
|
51
|
+
const skill = await readFile(skillFile, 'utf8');
|
|
52
|
+
const cols = await readFile(referencePath('columns-and-templates.md'), 'utf8');
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Repository Structure
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
├── SKILL.md # Main entry point
|
|
59
|
+
├── .cursorrules # Self-contained Cursor / Windsurf version
|
|
60
|
+
├── references/
|
|
61
|
+
│ ├── columns-and-templates.md # ColumnConfiguration, types, Lit templates
|
|
62
|
+
│ ├── sort-and-filter.md # operands, expressions, events, multi-column
|
|
63
|
+
│ ├── data-pipeline.md # async hooks, server-side, virtualization
|
|
64
|
+
│ └── framework-integration.md # Lit, React, Vue, Angular
|
|
65
|
+
└── install/
|
|
66
|
+
├── claude-code.md
|
|
67
|
+
├── cursor.md
|
|
68
|
+
└── copilot.md
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Links
|
|
72
|
+
|
|
73
|
+
- [apex-grid GitHub](https://github.com/apexcharts/apexgrid)
|
|
74
|
+
- [npm: apex-grid](https://www.npmjs.com/package/apex-grid)
|
|
75
|
+
- [Lit framework](https://lit.dev)
|
|
76
|
+
- [igniteui-webcomponents](https://www.npmjs.com/package/igniteui-webcomponents) (runtime peer)
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: apex-grid
|
|
3
|
+
description: >
|
|
4
|
+
AI skill for building UIs with the `apex-grid` web component (the Lit-based
|
|
5
|
+
ApexCharts data grid). Use whenever the user asks to create, configure, or
|
|
6
|
+
troubleshoot a sortable / filterable / virtualized data table with
|
|
7
|
+
`<apex-grid>`. Covers the generic `ColumnConfiguration<T>` shape, cell &
|
|
8
|
+
header templates that return Lit `TemplateResult`s, the
|
|
9
|
+
`sort` / `filter` / `sorted` / `filtered` events, programmatic
|
|
10
|
+
`sort()` / `filter()` / `clearSort()` / `clearFilter()` / `updateColumns()`
|
|
11
|
+
APIs, the `dataPipelineConfiguration` hooks for server-side data, and the
|
|
12
|
+
one-time `ApexGrid.register()` custom-element registration.
|
|
13
|
+
metadata:
|
|
14
|
+
author: ApexCharts
|
|
15
|
+
version: "1.0.0"
|
|
16
|
+
library_version: ">=0.0.0"
|
|
17
|
+
category: data-visualization
|
|
18
|
+
tags: [grid, data-grid, table, web-component, lit, apex-grid]
|
|
19
|
+
docs: https://github.com/apexcharts/apexgrid
|
|
20
|
+
npm: apex-grid
|
|
21
|
+
github: https://github.com/apexcharts/apexgrid
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Apex Grid AI Skill
|
|
25
|
+
|
|
26
|
+
> **Heads-up on naming.** The npm package is **`apex-grid`** (with a hyphen). The custom-element tag is **`<apex-grid>`**. The exported class is **`ApexGrid<T>`**. There is no `apexgrid` package — using `import ... from 'apexgrid'` will fail.
|
|
27
|
+
|
|
28
|
+
## 1. Critical Rules
|
|
29
|
+
|
|
30
|
+
1. **Apex Grid is a Lit web component, not a JS class with a constructor.** You don't `new ApexGrid(...)`. You register the custom element once and then use `<apex-grid>` in the DOM, setting `.data` and `.columns` as JS properties.
|
|
31
|
+
2. **Register the element exactly once at app startup**: either `import 'apex-grid/define'` (auto-registers as a side-effect) or `import { ApexGrid } from 'apex-grid'; ApexGrid.register();`. Without this, the tag renders as an empty inert element.
|
|
32
|
+
3. **`columns` and `data` are properties, not attributes.** Always bind with the property syntax (`.columns=${...}` in Lit, `[columns]="..."` in Angular, `:columns=` in Vue, ref-based in plain JS) — never as a stringified `columns="..."` attribute.
|
|
33
|
+
4. **`column.key` must be a real key of `T`** (the data row type). It is what the grid reads from each row, what sort/filter expressions reference, and what's emitted in events. There is no `field` / `dataIndex` / `accessor` alias.
|
|
34
|
+
5. **`type` defaults to `'string'`.** Set it to `'number'` or `'boolean'` to get the right filter operands and comparer. There is no `'date'` type — use `'number'` (timestamps) or `'string'` (ISO) and a custom `comparer` / `cellTemplate`.
|
|
35
|
+
6. **`sort` and `filter` per-column are `boolean | configObject`.** They are `false` by default — no opt-in, no UI. Set `sort: true` / `filter: true` to enable, or pass a config object for fine-grained control.
|
|
36
|
+
7. **`cellTemplate` and `headerTemplate` must return Lit `TemplateResult`s** (`html\`...\``) — not strings, DOM nodes, or framework-specific JSX. Plain HTML strings render as escaped text.
|
|
37
|
+
8. **Sort/filter events fire on UI-initiated operations only.** Programmatic calls (`grid.sort(...)`, `grid.filter(...)`) do not emit `sorting` / `sorted` / `filtering` / `filtered` events.
|
|
38
|
+
9. **`sorting` and `filtering` events are cancellable** (`event.preventDefault()`) and their `event.detail` is mutable so you can rewrite the expression before it runs.
|
|
39
|
+
10. **For server-side data, set `dataPipelineConfiguration.sort` / `.filter`** — async hooks that receive `{ data, grid, type }` and return the new array. The grid skips its built-in pipeline entirely when a hook is provided for that operation.
|
|
40
|
+
11. **`autoGenerate` is ignored when `columns` is non-empty.** To re-trigger auto-generation after a data swap, set `grid.columns = []` first, then assign the new `data`.
|
|
41
|
+
12. **`grid.rows` only returns currently rendered rows.** The grid uses `@lit-labs/virtualizer`, so off-screen rows are not in the DOM.
|
|
42
|
+
13. **`apex-grid` depends on `igniteui-webcomponents`** for filter/sort UI controls. The package lists it as a runtime dependency — don't try to remove it.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 2. Data Format & Column Configuration
|
|
47
|
+
|
|
48
|
+
Apex Grid is generic over a row type `T`:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
interface ColumnConfiguration<T extends object, K extends keyof T = keyof T> {
|
|
52
|
+
key: K; // REQUIRED — must be a key of T
|
|
53
|
+
type?: 'string' | 'number' | 'boolean'; // default 'string'
|
|
54
|
+
headerText?: string; // default: the key itself
|
|
55
|
+
width?: string; // any CSS width
|
|
56
|
+
hidden?: boolean; // default false
|
|
57
|
+
resizable?: boolean; // default false
|
|
58
|
+
sort?: boolean | { caseSensitive?: boolean; comparer?: (a: T[K], b: T[K]) => number };
|
|
59
|
+
filter?: boolean | { caseSensitive?: boolean };
|
|
60
|
+
headerTemplate?: (ctx: ApexHeaderContext<T>) => TemplateResult | unknown;
|
|
61
|
+
cellTemplate?: (ctx: ApexCellContext<T, K>) => TemplateResult | unknown;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Minimal example:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { html, render } from 'lit';
|
|
69
|
+
import 'apex-grid/define'; // registers <apex-grid>
|
|
70
|
+
import type { ColumnConfiguration } from 'apex-grid';
|
|
71
|
+
|
|
72
|
+
type User = { id: number; name: string; age: number; subscribed: boolean };
|
|
73
|
+
|
|
74
|
+
const data: User[] = [
|
|
75
|
+
{ id: 1, name: 'Ada', age: 36, subscribed: true },
|
|
76
|
+
{ id: 2, name: 'Carl', age: 41, subscribed: false },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const columns: ColumnConfiguration<User>[] = [
|
|
80
|
+
{ key: 'id', type: 'number', sort: true, filter: true, headerText: 'ID' },
|
|
81
|
+
{ key: 'name', type: 'string', sort: true, filter: true },
|
|
82
|
+
{ key: 'age', type: 'number', sort: true, filter: true },
|
|
83
|
+
{ key: 'subscribed', type: 'boolean', sort: true, filter: true },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
render(
|
|
87
|
+
html`<apex-grid .data=${data} .columns=${columns}></apex-grid>`,
|
|
88
|
+
document.getElementById('app')!,
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Column types and filter operands
|
|
93
|
+
|
|
94
|
+
The `type` field selects which filter operand set is exposed in the filter UI:
|
|
95
|
+
|
|
96
|
+
| `type` | Operands available |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `'string'` *(default)* | `contains`, `doesNotContain`, `startsWith`, `endsWith`, `equals`, `doesNotEqual`, `empty`, `notEmpty` |
|
|
99
|
+
| `'number'` | `equals`, `doesNotEqual`, `greaterThan`, `lessThan`, `greaterThanOrEqual`, `lessThanOrEqual`, `empty`, `notEmpty` |
|
|
100
|
+
| `'boolean'` | `all`, `true`, `false`, `empty`, `notEmpty` |
|
|
101
|
+
|
|
102
|
+
Programmatic filter expressions can reference operands by name (`condition: 'contains'`) or by importing them as a function:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { StringOperands } from 'apex-grid';
|
|
106
|
+
|
|
107
|
+
grid.filter({ key: 'name', condition: StringOperands.contains, searchTerm: 'ada' });
|
|
108
|
+
// or shorthand
|
|
109
|
+
grid.filter({ key: 'name', condition: 'contains', searchTerm: 'ada' });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Cell & header templates (Lit)
|
|
113
|
+
|
|
114
|
+
Templates are Lit render callbacks. They receive a context object and must return a `TemplateResult`.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const columns: ColumnConfiguration<User>[] = [
|
|
118
|
+
{
|
|
119
|
+
key: 'name',
|
|
120
|
+
cellTemplate: ({ value, row, parent, column }) => html`
|
|
121
|
+
<strong style="color:#1f4ed8">${value}</strong>
|
|
122
|
+
`,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: 'subscribed',
|
|
126
|
+
type: 'boolean',
|
|
127
|
+
cellTemplate: ({ value }) => html`
|
|
128
|
+
<input type="checkbox" .checked=${value} disabled />
|
|
129
|
+
`,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: 'age',
|
|
133
|
+
headerTemplate: ({ column }) => html`
|
|
134
|
+
<em>${column.headerText ?? String(column.key)}</em>
|
|
135
|
+
`,
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Context shapes:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
interface ApexCellContext<T, K extends keyof T> {
|
|
144
|
+
parent: ApexGridCell<T>; // the cell element
|
|
145
|
+
row: ApexGridRow<T>; // the row element
|
|
146
|
+
column: ColumnConfiguration<T, K>;
|
|
147
|
+
value: T[K];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface ApexHeaderContext<T> {
|
|
151
|
+
parent: ApexGridHeader<T>;
|
|
152
|
+
column: ColumnConfiguration<T>;
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Auto-generated columns
|
|
157
|
+
|
|
158
|
+
`auto-generate` (or `.autoGenerate=${true}`) infers columns from the first data row's keys. **It is ignored if `columns` is non-empty.** To re-trigger after a data swap:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
grid.columns = []; // reset first
|
|
162
|
+
grid.data = newData; // then replace data — columns are inferred
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 3. Public API
|
|
168
|
+
|
|
169
|
+
### Properties
|
|
170
|
+
|
|
171
|
+
| Property | Type | Default | Notes |
|
|
172
|
+
|---|---|---|---|
|
|
173
|
+
| `data` | `T[]` | `[]` | Row data. Setting it triggers a pipeline run. |
|
|
174
|
+
| `columns` | `ColumnConfiguration<T>[]` | `[]` | Column definitions. Each entry is merged with `DEFAULT_COLUMN_CONFIG`. |
|
|
175
|
+
| `autoGenerate` | `boolean` (attr `auto-generate`) | `false` | Infer columns from data. Ignored when `columns` is non-empty. |
|
|
176
|
+
| `sortConfiguration` | `{ multiple: boolean; triState: boolean }` | `{ true, true }` | Grid-wide sort behaviour. |
|
|
177
|
+
| `dataPipelineConfiguration` | `{ sort?, filter? }` | — | Server-side / async data hooks. |
|
|
178
|
+
| `sortExpressions` | `SortExpression<T>[]` | `[]` | Get/set the active sort state. Setting an empty array is a no-op — use `clearSort()`. |
|
|
179
|
+
| `filterExpressions` | `FilterExpression<T>[]` | `[]` | Get/set the active filter state. Empty-set semantics same as above. |
|
|
180
|
+
| `rows` | `ApexGridRow<T>[]` *(getter)* | — | Currently rendered rows only (virtualized). |
|
|
181
|
+
| `dataView` | `readonly T[]` *(getter)* | — | Data after sort + filter pipeline. |
|
|
182
|
+
| `totalItems` | `number` *(getter)* | — | `dataView.length`. |
|
|
183
|
+
|
|
184
|
+
### Methods
|
|
185
|
+
|
|
186
|
+
| Method | Description |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `sort(expr \| expr[])` | Apply a sort. Does **not** fire `sorting` / `sorted` events. |
|
|
189
|
+
| `filter(expr \| expr[])` | Apply a filter. Does **not** fire `filtering` / `filtered` events. |
|
|
190
|
+
| `clearSort(key?)` | Clear all sort state, or just the column with `key`. |
|
|
191
|
+
| `clearFilter(key?)` | Clear all filter state, or just the column with `key`. |
|
|
192
|
+
| `getColumn(keyOrIndex)` | Find a column by `key` or numeric index. |
|
|
193
|
+
| `updateColumns(col \| col[])` | Merge new properties into existing columns by `key`. Triggers a pipeline run. |
|
|
194
|
+
|
|
195
|
+
### Static
|
|
196
|
+
|
|
197
|
+
| Member | Description |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `ApexGrid.tagName` | The string `'apex-grid'`. |
|
|
200
|
+
| `ApexGrid.register()` | Registers `<apex-grid>` and all internal sub-components. Idempotent. Either call this once, or import `'apex-grid/define'` for auto-registration. |
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 4. Events
|
|
205
|
+
|
|
206
|
+
Events fire **only** when the user interacts with sort / filter UI. Programmatic `grid.sort(...)` / `grid.filter(...)` calls are silent.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import type { ApexGridEventMap } from 'apex-grid';
|
|
210
|
+
|
|
211
|
+
grid.addEventListener('sorting', (e) => {
|
|
212
|
+
// CustomEvent<SortExpression<T>>
|
|
213
|
+
// e.detail is the expression about to be applied — mutate to rewrite, preventDefault to cancel
|
|
214
|
+
if (e.detail.direction === 'descending') {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
grid.addEventListener('sorted', (e) => {
|
|
220
|
+
// CustomEvent<SortExpression<T>>
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
grid.addEventListener('filtering', (e) => {
|
|
224
|
+
// CustomEvent<{ key, expressions, type: 'add'|'modify'|'remove' }>
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
grid.addEventListener('filtered', (e) => {
|
|
228
|
+
// CustomEvent<{ key, state: FilterExpression<T>[] }>
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
| Event | Cancellable | `event.detail` |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| `sorting` | yes | `SortExpression<T>` (mutable) |
|
|
235
|
+
| `sorted` | no | `SortExpression<T>` |
|
|
236
|
+
| `filtering` | yes | `{ key: keyof T; expressions: FilterExpression<T>[]; type: 'add' \| 'modify' \| 'remove' }` |
|
|
237
|
+
| `filtered` | no | `{ key: keyof T; state: FilterExpression<T>[] }` |
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 5. Server-Side / Async Data — `dataPipelineConfiguration`
|
|
242
|
+
|
|
243
|
+
When sort or filter should run on the server, supply async hooks. Each hook receives `{ data, grid, type }` and returns the new array. **The built-in pipeline is skipped for whichever operations have a hook.**
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
grid.dataPipelineConfiguration = {
|
|
247
|
+
sort: async ({ data, grid }) => {
|
|
248
|
+
const expressions = grid.sortExpressions;
|
|
249
|
+
return fetch('/api/users/sort', {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
body: JSON.stringify(expressions),
|
|
252
|
+
}).then((r) => r.json());
|
|
253
|
+
},
|
|
254
|
+
filter: async ({ data, grid }) => {
|
|
255
|
+
const expressions = grid.filterExpressions;
|
|
256
|
+
return fetch('/api/users/filter', {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
body: JSON.stringify(expressions),
|
|
259
|
+
}).then((r) => r.json());
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
You can supply just `sort`, just `filter`, or both. Anything you don't override falls back to the in-memory pipeline.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 6. Programmatic Sort & Filter
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { StringOperands } from 'apex-grid';
|
|
272
|
+
|
|
273
|
+
// Single sort
|
|
274
|
+
grid.sort({ key: 'name', direction: 'ascending' });
|
|
275
|
+
|
|
276
|
+
// Multiple (multi-column) sort — works only when sortConfiguration.multiple is true
|
|
277
|
+
grid.sort([
|
|
278
|
+
{ key: 'subscribed', direction: 'descending' },
|
|
279
|
+
{ key: 'age', direction: 'ascending' },
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
// Filter — by operand name
|
|
283
|
+
grid.filter({ key: 'name', condition: 'contains', searchTerm: 'ad' });
|
|
284
|
+
|
|
285
|
+
// Filter — by operand object (typed)
|
|
286
|
+
grid.filter({ key: 'name', condition: StringOperands.contains, searchTerm: 'ad' });
|
|
287
|
+
|
|
288
|
+
// Multiple filters with AND/OR
|
|
289
|
+
grid.filter([
|
|
290
|
+
{ key: 'age', condition: 'greaterThan', searchTerm: 30 },
|
|
291
|
+
{ key: 'age', condition: 'lessThan', searchTerm: 50, criteria: 'and' },
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
// Clear
|
|
295
|
+
grid.clearSort(); // all columns
|
|
296
|
+
grid.clearSort('name'); // just `name`
|
|
297
|
+
grid.clearFilter();
|
|
298
|
+
grid.clearFilter('name');
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
`SortExpression<T>` and `FilterExpression<T>` are exported from `apex-grid` for typed arrays.
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## 7. Lifecycle Pattern
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
// 1. Register the custom element ONCE at app startup
|
|
309
|
+
import 'apex-grid/define';
|
|
310
|
+
// — or —
|
|
311
|
+
import { ApexGrid } from 'apex-grid';
|
|
312
|
+
ApexGrid.register();
|
|
313
|
+
|
|
314
|
+
// 2. Stamp the element into the DOM
|
|
315
|
+
import { html, render } from 'lit';
|
|
316
|
+
render(html`<apex-grid></apex-grid>`, document.getElementById('app')!);
|
|
317
|
+
|
|
318
|
+
// 3. Set properties (after the element is in the DOM)
|
|
319
|
+
const grid = document.querySelector('apex-grid')!;
|
|
320
|
+
grid.columns = [...];
|
|
321
|
+
grid.data = [...];
|
|
322
|
+
|
|
323
|
+
// 4. Listen for UI events
|
|
324
|
+
grid.addEventListener('sorted', (e) => console.log(e.detail));
|
|
325
|
+
|
|
326
|
+
// 5. Optional: programmatic operations
|
|
327
|
+
grid.sort({ key: 'name', direction: 'ascending' });
|
|
328
|
+
|
|
329
|
+
// 6. Removing the element from the DOM tears down its observers automatically.
|
|
330
|
+
// There is no destroy() — Lit's disconnectedCallback handles cleanup.
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## 8. Pitfalls — ❌ Wrong vs ✅ Correct
|
|
336
|
+
|
|
337
|
+
### 1. Treating it like a class-based JS API
|
|
338
|
+
❌ `const grid = new ApexGrid({ container: '#app', columns, data })` — this is a custom element, not a constructor.
|
|
339
|
+
✅ Render `<apex-grid .data=${data} .columns=${columns}></apex-grid>` and let the DOM instantiate it.
|
|
340
|
+
|
|
341
|
+
### 2. Forgetting to register the element
|
|
342
|
+
❌ Importing `ApexGrid` but never calling `register()` — `<apex-grid>` renders as an inert tag.
|
|
343
|
+
✅ `import 'apex-grid/define'` once, or `ApexGrid.register()` at app startup.
|
|
344
|
+
|
|
345
|
+
### 3. Wrong package name
|
|
346
|
+
❌ `import { ApexGrid } from 'apexgrid'` — the package is `apex-grid` (with a hyphen).
|
|
347
|
+
✅ `import { ApexGrid } from 'apex-grid'`.
|
|
348
|
+
|
|
349
|
+
### 4. Stringified `columns` attribute
|
|
350
|
+
❌ `<apex-grid columns="[...]">` — attributes are strings; `columns` is an object array.
|
|
351
|
+
✅ Property binding: `.columns=${columns}` (Lit), `:columns="columns"` (Vue), `[columns]="columns"` (Angular), or `el.columns = columns` (vanilla).
|
|
352
|
+
|
|
353
|
+
### 5. `column.key` not a real key of `T`
|
|
354
|
+
❌ `{ key: 'fullName' }` when `User` has only `firstName` / `lastName`.
|
|
355
|
+
✅ Either rename the field on `User` or compute it via `cellTemplate`: `cellTemplate: ({ row }) => html\`${row.firstName} ${row.lastName}\``.
|
|
356
|
+
|
|
357
|
+
### 6. Returning a string from `cellTemplate`
|
|
358
|
+
❌ `cellTemplate: ({ value }) => \`<strong>\${value}</strong>\`` — renders as escaped text.
|
|
359
|
+
✅ `cellTemplate: ({ value }) => html\`<strong>\${value}</strong>\``.
|
|
360
|
+
|
|
361
|
+
### 7. Expecting events from programmatic calls
|
|
362
|
+
❌ Listening for `sorting` after calling `grid.sort(...)` — never fires.
|
|
363
|
+
✅ The UI-initiated path fires events; the programmatic path is silent. If you need a side-effect on every change, watch `grid.dataView` or wrap the call yourself.
|
|
364
|
+
|
|
365
|
+
### 8. `sort: true` without setting `type`
|
|
366
|
+
❌ Sorting numbers as strings: `{ key: 'age', sort: true }` with no `type`.
|
|
367
|
+
✅ `{ key: 'age', type: 'number', sort: true }` — gets the correct comparer and filter operands.
|
|
368
|
+
|
|
369
|
+
### 9. Looking for a `'date'` column type
|
|
370
|
+
❌ `{ key: 'created', type: 'date' }` — there is no `'date'` type.
|
|
371
|
+
✅ Store dates as numbers (timestamps) with `type: 'number'`, or as ISO strings with a custom `sort.comparer` and a `cellTemplate` that formats:
|
|
372
|
+
```ts
|
|
373
|
+
{
|
|
374
|
+
key: 'created',
|
|
375
|
+
type: 'string',
|
|
376
|
+
sort: { comparer: (a, b) => a.localeCompare(b) },
|
|
377
|
+
cellTemplate: ({ value }) => html`${new Date(value).toLocaleDateString()}`,
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 10. Calling `clearSort()` to flush an empty array
|
|
382
|
+
❌ `grid.sortExpressions = []` — the setter is a no-op for empty arrays.
|
|
383
|
+
✅ `grid.clearSort()`.
|
|
384
|
+
|
|
385
|
+
### 11. Querying all rows
|
|
386
|
+
❌ `grid.rows.length === grid.data.length` — false; rows are virtualized.
|
|
387
|
+
✅ Use `grid.totalItems` for the post-pipeline count, or `grid.dataView` for the post-pipeline rows.
|
|
388
|
+
|
|
389
|
+
### 12. Auto-generating after replacing data
|
|
390
|
+
❌ `grid.data = newData` with `autoGenerate: true` and existing columns — the old columns persist.
|
|
391
|
+
✅ Reset first: `grid.columns = []; grid.data = newData;`.
|
|
392
|
+
|
|
393
|
+
### 13. Mixing operand keys across types
|
|
394
|
+
❌ `grid.filter({ key: 'age' /* number */, condition: 'contains' /* string operand */ })` — runtime error.
|
|
395
|
+
✅ Match the operand to the column type (or import the typed operand: `NumberOperands.greaterThan`).
|
|
396
|
+
|
|
397
|
+
### 14. Rendering before `register()` in the same task
|
|
398
|
+
❌ Stamping `<apex-grid>` before the registration import has resolved — the element is upgraded later but properties set before upgrade may be lost.
|
|
399
|
+
✅ Import `'apex-grid/define'` at the top of your entry file, then render.
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## 9. Reference Routing Table
|
|
404
|
+
|
|
405
|
+
| Topic | Reference File |
|
|
406
|
+
|---|---|
|
|
407
|
+
| Column configuration, types, templates, auto-generation | `references/columns-and-templates.md` |
|
|
408
|
+
| Sort & filter — UI events, programmatic API, expressions, operands | `references/sort-and-filter.md` |
|
|
409
|
+
| Server-side data pipeline, async hooks, virtualization notes | `references/data-pipeline.md` |
|
|
410
|
+
| Framework integration — Lit, React, Vue, Angular | `references/framework-integration.md` |
|
package/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const packageRoot = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
|
|
6
|
+
/** Absolute path to the package root (where SKILL.md lives). */
|
|
7
|
+
export const packageDir = packageRoot;
|
|
8
|
+
|
|
9
|
+
/** Absolute path to SKILL.md (the top-level skill index). */
|
|
10
|
+
export const skillFile = join(packageRoot, 'SKILL.md');
|
|
11
|
+
|
|
12
|
+
/** Absolute path to the references/ directory containing per-topic markdown docs. */
|
|
13
|
+
export const referencesDir = join(packageRoot, 'references');
|
|
14
|
+
|
|
15
|
+
export const referenceFiles = [
|
|
16
|
+
'columns-and-templates.md',
|
|
17
|
+
'data-pipeline.md',
|
|
18
|
+
'framework-integration.md',
|
|
19
|
+
'sort-and-filter.md',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/** Resolve a reference file by its filename. Throws if not in the known list. */
|
|
23
|
+
export function referencePath(filename) {
|
|
24
|
+
if (!referenceFiles.includes(filename)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Unknown reference file "${filename}". Known files: ${referenceFiles.join(', ')}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return join(referencesDir, filename);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
packageDir,
|
|
34
|
+
skillFile,
|
|
35
|
+
referencesDir,
|
|
36
|
+
referenceFiles,
|
|
37
|
+
referencePath,
|
|
38
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "apexgrid-skill",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI coding skill for the apex-grid web component — structured reference docs that help AI assistants (Claude, Cursor, Copilot, etc.) generate correct apex-grid code.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"apex-grid",
|
|
7
|
+
"apexgrid",
|
|
8
|
+
"data-grid",
|
|
9
|
+
"grid",
|
|
10
|
+
"table",
|
|
11
|
+
"web-component",
|
|
12
|
+
"lit",
|
|
13
|
+
"ai",
|
|
14
|
+
"skill",
|
|
15
|
+
"claude",
|
|
16
|
+
"cursor",
|
|
17
|
+
"copilot",
|
|
18
|
+
"documentation",
|
|
19
|
+
"reference"
|
|
20
|
+
],
|
|
21
|
+
"author": "ApexCharts <info@apexcharts.com>",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"homepage": "https://github.com/apexcharts/apexgrid-skill#readme",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/apexcharts/apexgrid-skill.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/apexcharts/apexgrid-skill/issues"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"main": "index.js",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": "./index.js",
|
|
35
|
+
"./SKILL.md": "./SKILL.md",
|
|
36
|
+
"./references/*": "./references/*",
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"index.js",
|
|
41
|
+
"SKILL.md",
|
|
42
|
+
"references"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Column Configuration & Lit Templates
|
|
2
|
+
|
|
3
|
+
## `ColumnConfiguration<T, K>`
|
|
4
|
+
|
|
5
|
+
Apex Grid is generic over a row type `T extends object`. Every column is typed against a key `K extends keyof T` so the cell template's `value` is correctly inferred.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
interface BaseColumnConfiguration<T extends object, K extends keyof T = keyof T> {
|
|
9
|
+
key: K; // REQUIRED — keyof T
|
|
10
|
+
type?: 'string' | 'number' | 'boolean'; // default 'string'
|
|
11
|
+
headerText?: string; // default: String(key)
|
|
12
|
+
width?: string; // any CSS width unit
|
|
13
|
+
hidden?: boolean; // default false
|
|
14
|
+
resizable?: boolean; // default false
|
|
15
|
+
sort?: boolean | { caseSensitive?: boolean; comparer?: (a: T[K], b: T[K]) => number };
|
|
16
|
+
filter?: boolean | { caseSensitive?: boolean };
|
|
17
|
+
headerTemplate?: (ctx: ApexHeaderContext<T>) => TemplateResult | unknown;
|
|
18
|
+
cellTemplate?: (ctx: ApexCellContext<T, K>) => TemplateResult | unknown;
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Defaults applied on assignment (`DEFAULT_COLUMN_CONFIG`):
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
{ type: 'string', resizable: false, hidden: false, sort: false, filter: false }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Type field — what it actually does
|
|
29
|
+
|
|
30
|
+
| `type` | Default sort comparer | Filter operands |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `'string'` *(default)* | `String#localeCompare` (case-insensitive unless `caseSensitive: true`) | `contains`, `doesNotContain`, `startsWith`, `endsWith`, `equals`, `doesNotEqual`, `empty`, `notEmpty` |
|
|
33
|
+
| `'number'` | numeric `<` / `>` | `equals`, `doesNotEqual`, `greaterThan`, `lessThan`, `greaterThanOrEqual`, `lessThanOrEqual`, `empty`, `notEmpty` |
|
|
34
|
+
| `'boolean'` | `false < true` | `all`, `true`, `false`, `empty`, `notEmpty` |
|
|
35
|
+
|
|
36
|
+
There is **no `'date'` type**. For dates:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// Option A — store as timestamps
|
|
40
|
+
{ key: 'createdAt', type: 'number', sort: true, filter: true,
|
|
41
|
+
cellTemplate: ({ value }) => html`${new Date(value).toLocaleDateString()}` }
|
|
42
|
+
|
|
43
|
+
// Option B — store as ISO strings + custom comparer
|
|
44
|
+
{ key: 'createdAt', type: 'string',
|
|
45
|
+
sort: { comparer: (a, b) => a.localeCompare(b) },
|
|
46
|
+
cellTemplate: ({ value }) => html`${new Date(value).toLocaleDateString()}` }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Sort configuration per column
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// Boolean shorthand — uses the type-default comparer
|
|
53
|
+
{ key: 'name', sort: true }
|
|
54
|
+
|
|
55
|
+
// Object form — case-sensitive comparison
|
|
56
|
+
{ key: 'name', sort: { caseSensitive: true } }
|
|
57
|
+
|
|
58
|
+
// Custom comparer (overrides the type-default)
|
|
59
|
+
{ key: 'priority',
|
|
60
|
+
type: 'string',
|
|
61
|
+
sort: { comparer: (a, b) => ['low','standard','high'].indexOf(a) - ['low','standard','high'].indexOf(b) },
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `comparer` signature is fully typed against `T[K]`.
|
|
66
|
+
|
|
67
|
+
## Filter configuration per column
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
{ key: 'name', filter: true } // case-insensitive
|
|
71
|
+
{ key: 'name', filter: { caseSensitive: true } } // case-sensitive
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
There is no per-column `defaultOperand` field — operands come from `type`.
|
|
75
|
+
|
|
76
|
+
## Cell template (`cellTemplate`)
|
|
77
|
+
|
|
78
|
+
Receives `ApexCellContext<T, K>`:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
interface BaseApexCellContext<T, K> {
|
|
82
|
+
parent: ApexGridCell<T>; // <apex-grid-cell> element
|
|
83
|
+
row: ApexGridRow<T>; // <apex-grid-row> element
|
|
84
|
+
column: ColumnConfiguration<T, K>;
|
|
85
|
+
value: T[K]; // T[K] — fully typed
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { html } from 'lit';
|
|
91
|
+
|
|
92
|
+
const columns: ColumnConfiguration<User>[] = [
|
|
93
|
+
// Direct value — no template needed
|
|
94
|
+
{ key: 'id', type: 'number' },
|
|
95
|
+
|
|
96
|
+
// Inline emphasis
|
|
97
|
+
{ key: 'name',
|
|
98
|
+
cellTemplate: ({ value }) => html`<strong>${value}</strong>` },
|
|
99
|
+
|
|
100
|
+
// Conditional render with row context
|
|
101
|
+
{ key: 'subscribed', type: 'boolean',
|
|
102
|
+
cellTemplate: ({ value, row }) => html`
|
|
103
|
+
<input type="checkbox" .checked=${value} disabled />
|
|
104
|
+
<small>${row.name}</small>` },
|
|
105
|
+
|
|
106
|
+
// Embedded igniteui-webcomponents control
|
|
107
|
+
{ key: 'avatar',
|
|
108
|
+
cellTemplate: ({ value }) => html`<igc-avatar shape="circle" .src=${value}></igc-avatar>` },
|
|
109
|
+
|
|
110
|
+
// Rating widget tied to data type
|
|
111
|
+
{ key: 'satisfaction', type: 'number',
|
|
112
|
+
cellTemplate: ({ value }) => html`<igc-rating readonly .value=${value}></igc-rating>` },
|
|
113
|
+
];
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Templates must return `TemplateResult`** (the value of an `html\`...\`` literal). Returning a string renders as escaped text.
|
|
117
|
+
|
|
118
|
+
## Header template (`headerTemplate`)
|
|
119
|
+
|
|
120
|
+
Receives `ApexHeaderContext<T>`:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
interface ApexHeaderContext<T> {
|
|
124
|
+
parent: ApexGridHeader<T>;
|
|
125
|
+
column: ColumnConfiguration<T>;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
{
|
|
131
|
+
key: 'age',
|
|
132
|
+
headerTemplate: ({ column }) => html`
|
|
133
|
+
<span style="display:flex;align-items:center;gap:4px;">
|
|
134
|
+
<span>${column.headerText ?? String(column.key)}</span>
|
|
135
|
+
<small style="opacity:.6;">(yrs)</small>
|
|
136
|
+
</span>`,
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Auto-generation
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
<apex-grid auto-generate .data=${data}></apex-grid>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- Reads `Object.keys(data[0])` and creates a column for each, using the value's runtime type to pick `type`.
|
|
147
|
+
- **Skipped entirely if `columns` is non-empty.** Set `grid.columns = []` before swapping data to re-trigger.
|
|
148
|
+
- Late-bound data (e.g. fetched after first render) infers correctly because the watcher re-runs on `data` changes — but only when `columns` is still empty.
|
|
149
|
+
|
|
150
|
+
## Updating columns at runtime
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// Replace the whole array (any change re-applies DEFAULT_COLUMN_CONFIG)
|
|
154
|
+
grid.columns = [...newColumns];
|
|
155
|
+
|
|
156
|
+
// Patch existing columns by key (preserves order, merges fields)
|
|
157
|
+
grid.updateColumns({ key: 'name', hidden: true });
|
|
158
|
+
grid.updateColumns([
|
|
159
|
+
{ key: 'age', width: '120px' },
|
|
160
|
+
{ key: 'name', headerText: 'Full Name' },
|
|
161
|
+
]);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`updateColumns` triggers the data pipeline (so any sort/filter state is re-applied). It does **not** add new columns — only existing keys are patched.
|
|
165
|
+
|
|
166
|
+
## Hidden columns
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
{ key: 'internalId', hidden: true }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Column data is still accessible via `grid.dataView` and the cell template's `row` context, but no cell or header is rendered for it. Toggle at runtime via `updateColumns`.
|
|
173
|
+
|
|
174
|
+
## Resizable columns
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
{ key: 'name', resizable: true, width: '200px' }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The grid renders a drag handle on the right edge of the column header. Minimum resize width is hard-coded to `80px`.
|
|
181
|
+
|
|
182
|
+
## Common pitfalls
|
|
183
|
+
|
|
184
|
+
| ❌ | ✅ |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `{ key: 'fullName' }` when `fullName` isn't in `T` | Add to `T`, or compute via `cellTemplate: ({ row }) => html\`${row.firstName} ${row.lastName}\`` |
|
|
187
|
+
| `cellTemplate: ({ value }) => \`<b>\${value}</b>\`` (string) | `cellTemplate: ({ value }) => html\`<b>\${value}</b>\`` |
|
|
188
|
+
| `{ key: 'age', sort: true }` (default `type: 'string'`) | Set `type: 'number'` |
|
|
189
|
+
| `{ key: 'created', type: 'date' }` | No `'date'` type — use timestamp + `'number'` or ISO + custom `comparer` |
|
|
190
|
+
| `auto-generate` with existing columns | Reset `grid.columns = []` first |
|
|
191
|
+
| `updateColumns({ key: 'newField', ... })` to add | `updateColumns` only patches existing — assign a new `columns` array to add |
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Data Pipeline — Server-Side, Async, Virtualization
|
|
2
|
+
|
|
3
|
+
## How the pipeline runs
|
|
4
|
+
|
|
5
|
+
When `data`, `columns`, sort state, or filter state changes, Apex Grid runs an async pipeline:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
data ─► (filter) ─► (sort) ─► dataState ─► virtualizer ─► rendered rows
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
By default both stages run in-memory. Override either stage (or both) with `dataPipelineConfiguration`.
|
|
12
|
+
|
|
13
|
+
## `dataPipelineConfiguration`
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
interface DataPipelineConfiguration<T extends object> {
|
|
17
|
+
sort?: (state: { data: T[]; grid: ApexGrid<T>; type: 'sort' }) => T[] | Promise<T[]>;
|
|
18
|
+
filter?: (state: { data: T[]; grid: ApexGrid<T>; type: 'filter' }) => T[] | Promise<T[]>;
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The grid invokes the matching hook in place of its built-in pipeline stage. Whatever the hook returns becomes the new `dataState` for the next stage (or the final view).
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
grid.dataPipelineConfiguration = {
|
|
26
|
+
sort: async ({ data, grid }) => {
|
|
27
|
+
const expr = grid.sortExpressions;
|
|
28
|
+
const res = await fetch('/api/users/sort', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify(expr),
|
|
32
|
+
});
|
|
33
|
+
return res.json() as Promise<User[]>;
|
|
34
|
+
},
|
|
35
|
+
filter: async ({ data, grid }) => {
|
|
36
|
+
const expr = grid.filterExpressions;
|
|
37
|
+
const res = await fetch('/api/users/filter', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(expr),
|
|
41
|
+
});
|
|
42
|
+
return res.json() as Promise<User[]>;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Hook-only sort, in-memory filter (or vice versa)
|
|
48
|
+
|
|
49
|
+
Just omit one of the two:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
grid.dataPipelineConfiguration = {
|
|
53
|
+
filter: async ({ grid }) => fetchFilteredFromServer(grid.filterExpressions),
|
|
54
|
+
// sort omitted — uses the built-in in-memory sort on the filtered data the hook returns
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Order of operations
|
|
59
|
+
|
|
60
|
+
The grid runs **filter first, then sort** — and that ordering is fixed. If your server endpoint sorts and filters together, do both in the `filter` hook (returning the fully sorted+filtered array) and skip the `sort` hook.
|
|
61
|
+
|
|
62
|
+
### Hooks fire on every state change
|
|
63
|
+
|
|
64
|
+
The pipeline runs whenever `data`, `columns`, sort state, or filter state changes — including programmatic `sort()` / `filter()` calls. If your server endpoint is expensive, debounce inside the hook:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
let pending: AbortController | null = null;
|
|
68
|
+
grid.dataPipelineConfiguration = {
|
|
69
|
+
filter: async ({ grid }) => {
|
|
70
|
+
pending?.abort();
|
|
71
|
+
pending = new AbortController();
|
|
72
|
+
const res = await fetch('/api/users/filter', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
body: JSON.stringify(grid.filterExpressions),
|
|
75
|
+
signal: pending.signal,
|
|
76
|
+
});
|
|
77
|
+
return res.json();
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Hooks have access to the grid instance
|
|
83
|
+
|
|
84
|
+
Use it to read the latest sort / filter state, query metadata, or trigger follow-up operations:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
grid.dataPipelineConfiguration = {
|
|
88
|
+
sort: async ({ data, grid }) => {
|
|
89
|
+
const expressions = grid.sortExpressions;
|
|
90
|
+
const customComparer = grid.getColumn('priority')?.sort; // ColumnSortConfiguration
|
|
91
|
+
// ...
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Virtualization
|
|
97
|
+
|
|
98
|
+
Apex Grid uses [`@lit-labs/virtualizer`](https://www.npmjs.com/package/@lit-labs/virtualizer) (`<apex-virtualizer>`) for row recycling. **Only currently visible rows + a small overscan are present in the DOM.**
|
|
99
|
+
|
|
100
|
+
| API | What it returns |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `grid.rows` | `ApexGridRow<T>[]` — only the rendered chunk |
|
|
103
|
+
| `grid.dataView` | `readonly T[]` — full post-pipeline data (all rows) |
|
|
104
|
+
| `grid.totalItems` | `number` — `dataView.length` |
|
|
105
|
+
|
|
106
|
+
Implications:
|
|
107
|
+
|
|
108
|
+
- **Don't size the grid based on `grid.rows.length`.** Use `grid.totalItems`.
|
|
109
|
+
- **Don't hold references to `<apex-grid-row>` elements** across scrolls — they get recycled.
|
|
110
|
+
- **DOM-driven row queries** (`document.querySelectorAll('apex-grid-row')`) only see visible rows.
|
|
111
|
+
|
|
112
|
+
## When the pipeline runs
|
|
113
|
+
|
|
114
|
+
Internally the grid watches the `'pipeline'` reactive symbol. It runs after:
|
|
115
|
+
|
|
116
|
+
- `data` is reassigned
|
|
117
|
+
- `columns` is reassigned (or `updateColumns` is called)
|
|
118
|
+
- A sort operation is applied (UI or programmatic)
|
|
119
|
+
- A filter operation is applied (UI or programmatic)
|
|
120
|
+
- Sort / filter state is cleared
|
|
121
|
+
|
|
122
|
+
It does **not** run when only column properties like `width` / `hidden` / `resizable` change, since those are presentational.
|
|
123
|
+
|
|
124
|
+
## `dataView` for derived UI
|
|
125
|
+
|
|
126
|
+
`dataView` is the canonical source of truth for "what's on screen after the pipeline":
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// React-style consumer outside the grid (e.g. a footer summary)
|
|
130
|
+
const updateSummary = () => {
|
|
131
|
+
const view = grid.dataView;
|
|
132
|
+
document.getElementById('count')!.textContent = `${view.length} rows`;
|
|
133
|
+
document.getElementById('avg')!.textContent =
|
|
134
|
+
String(view.reduce((s, u) => s + u.age, 0) / view.length);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
grid.addEventListener('sorted', updateSummary);
|
|
138
|
+
grid.addEventListener('filtered', updateSummary);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Common pitfalls
|
|
142
|
+
|
|
143
|
+
| ❌ | ✅ |
|
|
144
|
+
|---|---|
|
|
145
|
+
| Reading `grid.rows.length` for total count | `grid.totalItems` (or `grid.dataView.length`) |
|
|
146
|
+
| Caching `<apex-grid-row>` references across scrolls | Look them up fresh from `grid.rows` (or operate on `grid.dataView` data) |
|
|
147
|
+
| Re-fetching from the server only on UI events | Hooks fire for both UI and programmatic — debounce / abort inside the hook |
|
|
148
|
+
| Sorting in the `filter` hook expecting it to override the built-in sort | The grid still runs the built-in sort after your filter hook unless you also supply a `sort` hook |
|
|
149
|
+
| Returning a Promise that rejects | Errors propagate; the grid keeps the previous `dataView`. Catch + log inside the hook to surface failures |
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Framework Integration — Lit, React, Vue, Angular
|
|
2
|
+
|
|
3
|
+
`apex-grid` is a standard custom element with no first-party framework wrappers. Every framework consumes it the same way: register the tag once, then bind `data` and `columns` as **properties** (not attributes) and listen for `sorting` / `sorted` / `filtering` / `filtered` events.
|
|
4
|
+
|
|
5
|
+
## Lit (recommended)
|
|
6
|
+
|
|
7
|
+
The most natural host — Lit `html` templates already understand the `.property` and `@event` binding syntax.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import 'apex-grid/define';
|
|
11
|
+
import { html, render } from 'lit';
|
|
12
|
+
import type { ColumnConfiguration } from 'apex-grid';
|
|
13
|
+
|
|
14
|
+
type User = { id: number; name: string; age: number; subscribed: boolean };
|
|
15
|
+
|
|
16
|
+
const data: User[] = [/* … */];
|
|
17
|
+
const columns: ColumnConfiguration<User>[] = [
|
|
18
|
+
{ key: 'id', type: 'number', sort: true, filter: true },
|
|
19
|
+
{ key: 'name', type: 'string', sort: true, filter: true,
|
|
20
|
+
cellTemplate: ({ value }) => html`<strong>${value}</strong>` },
|
|
21
|
+
{ key: 'age', type: 'number', sort: true, filter: true },
|
|
22
|
+
{ key: 'subscribed', type: 'boolean', sort: true, filter: true },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
render(
|
|
26
|
+
html`<apex-grid
|
|
27
|
+
.data=${data}
|
|
28
|
+
.columns=${columns}
|
|
29
|
+
@sorted=${(e) => console.log('sorted', e.detail)}
|
|
30
|
+
@filtered=${(e) => console.log('filtered', e.detail)}
|
|
31
|
+
></apex-grid>`,
|
|
32
|
+
document.getElementById('app')!,
|
|
33
|
+
);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## React
|
|
37
|
+
|
|
38
|
+
React 19+ supports custom elements natively, including property binding via `ref` for non-string values. For broad compatibility, use a `useEffect` + `ref` pattern:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { useEffect, useRef } from 'react';
|
|
42
|
+
import 'apex-grid/define';
|
|
43
|
+
import type { ApexGrid, ColumnConfiguration } from 'apex-grid';
|
|
44
|
+
import { html } from 'lit';
|
|
45
|
+
|
|
46
|
+
type User = { id: number; name: string; age: number; subscribed: boolean };
|
|
47
|
+
|
|
48
|
+
export function UsersGrid({ data }: { data: User[] }) {
|
|
49
|
+
const ref = useRef<ApexGrid<User> | null>(null);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const grid = ref.current;
|
|
53
|
+
if (!grid) return;
|
|
54
|
+
|
|
55
|
+
grid.columns = [
|
|
56
|
+
{ key: 'id', type: 'number', sort: true, filter: true },
|
|
57
|
+
{ key: 'name', type: 'string', sort: true, filter: true,
|
|
58
|
+
cellTemplate: ({ value }) => html`<strong>${value}</strong>` },
|
|
59
|
+
{ key: 'age', type: 'number', sort: true, filter: true },
|
|
60
|
+
{ key: 'subscribed', type: 'boolean', sort: true, filter: true },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const onSorted = (e: Event) => console.log('sorted', (e as CustomEvent).detail);
|
|
64
|
+
grid.addEventListener('sorted', onSorted);
|
|
65
|
+
return () => grid.removeEventListener('sorted', onSorted);
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
// Set data whenever it changes
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (ref.current) ref.current.data = data;
|
|
71
|
+
}, [data]);
|
|
72
|
+
|
|
73
|
+
// @ts-expect-error — apex-grid is a custom element
|
|
74
|
+
return <apex-grid ref={ref} style={{ height: '500px', display: 'block' }} />;
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For full TypeScript / JSX intrinsic-element support, declare:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// apex-grid.d.ts
|
|
82
|
+
import type { ApexGrid } from 'apex-grid';
|
|
83
|
+
|
|
84
|
+
declare module 'react' {
|
|
85
|
+
namespace JSX {
|
|
86
|
+
interface IntrinsicElements {
|
|
87
|
+
'apex-grid': React.DetailedHTMLProps<React.HTMLAttributes<ApexGrid<any>>, ApexGrid<any>>;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Vue 3
|
|
94
|
+
|
|
95
|
+
Vue understands property binding for custom elements out of the box. Mark `apex-grid` as a custom element in the compiler config so Vue doesn't try to resolve it as a Vue component:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// vite.config.ts
|
|
99
|
+
export default defineConfig({
|
|
100
|
+
plugins: [
|
|
101
|
+
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('apex-') } } }),
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```vue
|
|
107
|
+
<script setup lang="ts">
|
|
108
|
+
import { ref, onMounted } from 'vue';
|
|
109
|
+
import 'apex-grid/define';
|
|
110
|
+
import { html } from 'lit';
|
|
111
|
+
import type { ApexGrid, ColumnConfiguration } from 'apex-grid';
|
|
112
|
+
|
|
113
|
+
type User = { id: number; name: string; age: number; subscribed: boolean };
|
|
114
|
+
|
|
115
|
+
const grid = ref<ApexGrid<User> | null>(null);
|
|
116
|
+
|
|
117
|
+
const data = ref<User[]>([/* … */]);
|
|
118
|
+
const columns: ColumnConfiguration<User>[] = [
|
|
119
|
+
{ key: 'id', type: 'number', sort: true, filter: true },
|
|
120
|
+
{ key: 'name', type: 'string', sort: true, filter: true,
|
|
121
|
+
cellTemplate: ({ value }) => html`<strong>${value}</strong>` },
|
|
122
|
+
{ key: 'age', type: 'number', sort: true, filter: true },
|
|
123
|
+
{ key: 'subscribed', type: 'boolean', sort: true, filter: true },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
onMounted(() => {
|
|
127
|
+
grid.value!.columns = columns;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
function onSorted(e: CustomEvent) {
|
|
131
|
+
console.log('sorted', e.detail);
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<template>
|
|
136
|
+
<apex-grid
|
|
137
|
+
ref="grid"
|
|
138
|
+
:data.prop="data"
|
|
139
|
+
@sorted="onSorted"
|
|
140
|
+
style="height: 500px; display: block;"
|
|
141
|
+
/>
|
|
142
|
+
</template>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> Use `:data.prop="data"` (or set via `ref` in `onMounted`) to bind as a property. Vue's default `:data="data"` sets an attribute (a stringified object) which `apex-grid` will not read.
|
|
146
|
+
|
|
147
|
+
## Angular
|
|
148
|
+
|
|
149
|
+
Add `CUSTOM_ELEMENTS_SCHEMA` to the module / standalone component, then use property binding `[data]="..."` and event binding `(sorted)="..."`:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// app.component.ts
|
|
153
|
+
import { Component, CUSTOM_ELEMENTS_SCHEMA, OnInit, ViewChild, ElementRef } from '@angular/core';
|
|
154
|
+
import 'apex-grid/define';
|
|
155
|
+
import { html } from 'lit';
|
|
156
|
+
import type { ApexGrid, ColumnConfiguration } from 'apex-grid';
|
|
157
|
+
|
|
158
|
+
type User = { id: number; name: string; age: number; subscribed: boolean };
|
|
159
|
+
|
|
160
|
+
@Component({
|
|
161
|
+
selector: 'app-users-grid',
|
|
162
|
+
standalone: true,
|
|
163
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
164
|
+
template: `
|
|
165
|
+
<apex-grid
|
|
166
|
+
#grid
|
|
167
|
+
[data]="data"
|
|
168
|
+
(sorted)="onSorted($event)"
|
|
169
|
+
style="height: 500px; display: block;"
|
|
170
|
+
></apex-grid>
|
|
171
|
+
`,
|
|
172
|
+
})
|
|
173
|
+
export class UsersGridComponent implements OnInit {
|
|
174
|
+
@ViewChild('grid', { static: true }) grid!: ElementRef<ApexGrid<User>>;
|
|
175
|
+
|
|
176
|
+
data: User[] = [/* … */];
|
|
177
|
+
|
|
178
|
+
ngOnInit() {
|
|
179
|
+
this.grid.nativeElement.columns = [
|
|
180
|
+
{ key: 'id', type: 'number', sort: true, filter: true },
|
|
181
|
+
{ key: 'name', type: 'string', sort: true, filter: true,
|
|
182
|
+
cellTemplate: ({ value }) => html`<strong>${value}</strong>` },
|
|
183
|
+
{ key: 'age', type: 'number', sort: true, filter: true },
|
|
184
|
+
{ key: 'subscribed', type: 'boolean', sort: true, filter: true },
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
onSorted(e: Event) {
|
|
189
|
+
console.log('sorted', (e as CustomEvent).detail);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Common pitfalls across frameworks
|
|
195
|
+
|
|
196
|
+
| ❌ | ✅ |
|
|
197
|
+
|---|---|
|
|
198
|
+
| Vue: `:data="data"` (attribute, becomes `"[object Object]"`) | `:data.prop="data"` or set via `ref` in `onMounted` |
|
|
199
|
+
| React: `<apex-grid columns={[...]}>` before React 19 (passes as attribute) | Use `ref` + `useEffect` to set as property, or React 19+ |
|
|
200
|
+
| Angular: missing `CUSTOM_ELEMENTS_SCHEMA` | Add to the component / module schemas |
|
|
201
|
+
| All: forgetting to register the element | Top-level `import 'apex-grid/define'` once |
|
|
202
|
+
| Lit: writing string templates instead of `html` literals | Always import `html` from `lit` and use tagged literals |
|
|
203
|
+
| Treating `cellTemplate` as framework-specific JSX | Always use Lit `html\`...\`` — the template runs inside the web component, not in the host framework |
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Sort & Filter — Operands, Expressions, Events
|
|
2
|
+
|
|
3
|
+
## Sort
|
|
4
|
+
|
|
5
|
+
### Sort expression shape
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
interface BaseSortExpression<T, K extends keyof T> {
|
|
9
|
+
key: K; // target column
|
|
10
|
+
direction: 'ascending' | 'descending' | 'none';
|
|
11
|
+
caseSensitive?: boolean; // overrides column config
|
|
12
|
+
comparer?: (a: T[K], b: T[K]) => number; // overrides column config
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`'none'` is the explicit reset state and only meaningful when `sortConfiguration.triState: true`.
|
|
17
|
+
|
|
18
|
+
### Programmatic sort
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
grid.sort({ key: 'name', direction: 'ascending' });
|
|
22
|
+
grid.sort({ key: 'age', direction: 'descending' });
|
|
23
|
+
|
|
24
|
+
// Multi-column (requires sortConfiguration.multiple === true, default true)
|
|
25
|
+
grid.sort([
|
|
26
|
+
{ key: 'subscribed', direction: 'descending' },
|
|
27
|
+
{ key: 'age', direction: 'ascending' },
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// Override the column comparer for this operation
|
|
31
|
+
grid.sort({
|
|
32
|
+
key: 'priority',
|
|
33
|
+
direction: 'ascending',
|
|
34
|
+
comparer: (a, b) => ['low', 'standard', 'high'].indexOf(a) - ['low', 'standard', 'high'].indexOf(b),
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`grid.sort(...)` does **not** fire `sorting` / `sorted` events.
|
|
39
|
+
|
|
40
|
+
### Reading the active sort state
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
grid.sortExpressions; // SortExpression<T>[]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The array is in the order operations were applied (insertion order on the underlying `Map`).
|
|
47
|
+
|
|
48
|
+
### Clearing sort
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
grid.clearSort(); // clear everything
|
|
52
|
+
grid.clearSort('name'); // clear only the `name` column
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> **Heads-up:** assigning an empty array (`grid.sortExpressions = []`) is a no-op. Always call `clearSort()` to reset.
|
|
56
|
+
|
|
57
|
+
### `sortConfiguration`
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
grid.sortConfiguration = {
|
|
61
|
+
multiple: true, // default — allow multi-column sort
|
|
62
|
+
triState: true, // default — clicking a sorted-asc column twice goes ascending → descending → none
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Filter
|
|
67
|
+
|
|
68
|
+
### Filter expression shape
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
interface BaseFilterExpression<T, K extends keyof T> {
|
|
72
|
+
key: K;
|
|
73
|
+
condition: FilterOperation<T[K]> | OperandKeys<T[K]>; // operand object or string key
|
|
74
|
+
searchTerm?: T[K]; // optional for unary operands (empty / notEmpty / true / false / all)
|
|
75
|
+
criteria?: 'and' | 'or'; // for combining with previous expression on same column
|
|
76
|
+
caseSensitive?: boolean; // overrides column config
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Operand sets
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { StringOperands, NumberOperands, BooleanOperands } from 'apex-grid';
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| Type | Operand keys |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `'string'` | `contains`, `doesNotContain`, `startsWith`, `endsWith`, `equals`, `doesNotEqual`, `empty`, `notEmpty` |
|
|
89
|
+
| `'number'` | `equals`, `doesNotEqual`, `greaterThan`, `lessThan`, `greaterThanOrEqual`, `lessThanOrEqual`, `empty`, `notEmpty` |
|
|
90
|
+
| `'boolean'` | `all`, `true`, `false`, `empty`, `notEmpty` |
|
|
91
|
+
|
|
92
|
+
Unary operands (no `searchTerm` needed): `empty`, `notEmpty`, `all`, `true`, `false`.
|
|
93
|
+
|
|
94
|
+
### Programmatic filter
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// String operand by name
|
|
98
|
+
grid.filter({ key: 'name', condition: 'contains', searchTerm: 'ada' });
|
|
99
|
+
|
|
100
|
+
// String operand by reference (typed)
|
|
101
|
+
grid.filter({ key: 'name', condition: StringOperands.contains, searchTerm: 'ada' });
|
|
102
|
+
|
|
103
|
+
// Number operand
|
|
104
|
+
grid.filter({ key: 'age', condition: 'greaterThan', searchTerm: 30 });
|
|
105
|
+
|
|
106
|
+
// Boolean operand
|
|
107
|
+
grid.filter({ key: 'subscribed', condition: 'true' });
|
|
108
|
+
|
|
109
|
+
// Unary — no searchTerm
|
|
110
|
+
grid.filter({ key: 'avatar', condition: 'empty' });
|
|
111
|
+
|
|
112
|
+
// Combined AND / OR
|
|
113
|
+
grid.filter([
|
|
114
|
+
{ key: 'age', condition: 'greaterThan', searchTerm: 30 },
|
|
115
|
+
{ key: 'age', condition: 'lessThan', searchTerm: 50, criteria: 'and' },
|
|
116
|
+
]);
|
|
117
|
+
grid.filter([
|
|
118
|
+
{ key: 'priority', condition: 'equals', searchTerm: 'high' },
|
|
119
|
+
{ key: 'priority', condition: 'equals', searchTerm: 'standard', criteria: 'or' },
|
|
120
|
+
]);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`grid.filter(...)` does **not** fire `filtering` / `filtered` events.
|
|
124
|
+
|
|
125
|
+
### Reading the active filter state
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
grid.filterExpressions; // FilterExpression<T>[]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Combined across columns. To get state for a single column, find by `key`:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const ageFilters = grid.filterExpressions.filter((e) => e.key === 'age');
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Clearing filter
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
grid.clearFilter(); // clear everything
|
|
141
|
+
grid.clearFilter('age'); // clear only the `age` column
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Events
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// UI-initiated sort about to apply — cancellable, mutable
|
|
148
|
+
grid.addEventListener('sorting', (e) => {
|
|
149
|
+
// e: CustomEvent<SortExpression<T>>
|
|
150
|
+
if (e.detail.key === 'id') e.preventDefault(); // veto
|
|
151
|
+
if (e.detail.direction === 'descending') // rewrite
|
|
152
|
+
e.detail.direction = 'ascending';
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// UI-initiated sort completed
|
|
156
|
+
grid.addEventListener('sorted', (e) => {
|
|
157
|
+
// e: CustomEvent<SortExpression<T>>
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// UI-initiated filter about to apply — cancellable, mutable
|
|
161
|
+
grid.addEventListener('filtering', (e) => {
|
|
162
|
+
// e: CustomEvent<{ key, expressions, type: 'add'|'modify'|'remove' }>
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// UI-initiated filter completed
|
|
166
|
+
grid.addEventListener('filtered', (e) => {
|
|
167
|
+
// e: CustomEvent<{ key, state: FilterExpression<T>[] }>
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
| Event | Cancellable | `event.detail` | Fires for programmatic? |
|
|
172
|
+
|---|---|---|---|
|
|
173
|
+
| `sorting` | yes | `SortExpression<T>` (mutable) | no |
|
|
174
|
+
| `sorted` | no | `SortExpression<T>` | no |
|
|
175
|
+
| `filtering` | yes | `{ key, expressions, type }` | no |
|
|
176
|
+
| `filtered` | no | `{ key, state }` | no |
|
|
177
|
+
|
|
178
|
+
If you need a side-effect on every change regardless of source, watch `grid.dataView` (after `requestUpdate` / via `updated()` in a Lit parent) or wrap your programmatic calls.
|
|
179
|
+
|
|
180
|
+
## Combining UI + programmatic operations
|
|
181
|
+
|
|
182
|
+
UI operations and programmatic operations write to the **same underlying state** (the `StateController`). Order matters:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// Persisting and restoring state across mounts
|
|
186
|
+
const savedSort = grid.sortExpressions;
|
|
187
|
+
const savedFilter = grid.filterExpressions;
|
|
188
|
+
|
|
189
|
+
// ... later, after re-mounting
|
|
190
|
+
grid.sort(savedSort);
|
|
191
|
+
grid.filter(savedFilter);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Common pitfalls
|
|
195
|
+
|
|
196
|
+
| ❌ | ✅ |
|
|
197
|
+
|---|---|
|
|
198
|
+
| Listening for events on programmatic calls | Wrap the call yourself, or watch `grid.dataView` |
|
|
199
|
+
| `grid.filter({ key: 'age', condition: 'contains' })` (string operand on number column) | Match operand to type — `condition: 'greaterThan'` for number columns |
|
|
200
|
+
| `grid.sortExpressions = []` to clear | `grid.clearSort()` |
|
|
201
|
+
| Multi-column sort with `sortConfiguration.multiple = false` | Either set `multiple: true` or call `grid.sort` with a single expression |
|
|
202
|
+
| Forgetting `searchTerm` on a non-unary operand | Add `searchTerm`, or use a unary operand (`empty` / `notEmpty` / `true` / `false` / `all`) |
|
|
203
|
+
| Mutating `e.detail` in a `sorted` / `filtered` listener | Only `sorting` / `filtering` are cancellable / mutable; the post-events are read-only by design |
|