apexgrid-skill 1.0.0 → 2.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/index.js CHANGED
@@ -17,6 +17,7 @@ export const referenceFiles = [
17
17
  'data-pipeline.md',
18
18
  'framework-integration.md',
19
19
  'sort-and-filter.md',
20
+ 'vanilla-js.md',
20
21
  ];
21
22
 
22
23
  /** Resolve a reference file by its filename. Throws if not in the known list. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apexgrid-skill",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
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
5
  "keywords": [
6
6
  "apex-grid",
@@ -1,5 +1,11 @@
1
1
  # Column Configuration & Lit Templates
2
2
 
3
+ > **Before you start**: this reference assumes the Quickstart setup from `SKILL.md` is done — element registered and the host element sized (e.g. `setup()`). **No theme CSS import is needed**; the grid self-styles via `--ag-*` CSS variables. Without a sized host, no column configuration will produce a visible grid.
4
+
5
+ ## A note on column widths
6
+
7
+ If you omit `width`, the column flex-fills the remaining space. With one un-widthed column among several explicit ones, that column **stretches to fill the grid** — including pushing its sort arrow to the far edge of the viewport, which looks like a phantom column. For predictable layouts, set explicit widths on every column.
8
+
3
9
  ## `ColumnConfiguration<T, K>`
4
10
 
5
11
  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.
@@ -7,18 +13,38 @@ Apex Grid is generic over a row type `T extends object`. Every column is typed a
7
13
  ```ts
8
14
  interface BaseColumnConfiguration<T extends object, K extends keyof T = keyof T> {
9
15
  key: K; // REQUIRED — keyof T
10
- type?: 'string' | 'number' | 'boolean'; // default 'string'
16
+ type?: DataType; // default 'string' (see "Column types" below)
11
17
  headerText?: string; // default: String(key)
12
18
  width?: string; // any CSS width unit
13
19
  hidden?: boolean; // default false
14
20
  resizable?: boolean; // default false
21
+ pinned?: 'start' | 'end' | null; // freeze to a side during horizontal scroll
22
+ reorderable?: boolean; // per-column opt-out of drag reorder
23
+ exportable?: boolean; // default true; false omits from CSV export
24
+ editable?: boolean; // requires grid editing.enabled
15
25
  sort?: boolean | { caseSensitive?: boolean; comparer?: (a: T[K], b: T[K]) => number };
16
26
  filter?: boolean | { caseSensitive?: boolean };
17
27
  headerTemplate?: (ctx: ApexHeaderContext<T>) => TemplateResult | unknown;
18
28
  cellTemplate?: (ctx: ApexCellContext<T, K>) => TemplateResult | unknown;
29
+ editorTemplate?: (ctx: ApexEditorContext<T, K>) => TemplateResult | unknown;
30
+ // type-specific (ignored unless `type` matches):
31
+ options?: (V | { value: V; label?: string })[]; // 'select'
32
+ max?: number; // 'rating' (stars, def 5) / 'progress' (full, def 100)
33
+ format?: 'short' | 'medium' | 'long' | 'full'; // 'date' (def 'medium')
34
+ shape?: 'square' | 'circle'; // 'image' (def 'square')
35
+ alt?: string; // 'image' alt text
36
+ currency?: string; // 'currency' ISO 4217 (def 'USD')
37
+ locale?: string; // 'currency' BCP 47 locale
38
+ badgeVariant?: 'gold' | 'brand' | 'neutral' | 'muted' | ((v) => …); // 'badge'
39
+ statusVariant?: 'active' | 'trial' | 'churn' | ((v) => …); // 'status'
40
+ showDelta?: boolean; // 'sparkline' (def true)
19
41
  }
20
42
  ```
21
43
 
44
+ ### Column types (`DataType`)
45
+
46
+ `'string'` / `'number'` / `'boolean'` are the primitive types driving sort, filter, and the default editor. The rest are presentation renderers over a primitive value (and sort/filter as that value): `'select'` (label from `options`, `<select>` editor), `'rating'` (stars, 0..`max`), `'date'` (locale date via `format`; accepts `Date`/ISO/timestamp; `<input type="date">` editor), `'image'` (`<img>`, `shape`/`alt`), `'currency'` (`Intl.NumberFormat`, `currency`/`locale`), `'avatar'`, `'badge'` (`badgeVariant`), `'progress'` (0..`max`), `'sparkline'` (`number[]`, `showDelta`), `'status'` (`statusVariant`).
47
+
22
48
  Defaults applied on assignment (`DEFAULT_COLUMN_CONFIG`):
23
49
 
24
50
  ```ts
@@ -33,17 +59,10 @@ Defaults applied on assignment (`DEFAULT_COLUMN_CONFIG`):
33
59
  | `'number'` | numeric `<` / `>` | `equals`, `doesNotEqual`, `greaterThan`, `lessThan`, `greaterThanOrEqual`, `lessThanOrEqual`, `empty`, `notEmpty` |
34
60
  | `'boolean'` | `false < true` | `all`, `true`, `false`, `empty`, `notEmpty` |
35
61
 
36
- There is **no `'date'` type**. For dates:
62
+ The table above lists the three **primitive** types. The presentation types (`'select'`, `'rating'`, `'date'`, `'image'`, `'currency'`, `'avatar'`, `'badge'`, `'progress'`, `'sparkline'`, `'status'`) sort and filter as their underlying value. For dates, use the real `'date'` type — it accepts `Date`, ISO strings, or millisecond timestamps, renders a locale date (`format`), and sorts chronologically. No timestamp/comparer workaround needed:
37
63
 
38
64
  ```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()}` }
65
+ { key: 'createdAt', type: 'date', format: 'medium', sort: true, filter: true }
47
66
  ```
48
67
 
49
68
  ## Sort configuration per column
@@ -137,16 +156,39 @@ interface ApexHeaderContext<T> {
137
156
  }
138
157
  ```
139
158
 
140
- ## Auto-generation
159
+ ## Auto-generation — the fastest demo
160
+
161
+ The shortest possible "just show me a table" example:
141
162
 
142
163
  ```ts
143
- <apex-grid auto-generate .data=${data}></apex-grid>
164
+ import { html, render } from 'lit';
165
+ import { setup } from 'apex-grid';
166
+ setup();
167
+
168
+ const data = [
169
+ { id: 1, name: 'Ada', age: 36 },
170
+ { id: 2, name: 'Carl', age: 62 },
171
+ ];
172
+
173
+ render(
174
+ html`<apex-grid auto-generate .data=${data}></apex-grid>`,
175
+ document.getElementById('app')!,
176
+ );
144
177
  ```
145
178
 
179
+ > Remember the host CSS — `apex-grid { height: 480px; }` — or you'll see no rows even with `auto-generate`.
180
+
181
+ **How it works:**
182
+
146
183
  - Reads `Object.keys(data[0])` and creates a column for each, using the value's runtime type to pick `type`.
147
184
  - **Skipped entirely if `columns` is non-empty.** Set `grid.columns = []` before swapping data to re-trigger.
148
185
  - 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
186
 
187
+ **Caveats:**
188
+
189
+ - `sort` and `filter` default to `false`, so the auto path is read-only. To get sortable / filterable columns from inferred data, run auto-generate first then patch via `grid.updateColumns([{ key: 'name', sort: true, filter: true }, ...])`.
190
+ - Auto-generated columns get no explicit `width` — they flex-fill across the grid. For mixed-type columns that's usually not what you want; assign explicit `columns` instead.
191
+
150
192
  ## Updating columns at runtime
151
193
 
152
194
  ```ts
@@ -186,6 +228,6 @@ The grid renders a drag handle on the right edge of the column header. Minimum r
186
228
  | `{ key: 'fullName' }` when `fullName` isn't in `T` | Add to `T`, or compute via `cellTemplate: ({ row }) => html\`${row.firstName} ${row.lastName}\`` |
187
229
  | `cellTemplate: ({ value }) => \`<b>\${value}</b>\`` (string) | `cellTemplate: ({ value }) => html\`<b>\${value}</b>\`` |
188
230
  | `{ 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` |
231
+ | `{ key: 'created', type: 'string', sort: { comparer } }` to fake dates | `{ key: 'created', type: 'date', format: 'medium' }` — a real type (accepts `Date`/ISO/timestamp) |
190
232
  | `auto-generate` with existing columns | Reset `grid.columns = []` first |
191
233
  | `updateColumns({ key: 'newField', ... })` to add | `updateColumns` only patches existing — assign a new `columns` array to add |
@@ -2,24 +2,29 @@
2
2
 
3
3
  ## How the pipeline runs
4
4
 
5
- When `data`, `columns`, sort state, or filter state changes, Apex Grid runs an async pipeline:
5
+ When `data`, `columns`, sort state, filter state, the quick-filter value, or the page changes, Apex Grid runs an async pipeline:
6
6
 
7
7
  ```
8
- data ─► (filter) ─► (sort) ─► dataState ─► virtualizer ─► rendered rows
8
+ data ─► (quickFilter) ─► (filter) ─► (sort) ─► (pagination) ─► virtualizer ─► rendered rows
9
9
  ```
10
10
 
11
- By default both stages run in-memory. Override either stage (or both) with `dataPipelineConfiguration`.
11
+ By default every stage runs in-memory. Override any stage with `dataPipelineConfiguration`.
12
12
 
13
13
  ## `dataPipelineConfiguration`
14
14
 
15
15
  ```ts
16
+ type DataPipelineParams<T> = { data: T[]; grid: ApexGrid<T>; type: 'sort' | 'filter' | 'quickFilter' | 'pagination' };
17
+ type DataPipelineHook<T> = (state: DataPipelineParams<T>) => T[] | Promise<T[]>;
18
+
16
19
  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[]>;
20
+ sort?: DataPipelineHook<T>;
21
+ filter?: DataPipelineHook<T>; // column filters
22
+ quickFilter?: DataPipelineHook<T>; // global search (runs before column filter)
23
+ pagination?: DataPipelineHook<T>; // return the page slice; remote: set pagination.totalItems
19
24
  }
20
25
  ```
21
26
 
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).
27
+ The grid invokes the matching hook in place of its built-in pipeline stage. Whatever the hook returns becomes the input for the next stage (or the final view). All four hooks are optional — omit a stage to keep it in-memory.
23
28
 
24
29
  ```ts
25
30
  grid.dataPipelineConfiguration = {
@@ -57,7 +62,7 @@ grid.dataPipelineConfiguration = {
57
62
 
58
63
  ### Order of operations
59
64
 
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.
65
+ The grid runs the stages in a fixed order: **quick-filter → column filter → sort → pagination**. If your server endpoint does several of these together, do the work in the earliest relevant hook (returning the fully processed array) and omit the later hooks. For server-driven paging, return the current page from the `pagination` hook and set `grid.pagination = { ..., mode: 'remote', totalItems }` so the paginator can compute the page count.
61
66
 
62
67
  ### Hooks fire on every state change
63
68
 
@@ -1,13 +1,46 @@
1
1
  # Framework Integration — Lit, React, Vue, Angular
2
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.
3
+ `apex-grid` is a standard custom element with no first-party framework wrappers. Every framework consumes it the same way:
4
4
 
5
- ## Lit (recommended)
5
+ 1. **Register the element + size the host once at app startup** — `setup()` does both (registers `<apex-grid>` and adopts a bounded host height). No theme CSS import is needed; the grid self-styles via `--ag-*` variables.
6
+ 2. **Bind `data` and `columns` as properties** (not attributes).
7
+ 3. **Listen for `sorting` / `sorted` / `filtering` / `filtered` (and the other UI) events** as standard DOM events.
6
8
 
7
- The most natural host — Lit `html` templates already understand the `.property` and `@event` binding syntax.
9
+ The `setup()` call ideally lives in your application entry file (`main.ts` / `main.js` / `app.module.ts`) so it runs once, not per component.
10
+
11
+ ---
12
+
13
+ ## App-entry setup (all frameworks)
14
+
15
+ Put this at the top of your application entry point — `src/main.ts` for Vite/Lit/Vue, `src/index.tsx` for React, `src/main.ts` for Angular:
16
+
17
+ ```ts
18
+ import { setup } from 'apex-grid';
19
+ setup(); // registers <apex-grid> + adopts a bounded host height
20
+ ```
21
+
22
+ No theme import needed — the grid self-styles. Retheme by overriding `--ag-*` CSS variables on `apex-grid`. If you prefer manual control over sizing, call `setup({ hostStyles: false })` (or use `import 'apex-grid/define'`) and add your own rule:
23
+
24
+ ```css
25
+ apex-grid {
26
+ height: 480px; /* or whatever fits your layout; + any --ag-* overrides */
27
+ }
28
+ ```
29
+
30
+ The framework examples below assume this app-entry setup is already done.
31
+
32
+ ---
33
+
34
+ ## Lit
35
+
36
+ The most natural host — Lit `html` templates already understand `.property` and `@event` binding syntax.
8
37
 
9
38
  ```ts
