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 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 |