@alaarab/ogrid-mcp 2.4.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/README.md +68 -0
- package/bundled-docs/api/README.md +94 -0
- package/bundled-docs/api/column-def.mdx +379 -0
- package/bundled-docs/api/components-column-chooser.mdx +310 -0
- package/bundled-docs/api/components-column-header-filter.mdx +363 -0
- package/bundled-docs/api/components-datagrid-table.mdx +316 -0
- package/bundled-docs/api/components-pagination-controls.mdx +344 -0
- package/bundled-docs/api/components-sidebar.mdx +427 -0
- package/bundled-docs/api/components-status-bar.mdx +309 -0
- package/bundled-docs/api/grid-api.mdx +299 -0
- package/bundled-docs/api/js-api.mdx +198 -0
- package/bundled-docs/api/ogrid-props.mdx +244 -0
- package/bundled-docs/api/types.mdx +640 -0
- package/bundled-docs/features/cell-references.mdx +225 -0
- package/bundled-docs/features/column-chooser.mdx +279 -0
- package/bundled-docs/features/column-groups.mdx +290 -0
- package/bundled-docs/features/column-pinning.mdx +282 -0
- package/bundled-docs/features/column-reordering.mdx +359 -0
- package/bundled-docs/features/column-types.mdx +181 -0
- package/bundled-docs/features/context-menu.mdx +216 -0
- package/bundled-docs/features/csv-export.mdx +227 -0
- package/bundled-docs/features/editing.mdx +377 -0
- package/bundled-docs/features/filtering.mdx +330 -0
- package/bundled-docs/features/formulas.mdx +381 -0
- package/bundled-docs/features/grid-api.mdx +311 -0
- package/bundled-docs/features/keyboard-navigation.mdx +236 -0
- package/bundled-docs/features/pagination.mdx +245 -0
- package/bundled-docs/features/performance.mdx +433 -0
- package/bundled-docs/features/row-selection.mdx +256 -0
- package/bundled-docs/features/server-side-data.mdx +291 -0
- package/bundled-docs/features/sidebar.mdx +234 -0
- package/bundled-docs/features/sorting.mdx +241 -0
- package/bundled-docs/features/spreadsheet-selection.mdx +201 -0
- package/bundled-docs/features/status-bar.mdx +205 -0
- package/bundled-docs/features/toolbar.mdx +284 -0
- package/bundled-docs/features/virtual-scrolling.mdx +624 -0
- package/bundled-docs/getting-started/installation.mdx +216 -0
- package/bundled-docs/getting-started/overview.mdx +151 -0
- package/bundled-docs/getting-started/quick-start.mdx +425 -0
- package/bundled-docs/getting-started/vanilla-js.mdx +191 -0
- package/bundled-docs/guides/accessibility.mdx +550 -0
- package/bundled-docs/guides/controlled-vs-uncontrolled.mdx +153 -0
- package/bundled-docs/guides/custom-cell-editors.mdx +201 -0
- package/bundled-docs/guides/framework-showcase.mdx +200 -0
- package/bundled-docs/guides/mcp-live-testing.mdx +291 -0
- package/bundled-docs/guides/mcp.mdx +172 -0
- package/bundled-docs/guides/migration-from-ag-grid.mdx +223 -0
- package/bundled-docs/guides/theming.mdx +211 -0
- package/dist/esm/bridge-client.d.ts +87 -0
- package/dist/esm/bridge-client.js +162 -0
- package/dist/esm/index.js +1060 -0
- package/package.json +43 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_position: 4
|
|
3
|
+
title: Editing & Clipboard
|
|
4
|
+
description: Inline editing, clipboard operations, fill handle, and undo/redo
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Editing & Clipboard
|
|
9
|
+
|
|
10
|
+
OGrid supports inline cell editing, clipboard copy/paste, drag-to-fill, and undo/redo -- all working together through `onCellValueChanged` and `valueParser`.
|
|
11
|
+
|
|
12
|
+
## Live Demo
|
|
13
|
+
|
|
14
|
+
<CellEditingDemo />
|
|
15
|
+
|
|
16
|
+
:::tip Try it in your framework
|
|
17
|
+
The demo above uses Radix UI for styling. To see this feature with your framework's design system (Fluent UI, Material UI, Vuetify, PrimeNG, etc.), click **"Open in online demo"** below the demo.
|
|
18
|
+
:::
|
|
19
|
+
|
|
20
|
+
## Quick Example
|
|
21
|
+
|
|
22
|
+
<Tabs groupId="framework">
|
|
23
|
+
<TabItem value="react" label="React" default>
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
|
|
27
|
+
const DEPTS = ['Engineering', 'Marketing', 'Sales', 'Finance', 'Operations'];
|
|
28
|
+
const STATUSES = ['Active', 'Draft', 'Archived'];
|
|
29
|
+
|
|
30
|
+
const columns = [
|
|
31
|
+
{ columnId: 'name', name: 'Name', editable: true },
|
|
32
|
+
{
|
|
33
|
+
columnId: 'department',
|
|
34
|
+
name: 'Department',
|
|
35
|
+
editable: true,
|
|
36
|
+
cellEditor: 'richSelect',
|
|
37
|
+
cellEditorParams: { values: DEPTS },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
columnId: 'status',
|
|
41
|
+
name: 'Status',
|
|
42
|
+
editable: true,
|
|
43
|
+
cellEditor: 'select',
|
|
44
|
+
cellEditorParams: { values: STATUSES },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
columnId: 'salary',
|
|
48
|
+
name: 'Salary',
|
|
49
|
+
editable: true,
|
|
50
|
+
type: 'numeric',
|
|
51
|
+
valueParser: ({ newValue }) => {
|
|
52
|
+
const num = Number(newValue);
|
|
53
|
+
return isNaN(num) || num < 0 ? undefined : num; // reject invalid
|
|
54
|
+
},
|
|
55
|
+
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function App() {
|
|
60
|
+
const [data, setData] = useState(initialData);
|
|
61
|
+
const { handleCellValueChanged, undo, redo, canUndo, canRedo } = useUndoRedo({
|
|
62
|
+
data, setData, getRowId: (item) => item.id,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<OGrid
|
|
67
|
+
columns={columns}
|
|
68
|
+
data={data}
|
|
69
|
+
getRowId={(item) => item.id}
|
|
70
|
+
editable
|
|
71
|
+
onCellValueChanged={handleCellValueChanged}
|
|
72
|
+
onUndo={undo}
|
|
73
|
+
onRedo={redo}
|
|
74
|
+
canUndo={canUndo}
|
|
75
|
+
canRedo={canRedo}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
:::tip Switching UI libraries
|
|
82
|
+
The `OGrid` component has the same props across all React UI packages. To switch, just change the import:
|
|
83
|
+
|
|
84
|
+
- **Radix** (lightweight, default): `from '@alaarab/ogrid-react-radix'`
|
|
85
|
+
- **Fluent UI** (Microsoft 365 / SPFx): `from '@alaarab/ogrid-react-fluent'` — wrap in `<FluentProvider>`
|
|
86
|
+
- **Material UI** (MUI v7): `from '@alaarab/ogrid-react-material'` — wrap in `<ThemeProvider>`
|
|
87
|
+
:::
|
|
88
|
+
|
|
89
|
+
</TabItem>
|
|
90
|
+
<TabItem value="angular" label="Angular">
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
|
|
94
|
+
const DEPTS = ['Engineering', 'Marketing', 'Sales', 'Finance', 'Operations'];
|
|
95
|
+
const STATUSES = ['Active', 'Draft', 'Archived'];
|
|
96
|
+
|
|
97
|
+
@Component({
|
|
98
|
+
standalone: true,
|
|
99
|
+
imports: [OGridComponent],
|
|
100
|
+
template: `<ogrid [props]="gridProps" />`
|
|
101
|
+
})
|
|
102
|
+
export class GridComponent {
|
|
103
|
+
gridProps = {
|
|
104
|
+
columns: [
|
|
105
|
+
{ columnId: 'name', name: 'Name', editable: true },
|
|
106
|
+
{
|
|
107
|
+
columnId: 'department', name: 'Department', editable: true,
|
|
108
|
+
cellEditor: 'richSelect', cellEditorParams: { values: DEPTS },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
columnId: 'status', name: 'Status', editable: true,
|
|
112
|
+
cellEditor: 'select', cellEditorParams: { values: STATUSES },
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
columnId: 'salary', name: 'Salary', editable: true, type: 'numeric',
|
|
116
|
+
valueParser: ({ newValue }: any) => {
|
|
117
|
+
const num = Number(newValue);
|
|
118
|
+
return isNaN(num) || num < 0 ? undefined : num;
|
|
119
|
+
},
|
|
120
|
+
valueFormatter: (v: unknown) => `$${Number(v).toLocaleString()}`,
|
|
121
|
+
},
|
|
122
|
+
] as IColumnDef<Person>[],
|
|
123
|
+
data: initialData,
|
|
124
|
+
getRowId: (item: Person) => item.id,
|
|
125
|
+
editable: true,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
:::tip Switching UI libraries
|
|
131
|
+
Same component API across Angular packages. To switch, just change the import:
|
|
132
|
+
|
|
133
|
+
- **Radix (CDK)**: `from '@alaarab/ogrid-angular-radix'` *(default, lightweight)*
|
|
134
|
+
- **Angular Material**: `from '@alaarab/ogrid-angular-material'`
|
|
135
|
+
- **PrimeNG**: `from '@alaarab/ogrid-angular-primeng'`
|
|
136
|
+
|
|
137
|
+
All components are standalone — no NgModule required.
|
|
138
|
+
:::
|
|
139
|
+
|
|
140
|
+
</TabItem>
|
|
141
|
+
<TabItem value="vue" label="Vue">
|
|
142
|
+
|
|
143
|
+
```vue
|
|
144
|
+
<script setup lang="ts">
|
|
145
|
+
|
|
146
|
+
const DEPTS = ['Engineering', 'Marketing', 'Sales', 'Finance', 'Operations'];
|
|
147
|
+
const STATUSES = ['Active', 'Draft', 'Archived'];
|
|
148
|
+
|
|
149
|
+
const columns: IColumnDef<Person>[] = [
|
|
150
|
+
{ columnId: 'name', name: 'Name', editable: true },
|
|
151
|
+
{
|
|
152
|
+
columnId: 'department', name: 'Department', editable: true,
|
|
153
|
+
cellEditor: 'richSelect', cellEditorParams: { values: DEPTS },
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
columnId: 'status', name: 'Status', editable: true,
|
|
157
|
+
cellEditor: 'select', cellEditorParams: { values: STATUSES },
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
columnId: 'salary', name: 'Salary', editable: true, type: 'numeric',
|
|
161
|
+
valueParser: ({ newValue }) => {
|
|
162
|
+
const num = Number(newValue);
|
|
163
|
+
return isNaN(num) || num < 0 ? undefined : num;
|
|
164
|
+
},
|
|
165
|
+
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const gridProps = {
|
|
170
|
+
columns,
|
|
171
|
+
data: initialData,
|
|
172
|
+
getRowId: (item) => item.id,
|
|
173
|
+
editable: true,
|
|
174
|
+
};
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<template>
|
|
178
|
+
<OGrid :gridProps="gridProps" />
|
|
179
|
+
</template>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
:::tip Switching UI libraries
|
|
183
|
+
Same component API across Vue packages. To switch, just change the import:
|
|
184
|
+
|
|
185
|
+
- **Radix (Headless UI)**: `from '@alaarab/ogrid-vue-radix'` *(default, lightweight)*
|
|
186
|
+
- **Vuetify**: `from '@alaarab/ogrid-vue-vuetify'` — wrap in `<v-app>` for theming
|
|
187
|
+
- **PrimeVue**: `from '@alaarab/ogrid-vue-primevue'`
|
|
188
|
+
:::
|
|
189
|
+
|
|
190
|
+
</TabItem>
|
|
191
|
+
<TabItem value="js" label="Vanilla JS">
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
|
|
195
|
+
const DEPTS = ['Engineering', 'Marketing', 'Sales', 'Finance', 'Operations'];
|
|
196
|
+
const STATUSES = ['Active', 'Draft', 'Archived'];
|
|
197
|
+
|
|
198
|
+
const grid = new OGrid(document.getElementById('grid'), {
|
|
199
|
+
columns: [
|
|
200
|
+
{ columnId: 'name', name: 'Name', editable: true },
|
|
201
|
+
{
|
|
202
|
+
columnId: 'department',
|
|
203
|
+
name: 'Department',
|
|
204
|
+
editable: true,
|
|
205
|
+
cellEditor: 'richSelect',
|
|
206
|
+
cellEditorParams: { values: DEPTS },
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
columnId: 'status',
|
|
210
|
+
name: 'Status',
|
|
211
|
+
editable: true,
|
|
212
|
+
cellEditor: 'select',
|
|
213
|
+
cellEditorParams: { values: STATUSES },
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
columnId: 'salary',
|
|
217
|
+
name: 'Salary',
|
|
218
|
+
editable: true,
|
|
219
|
+
type: 'numeric',
|
|
220
|
+
valueParser: ({ newValue }) => {
|
|
221
|
+
const num = Number(newValue);
|
|
222
|
+
return isNaN(num) || num < 0 ? undefined : num;
|
|
223
|
+
},
|
|
224
|
+
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
data: initialData,
|
|
228
|
+
getRowId: (item) => item.id,
|
|
229
|
+
editable: true,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Listen for cell edits
|
|
233
|
+
grid.on('cellValueChanged', (event) => {
|
|
234
|
+
console.log(`${event.columnId} changed to ${event.newValue}`);
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
</TabItem>
|
|
239
|
+
</Tabs>
|
|
240
|
+
|
|
241
|
+
This single setup gives you inline editing, clipboard paste, fill handle, and full undo/redo.
|
|
242
|
+
|
|
243
|
+
## Inline Editing
|
|
244
|
+
|
|
245
|
+
Double-click or press **F2** to edit. **Enter** commits and moves down, **Tab** commits and moves right, **Escape** cancels.
|
|
246
|
+
|
|
247
|
+
### Built-in Editors
|
|
248
|
+
|
|
249
|
+
| `cellEditor` | Description |
|
|
250
|
+
|--------------|-------------|
|
|
251
|
+
| `'text'` (default) | Standard text input |
|
|
252
|
+
| `'select'` | Dropdown from `cellEditorParams.values` |
|
|
253
|
+
| `'checkbox'` | Toggle boolean |
|
|
254
|
+
| `'richSelect'` | Searchable dropdown with keyboard nav |
|
|
255
|
+
|
|
256
|
+
### Rich Select
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
{
|
|
260
|
+
columnId: 'category',
|
|
261
|
+
editable: true,
|
|
262
|
+
cellEditor: 'richSelect',
|
|
263
|
+
cellEditorParams: {
|
|
264
|
+
values: ['Engineering', 'Design', 'Marketing', 'Sales'],
|
|
265
|
+
formatValue: (v) => `Dept: ${v}`,
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Per-Row Editability
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
{ columnId: 'name', editable: (item) => item.status !== 'locked' }
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Custom Editor
|
|
277
|
+
|
|
278
|
+
Pass a component to `cellEditor`. Use `cellEditorPopup: true` for a popover:
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
function DateEditor({ value, onValueChange, onCommit, onCancel }: ICellEditorProps<Task>) {
|
|
282
|
+
return (
|
|
283
|
+
<input
|
|
284
|
+
type="date"
|
|
285
|
+
value={value as string}
|
|
286
|
+
onChange={(e) => onValueChange(e.target.value)}
|
|
287
|
+
onKeyDown={(e) => {
|
|
288
|
+
if (e.key === 'Enter') onCommit();
|
|
289
|
+
if (e.key === 'Escape') onCancel();
|
|
290
|
+
}}
|
|
291
|
+
autoFocus
|
|
292
|
+
/>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// In column def:
|
|
297
|
+
{ columnId: 'dueDate', editable: true, cellEditor: DateEditor, cellEditorPopup: true }
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Value Parsing
|
|
301
|
+
|
|
302
|
+
`valueParser` validates input for editing, paste, fill, and delete. Return `undefined` to reject:
|
|
303
|
+
|
|
304
|
+
```tsx
|
|
305
|
+
{ columnId: 'age', editable: true, type: 'numeric',
|
|
306
|
+
valueParser: ({ newValue }) => {
|
|
307
|
+
const num = Number(newValue);
|
|
308
|
+
return isNaN(num) || num < 0 || num > 150 ? undefined : num;
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Clipboard
|
|
314
|
+
|
|
315
|
+
<ClipboardDemo />
|
|
316
|
+
|
|
317
|
+
Standard keyboard shortcuts on selected cells:
|
|
318
|
+
|
|
319
|
+
| Key | Action | Requires `editable` |
|
|
320
|
+
|-----|--------|-------------------|
|
|
321
|
+
| Ctrl+C (Cmd+C) | Copy as tab-delimited text | No |
|
|
322
|
+
| Ctrl+V (Cmd+V) | Paste into cells | Yes |
|
|
323
|
+
| Ctrl+X (Cmd+X) | Cut (copy + clear) | Yes |
|
|
324
|
+
| Delete / Backspace | Clear selected cells | Yes |
|
|
325
|
+
|
|
326
|
+
- **Copy** uses `valueFormatter` so copied values match the display.
|
|
327
|
+
- **Paste** calls `valueParser` on each value and fires `onCellValueChanged` per cell.
|
|
328
|
+
|
|
329
|
+
## Fill Handle
|
|
330
|
+
|
|
331
|
+
<FillHandleDemo />
|
|
332
|
+
|
|
333
|
+
A small square at the bottom-right of the selection. Drag it down to fill cells with the source value (like Excel).
|
|
334
|
+
|
|
335
|
+
- Requires `editable={true}` and `cellSelection={true}` (default).
|
|
336
|
+
- Calls `valueParser` per cell -- return `undefined` to skip.
|
|
337
|
+
- Each filled cell fires `onCellValueChanged`.
|
|
338
|
+
- Uses `requestAnimationFrame` for smooth drag performance.
|
|
339
|
+
|
|
340
|
+
## Undo / Redo
|
|
341
|
+
|
|
342
|
+
The `useUndoRedo` hook tracks edit history (including paste, fill, and delete). See the quick example above.
|
|
343
|
+
|
|
344
|
+
| Hook Input | Type | Description |
|
|
345
|
+
|------------|------|-------------|
|
|
346
|
+
| `data` | `T[]` | Current data array |
|
|
347
|
+
| `setData` | `(data: T[]) => void` | State setter |
|
|
348
|
+
| `getRowId` | `(item: T) => RowId` | Row identifier |
|
|
349
|
+
|
|
350
|
+
| Hook Output | Type | Description |
|
|
351
|
+
|-------------|------|-------------|
|
|
352
|
+
| `handleCellValueChanged` | `(event) => void` | Wraps edits onto the undo stack |
|
|
353
|
+
| `undo` / `redo` | `() => void` | Revert / re-apply |
|
|
354
|
+
| `canUndo` / `canRedo` | `boolean` | Stack status |
|
|
355
|
+
|
|
356
|
+
**Shortcuts:** Ctrl+Z (undo), Ctrl+Y (redo). Also available in the right-click [context menu](./context-menu).
|
|
357
|
+
|
|
358
|
+
## Props Summary
|
|
359
|
+
|
|
360
|
+
| Prop | Type | Description |
|
|
361
|
+
|------|------|-------------|
|
|
362
|
+
| `editable` | `boolean` | Enable editing grid-wide (on `OGrid`) |
|
|
363
|
+
| `editable` | `boolean \| (item: T) => boolean` | Per-column/row (on `IColumnDef`) |
|
|
364
|
+
| `cellEditor` | `'text' \| 'select' \| 'checkbox' \| 'richSelect' \| ComponentType` | Editor type |
|
|
365
|
+
| `cellEditorPopup` | `boolean` | Render custom editor in a popover |
|
|
366
|
+
| `cellEditorParams` | `CellEditorParams` | Editor parameters (e.g., `{ values: [...] }`) |
|
|
367
|
+
| `valueParser` | `(params) => unknown` | Validate on edit/paste/fill/delete. Return `undefined` to reject. |
|
|
368
|
+
| `valueFormatter` | `(value, item) => string` | Format display & copy output |
|
|
369
|
+
| `onCellValueChanged` | `(event) => void` | Fired after any cell value change |
|
|
370
|
+
| `onUndo` / `onRedo` | `() => void` | Undo/redo callbacks |
|
|
371
|
+
| `canUndo` / `canRedo` | `boolean` | Enable/disable undo/redo in context menu |
|
|
372
|
+
|
|
373
|
+
## Related
|
|
374
|
+
|
|
375
|
+
- [Spreadsheet Selection](./spreadsheet-selection) -- select cells for editing
|
|
376
|
+
- [Context Menu](./context-menu) -- right-click menu with undo/redo
|
|
377
|
+
- [Custom Cell Editors](../guides/custom-cell-editors) -- advanced editor guide
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_position: 2
|
|
3
|
+
title: Filtering
|
|
4
|
+
description: Column-level filtering with text, multi-select, and people filter types.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Filtering
|
|
9
|
+
|
|
10
|
+
OGrid supports four built-in filter types -- **text**, **multiSelect**, **people**, and **date** -- configured per column. Filters appear as popovers triggered from column headers.
|
|
11
|
+
|
|
12
|
+
## Live Demo
|
|
13
|
+
|
|
14
|
+
<FilteringDemo />
|
|
15
|
+
|
|
16
|
+
:::tip Framework-Specific Styling
|
|
17
|
+
The live demo above shows **React Radix UI** styling (lightweight default). To see how filter popovers look in your framework's design system, click the framework buttons above the demo to open online demo. Each framework renders filter UI with its native components (dropdowns, inputs, date pickers, etc.), so the styling matches your design system.
|
|
18
|
+
:::
|
|
19
|
+
|
|
20
|
+
## Quick Example
|
|
21
|
+
|
|
22
|
+
<Tabs groupId="framework">
|
|
23
|
+
<TabItem value="react" label="React" default>
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
|
|
27
|
+
const columns = [
|
|
28
|
+
{
|
|
29
|
+
columnId: 'name',
|
|
30
|
+
name: 'Name',
|
|
31
|
+
filterable: { type: 'text' },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
columnId: 'department',
|
|
35
|
+
name: 'Department',
|
|
36
|
+
filterable: { type: 'multiSelect', filterField: 'department' },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
columnId: 'status',
|
|
40
|
+
name: 'Status',
|
|
41
|
+
filterable: { type: 'multiSelect', filterField: 'status' },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
columnId: 'salary',
|
|
45
|
+
name: 'Salary',
|
|
46
|
+
type: 'numeric' as const,
|
|
47
|
+
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function App() {
|
|
52
|
+
return (
|
|
53
|
+
<OGrid
|
|
54
|
+
columns={columns}
|
|
55
|
+
data={people}
|
|
56
|
+
getRowId={(item) => item.id}
|
|
57
|
+
defaultPageSize={10}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
:::tip Switching UI libraries
|
|
64
|
+
The `OGrid` component has the same props across all React UI packages. To switch, just change the import:
|
|
65
|
+
|
|
66
|
+
- **Radix** (lightweight, default): `from '@alaarab/ogrid-react-radix'`
|
|
67
|
+
- **Fluent UI** (Microsoft 365 / SPFx): `from '@alaarab/ogrid-react-fluent'` — wrap in `<FluentProvider>`
|
|
68
|
+
- **Material UI** (MUI v7): `from '@alaarab/ogrid-react-material'` — wrap in `<ThemeProvider>`
|
|
69
|
+
:::
|
|
70
|
+
|
|
71
|
+
</TabItem>
|
|
72
|
+
<TabItem value="angular" label="Angular">
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
|
|
76
|
+
@Component({
|
|
77
|
+
standalone: true,
|
|
78
|
+
imports: [OGridComponent],
|
|
79
|
+
template: `<ogrid [props]="gridProps" />`
|
|
80
|
+
})
|
|
81
|
+
export class GridComponent {
|
|
82
|
+
gridProps = {
|
|
83
|
+
columns: [
|
|
84
|
+
{ columnId: 'name', name: 'Name', filterable: { type: 'text' } },
|
|
85
|
+
{
|
|
86
|
+
columnId: 'department', name: 'Department',
|
|
87
|
+
filterable: { type: 'multiSelect', filterField: 'department' },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
columnId: 'status', name: 'Status',
|
|
91
|
+
filterable: { type: 'multiSelect', filterField: 'status' },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
columnId: 'salary', name: 'Salary', type: 'numeric',
|
|
95
|
+
valueFormatter: (v: unknown) => `$${Number(v).toLocaleString()}`,
|
|
96
|
+
},
|
|
97
|
+
] as IColumnDef<Person>[],
|
|
98
|
+
data: people,
|
|
99
|
+
getRowId: (item: Person) => item.id,
|
|
100
|
+
defaultPageSize: 10,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
:::tip Switching UI libraries
|
|
106
|
+
Same component API across Angular packages. To switch, just change the import:
|
|
107
|
+
|
|
108
|
+
- **Radix (CDK)**: `from '@alaarab/ogrid-angular-radix'` *(default, lightweight)*
|
|
109
|
+
- **Angular Material**: `from '@alaarab/ogrid-angular-material'`
|
|
110
|
+
- **PrimeNG**: `from '@alaarab/ogrid-angular-primeng'`
|
|
111
|
+
|
|
112
|
+
All components are standalone — no NgModule required.
|
|
113
|
+
:::
|
|
114
|
+
|
|
115
|
+
</TabItem>
|
|
116
|
+
<TabItem value="vue" label="Vue">
|
|
117
|
+
|
|
118
|
+
```vue
|
|
119
|
+
<script setup lang="ts">
|
|
120
|
+
|
|
121
|
+
const columns: IColumnDef<Person>[] = [
|
|
122
|
+
{ columnId: 'name', name: 'Name', filterable: { type: 'text' } },
|
|
123
|
+
{
|
|
124
|
+
columnId: 'department', name: 'Department',
|
|
125
|
+
filterable: { type: 'multiSelect', filterField: 'department' },
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
columnId: 'status', name: 'Status',
|
|
129
|
+
filterable: { type: 'multiSelect', filterField: 'status' },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
columnId: 'salary', name: 'Salary', type: 'numeric',
|
|
133
|
+
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const gridProps = {
|
|
138
|
+
columns,
|
|
139
|
+
data: people,
|
|
140
|
+
getRowId: (item) => item.id,
|
|
141
|
+
defaultPageSize: 10,
|
|
142
|
+
};
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<template>
|
|
146
|
+
<OGrid :gridProps="gridProps" />
|
|
147
|
+
</template>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
:::tip Switching UI libraries
|
|
151
|
+
Same component API across Vue packages. To switch, just change the import:
|
|
152
|
+
|
|
153
|
+
- **Radix (Headless UI)**: `from '@alaarab/ogrid-vue-radix'` *(default, lightweight)*
|
|
154
|
+
- **Vuetify**: `from '@alaarab/ogrid-vue-vuetify'` — wrap in `<v-app>` for theming
|
|
155
|
+
- **PrimeVue**: `from '@alaarab/ogrid-vue-primevue'`
|
|
156
|
+
:::
|
|
157
|
+
|
|
158
|
+
</TabItem>
|
|
159
|
+
<TabItem value="js" label="Vanilla JS">
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
|
|
163
|
+
const grid = new OGrid(document.getElementById('grid'), {
|
|
164
|
+
columns: [
|
|
165
|
+
{
|
|
166
|
+
columnId: 'name',
|
|
167
|
+
name: 'Name',
|
|
168
|
+
filterable: { type: 'text' },
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
columnId: 'department',
|
|
172
|
+
name: 'Department',
|
|
173
|
+
filterable: { type: 'multiSelect', filterField: 'department' },
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
columnId: 'status',
|
|
177
|
+
name: 'Status',
|
|
178
|
+
filterable: { type: 'multiSelect', filterField: 'status' },
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
columnId: 'salary',
|
|
182
|
+
name: 'Salary',
|
|
183
|
+
type: 'numeric',
|
|
184
|
+
valueFormatter: (v) => `$${Number(v).toLocaleString()}`,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
data: people,
|
|
188
|
+
getRowId: (item) => item.id,
|
|
189
|
+
pageSize: 10,
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
</TabItem>
|
|
194
|
+
</Tabs>
|
|
195
|
+
|
|
196
|
+
## How It Works
|
|
197
|
+
|
|
198
|
+
Set the `filterable` property on a column definition to an `IColumnFilterDef` object. The `type` field determines the filter UI:
|
|
199
|
+
|
|
200
|
+
### Text Filter
|
|
201
|
+
|
|
202
|
+
A simple text input. Filters rows where the column value contains the search string (case-insensitive).
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
{ columnId: 'name', name: 'Name', filterable: { type: 'text' } }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Multi-Select Filter
|
|
209
|
+
|
|
210
|
+
A checkbox list with **Select All** and **Clear All** buttons. Users pick one or more values to include.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
{
|
|
214
|
+
columnId: 'status',
|
|
215
|
+
name: 'Status',
|
|
216
|
+
filterable: {
|
|
217
|
+
type: 'multiSelect',
|
|
218
|
+
filterField: 'status', // field to filter on (defaults to columnId)
|
|
219
|
+
optionsSource: 'static', // 'static' | 'api' | 'years'
|
|
220
|
+
options: ['Active', 'Draft', 'Archived'],
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Options sources:**
|
|
226
|
+
|
|
227
|
+
| `optionsSource` | Behavior |
|
|
228
|
+
|-----------------|----------|
|
|
229
|
+
| `'static'` | Uses the `options` array you provide. |
|
|
230
|
+
| `'api'` | Calls `dataSource.fetchFilterOptions(field)` to load options dynamically. |
|
|
231
|
+
| `'years'` | Auto-generates a year list (current year down, configurable via `yearsCount`). |
|
|
232
|
+
| _(omitted)_ | Client-side: unique values extracted from the data array. Server-side: uses `'api'`. |
|
|
233
|
+
|
|
234
|
+
### People Filter
|
|
235
|
+
|
|
236
|
+
A search-as-you-type input that finds users via `dataSource.searchPeople(query)`. Displays `UserLike` objects with name, email, and photo.
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
{
|
|
240
|
+
columnId: 'assignee',
|
|
241
|
+
name: 'Assignee',
|
|
242
|
+
filterable: { type: 'people' },
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
:::info
|
|
247
|
+
The people filter requires a `dataSource` with a `searchPeople` method. It is designed for directory lookups (e.g., Microsoft Graph, LDAP).
|
|
248
|
+
:::
|
|
249
|
+
|
|
250
|
+
### Date Filter
|
|
251
|
+
|
|
252
|
+
A date range picker with **From** and **To** date inputs. Filters rows where the column value falls within the specified range.
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
{
|
|
256
|
+
columnId: 'createdDate',
|
|
257
|
+
name: 'Created',
|
|
258
|
+
type: 'date',
|
|
259
|
+
filterable: { type: 'date' },
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
:::tip
|
|
264
|
+
Columns with `type: 'date'` automatically get date formatting, chronological sorting, and a native date input editor — no additional configuration needed.
|
|
265
|
+
:::
|
|
266
|
+
|
|
267
|
+
### Controlled vs. Uncontrolled
|
|
268
|
+
|
|
269
|
+
- **Uncontrolled** -- the grid manages filter state internally. Filters reset when the component unmounts.
|
|
270
|
+
- **Controlled** -- pass `filters` and `onFiltersChange` to own the filter state externally (e.g., for URL sync or persistence).
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
function App() {
|
|
274
|
+
const [filters, setFilters] = useState<IFilters>({
|
|
275
|
+
status: { type: 'multiSelect', value: ['Active'] },
|
|
276
|
+
name: { type: 'text', value: 'John' },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<OGrid
|
|
281
|
+
columns={columns}
|
|
282
|
+
data={tasks}
|
|
283
|
+
getRowId={(item) => item.id}
|
|
284
|
+
filters={filters}
|
|
285
|
+
onFiltersChange={setFilters}
|
|
286
|
+
/>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Server-Side Filtering
|
|
292
|
+
|
|
293
|
+
When using a `dataSource`, filter values are passed to `fetchPage()` in the `filters` param. The grid does not filter client-side.
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
const dataSource: IDataSource<Task> = {
|
|
297
|
+
fetchPage: async ({ filters, page, pageSize, sort }) => {
|
|
298
|
+
const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
|
|
299
|
+
if (filters.status?.type === 'multiSelect') params.set('status', filters.status.value.join(','));
|
|
300
|
+
const res = await fetch(`/api/tasks?${params}`);
|
|
301
|
+
return res.json();
|
|
302
|
+
},
|
|
303
|
+
fetchFilterOptions: async (field) => {
|
|
304
|
+
const res = await fetch(`/api/tasks/filter-options/${field}`);
|
|
305
|
+
return res.json();
|
|
306
|
+
},
|
|
307
|
+
searchPeople: async (query) => {
|
|
308
|
+
const res = await fetch(`/api/users/search?q=${query}`);
|
|
309
|
+
return res.json();
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Props
|
|
315
|
+
|
|
316
|
+
| Prop | Type | Default | Description |
|
|
317
|
+
|------|------|---------|-------------|
|
|
318
|
+
| `filterable` | `IColumnFilterDef` | -- | Set on `IColumnDef`. Defines filter type and options. |
|
|
319
|
+
| `filterable.type` | `'text' \| 'multiSelect' \| 'people' \| 'date'` | -- | Filter UI type. |
|
|
320
|
+
| `filterable.filterField` | `string` | `columnId` | Field name used in the filter model. |
|
|
321
|
+
| `filterable.optionsSource` | `'api' \| 'static' \| 'years'` | auto | Where multi-select options come from. |
|
|
322
|
+
| `filterable.options` | `string[]` | -- | Static options for multi-select. |
|
|
323
|
+
| `filterable.yearsCount` | `number` | -- | Number of years to generate for `'years'` source. |
|
|
324
|
+
| `filters` | `IFilters` | -- | Controlled filter state on `OGrid`. |
|
|
325
|
+
| `onFiltersChange` | `(filters: IFilters) => void` | -- | Called when filters change (controlled mode). |
|
|
326
|
+
|
|
327
|
+
## Related
|
|
328
|
+
|
|
329
|
+
- [Sorting](./sorting) -- sort filtered results
|
|
330
|
+
- [Pagination](./pagination) -- paginate filtered results
|