10
- import 'apex-grid/define';
39
+ // src/main.ts — done once
40
+ import { setup } from 'apex-grid';
41
+ setup();
42
+
43
+ // component
11
44
  import { html, render } from 'lit';
12
45
  import type { ColumnConfiguration } from 'apex-grid';
13
46
 
@@ -15,11 +48,11 @@ type User = { id: number; name: string; age: number; subscribed: boolean };
15
48
 
16
49
  const data: User[] = [/* … */];
17
50
  const columns: ColumnConfiguration<User>[] = [
18
- { key: 'id', type: 'number', sort: true, filter: true },
19
- { key: 'name', type: 'string', sort: true, filter: true,
51
+ { key: 'id', type: 'number', width: '80px', sort: true, filter: true },
52
+ { key: 'name', type: 'string', width: '240px', sort: true, filter: true,
20
53
  cellTemplate: ({ value }) => html`<strong>${value}</strong>` },
21
- { key: 'age', type: 'number', sort: true, filter: true },
22
- { key: 'subscribed', type: 'boolean', sort: true, filter: true },
54
+ { key: 'age', type: 'number', width: '100px', sort: true, filter: true },
55
+ { key: 'subscribed', type: 'boolean', width: '140px', sort: true, filter: true },
23
56
  ];
24
57
 
25
58
  render(
@@ -33,15 +66,16 @@ render(
33
66
  );
34
67
  ```
35
68
 
69
+ ---
70
+
36
71
  ## React
37
72
 
38
- React 19+ supports custom elements natively, including property binding via `ref` for non-string values. For broad compatibility, use a `useEffect` + `ref` pattern:
73
+ React 19+ supports custom elements natively, including property binding for non-string values. For broad compatibility, use `ref` + `useEffect`:
39
74
 
40
75
  ```tsx
41
76
  import { useEffect, useRef } from 'react';
42
- import 'apex-grid/define';
77
+ import { html } from 'lit'; // required for cellTemplate
43
78
  import type { ApexGrid, ColumnConfiguration } from 'apex-grid';
44
- import { html } from 'lit';
45
79
 
46
80
  type User = { id: number; name: string; age: number; subscribed: boolean };
47
81
 
@@ -53,11 +87,11 @@ export function UsersGrid({ data }: { data: User[] }) {
53
87
  if (!grid) return;
54
88
 
55
89
  grid.columns = [
56
- { key: 'id', type: 'number', sort: true, filter: true },
57
- { key: 'name', type: 'string', sort: true, filter: true,
90
+ { key: 'id', type: 'number', width: '80px', sort: true, filter: true },
91
+ { key: 'name', type: 'string', width: '240px', sort: true, filter: true,
58
92
  cellTemplate: ({ value }) => html`<strong>${value}</strong>` },
59
- { key: 'age', type: 'number', sort: true, filter: true },
60
- { key: 'subscribed', type: 'boolean', sort: true, filter: true },
93
+ { key: 'age', type: 'number', width: '100px', sort: true, filter: true },
94
+ { key: 'subscribed', type: 'boolean', width: '140px', sort: true, filter: true },
61
95
  ];
62
96
 
63
97
  const onSorted = (e: Event) => console.log('sorted', (e as CustomEvent).detail);
@@ -65,20 +99,22 @@ export function UsersGrid({ data }: { data: User[] }) {
65
99
  return () => grid.removeEventListener('sorted', onSorted);
66
100
  }, []);
67
101
 
68
- // Set data whenever it changes
69
102
  useEffect(() => {
70
- if (ref.current) ref.current.data = data;
103
+ if (ref.current) ref.current.data = data; // set property whenever data changes
71
104
  }, [data]);
72
105
 
106
+ // The global `apex-grid { height: ... }` CSS handles sizing.
73
107
  // @ts-expect-error — apex-grid is a custom element
74
- return <apex-grid ref={ref} style={{ height: '500px', display: 'block' }} />;
108
+ return <apex-grid ref={ref} />;
75
109
  }
76
110
  ```
77
111
 
78
- For full TypeScript / JSX intrinsic-element support, declare:
112
+ If you'd rather size per instance instead of globally, replace the global CSS rule with a `style` prop: `style={{ height: '500px', display: 'block' }}`.
113
+
114
+ For full TypeScript / JSX intrinsic-element support:
79
115
 
80
116
  ```ts
81
- // apex-grid.d.ts
117
+ // types/apex-grid.d.ts
82
118
  import type { ApexGrid } from 'apex-grid';
83
119
 
84
120
  declare module 'react' {
@@ -90,24 +126,31 @@ declare module 'react' {
90
126
  }
91
127
  ```
92
128
 
129
+ ---
130
+
93
131
  ## Vue 3
94
132
 
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:
133
+ ### Vite plugin config
134
+
135
+ Tell the Vue compiler that `apex-grid` is a custom element so it doesn't try to resolve it as a Vue component:
96
136
 
97
137
  ```ts
98
138
  // vite.config.ts
99
139
  export default defineConfig({
100
140
  plugins: [
101
- vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('apex-') } } }),
141
+ vue({
142
+ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('apex-') } },
143
+ }),
102
144
  ],
103
145
  });
104
146
  ```
105
147
 
148
+ ### Component
149
+
106
150
  ```vue
107
151
  <script setup lang="ts">
108
152
  import { ref, onMounted } from 'vue';
109
- import 'apex-grid/define';
110
- import { html } from 'lit';
153
+ import { html } from 'lit'; // required for cellTemplate
111
154
  import type { ApexGrid, ColumnConfiguration } from 'apex-grid';
112
155
 
113
156
  type User = { id: number; name: string; age: number; subscribed: boolean };
@@ -116,15 +159,15 @@ const grid = ref<ApexGrid<User> | null>(null);
116
159
 
117
160
  const data = ref<User[]>([/* … */]);
118
161
  const columns: ColumnConfiguration<User>[] = [
119
- { key: 'id', type: 'number', sort: true, filter: true },
120
- { key: 'name', type: 'string', sort: true, filter: true,
162
+ { key: 'id', type: 'number', width: '80px', sort: true, filter: true },
163
+ { key: 'name', type: 'string', width: '240px', sort: true, filter: true,
121
164
  cellTemplate: ({ value }) => html`<strong>${value}</strong>` },
122
- { key: 'age', type: 'number', sort: true, filter: true },
123
- { key: 'subscribed', type: 'boolean', sort: true, filter: true },
165
+ { key: 'age', type: 'number', width: '100px', sort: true, filter: true },
166
+ { key: 'subscribed', type: 'boolean', width: '140px', sort: true, filter: true },
124
167
  ];
125
168
 
126
169
  onMounted(() => {
127
- grid.value!.columns = columns;
170
+ grid.value!.columns = columns; // set as property after mount
128
171
  });
129
172
 
130
173
  function onSorted(e: CustomEvent) {
@@ -137,22 +180,21 @@ function onSorted(e: CustomEvent) {
137
180
  ref="grid"
138
181
  :data.prop="data"
139
182
  @sorted="onSorted"
140
- style="height: 500px; display: block;"
141
183
  />
142
184
  </template>
143
185
  ```
144
186
 
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.
187
+ > Use `:data.prop="data"` to bind as a property. Vue's default `:data="data"` sets an attribute, which becomes the literal string `"[object Object]"` and the grid renders nothing.
188
+
189
+ ---
146
190
 
147
191
  ## Angular
148
192
 
149
- Add `CUSTOM_ELEMENTS_SCHEMA` to the module / standalone component, then use property binding `[data]="..."` and event binding `(sorted)="..."`:
193
+ Add `CUSTOM_ELEMENTS_SCHEMA` to the component (or module). Use `[data]="..."` for property binding and `(sorted)="..."` for events:
150
194
 
151
195
  ```ts
152
- // app.component.ts
153
196
  import { Component, CUSTOM_ELEMENTS_SCHEMA, OnInit, ViewChild, ElementRef } from '@angular/core';
154
- import 'apex-grid/define';
155
- import { html } from 'lit';
197
+ import { html } from 'lit'; // required for cellTemplate
156
198
  import type { ApexGrid, ColumnConfiguration } from 'apex-grid';
157
199
 
158
200
  type User = { id: number; name: string; age: number; subscribed: boolean };
@@ -165,9 +207,8 @@ type User = { id: number; name: string; age: number; subscribed: boolean };
165
207
  <apex-grid
166
208
  #grid
167
209
  [data]="data"
168
- (sorted)="onSorted($event)"
169
- style="height: 500px; display: block;"
170
- ></apex-grid>
210
+ (sorted)="onSorted($event)">
211
+ </apex-grid>
171
212
  `,
172
213
  })
173
214
  export class UsersGridComponent implements OnInit {
@@ -177,11 +218,11 @@ export class UsersGridComponent implements OnInit {
177
218
 
178
219
  ngOnInit() {
179
220
  this.grid.nativeElement.columns = [
180
- { key: 'id', type: 'number', sort: true, filter: true },
181
- { key: 'name', type: 'string', sort: true, filter: true,
221
+ { key: 'id', type: 'number', width: '80px', sort: true, filter: true },
222
+ { key: 'name', type: 'string', width: '240px', sort: true, filter: true,
182
223
  cellTemplate: ({ value }) => html`<strong>${value}</strong>` },
183
- { key: 'age', type: 'number', sort: true, filter: true },
184
- { key: 'subscribed', type: 'boolean', sort: true, filter: true },
224
+ { key: 'age', type: 'number', width: '100px', sort: true, filter: true },
225
+ { key: 'subscribed', type: 'boolean', width: '140px', sort: true, filter: true },
185
226
  ];
186
227
  }
187
228
 
@@ -191,13 +232,17 @@ export class UsersGridComponent implements OnInit {
191
232
  }
192
233
  ```
193
234
 
235
+ ---
236
+
194
237
  ## Common pitfalls across frameworks
195
238
 
196
239
  | ❌ | ✅ |
197
240
  |---|---|
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+ |
241
+ | Forgetting to register the element | Top-level `import 'apex-grid/define'` once in the app entry |
242
+ | Importing an Ignite UI theme + `configureTheme(name)` | Obsolete — the grid self-styles; retheme via `--ag-*` CSS variables. No import does anything for appearance |
243
+ | Forgetting `setup()` / no host-height rule for `apex-grid` | `setup()` registers + sizes the host. Or `import 'apex-grid/define'` + `apex-grid { height: ... }` (never set `display` — it overrides the component's `:host { display: grid }`) |
244
+ | Vue: `:data="data"` (attribute, becomes `"[object Object]"`) | `:data.prop="data"`, or set via `ref` in `onMounted` |
245
+ | React: `<apex-grid columns={[...]}>` before React 19 | Use `ref` + `useEffect` to set as property |
200
246
  | 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 |
247
+ | Writing string templates for cells | Always `import { html } from 'lit'` and use `html\`...\`` tagged literals |
248
+ | Treating `cellTemplate` as framework-specific JSX | The template runs inside the web component — only Lit `html` works |
@@ -0,0 +1,154 @@
1
+ # Vanilla JavaScript — Without Lit's `render` in Your App Code
2
+
3
+ `apex-grid` is built with Lit internally, but you don't have to use Lit's `render` / `html` in your application code. You can write a pure-DOM page that imports the custom element, sets properties directly, and listens for events with `addEventListener`.
4
+
5
+ **One honest caveat:** `cellTemplate` and `headerTemplate` are the **only** places that still require Lit's `html` tag — they must return a `TemplateResult` and there is no string-template escape hatch. Returning a plain string renders as escaped text. If you only need the default rendering (no custom cells), you can avoid Lit entirely.
6
+
7
+ ---
8
+
9
+ ## 1. Pure-DOM, default rendering (no Lit at all)
10
+
11
+ Open this in a Vite dev server (`npm create vite@latest`, then `npm run dev`):
12
+
13
+ ```html
14
+ <!DOCTYPE html>
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="UTF-8" />
18
+ <title>Apex Grid — vanilla JS</title>
19
+ <style>
20
+ body { margin: 0; padding: 20px; font-family: system-ui; }
21
+ apex-grid { height: 480px; } /* REQUIRED — bounded host. NEVER set `display` here — it overrides the component's `:host { display: grid }` and collapses the virtualizer. */
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <h1>Users</h1>
26
+ <apex-grid id="grid"></apex-grid>
27
+
28
+ <script type="module">
29
+ // 1. Register the element + adopt host sizing (no theme import needed)
30
+ import { setup } from 'apex-grid';
31
+ setup();
32
+
33
+ // 2. Set properties on the element
34
+ const grid = document.getElementById('grid');
35
+
36
+ grid.columns = [
37
+ { key: 'id', type: 'number', headerText: 'ID', width: '80px', sort: true, filter: true },
38
+ { key: 'name', type: 'string', headerText: 'Name', width: '240px', sort: true, filter: true },
39
+ { key: 'age', type: 'number', headerText: 'Age', width: '100px', sort: true, filter: true },
40
+ { key: 'subscribed', type: 'boolean', headerText: 'Subscribed', width: '140px', sort: true, filter: true },
41
+ ];
42
+
43
+ grid.data = [
44
+ { id: 1, name: 'Ada Lovelace', age: 36, subscribed: true },
45
+ { id: 2, name: 'Carl Sagan', age: 62, subscribed: false },
46
+ { id: 3, name: 'Grace Hopper', age: 85, subscribed: true },
47
+ ];
48
+
49
+ // 3. Listen for events
50
+ grid.addEventListener('sorted', (e) => console.log('sorted', e.detail));
51
+ grid.addEventListener('filtered', (e) => console.log('filtered', e.detail));
52
+ </script>
53
+ </body>
54
+ </html>
55
+ ```
56
+
57
+ This works without any `import { html, render } from 'lit'` in your code. The grid renders, sorts, and filters with the default cell display.
58
+
59
+ ---
60
+
61
+ ## 2. When you need a custom cell — minimal Lit usage
62
+
63
+ The moment you want to style a cell (e.g. render the `subscribed` column as a checkbox), you must import `html` from Lit. There's no way around it.
64
+
65
+ ```js
66
+ // Add this to the imports above:
67
+ import { html } from 'lit';
68
+
69
+ grid.columns = [
70
+ { key: 'id', type: 'number', width: '80px', sort: true, filter: true },
71
+ { key: 'name', type: 'string', width: '240px', sort: true, filter: true,
72
+ cellTemplate: ({ value }) => html`<strong>${value}</strong>`,
73
+ },
74
+ { key: 'age', type: 'number', width: '100px', sort: true, filter: true },
75
+ { key: 'subscribed', type: 'boolean', width: '140px', sort: true, filter: true,
76
+ cellTemplate: ({ value }) => html`<input type="checkbox" .checked=${value} disabled />`,
77
+ },
78
+ ];
79
+ ```
80
+
81
+ You're still not calling Lit's `render(...)` anywhere — you only import `html` to author template values that the custom element internally renders for each cell.
82
+
83
+ ### Why not a string template?
84
+
85
+ ```js
86
+ // ❌ This renders as literal text: <strong>Ada Lovelace</strong>
87
+ cellTemplate: ({ value }) => `<strong>${value}</strong>`,
88
+ ```
89
+
90
+ The cell renderer treats the return value as a Lit template, not as HTML markup. Strings are coerced to text nodes, so any `<tag>` characters are escaped. There is no `dangerouslySetInnerHTML` equivalent.
91
+
92
+ ---
93
+
94
+ ## 3. Auto-generation — the genuinely 3-line demo
95
+
96
+ If you don't care about specific columns and just want to see your data in a grid:
97
+
98
+ ```html
99
+ <apex-grid auto-generate id="grid"></apex-grid>
100
+
101
+ <script type="module">
102
+ import { setup } from 'apex-grid';
103
+ setup();
104
+
105
+ document.getElementById('grid').data = [
106
+ { id: 1, name: 'Ada', age: 36 },
107
+ { id: 2, name: 'Carl', age: 62 },
108
+ ];
109
+ </script>
110
+ ```
111
+
112
+ Auto-generation infers columns from `Object.keys(data[0])` and runtime types. `sort` and `filter` default to `false`, so the auto path is read-only out of the box. To enable them with auto-generated columns, run `updateColumns` after the auto-pass:
113
+
114
+ ```js
115
+ grid.updateColumns([
116
+ { key: 'id', sort: true, filter: true },
117
+ { key: 'name', sort: true, filter: true },
118
+ { key: 'age', sort: true, filter: true },
119
+ ]);
120
+ ```
121
+
122
+ ---
123
+
124
+ ## 4. Programmatic sort & filter without templates
125
+
126
+ ```js
127
+ grid.sort({ key: 'age', direction: 'descending' });
128
+
129
+ grid.filter({ key: 'age', condition: 'greaterThan', searchTerm: 30 });
130
+
131
+ grid.filter({ key: 'subscribed', condition: 'true' }); // boolean operands are unary
132
+
133
+ grid.clearSort();
134
+ grid.clearFilter();
135
+ ```
136
+
137
+ Programmatic calls don't fire `sorting` / `sorted` / `filtering` / `filtered` events. If you need a hook for every state change regardless of source, watch `grid.dataView` after each programmatic call:
138
+
139
+ ```js
140
+ grid.sort({ key: 'age', direction: 'descending' });
141
+ queueMicrotask(() => console.log('post-sort:', grid.dataView));
142
+ ```
143
+
144
+ ---
145
+
146
+ ## 5. Common vanilla-JS gotchas
147
+
148
+ | ❌ | ✅ |
149
+ |---|---|
150
+ | Setting `grid.setAttribute('columns', JSON.stringify(cols))` | `grid.columns = cols` — must be a property, not an attribute |
151
+ | Forgetting `<script type="module">` | All `apex-grid` imports are ESM; you need `type="module"` |
152
+ | Opening the HTML with `file://` | Use a dev server — ESM imports fail CORS over `file://` |
153
+ | `cellTemplate: ({ value }) => '<b>' + value + '</b>'` | `cellTemplate: ({ value }) => html\`<b>${value}</b>\`` — Lit `html` is required |
154
+ | Setting `grid.columns` *before* the script tag's module imports resolve | Set them inside the same `<script type="module">` as the imports |