react-gridix 0.1.0 → 0.2.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.html +1742 -0
- package/README.md +2080 -0
- package/dist/index.d.ts +287 -81
- package/dist/react-gridix.cjs +2 -1
- package/dist/react-gridix.js +1238 -499
- package/dist/style.css +1 -1
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,2080 @@
|
|
|
1
|
+
# react-gridix Docs
|
|
2
|
+
|
|
3
|
+
A React `DataGrid` with row selection, server-side mode, typed per-column filters, date columns, in-cell editing, resizing and pinning, virtualization, CSV export and 8 themes, plus the `Checkbox` it uses.
|
|
4
|
+
|
|
5
|
+
**Contents**
|
|
6
|
+
|
|
7
|
+
1. [Overview](#1-overview)
|
|
8
|
+
2. [Installation](#2-installation)
|
|
9
|
+
3. [Quick start](#3-quick-start)
|
|
10
|
+
4. [Theming](#4-theming)
|
|
11
|
+
5. [Checkbox](#5-checkbox)
|
|
12
|
+
6. [DataGrid](#6-datagrid)
|
|
13
|
+
7. [Demos](#7-demos)
|
|
14
|
+
8. [Troubleshooting](#9-troubleshooting)
|
|
15
|
+
9. [Migrating from the earlier version](#10-migrating-from-the-earlier-version)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Overview
|
|
20
|
+
|
|
21
|
+
| Component | Purpose |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `DataGrid` | Data table with per-column sorting, search, typed per-column filters, pagination, in-cell editing, date columns, row selection, server-side mode, loading skeletons, resize / pin / hide, keyboard navigation, CSV export, virtualization, nested fields, striping, themes and custom column components |
|
|
24
|
+
| `Checkbox` | Checkbox in three looks (button with a span, tick, native), toggles with one click. The grid uses it for boolean columns and row selection, and you can use it on its own |
|
|
25
|
+
|
|
26
|
+
**Everything extra is opt-in.** A bare `<DataGrid columns={...} rows={...} />` is a plain read-only table. Sorting, the search box, page info, selection, resizing and the rest only appear when you switch them on:
|
|
27
|
+
|
|
28
|
+
| Feature | How you switch it on | Default |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| Sorting | `sortable: true` **on a column** | off |
|
|
31
|
+
| Multi-column sort | `multiSort` on the grid | off |
|
|
32
|
+
| Global search box | `searchable` | off |
|
|
33
|
+
| Pagination bar (row count, page controls) | `pagination` | off |
|
|
34
|
+
| Column filter | `filterable: true` on a column | off |
|
|
35
|
+
| Editing | `editable` | off |
|
|
36
|
+
| Row selection | `rowSelection` | off |
|
|
37
|
+
| Server-side mode | `serverSide` | off |
|
|
38
|
+
| Resize / pin | `resizable: true` / `pinned` on a column | off |
|
|
39
|
+
| Show / hide columns button | `columnMenu` | off |
|
|
40
|
+
| Arrow-key navigation | `keyboardNavigation` | off |
|
|
41
|
+
| CSV export button | `exportable` | off |
|
|
42
|
+
| Virtualization | `virtualize` | off |
|
|
43
|
+
| Theme | `theme` | none (plain look) |
|
|
44
|
+
|
|
45
|
+
Everything is written in TypeScript, ships its own type definitions, and uses plain CSS (no CSS-in-JS runtime). The grid is 100% of its container's width by default. There is deliberately **no column reordering**.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Installation
|
|
50
|
+
|
|
51
|
+
**Requirements:** React 18 or newer and `react-dom`.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install react-gridix
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The stylesheet is included in the package's `style.css`. Import it once, near the root of your app:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import "react-gridix/style.css";
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The library does not inject CSS by itself. If the grid looks unstyled, this import is missing.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 3. Quick start
|
|
68
|
+
|
|
69
|
+
A read-only table, the smallest possible grid:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { DataGrid, type ColumnDef } from "react-gridix";
|
|
73
|
+
import "react-gridix/style.css";
|
|
74
|
+
|
|
75
|
+
interface Person { id: number; name: string; city: string }
|
|
76
|
+
|
|
77
|
+
const columns: ColumnDef<Person>[] = [
|
|
78
|
+
{ field: "id", headerName: "ID" },
|
|
79
|
+
{ field: "name", headerName: "Name" },
|
|
80
|
+
{ field: "city", headerName: "City" },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const rows: Person[] = [
|
|
84
|
+
{ id: 1, name: "Amina", city: "Kampala" },
|
|
85
|
+
{ id: 2, name: "Brian", city: "Nairobi" },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
export default function App() {
|
|
89
|
+
return <DataGrid columns={columns} rows={rows} />;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The same grid with the common features switched on:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import { useState } from "react";
|
|
97
|
+
import { DataGrid, type ColumnDef } from "react-gridix";
|
|
98
|
+
import "react-gridix/style.css";
|
|
99
|
+
|
|
100
|
+
interface Person {
|
|
101
|
+
id: number;
|
|
102
|
+
name: string;
|
|
103
|
+
joined: string; // "2026-03-14"
|
|
104
|
+
contact: { email: string };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const columns: ColumnDef<Person>[] = [
|
|
108
|
+
{ field: "id", headerName: "ID", type: "number", editable: false, sortable: true },
|
|
109
|
+
{ field: "name", headerName: "Name", sortable: true, filterable: true },
|
|
110
|
+
{ field: "joined", headerName: "Joined", type: "date", sortable: true, filterable: true },
|
|
111
|
+
{ field: "contact.email", headerName: "Email" },
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
export default function App() {
|
|
115
|
+
const [rows, setRows] = useState<Person[]>([
|
|
116
|
+
{ id: 1, name: "Amina", joined: "2026-03-14", contact: { email: "amina@example.com" } },
|
|
117
|
+
{ id: 2, name: "Brian", joined: "2025-11-02", contact: { email: "brian@example.com" } },
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<DataGrid
|
|
122
|
+
columns={columns}
|
|
123
|
+
rows={rows}
|
|
124
|
+
getRowId={(r) => r.id}
|
|
125
|
+
editable
|
|
126
|
+
onRowsChange={setRows}
|
|
127
|
+
searchable // search box
|
|
128
|
+
pagination // row count and page controls
|
|
129
|
+
rowSelection // checkbox column
|
|
130
|
+
theme="ocean"
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 4. Theming
|
|
139
|
+
|
|
140
|
+
### 4.1 Built-in themes
|
|
141
|
+
|
|
142
|
+
Pass a theme name to the grid. **Without a `theme` prop nothing changes**: the grid keeps its plain default look.
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
<DataGrid theme="midnight" ... />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Theme | Look |
|
|
149
|
+
| --- | --- |
|
|
150
|
+
| `"ocean"` | Light, cool blues, blue gradient header, soft shadow |
|
|
151
|
+
| `"midnight"` | Dark slate, indigo accent, dark popups and inputs |
|
|
152
|
+
| `"sunset"` | Warm cream, orange-to-pink gradient header, serif font, rounded corners |
|
|
153
|
+
| `"forest"` | Calm greens, solid dark-green header, sharper corners |
|
|
154
|
+
| `"lavender"` | Light purple, soft violet gradient header, rounded corners |
|
|
155
|
+
| `"coffee"` | Warm browns and cream, serif font, rich brown header |
|
|
156
|
+
| `"graphite"` | Dark neutral grey with a teal accent, flat and sharp |
|
|
157
|
+
| `"aurora"` | Deep night blue with a green-to-blue-to-violet gradient header and a soft glow |
|
|
158
|
+
|
|
159
|
+
A theme is only a set of CSS variables on `[data-rk-theme="name"]`, written with `:where()` (zero specificity). So you can still override any variable or any class on top of a theme. Popups (the filter popup, the column menu) are drawn in `document.body`, and the grid copies the theme attribute onto them, so they follow the theme too.
|
|
160
|
+
|
|
161
|
+
**Switching themes at run time** is just a state change:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
const [theme, setTheme] = useState<GridTheme>("ocean");
|
|
165
|
+
|
|
166
|
+
<select value={theme} onChange={(e) => setTheme(e.target.value as GridTheme)}>
|
|
167
|
+
{["ocean", "midnight", "sunset", "forest", "lavender", "coffee", "graphite", "aurora"].map((t) => (
|
|
168
|
+
<option key={t}>{t}</option>
|
|
169
|
+
))}
|
|
170
|
+
</select>
|
|
171
|
+
<DataGrid theme={theme} ... />
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Following the system dark mode.** Choose the theme from a media query:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
178
|
+
<DataGrid theme={dark ? "graphite" : "lavender"} ... />
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Tweaking a built-in theme.** Because themes have zero specificity, any variable or rule you write wins over them:
|
|
182
|
+
|
|
183
|
+
```css
|
|
184
|
+
/* make the aurora header a single colour */
|
|
185
|
+
[data-rk-theme="aurora"] { --rk-header-bg: #0f766e; }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Making your own theme.** Any other string works as a theme name. Define the variables you want:
|
|
189
|
+
|
|
190
|
+
```css
|
|
191
|
+
[data-rk-theme="brand"] {
|
|
192
|
+
--rk-header-bg: #7c3aed;
|
|
193
|
+
--rk-header-text: #fff;
|
|
194
|
+
--rk-row-hover: #f5f3ff;
|
|
195
|
+
--rk-selected-bg: #ede9fe; /* selected rows */
|
|
196
|
+
--rk-primary: #7c3aed;
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
<DataGrid theme="brand" ... />
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
A dark custom theme should also set the colours that the light defaults assume, for example `--rk-surface`, `--rk-text`, `--rk-border`, `--rk-pinned-bg`, `--rk-skeleton-a` and `--rk-skeleton-b`. Copy the `graphite` block in `themes.css` as a starting point.
|
|
205
|
+
|
|
206
|
+
### 4.2 CSS variables
|
|
207
|
+
|
|
208
|
+
Every variable is optional and is read with a fallback, so nothing is declared on the grid itself. Set them on `:root`, on any parent element, in your own theme, or in the grid's `style`.
|
|
209
|
+
|
|
210
|
+
| Variable | Default | Used for |
|
|
211
|
+
| --- | --- | --- |
|
|
212
|
+
| `--rk-font` | system UI stack | Font family |
|
|
213
|
+
| `--rk-text` | `#111827` | Main text |
|
|
214
|
+
| `--rk-muted` | `#6b7280` | Secondary text, empty state |
|
|
215
|
+
| `--rk-border` | `#d1d5db` | Borders |
|
|
216
|
+
| `--rk-surface` | `#ffffff` | Background of the grid, inputs, buttons |
|
|
217
|
+
| `--rk-bg-subtle` | `#f3f4f6` | Header and footer background |
|
|
218
|
+
| `--rk-primary` | `#4f46e5` | Focus rings, active editor border |
|
|
219
|
+
| `--rk-danger` | `#dc2626` | Invalid editor border |
|
|
220
|
+
| `--rk-invalid-bg` | `#fef2f2` | Invalid editor background |
|
|
221
|
+
| `--rk-grid-bg` | `--rk-surface` | Grid background |
|
|
222
|
+
| `--rk-grid-radius` | `10px` | Grid corner radius |
|
|
223
|
+
| `--rk-grid-shadow` | `none` | Grid shadow |
|
|
224
|
+
| `--rk-toolbar-bg` | `transparent` | Toolbar background |
|
|
225
|
+
| `--rk-input-bg`, `--rk-input-border`, `--rk-input-text` | surface, border, inherit | Search box, filter inputs, editor, page controls |
|
|
226
|
+
| `--rk-header-bg` | `--rk-bg-subtle` | Header background. Any CSS background, so gradients work |
|
|
227
|
+
| `--rk-header-text` | inherit | Header text colour |
|
|
228
|
+
| `--rk-header-hover-bg` | `rgba(0,0,0,.06)` | Header hover |
|
|
229
|
+
| `--rk-header-border` | `--rk-border` | Line under the header |
|
|
230
|
+
| `--rk-cell-border` | `#eef0f3` | Lines between rows |
|
|
231
|
+
| `--rk-row-hover` | `#f3f4f6` | Row hover |
|
|
232
|
+
| `--rk-selected-bg` | `#eef2ff` | Background of selected rows |
|
|
233
|
+
| `--rk-pinned-bg` | `--rk-grid-bg` | Opaque background behind pinned body cells (so scrolled cells do not show through) |
|
|
234
|
+
| `--rk-pinned-header-bg` | `--rk-bg-subtle` | Opaque background behind pinned header cells |
|
|
235
|
+
| `--rk-skeleton-a`, `--rk-skeleton-b` | `#e5e7eb`, `#f3f4f6` | The two colours of the shimmering loading bars |
|
|
236
|
+
| `--rk-stripe-a`, `--rk-stripe-b` | `transparent`, `#fafafa` | Alternating row colours (see [stripes](#striped-rows)) |
|
|
237
|
+
| `--rk-footer-bg`, `--rk-footer-text` | subtle, muted | Pagination bar |
|
|
238
|
+
| `--rk-btn-hover` | `#e5e7eb` | Page button, toolbar button and Clear button hover |
|
|
239
|
+
| `--rk-filter-active` | `--rk-primary` | Filter dot colour while a filter is set |
|
|
240
|
+
| `--rk-filter-glow` | `rgba(79,70,229,.25)` | Ring around an active filter dot |
|
|
241
|
+
| `--rk-popup-bg`, `--rk-popup-text`, `--rk-popup-border` | white, dark, grey | Filter popup and column menu |
|
|
242
|
+
| `--rk-popup-radius`, `--rk-popup-shadow` | `10px`, soft shadow | Popup shape |
|
|
243
|
+
| `--rk-row-duration` | `0.38s` | Row entrance animation length |
|
|
244
|
+
| `--rk-row-stagger` | `28ms` | Extra delay per row in the entrance animation |
|
|
245
|
+
| `--rk-checkbox-size` | `20px` | Checkbox width and height |
|
|
246
|
+
| `--rk-checkbox-border-width` | `1px` | Checkbox border width |
|
|
247
|
+
| `--rk-checkbox-radius` | `0` | Checkbox corner radius (no rounding by default) |
|
|
248
|
+
| `--rk-checkbox-color` | `#4f46e5` | Checked colour (also the focus ring and the native accent) |
|
|
249
|
+
| `--rk-checkbox-border` | `#9ca3af` | Border colour of an unchecked `"button"` / `"tick"` checkbox |
|
|
250
|
+
| `--rk-checkbox-bg` | `#ffffff` | Background of an unchecked `"button"` / `"tick"` checkbox |
|
|
251
|
+
| `--rk-checkbox-tick` | `#ffffff` | Colour of the tick, and of a custom character in the `"tick"` variant |
|
|
252
|
+
|
|
253
|
+
```css
|
|
254
|
+
:root {
|
|
255
|
+
--rk-primary: #0d9488;
|
|
256
|
+
--rk-row-hover: #f0fdfa;
|
|
257
|
+
--rk-selected-bg: #ccfbf1;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Scoped to one grid through its `style` prop:
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
<DataGrid style={{ "--rk-header-bg": "#0f172a", "--rk-header-text": "#fff" } as React.CSSProperties} ... />
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 5. Checkbox
|
|
270
|
+
|
|
271
|
+
A checkbox in three looks. Every variant toggles with **one click** (or the Space key when it has focus), and clicking the label text toggles it too.
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
<Checkbox variant="button" checked={a} onChange={setA} label="Button + span" />
|
|
275
|
+
<Checkbox variant="tick" checked={b} onChange={setB} label="Tick" />
|
|
276
|
+
<Checkbox variant="native" checked={c} onChange={setC} label="Native" />
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
| Variant | What it is |
|
|
280
|
+
| --- | --- |
|
|
281
|
+
| `"button"` | A `<button role="checkbox">` drawn as a box, with a `<span>` inside that is hidden when unchecked and shown when checked |
|
|
282
|
+
| `"tick"` | The same button, but the box fills with the checked colour and the span holds a tick icon |
|
|
283
|
+
| `"native"` | The browser's own `<input type="checkbox">` |
|
|
284
|
+
|
|
285
|
+
| Prop | Type | Default | Description |
|
|
286
|
+
| --- | --- | --- | --- |
|
|
287
|
+
| `checked` | `boolean` | required | Current value (controlled) |
|
|
288
|
+
| `indeterminate` | `boolean` | `false` | Shows the mixed ("some selected") state: a dash instead of a tick. Use it with `checked={false}`. A click then calls `onChange(true)` |
|
|
289
|
+
| `onChange` | `(checked: boolean) => void` | required | Called once per toggle with the new value |
|
|
290
|
+
| `variant` | `"button" \| "tick" \| "native"` | `"native"` | Which look to use |
|
|
291
|
+
| `disabled` | `boolean` | `false` | Disables the checkbox and dims it |
|
|
292
|
+
| `label` | `ReactNode` | | Text next to the box. Clicking it toggles the checkbox. An empty string, `null` or `false` renders no label |
|
|
293
|
+
| `aria-label` | `string` | | Accessible name when there is no visible `label` |
|
|
294
|
+
| `id` | `string` | | `id` of the control |
|
|
295
|
+
| `name` | `string` | | `name` of the input (`"native"` variant only) |
|
|
296
|
+
| `indicator` | `ReactNode` | | A custom character or icon drawn inside the `"button"` and `"tick"` variants when checked, for example `"★"` |
|
|
297
|
+
| `className` | `string` | | Classes for the root element (same as `classNames.root`) |
|
|
298
|
+
| `style` | `CSSProperties` | | Inline styles for the root element (same as `styles.root`) |
|
|
299
|
+
| `classNames` | `Partial<Record<CheckboxSlot, string>>` | | Classes per part |
|
|
300
|
+
| `styles` | `Partial<Record<CheckboxSlot, CSSProperties>>` | | Inline styles per part |
|
|
301
|
+
|
|
302
|
+
**Parts (`CheckboxSlot`):** `root` (the wrapping `<label>`), `control` (the `<button>` or `<input>`), `indicator` (the span inside, button variants only) and `label` (the text).
|
|
303
|
+
|
|
304
|
+
**Customising the look from outside**
|
|
305
|
+
|
|
306
|
+
1. **Classes and inline styles per part**, including Tailwind:
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
<Checkbox
|
|
310
|
+
variant="tick"
|
|
311
|
+
checked={on}
|
|
312
|
+
onChange={setOn}
|
|
313
|
+
classNames={{ control: "rounded-full border-emerald-500 aria-checked:bg-emerald-500" }}
|
|
314
|
+
styles={{ root: { gap: 12 } }}
|
|
315
|
+
/>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
(`aria-checked:` works for the `"button"` and `"tick"` variants. The native input has no `aria-checked`, so use the `--rk-checkbox-color` variable for it.)
|
|
319
|
+
2. **CSS variables**: `--rk-checkbox-size`, `--rk-checkbox-border-width`, `--rk-checkbox-radius`, `--rk-checkbox-color`, `--rk-checkbox-border`, `--rk-checkbox-bg` and `--rk-checkbox-tick` (see [Theming](#42-css-variables)). Set them anywhere above the checkbox:
|
|
320
|
+
|
|
321
|
+
```tsx
|
|
322
|
+
<Checkbox styles={{ root: { "--rk-checkbox-color": "#dc2626" } as CSSProperties }} ... />
|
|
323
|
+
```
|
|
324
|
+
3. **A custom character**, for example `indicator="★"` (any text, emoji or React node). It shows when the box is checked, in the `"button"` and `"tick"` variants. The default fill is dropped; the character is sized to the box (70% of `--rk-checkbox-size`) and coloured with `--rk-checkbox-color` in the button variant and `--rk-checkbox-tick` in the tick variant. Restyle it through `classNames.indicator` / `styles.indicator`.
|
|
325
|
+
4. **Plain CSS** with these stable classes: `.rk-checkbox`, `.rk-checkbox--button`, `.rk-checkbox--tick`, `.rk-checkbox--native`, `.rk-checkbox--checked`, `.rk-checkbox--indeterminate`, `.rk-checkbox--disabled`, `.rk-checkbox__control`, `.rk-checkbox__indicator`, `.rk-checkbox__indicator--custom` and `.rk-checkbox__label`. State is also exposed as `data-state="checked"`, `"unchecked"` or `"indeterminate"` and, on the button variants, `aria-checked` (`"true"`, `"false"` or `"mixed"`).
|
|
326
|
+
|
|
327
|
+
**Defaults:** a 1px border and square corners (`--rk-checkbox-border-width: 1px`, `--rk-checkbox-radius: 0`). Round them with `--rk-checkbox-radius: 999px` or a Tailwind `rounded-*` class on `classNames.control`.
|
|
328
|
+
|
|
329
|
+
The checkbox CSS is written with `:where()` (zero specificity), so any class you add wins without `!important`.
|
|
330
|
+
|
|
331
|
+
**Accessibility:** the button variants have `role="checkbox"` and `aria-checked`, and the native variant is a real checkbox. When you don't pass a `label`, pass an `aria-label`.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## 6. DataGrid
|
|
336
|
+
|
|
337
|
+
### 6.1 Concepts
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
<DataGrid columns={columns} rows={rows} />
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
- **`rows`** is an array of plain objects.
|
|
344
|
+
- **`columns`** is an array of column definitions. Each one names a `field` (a key of the row object) and describes how to show, sort, filter and edit it.
|
|
345
|
+
- **Row ids.** Pass `getRowId` when your rows have a stable id. Without it the grid uses each row's index in `rows`, which is fine for static data. Selection, server mode and virtualization all work best with real ids.
|
|
346
|
+
- **`rowIndex`** in callbacks and params is always the row's index in your original `rows` array, not its position on screen.
|
|
347
|
+
- **The grid keeps its own copy of the rows** so edits appear immediately. It re-syncs whenever the `rows` prop changes identity. See [section 6.9](#69-editing).
|
|
348
|
+
- **Controlled or uncontrolled.** Search, filters, sort, page and selection each work two ways. Leave the prop out and the grid keeps the state itself (optionally starting from a `default…` value). Pass the prop together with its `on…Change` callback and you own the state. See [6.14](#614-controlled-state).
|
|
349
|
+
- **Opt-in everywhere.** Features are off until you switch them on (see the table in the [overview](#1-overview)).
|
|
350
|
+
|
|
351
|
+
The grid is generic: `DataGrid<T>` infers `T` from `rows`, and `ColumnDef<T>` checks that each `field` is a real key of `T` or a dot path into a nested object (see [nested fields](#612-nested-fields)).
|
|
352
|
+
|
|
353
|
+
### 6.2 DataGrid props
|
|
354
|
+
|
|
355
|
+
**Data and editing**
|
|
356
|
+
|
|
357
|
+
| Prop | Type | Default | Description |
|
|
358
|
+
| --- | --- | --- | --- |
|
|
359
|
+
| `columns` | `ColumnDef<T>[]` | required | Column definitions |
|
|
360
|
+
| `rows` | `T[]` | required | Data. In `serverSide` mode: the current page |
|
|
361
|
+
| `getRowId` | `(row, index) => string \| number` | row index | Stable id for each row |
|
|
362
|
+
| `editable` | `boolean` | `false` | Allow editing cells. Columns can override this |
|
|
363
|
+
| `enterMovesDown` | `boolean` | `true` | After Enter saves a cell, edit the cell below |
|
|
364
|
+
| `onCellEdit` | `(event: CellEditEvent<T>) => void` | | Called after a cell edit is saved |
|
|
365
|
+
| `onRowsChange` | `(rows: T[]) => void` | | Called with the full updated array after an edit |
|
|
366
|
+
| `onRowClick` | `(row, rowIndex) => void` | | Called when a row is clicked |
|
|
367
|
+
|
|
368
|
+
**Row selection** (see [6.13](#613-row-selection))
|
|
369
|
+
|
|
370
|
+
| Prop | Type | Default | Description |
|
|
371
|
+
| --- | --- | --- | --- |
|
|
372
|
+
| `rowSelection` | `boolean \| "single" \| "multiple"` | `false` | Adds a checkbox column. `true` means `"multiple"` |
|
|
373
|
+
| `selectedRowIds` | `(string \| number)[]` | | Controlled selection |
|
|
374
|
+
| `defaultSelectedRowIds` | `(string \| number)[]` | `[]` | Initial selection when not controlled |
|
|
375
|
+
| `onSelectionChange` | `(selection: RowSelection<T>) => void` | | Called with `{ ids, rows }` |
|
|
376
|
+
| `isRowSelectable` | `(row) => boolean` | all rows | Return `false` to disable a row's checkbox |
|
|
377
|
+
|
|
378
|
+
**Sorting** (see [6.4](#64-sorting))
|
|
379
|
+
|
|
380
|
+
| Prop | Type | Default | Description |
|
|
381
|
+
| --- | --- | --- | --- |
|
|
382
|
+
| `multiSort` | `boolean` | `false` | Shift+click adds more sort columns |
|
|
383
|
+
| `sort` | `SortModel[]` | | Controlled sort |
|
|
384
|
+
| `defaultSort` | `SortModel \| SortModel[] \| null` | `null` | Initial sort when not controlled |
|
|
385
|
+
| `onSortChange` | `(sort: SortModel[]) => void` | | Called when the sort changes |
|
|
386
|
+
|
|
387
|
+
**Search, filters and paging** (see [6.5](#65-searching-and-filtering), [6.7](#67-pagination), [6.14](#614-controlled-state))
|
|
388
|
+
|
|
389
|
+
| Prop | Type | Default | Description |
|
|
390
|
+
| --- | --- | --- | --- |
|
|
391
|
+
| `searchable` | `boolean` | `false` | Show the global search box |
|
|
392
|
+
| `searchPlaceholder` | `string` | `"Search…"` | Placeholder of the search box |
|
|
393
|
+
| `search` / `onSearchChange` | `string` / `(s) => void` | | Controlled search text |
|
|
394
|
+
| `filters` / `onFiltersChange` | `Record<string, FilterValue>` / `(f) => void` | | Controlled column filters |
|
|
395
|
+
| `pagination` | `boolean` | `false` | Split rows into pages and show the bar with the row count and page controls |
|
|
396
|
+
| `paginationPosition` | `"bottom" \| "top" \| "both"` | `"bottom"` | Where the bar goes |
|
|
397
|
+
| `pageSize` | `number` | `10` | Rows per page (initial value) |
|
|
398
|
+
| `pageSizeOptions` | `number[]` | `[5, 10, 25, 50]` | Choices in the page size menu. The current size is added if missing |
|
|
399
|
+
| `page` / `onPageChange` | `number` / `(page) => void` | | Controlled page (zero-based) |
|
|
400
|
+
| `onPageSizeChange` | `(pageSize) => void` | | Called when the page size menu changes |
|
|
401
|
+
|
|
402
|
+
**Server-side mode, loading and errors** (see [6.15](#615-server-side-mode), [6.16](#616-loading-skeleton))
|
|
403
|
+
|
|
404
|
+
| Prop | Type | Default | Description |
|
|
405
|
+
| --- | --- | --- | --- |
|
|
406
|
+
| `serverSide` | `boolean` | `false` | Your server sorts, filters, searches and pages |
|
|
407
|
+
| `rowCount` | `number` | | Total rows on the server (for the page count) |
|
|
408
|
+
| `onQueryChange` | `(query: GridQuery) => void \| Promise<unknown>` | | Called on mount and whenever the query changes. A returned promise drives the loading state |
|
|
409
|
+
| `queryDebounce` | `number` | `250` | Milliseconds to wait after the last keystroke before querying |
|
|
410
|
+
| `loading` | `boolean` | follows the promise | Force the loading state on or off |
|
|
411
|
+
| `loadingRows` | `number` | page size (max 20), or 5 | How many skeleton rows to draw |
|
|
412
|
+
| `skeletonRow` | `ComponentType<SkeletonRowProps<T>>` | shimmer bars | Your own component for each placeholder row |
|
|
413
|
+
| `errorMessage` | `ReactNode` | `"Could not load the data."` | Shown with a Retry button when the promise rejects |
|
|
414
|
+
|
|
415
|
+
**Columns, keyboard, big data and export**
|
|
416
|
+
|
|
417
|
+
| Prop | Type | Default | Description |
|
|
418
|
+
| --- | --- | --- | --- |
|
|
419
|
+
| `columnMenu` | `boolean` | `false` | A "Columns" button to show and hide columns |
|
|
420
|
+
| `onColumnVisibilityChange` | `(hiddenFields: string[]) => void` | | Called with the fields that are hidden |
|
|
421
|
+
| `keyboardNavigation` | `boolean` | `false` | Arrow keys, Home, End, Page Up and Page Down move between cells |
|
|
422
|
+
| `virtualize` | `boolean` | `false` | Render only the visible rows |
|
|
423
|
+
| `rowHeight` | `number` | `40` | Row height in px when virtualizing |
|
|
424
|
+
| `overscan` | `number` | `6` | Extra rows drawn above and below the window |
|
|
425
|
+
| `exportable` | `boolean \| ExportOptions` | `false` | Show an "Export CSV" button |
|
|
426
|
+
| `apiRef` | `MutableRefObject<DataGridApi<T> \| null>` | | Call `exportCsv()`, `getSelectedRows()` and more from outside |
|
|
427
|
+
|
|
428
|
+
**Look and feel**
|
|
429
|
+
|
|
430
|
+
| Prop | Type | Default | Description |
|
|
431
|
+
| --- | --- | --- | --- |
|
|
432
|
+
| `theme` | `GridTheme \| string` | | Applies a theme. Nothing changes without it. See [Theming](#4-theming) |
|
|
433
|
+
| `toolbar` | `ReactNode` | | Extra content on the right of the toolbar |
|
|
434
|
+
| `emptyMessage` | `ReactNode` | `"No rows to show"` | Shown when no rows match |
|
|
435
|
+
| `striped` | `boolean` | `false` | Alternate row shading |
|
|
436
|
+
| `stripeColors` | `[string, string]` | | Two colours for the alternating rows. Implies `striped`. See [striped rows](#striped-rows) |
|
|
437
|
+
| `animateRows` | `boolean` | `true` | Rows slide up and fade in when they render. See [row animation](#row-animation) |
|
|
438
|
+
| `checkboxVariant` | `"button" \| "tick" \| "native"` | `"native"` | Checkbox look for boolean columns and row selection. Columns can override it |
|
|
439
|
+
| `maxHeight` | `number \| string` | | Makes the body scroll, with a sticky header. Example: `400` or `"60vh"` |
|
|
440
|
+
| `className` | `string` | | Classes for the root element (Tailwind works). The grid is `width: 100%` of its container by default |
|
|
441
|
+
| `style` | `CSSProperties` | | Inline styles for the root element |
|
|
442
|
+
| `classNames` | `Partial<Record<GridSlot, string>>` | | Classes for individual parts. See [6.11](#611-styling) |
|
|
443
|
+
| `styles` | `Partial<Record<GridSlot, CSSProperties>>` | | Inline styles for individual parts |
|
|
444
|
+
| `rowClassName` | `string \| (row, rowIndex) => string` | | Classes for rows |
|
|
445
|
+
| `rowStyle` | `CSSProperties \| (row, rowIndex) => CSSProperties` | | Inline styles for rows |
|
|
446
|
+
|
|
447
|
+
### 6.3 Column definitions
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
const columns: ColumnDef<Employee>[] = [
|
|
451
|
+
{ field: "name", headerName: "Name", sortable: true, filterable: true },
|
|
452
|
+
{ field: "salary", headerName: "Salary", type: "number", align: "right", sortable: true },
|
|
453
|
+
{ field: "hired", headerName: "Hired", type: "date", sortable: true, filterable: true },
|
|
454
|
+
];
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
| Option | Type | Description |
|
|
458
|
+
| --- | --- | --- |
|
|
459
|
+
| `field` | `FieldPath<T>` | **Required.** Key of the row object this column reads and writes. Use dot notation for nested objects: `"contact.phone"` |
|
|
460
|
+
| `headerName` | `string` | Header text. Defaults to the field name, except in `type: "boolean"` columns, where it defaults to an empty string |
|
|
461
|
+
| `type` | `"text" \| "number" \| "boolean" \| "date"` | `"number"` columns edit and parse as numbers, `"boolean"` columns show a checkbox, `"date"` columns have a date picker (see [6.6](#66-date-columns)). Default `"text"` |
|
|
462
|
+
| `width` | `number \| string` | Header cell width |
|
|
463
|
+
| `minWidth` / `maxWidth` | `number \| string` / `number` | Smallest and largest width (`maxWidth` limits dragging when resizing) |
|
|
464
|
+
| `align` | `"left" \| "center" \| "right"` | Text alignment for the header, cells and editor |
|
|
465
|
+
| `sortable` | `boolean` | **Opt in to sorting this column** (default `false`) |
|
|
466
|
+
| `resizable` | `boolean` | Show a drag handle on the header's right edge (default `false`). See [6.17](#617-resize-pin-and-hide-columns) |
|
|
467
|
+
| `pinned` | `"left" \| "right"` | Stick the column to an edge while scrolling sideways |
|
|
468
|
+
| `hidden` | `boolean` | Start hidden (shown again from the Columns menu) |
|
|
469
|
+
| `hideable` | `boolean` | Set `false` to keep a column out of the Columns menu (default `true`) |
|
|
470
|
+
| `filterable` | `boolean` | Show a filter indicator in this column's header (default `false`). Clicking it opens the filter popup. See [6.5](#65-searching-and-filtering) |
|
|
471
|
+
| `filterType` | `"text" \| "exact" \| "number" \| "date" \| "select"` | Which filter controls the popup shows. Default follows `type` |
|
|
472
|
+
| `filterOptions` | `(string \| FilterOption)[]` | Choices for a `"select"` filter. Without it they are built from the data |
|
|
473
|
+
| `filterPlaceholder` | `string` | Placeholder of the text filter box |
|
|
474
|
+
| `filterIndicator` | `ComponentType<FilterIndicatorProps<T>>` | Your own component drawn instead of the default dot |
|
|
475
|
+
| `filterIndicatorClassName` / `filterIndicatorStyle` | `string` / `CSSProperties` | Classes and inline styles for this column's indicator button |
|
|
476
|
+
| `filterPopupClassName` / `filterPopupStyle` | `string` / `CSSProperties` | Classes and inline styles for this column's filter popup |
|
|
477
|
+
| `filterRenderer` | `ComponentType<FilterRendererProps<T>>` | Component that replaces the filter controls inside the popup |
|
|
478
|
+
| `editable` | `boolean \| (params: CellEditableParams<T>) => boolean` | Overrides the grid-level `editable`. A function decides per cell |
|
|
479
|
+
| `valueFormatter` | `(value, row) => string` | Text used for display, search and text filters. Does not change the stored value |
|
|
480
|
+
| `valueParser` | `(text, row) => unknown` | Turns typed text into the value to store |
|
|
481
|
+
| `validate` | `(value, row) => boolean` | Return `false` to reject an edit |
|
|
482
|
+
| `comparator` | `(a, b, rowA, rowB) => number` | Custom sort order |
|
|
483
|
+
| `dateFormat` / `dateLocale` | `Intl.DateTimeFormatOptions` / `string` | How a `"date"` column is displayed |
|
|
484
|
+
| `dateMin` / `dateMax` | `string` (`"YYYY-MM-DD"`) | Earliest and latest day the date picker accepts |
|
|
485
|
+
| `exportValue` | `(value, row) => string \| number \| boolean \| null` | What goes in the CSV file for this column |
|
|
486
|
+
| `cellRenderer` | `ComponentType<CellParams<T>>` | Component that renders the cell |
|
|
487
|
+
| `cellEditor` | `ComponentType<CellEditorProps<T>>` | Component used instead of the built-in editor |
|
|
488
|
+
| `headerRenderer` | `ComponentType<HeaderRendererProps<T>>` | Component that renders the header label |
|
|
489
|
+
| `className` / `style` | `string` / `CSSProperties` | Classes and inline styles for the whole column: header cell and every body cell (Tailwind works) |
|
|
490
|
+
| `headerClassName` / `headerStyle` | `string` / `CSSProperties` | Header cell only |
|
|
491
|
+
| `cellClassName` / `cellStyle` | `string \| (params) => string` / `CSSProperties \| (params) => CSSProperties` | Body cells only |
|
|
492
|
+
| `checkbox` | `ColumnCheckboxOptions` | Look of the checkbox in a `type: "boolean"` column: `variant`, `indicator`, `className`, `style`, `classNames`, `styles` |
|
|
493
|
+
| `headerCheckbox` | `boolean` | On a `type: "boolean"` column, draws a select-all checkbox in the header. See [Select-all header checkbox](#select-all-header-checkbox) |
|
|
494
|
+
|
|
495
|
+
### 6.4 Sorting
|
|
496
|
+
|
|
497
|
+
**Sorting is chosen per column.** A column is sortable only when its definition says `sortable: true`. There is no grid-level switch.
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
const columns: ColumnDef<Employee>[] = [
|
|
501
|
+
{ field: "id", type: "number" }, // not sortable
|
|
502
|
+
{ field: "name", sortable: true }, // click to sort
|
|
503
|
+
{ field: "salary", type: "number", sortable: true },
|
|
504
|
+
];
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
- Click a header to sort ascending, click again for descending, click a third time to clear the sort.
|
|
508
|
+
- Numbers sort numerically, dates by day, booleans false before true. Everything else sorts as text with natural number ordering (`"item 2"` before `"item 10"`), ignoring case.
|
|
509
|
+
- Empty values (`null`, `undefined`, `""`) always sort last, in both directions.
|
|
510
|
+
- Headers are keyboard accessible (Tab to a header, Enter or Space to sort) and expose `aria-sort`. A column that is not sortable gets no sort arrow, no pointer cursor and no focus stop.
|
|
511
|
+
|
|
512
|
+
**Custom order** with a `comparator`:
|
|
513
|
+
|
|
514
|
+
```tsx
|
|
515
|
+
const order = ["low", "medium", "high", "urgent"];
|
|
516
|
+
{ field: "priority", sortable: true, comparator: (a, b) => order.indexOf(a) - order.indexOf(b) }
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**Starting sorted** with `defaultSort`:
|
|
520
|
+
|
|
521
|
+
```tsx
|
|
522
|
+
<DataGrid defaultSort={{ field: "salary", direction: "desc" }} ... />
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Multi-column sort.** Switch on `multiSort`. A plain click still replaces the sort with that one column. **Shift+click** adds the column as the next tie-breaker (or cycles its direction if it is already in the sort). Small numbers in the headers show the priority.
|
|
526
|
+
|
|
527
|
+
```tsx
|
|
528
|
+
<DataGrid
|
|
529
|
+
multiSort
|
|
530
|
+
defaultSort={[
|
|
531
|
+
{ field: "department", direction: "asc" },
|
|
532
|
+
{ field: "salary", direction: "desc" }, // inside each department, highest salary first
|
|
533
|
+
]}
|
|
534
|
+
columns={columns}
|
|
535
|
+
rows={rows}
|
|
536
|
+
/>
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**Reacting to the sort** (always an array, empty when unsorted):
|
|
540
|
+
|
|
541
|
+
```tsx
|
|
542
|
+
<DataGrid
|
|
543
|
+
onSortChange={(sort) => console.log(sort)}
|
|
544
|
+
// [{ field: "department", direction: "asc" }, { field: "salary", direction: "desc" }]
|
|
545
|
+
...
|
|
546
|
+
/>
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Controlling the sort yourself** (also see [6.14](#614-controlled-state)):
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
const [sort, setSort] = useState<SortModel[]>([{ field: "name", direction: "asc" }]);
|
|
553
|
+
|
|
554
|
+
<button onClick={() => setSort([])}>Reset sort</button>
|
|
555
|
+
<DataGrid sort={sort} onSortChange={setSort} ... />
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### 6.5 Searching and filtering
|
|
559
|
+
|
|
560
|
+
**Global search.** Off by default. Switch it on with `searchable` and a box appears above the grid. It matches rows where any column's displayed text contains the typed text (case-insensitive). "Displayed text" means the result of `valueFormatter` if there is one.
|
|
561
|
+
|
|
562
|
+
```tsx
|
|
563
|
+
<DataGrid searchable searchPlaceholder="Search people…" ... />
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Column filters.** Filters are set per column. Only columns with `filterable: true` get one:
|
|
567
|
+
|
|
568
|
+
```tsx
|
|
569
|
+
const columns: ColumnDef<Person>[] = [
|
|
570
|
+
{ field: "name", filterable: true },
|
|
571
|
+
{ field: "department", filterable: true, filterPlaceholder: "Which team?" },
|
|
572
|
+
{ field: "salary" }, // no filter
|
|
573
|
+
];
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
- A filterable column shows a small **indicator** in its header, next to the label. By default it is a dot. It is dim while no filter is set, and lit (with a glow) while one is active.
|
|
577
|
+
- **Clicking the indicator** opens a popup right below it. The popup is not there until you click. It closes on Escape, on a click outside, or on Enter in a text box. Clicking the indicator does not sort the column.
|
|
578
|
+
- The popup shows a **Clear** button while a filter is set.
|
|
579
|
+
- All filters and the global search combine (a row must pass every one).
|
|
580
|
+
- The popup is drawn in `document.body` and positioned next to the indicator, so a scrolling grid never clips it. It follows scrolling and resizing, and stays inside the window.
|
|
581
|
+
- There is no grid-level filter prop to turn filters on: a column without `filterable: true` has no filter, and a filter value is ignored if its column stops being filterable.
|
|
582
|
+
|
|
583
|
+
#### Filter types
|
|
584
|
+
|
|
585
|
+
Each column's filter has a kind. By default it follows the column `type`; set `filterType` to choose another.
|
|
586
|
+
|
|
587
|
+
| `filterType` | Default for | Popup shows | Rows pass when |
|
|
588
|
+
| --- | --- | --- | --- |
|
|
589
|
+
| `"text"` | `type: "text"` | One text box | The displayed text **contains** the typed text (case-insensitive) |
|
|
590
|
+
| `"exact"` | | One text box | The displayed text **equals** the typed text (case-insensitive) |
|
|
591
|
+
| `"number"` | `type: "number"` | Min and Max boxes | The number is between Min and Max (either may be left empty). Rows without a number never match |
|
|
592
|
+
| `"date"` | `type: "date"` | From and To date pickers | The day is between From and To (either may be left empty) |
|
|
593
|
+
| `"select"` | `type: "boolean"` | A checkbox per value | The raw value is one of the ticked values |
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
const columns: ColumnDef<Task>[] = [
|
|
597
|
+
// "contains": typing "log" finds "Fix login" and "Catalogue"
|
|
598
|
+
{ field: "title", filterable: true },
|
|
599
|
+
|
|
600
|
+
// "exact": typing "active" finds "active" but NOT "inactive"
|
|
601
|
+
{ field: "status", filterable: true, filterType: "exact" },
|
|
602
|
+
|
|
603
|
+
// number range: type: "number" gives a Min / Max popup by default
|
|
604
|
+
{ field: "budget", type: "number", filterable: true },
|
|
605
|
+
|
|
606
|
+
// date range: type: "date" gives From / To date pickers by default
|
|
607
|
+
{ field: "due", type: "date", filterable: true },
|
|
608
|
+
|
|
609
|
+
// choose from a list: values are collected from the data...
|
|
610
|
+
{ field: "owner", filterable: true, filterType: "select" },
|
|
611
|
+
|
|
612
|
+
// ...or you decide the list and the labels
|
|
613
|
+
{
|
|
614
|
+
field: "priority",
|
|
615
|
+
filterable: true,
|
|
616
|
+
filterType: "select",
|
|
617
|
+
filterOptions: [
|
|
618
|
+
{ value: "p1", label: "Urgent" },
|
|
619
|
+
{ value: "p2", label: "Normal" },
|
|
620
|
+
"p3", // a plain string is its own label
|
|
621
|
+
],
|
|
622
|
+
},
|
|
623
|
+
];
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
Notes on `select`: when `filterOptions` is left out, the choices are the distinct values in the data, sorted. A boolean column gets "Yes" and "No". Ticking several values combines them with OR.
|
|
627
|
+
|
|
628
|
+
#### Reading and setting filters
|
|
629
|
+
|
|
630
|
+
The shape of a filter value depends on its kind:
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
// text / exact: "sales"
|
|
634
|
+
// number: { min: 1000, max: 5000 } (either key may be missing)
|
|
635
|
+
// date: { from: "2026-01-01", to: "2026-03-31" }
|
|
636
|
+
// select: ["active", "pending"]
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
Listen with `onFiltersChange`, or control the filters completely:
|
|
640
|
+
|
|
641
|
+
```tsx
|
|
642
|
+
const [filters, setFilters] = useState<Record<string, FilterValue>>({
|
|
643
|
+
status: ["open", "blocked"], // select
|
|
644
|
+
budget: { min: 1000 }, // number
|
|
645
|
+
due: { from: "2026-06-01" }, // date
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
<button onClick={() => setFilters({})}>Clear all filters</button>
|
|
649
|
+
<DataGrid filters={filters} onFiltersChange={setFilters} ... />
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
#### Styling each indicator
|
|
653
|
+
|
|
654
|
+
Every column has its own options:
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
{
|
|
658
|
+
field: "status",
|
|
659
|
+
filterable: true,
|
|
660
|
+
filterIndicatorClassName: "text-amber-300", // Tailwind on this indicator only
|
|
661
|
+
filterIndicatorStyle: { color: "#f59e0b" }, // or inline styles
|
|
662
|
+
filterPopupStyle: { borderColor: "#f59e0b" }, // style this column's popup
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
The dot takes its colour from the button's text colour, so `color` (or `text-*`) restyles it. The colour while a filter is active comes from `--rk-filter-active`. To style all indicators or popups at once, use `classNames.filterIndicator`, `classNames.filterPopup`, `classNames.filterInput` (or the matching `styles`).
|
|
667
|
+
|
|
668
|
+
#### A custom indicator component
|
|
669
|
+
|
|
670
|
+
Replace the dot with any component. It receives `{ column, active, open }`:
|
|
671
|
+
|
|
672
|
+
```tsx
|
|
673
|
+
function FunnelIndicator({ active }: FilterIndicatorProps<Employee>) {
|
|
674
|
+
return (
|
|
675
|
+
<svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
|
|
676
|
+
<path d="M1.5 2h13l-5 6v5l-3 1.5V8z" fill={active ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.5" />
|
|
677
|
+
</svg>
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
{ field: "department", filterable: true, filterIndicator: FunnelIndicator }
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
The component is drawn inside the indicator `<button>`, so do not put another button in it.
|
|
685
|
+
|
|
686
|
+
#### A custom filter control
|
|
687
|
+
|
|
688
|
+
`filterRenderer` replaces **all** the built-in controls inside the popup, whatever the filter type (see [filterRenderer](#filterrenderer-custom-filter-control)). A custom control produces a text filter, so it works with the "contains" matching.
|
|
689
|
+
|
|
690
|
+
#### Notes
|
|
691
|
+
|
|
692
|
+
- Searching and text filters use the displayed text, so a salary shown as `$52,000` is found by `52,000` and not by `52000` unless your formatter says so. Number, date and select filters look at the raw value instead, so formatting never gets in their way.
|
|
693
|
+
- Changing the search, a filter, the sort or the page size sends the grid back to page 1.
|
|
694
|
+
- In [server-side mode](#615-server-side-mode) the filters are not applied by the grid: they are handed to your server in the query.
|
|
695
|
+
|
|
696
|
+
### 6.6 Date columns
|
|
697
|
+
|
|
698
|
+
`type: "date"` is a built-in column type with formatting, a date picker editor, chronological sorting and a From / To filter.
|
|
699
|
+
|
|
700
|
+
```tsx
|
|
701
|
+
interface Task { id: number; title: string; due: string; created: Date }
|
|
702
|
+
|
|
703
|
+
const columns: ColumnDef<Task>[] = [
|
|
704
|
+
{ field: "title", editable: true },
|
|
705
|
+
{
|
|
706
|
+
field: "due",
|
|
707
|
+
type: "date",
|
|
708
|
+
sortable: true,
|
|
709
|
+
filterable: true,
|
|
710
|
+
editable: true,
|
|
711
|
+
dateMin: "2026-01-01", // the picker will not accept earlier days
|
|
712
|
+
dateMax: "2027-12-31",
|
|
713
|
+
},
|
|
714
|
+
];
|
|
715
|
+
|
|
716
|
+
<DataGrid columns={columns} rows={tasks} editable onRowsChange={setTasks} />
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
**What a row may hold.** The cell understands `"2026-11-05"`, a full ISO string (`"2026-11-05T10:30:00Z"`, whose date part is used as written), a `Date` object, a timestamp number, or any string `Date.parse` understands. Empty values show blank and sort last. Text that is not a date is shown as it is.
|
|
720
|
+
|
|
721
|
+
**Editing.** Double-click a date cell (or press Enter / F2) to open the browser's date picker. Typing characters does not start an editor on a date cell, because the picker is its own control. After picking or typing a valid date, Enter or a click elsewhere saves it. An empty picker saves `null`. Escape cancels. A half-typed date is ignored when you click away.
|
|
722
|
+
|
|
723
|
+
**The saved value keeps the shape the row already had.**
|
|
724
|
+
|
|
725
|
+
| Row held | Saved value |
|
|
726
|
+
| --- | --- |
|
|
727
|
+
| `"2026-03-14"` (text) | `"2026-03-14"` |
|
|
728
|
+
| a `Date` object | a `Date` at local midnight |
|
|
729
|
+
| a timestamp number | a timestamp at local midnight |
|
|
730
|
+
| empty | the same shape as the other rows in the column (the ISO text when there are none) |
|
|
731
|
+
|
|
732
|
+
Dates are handled in the **user's local time zone**, so typing 5 November never turns into 4 November because of UTC.
|
|
733
|
+
|
|
734
|
+
**Display.** The default is the browser's medium date format (`Nov 5, 2026`). Change it with `dateLocale` and `dateFormat` (any `Intl.DateTimeFormat` options):
|
|
735
|
+
|
|
736
|
+
```tsx
|
|
737
|
+
{ field: "created", type: "date", dateLocale: "en-GB", dateFormat: { dateStyle: "long" } } // 5 November 2026
|
|
738
|
+
{ field: "created", type: "date", dateLocale: "de-DE", dateFormat: { day: "2-digit", month: "2-digit", year: "numeric" } } // 05.11.2026
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
A `valueFormatter` still wins over these two if you set one.
|
|
742
|
+
|
|
743
|
+
**Sorting and filtering.** Dates sort by day (earliest first, empties last). The filter popup has From and To pickers; either may be empty for an open-ended range:
|
|
744
|
+
|
|
745
|
+
```tsx
|
|
746
|
+
<DataGrid filters={{ due: { from: "2026-03-01", to: "2026-06-30" } }} onFiltersChange={setFilters} ... />
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**CSV export** writes dates as `"YYYY-MM-DD"` unless you give `exportValue`.
|
|
750
|
+
|
|
751
|
+
### 6.7 Pagination
|
|
752
|
+
|
|
753
|
+
Pagination is off by default. Switch it on to split the rows into pages and show the bar with the row range ("11–20 of 200"), a rows-per-page menu, first / previous / next / last buttons and "Page x of y". **The bar (and its page information) only appears when `pagination` is passed.**
|
|
754
|
+
|
|
755
|
+
```tsx
|
|
756
|
+
<DataGrid pagination pageSize={20} pageSizeOptions={[10, 20, 50, 100]} paginationPosition="both" ... />
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
| `paginationPosition` | Result |
|
|
760
|
+
| --- | --- |
|
|
761
|
+
| `"bottom"` | Bar under the table (default) |
|
|
762
|
+
| `"top"` | Bar above the table, below the toolbar |
|
|
763
|
+
| `"both"` | Both bars, kept in sync |
|
|
764
|
+
|
|
765
|
+
Without `pagination` every row is rendered and there is no footer at all. Combine it with `maxHeight` for a scrolling grid with a sticky header:
|
|
766
|
+
|
|
767
|
+
```tsx
|
|
768
|
+
<DataGrid maxHeight={400} ... />
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
`pageSize` is only read when the grid first mounts. The user's choice in the menu is then kept inside the grid. To own the page, see [6.14](#614-controlled-state).
|
|
772
|
+
|
|
773
|
+
**Reacting to paging:**
|
|
774
|
+
|
|
775
|
+
```tsx
|
|
776
|
+
<DataGrid
|
|
777
|
+
pagination
|
|
778
|
+
onPageChange={(page) => console.log("page", page)} // zero-based
|
|
779
|
+
onPageSizeChange={(size) => localStorage.setItem("size", String(size))}
|
|
780
|
+
pageSize={Number(localStorage.getItem("size")) || 10}
|
|
781
|
+
...
|
|
782
|
+
/>
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### 6.8 Row click, toolbar and empty state
|
|
786
|
+
|
|
787
|
+
```tsx
|
|
788
|
+
<DataGrid
|
|
789
|
+
onRowClick={(row, rowIndex) => navigate(`/people/${row.id}`)}
|
|
790
|
+
toolbar={<button onClick={openNewPersonDialog}>Add person</button>}
|
|
791
|
+
emptyMessage="Nothing matches your search"
|
|
792
|
+
striped
|
|
793
|
+
/>
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
The toolbar row is drawn when at least one of `searchable`, `columnMenu`, `exportable` or `toolbar` is used. Your `toolbar` content sits on the right, next to the built-in buttons. To style a toolbar button the same way as the built-in ones, give it the class `rk-grid__tool-btn`:
|
|
797
|
+
|
|
798
|
+
```tsx
|
|
799
|
+
toolbar={<button className="rk-grid__tool-btn" onClick={refetch}>Reload</button>}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
### 6.9 Editing
|
|
803
|
+
|
|
804
|
+
Turn editing on with `editable` on the grid, then lock or unlock columns:
|
|
805
|
+
|
|
806
|
+
```tsx
|
|
807
|
+
<DataGrid editable columns={[
|
|
808
|
+
{ field: "id", editable: false }, // locked column
|
|
809
|
+
{ field: "name" }, // editable (grid default)
|
|
810
|
+
{ field: "status", editable: ({ row }) => row.department !== "HR" } // decided per cell
|
|
811
|
+
]} />
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
A column can also be the only editable one: leave the grid's `editable` off and set `editable: true` on that column.
|
|
815
|
+
|
|
816
|
+
**Starting an edit**
|
|
817
|
+
|
|
818
|
+
| Action | Result |
|
|
819
|
+
| --- | --- |
|
|
820
|
+
| Double-click a cell | Opens the editor, cursor at the end |
|
|
821
|
+
| Select a cell (click or Tab) and press **Enter** or **F2** | Opens the editor, cursor at the end |
|
|
822
|
+
| Select a cell and type a character | Opens the editor with that character replacing the old value |
|
|
823
|
+
|
|
824
|
+
Typing to start does not apply to columns with a custom `cellEditor`. Those open with double-click, Enter or F2.
|
|
825
|
+
|
|
826
|
+
**While editing**
|
|
827
|
+
|
|
828
|
+
| Key | Result |
|
|
829
|
+
| --- | --- |
|
|
830
|
+
| **Enter** | Save, then edit the cell below (the text is pre-selected so typing replaces it) |
|
|
831
|
+
| **Shift+Enter** | Save, then edit the cell above |
|
|
832
|
+
| **Tab** / **Shift+Tab** | Save, then edit the next / previous editable cell, wrapping across rows |
|
|
833
|
+
| **Esc** | Cancel without saving |
|
|
834
|
+
| Click elsewhere | Save |
|
|
835
|
+
|
|
836
|
+
Details:
|
|
837
|
+
|
|
838
|
+
- When there is no editable cell in that direction (for example Enter on the last row of the page), the value is saved and the editor closes. Focus returns to the cell.
|
|
839
|
+
- Set `enterMovesDown={false}` to make Enter save and stay put.
|
|
840
|
+
- Cells locked by `editable` are skipped when moving.
|
|
841
|
+
- If sorting, filtering or paging changes the position of rows while you move between cells, editing follows the row, not the screen position.
|
|
842
|
+
|
|
843
|
+
**How the typed text becomes a value**
|
|
844
|
+
|
|
845
|
+
1. If the column has a `valueParser`, its return value is stored.
|
|
846
|
+
2. Otherwise, in a `type: "number"` column, an empty text stores `null`, a valid number stores that number, and anything else is rejected.
|
|
847
|
+
3. Otherwise the text is stored as a string.
|
|
848
|
+
|
|
849
|
+
**Validation.** After parsing, `validate(value, row)` is called. If it returns `false` (or a number could not be parsed), the value is not saved, the editor stays open, and it gets the `rk-grid__editor--invalid` class and `aria-invalid="true"`. Pressing Esc or clicking away discards the invalid text.
|
|
850
|
+
|
|
851
|
+
```tsx
|
|
852
|
+
{ field: "email", validate: (v) => /^\S+@\S+\.\S+$/.test(String(v)) }
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
A value that equals the old value is treated as "no change" and fires no events.
|
|
856
|
+
|
|
857
|
+
**Events.** When a value is saved the grid calls, in this order:
|
|
858
|
+
|
|
859
|
+
```ts
|
|
860
|
+
onCellEdit({ rowId, rowIndex, field, oldValue, newValue, row }) // row is the updated row
|
|
861
|
+
onRowsChange(nextRows) // the whole updated array
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
**Keeping your data in sync.** The simplest pattern is state plus `onRowsChange`:
|
|
865
|
+
|
|
866
|
+
```tsx
|
|
867
|
+
const [rows, setRows] = useState(initialRows);
|
|
868
|
+
<DataGrid rows={rows} onRowsChange={setRows} editable />
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
Important: the grid copies `rows` into its own state and re-syncs whenever the `rows` prop *changes identity*. If your parent re-creates the array on every render (for example `rows={data.map(...)}` written inline), the grid resets to it each time and edits are lost. Keep `rows` in state or memoize it with `useMemo`.
|
|
872
|
+
|
|
873
|
+
**Saving to a server:**
|
|
874
|
+
|
|
875
|
+
```tsx
|
|
876
|
+
<DataGrid
|
|
877
|
+
rows={rows}
|
|
878
|
+
onRowsChange={setRows}
|
|
879
|
+
onCellEdit={async (e) => {
|
|
880
|
+
await fetch(`/api/people/${e.rowId}`, {
|
|
881
|
+
method: "PATCH",
|
|
882
|
+
headers: { "Content-Type": "application/json" },
|
|
883
|
+
body: JSON.stringify({ [e.field]: e.newValue }),
|
|
884
|
+
});
|
|
885
|
+
}}
|
|
886
|
+
/>
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### 6.10 Custom components per column
|
|
890
|
+
|
|
891
|
+
Four hooks let you replace parts of a column with your own React components. They are rendered as real components, so they can use hooks and state.
|
|
892
|
+
|
|
893
|
+
> **Define these components at module level**, not inline inside your render function. An inline component gets a new identity on every render, so React unmounts and remounts it each time (inputs lose focus, state resets).
|
|
894
|
+
|
|
895
|
+
#### `cellRenderer`: custom cell content
|
|
896
|
+
|
|
897
|
+
Receives `CellParams<T>`:
|
|
898
|
+
|
|
899
|
+
| Prop | Description |
|
|
900
|
+
| --- | --- |
|
|
901
|
+
| `value` | The raw value from the row |
|
|
902
|
+
| `formattedValue` | The value as text, after `valueFormatter` |
|
|
903
|
+
| `row` | The whole row |
|
|
904
|
+
| `rowIndex` | Index of the row in `rows` |
|
|
905
|
+
| `field` | The column's field |
|
|
906
|
+
| `column` | The column definition |
|
|
907
|
+
| `editable` | `true` if the cell may be changed (the column's `editable`, or the grid's) |
|
|
908
|
+
| `updateValue(value)` | Saves a new value for this cell right away, without opening the editor. It runs `validate` and fires `onCellEdit` / `onRowsChange`. Returns `false` if the value was rejected or the cell is not editable |
|
|
909
|
+
|
|
910
|
+
```tsx
|
|
911
|
+
function StatusCell({ value }: CellParams<Employee>) {
|
|
912
|
+
return <span className={value === "active" ? "text-green-700" : "text-gray-500"}>{String(value)}</span>;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
{ field: "status", cellRenderer: StatusCell }
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
Searching, filtering and sorting still use the underlying value and `valueFormatter`, not what your component draws.
|
|
919
|
+
|
|
920
|
+
#### `cellEditor`: custom editor
|
|
921
|
+
|
|
922
|
+
Replaces the text box when the cell is being edited. Receives `CellEditorProps<T>`:
|
|
923
|
+
|
|
924
|
+
| Prop | Description |
|
|
925
|
+
| --- | --- |
|
|
926
|
+
| `value` | The current draft value. Starts as the cell's raw value |
|
|
927
|
+
| `row`, `rowIndex`, `field`, `column` | Same as in `CellParams` |
|
|
928
|
+
| `onChange(value)` | Update the draft without saving |
|
|
929
|
+
| `onCommit(value?, move?)` | Save and close. Pass a value to save it, or nothing to save the latest draft. `move` can be `"down" \| "up" \| "left" \| "right" \| "none"` (default `"none"`) and also starts editing the neighbouring cell |
|
|
930
|
+
| `onCancel()` | Close without saving |
|
|
931
|
+
|
|
932
|
+
Rules your editor should follow:
|
|
933
|
+
|
|
934
|
+
- Focus something when it mounts (`autoFocus` or a ref) so the keyboard works straight away.
|
|
935
|
+
- Esc is handled by the grid. Clicking or tabbing out of the editor saves the current draft.
|
|
936
|
+
- A value passed to `onCommit` is stored as is. `valueParser` and the `type: "number"` conversion are not applied, but `validate` still is.
|
|
937
|
+
|
|
938
|
+
```tsx
|
|
939
|
+
function StatusEditor({ value, onCommit }: CellEditorProps<Employee>) {
|
|
940
|
+
return (
|
|
941
|
+
<select autoFocus value={String(value)} onChange={(e) => onCommit(e.target.value)}>
|
|
942
|
+
<option>active</option>
|
|
943
|
+
<option>pending</option>
|
|
944
|
+
<option>inactive</option>
|
|
945
|
+
</select>
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
{ field: "status", cellEditor: StatusEditor }
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
An editor with its own keyboard handling, saving and moving down on Enter:
|
|
953
|
+
|
|
954
|
+
```tsx
|
|
955
|
+
function SliderEditor({ value, onChange, onCommit }: CellEditorProps<Employee>) {
|
|
956
|
+
return (
|
|
957
|
+
<input
|
|
958
|
+
autoFocus
|
|
959
|
+
type="range"
|
|
960
|
+
value={Number(value) || 0}
|
|
961
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
962
|
+
onKeyDown={(e) => e.key === "Enter" && onCommit(undefined, "down")}
|
|
963
|
+
/>
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
#### `headerRenderer`: custom header label
|
|
969
|
+
|
|
970
|
+
Receives `{ column, sortDirection }`. The filter indicator (if `filterable`) and the sort arrow are still added next to it. The whole header cell is clickable for sorting, so call `event.stopPropagation()` on any interactive element you put inside.
|
|
971
|
+
|
|
972
|
+
```tsx
|
|
973
|
+
function SalaryHeader({ column }: HeaderRendererProps<Employee>) {
|
|
974
|
+
return <span>{column.headerName} <small>USD</small></span>;
|
|
975
|
+
}
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
#### `filterRenderer`: custom filter control
|
|
979
|
+
|
|
980
|
+
Shown inside the filter popup instead of the text box (the column still needs `filterable: true`). Receives `{ column, value, onChange }`, where `value` is the filter text. Whatever string you pass to `onChange` is matched with "contains" against the displayed text.
|
|
981
|
+
|
|
982
|
+
```tsx
|
|
983
|
+
function DepartmentFilter({ value, onChange }: FilterRendererProps<Employee>) {
|
|
984
|
+
return (
|
|
985
|
+
<select value={value} onChange={(e) => onChange(e.target.value)}>
|
|
986
|
+
<option value="">All</option>
|
|
987
|
+
<option>Engineering</option>
|
|
988
|
+
<option>Design</option>
|
|
989
|
+
</select>
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
#### Checkbox columns (`type: "boolean"`)
|
|
995
|
+
|
|
996
|
+
Set `type: "boolean"` on a column and each cell shows a [Checkbox](#5-checkbox). One click saves the new `true` / `false` value.
|
|
997
|
+
|
|
998
|
+
```tsx
|
|
999
|
+
const columns: ColumnDef<Task>[] = [
|
|
1000
|
+
{ field: "title", headerName: "Task" },
|
|
1001
|
+
{ field: "done", type: "boolean", align: "center", checkbox: { variant: "button" } }, // no headerName: the header is empty
|
|
1002
|
+
{ field: "starred", headerName: "Starred", type: "boolean", checkbox: { variant: "button", indicator: "★" } }, // custom character
|
|
1003
|
+
{ field: "urgent", headerName: "Urgent", type: "boolean", align: "center", checkbox: { variant: "tick" } },
|
|
1004
|
+
{ field: "archived", headerName: "Archived", type: "boolean", align: "center" }, // grid default: native
|
|
1005
|
+
{ field: "approved", headerName: "Approved", type: "boolean", editable: false }, // shown disabled
|
|
1006
|
+
];
|
|
1007
|
+
|
|
1008
|
+
<DataGrid columns={columns} rows={rows} editable onRowsChange={setRows} checkboxVariant="native" />
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
How it behaves:
|
|
1012
|
+
|
|
1013
|
+
- **One click saves.** The change goes through the same path as a typed edit: `validate` runs, then `onCellEdit` and `onRowsChange` fire. Space toggles a focused checkbox, and Tab reaches them.
|
|
1014
|
+
- **Who can click.** A checkbox is active only where the cell is editable: grid `editable`, overridden per column by `editable: true | false` or a function. Otherwise it is shown disabled.
|
|
1015
|
+
- **No text editor.** Boolean cells never open the text editor, and Enter and Tab navigation skip them. A click on the checkbox does not trigger `onRowClick`.
|
|
1016
|
+
- **Sorting, search and filters.** Unchecked sorts before checked. Search and filters match the text `Yes` / `No`, or whatever your `valueFormatter` returns. A missing value (`null`, `undefined`) shows unchecked.
|
|
1017
|
+
- **Header label.** The header of a boolean column is empty unless you set `headerName`. Screen readers still get the field name.
|
|
1018
|
+
- **Custom character.** `checkbox: { indicator: "★" }` draws that character instead of the default fill or tick when checked. Colour it with `--rk-checkbox-color` (button variant) or `--rk-checkbox-tick` (tick variant), for example through `checkbox.styles.root`.
|
|
1019
|
+
- **Defaults.** 1px border, no rounded corners. See [Checkbox](#5-checkbox).
|
|
1020
|
+
- **A column with its own `cellEditor`** is treated as a normal editable column instead.
|
|
1021
|
+
|
|
1022
|
+
**Choosing and styling the checkbox**
|
|
1023
|
+
|
|
1024
|
+
| Where | How |
|
|
1025
|
+
| --- | --- |
|
|
1026
|
+
| Whole grid | `checkboxVariant="tick"` |
|
|
1027
|
+
| One column | `checkbox: { variant: "tick" }` |
|
|
1028
|
+
| Whole grid, classes or inline styles | `classNames` / `styles` slots `checkbox`, `checkboxControl`, `checkboxIndicator` |
|
|
1029
|
+
| One column, classes or inline styles | `checkbox: { className, style, classNames, styles, indicator }` (same parts as the standalone `Checkbox`) |
|
|
1030
|
+
| Colours and size | CSS variables such as `--rk-checkbox-color`, set on the grid's `className` / `style`, or per column in `checkbox.styles.root` |
|
|
1031
|
+
|
|
1032
|
+
```tsx
|
|
1033
|
+
<DataGrid
|
|
1034
|
+
checkboxVariant="tick"
|
|
1035
|
+
classNames={{
|
|
1036
|
+
checkboxControl: "rounded-full border-emerald-500 aria-checked:bg-emerald-500",
|
|
1037
|
+
checkboxIndicator: "text-white",
|
|
1038
|
+
}}
|
|
1039
|
+
...
|
|
1040
|
+
/>
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
When both are given, the column's options are merged after the grid-level ones, so the column wins.
|
|
1044
|
+
|
|
1045
|
+
**Fully custom checkbox in a cell.** Any `cellRenderer` can toggle the value itself through `updateValue`. This example is the "button with a span inside" written by hand:
|
|
1046
|
+
|
|
1047
|
+
```tsx
|
|
1048
|
+
function PinCell({ value, editable, updateValue }: CellParams<Task>) {
|
|
1049
|
+
return (
|
|
1050
|
+
<button
|
|
1051
|
+
type="button"
|
|
1052
|
+
aria-pressed={!!value}
|
|
1053
|
+
disabled={!editable}
|
|
1054
|
+
onClick={(e) => {
|
|
1055
|
+
e.stopPropagation(); // keep onRowClick from also firing
|
|
1056
|
+
updateValue(!value);
|
|
1057
|
+
}}
|
|
1058
|
+
>
|
|
1059
|
+
<span style={{ visibility: value ? "visible" : "hidden" }}>✓</span>
|
|
1060
|
+
</button>
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
{ field: "pinned", cellRenderer: PinCell }
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
#### Select-all header checkbox
|
|
1068
|
+
|
|
1069
|
+
Add `headerCheckbox: true` to a boolean column. The header label becomes a checkbox (the header name is its label, or no label if the name is empty) that checks or clears the whole column.
|
|
1070
|
+
|
|
1071
|
+
```tsx
|
|
1072
|
+
{ field: "done", headerName: "Done", type: "boolean", align: "center", headerCheckbox: true, checkbox: { variant: "tick" } }
|
|
1073
|
+
{ field: "done", type: "boolean", headerCheckbox: true } // just the checkbox, no text
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
- **Click:** if every target row is checked, it unchecks them all. Otherwise (none or only some are checked) it checks them all.
|
|
1077
|
+
- **Which rows:** every row that matches the current search and column filters, on all pages, and whose cell is editable. Rows the filters hide are left alone.
|
|
1078
|
+
- **State:** checked when all target rows are checked, a dash (mixed) when only some are, unchecked when none are. It updates as you toggle individual rows.
|
|
1079
|
+
- **Events:** `onCellEdit` fires once for each row that changed, and `onRowsChange` fires once with the whole updated array. Rows already at the target value, and rows where `validate` rejects the value, are skipped.
|
|
1080
|
+
- **Locked columns:** if no cell in the column is editable, the header checkbox is disabled but still shows the real state.
|
|
1081
|
+
- **Look:** it uses the same variant and styling as the column's cell checkboxes (`checkbox` options, `checkboxVariant`, and the `checkbox`, `checkboxControl` and `checkboxIndicator` slots).
|
|
1082
|
+
- **Sorting:** clicking the checkbox or its label does not sort. Click the sort arrow beside it (or the empty part of the header cell) to sort.
|
|
1083
|
+
- **Custom header:** if the column also has a `headerRenderer`, that is used and `headerCheckbox` is ignored. `headerCheckbox` does nothing on columns that are not `type: "boolean"`.
|
|
1084
|
+
|
|
1085
|
+
### 6.11 Styling
|
|
1086
|
+
|
|
1087
|
+
#### Whole grid
|
|
1088
|
+
|
|
1089
|
+
```tsx
|
|
1090
|
+
<DataGrid className="rounded-xl shadow-lg" style={{ maxWidth: 900 }} ... />
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
#### Parts of the grid
|
|
1094
|
+
|
|
1095
|
+
`classNames` and `styles` take the same keys. Each key is a part ("slot") of the grid:
|
|
1096
|
+
|
|
1097
|
+
| Slot | Element |
|
|
1098
|
+
| --- | --- |
|
|
1099
|
+
| `root` | Outer container |
|
|
1100
|
+
| `toolbar` | Bar holding the search box and your `toolbar` content |
|
|
1101
|
+
| `search` | Global search input |
|
|
1102
|
+
| `scroll` | Scrolling wrapper around the table |
|
|
1103
|
+
| `table` | The `<table>` |
|
|
1104
|
+
| `thead` | The `<thead>` |
|
|
1105
|
+
| `headerRow` | The row of column headers |
|
|
1106
|
+
| `headerCell` | Each column header (`<th>`) |
|
|
1107
|
+
| `filterIndicator` | Each column's filter indicator button |
|
|
1108
|
+
| `filterPopup` | The filter popup (drawn in `document.body`) |
|
|
1109
|
+
| `filterInput` | The default filter text box in the popup |
|
|
1110
|
+
| `tbody` | The `<tbody>` |
|
|
1111
|
+
| `row` | Each body row |
|
|
1112
|
+
| `cell` | Each body cell (`<td>`) |
|
|
1113
|
+
| `editor` | The default text editor input |
|
|
1114
|
+
| `empty` | The "no rows" cell |
|
|
1115
|
+
| `footer` | The pagination bar (both top and bottom) |
|
|
1116
|
+
| `rowCount` | The "1–10 of 200" text |
|
|
1117
|
+
| `pageInfo` | The "Page 1 of 20" text |
|
|
1118
|
+
| `pageSize` | The rows-per-page `<select>` |
|
|
1119
|
+
| `pagination` | Wrapper around the page-size menu and buttons |
|
|
1120
|
+
| `toolButton` | Toolbar buttons (Columns, Export CSV, Retry) |
|
|
1121
|
+
| `skeleton` | The shimmering bars of the loading skeleton |
|
|
1122
|
+
| `pageButton` | Each first/previous/next/last button |
|
|
1123
|
+
| `checkbox` | Root of each checkbox in a `type: "boolean"` column |
|
|
1124
|
+
| `checkboxControl` | The checkbox's button or input |
|
|
1125
|
+
| `checkboxIndicator` | The span inside the `"button"` / `"tick"` checkboxes |
|
|
1126
|
+
|
|
1127
|
+
#### Tailwind
|
|
1128
|
+
|
|
1129
|
+
```tsx
|
|
1130
|
+
<DataGrid
|
|
1131
|
+
className="rounded-2xl border-0 shadow-xl ring-1 ring-slate-200"
|
|
1132
|
+
classNames={{
|
|
1133
|
+
toolbar: "bg-slate-50 px-4 py-3",
|
|
1134
|
+
search: "rounded-full border-slate-300 px-4",
|
|
1135
|
+
headerCell: "bg-slate-900 text-xs uppercase tracking-wider text-slate-100",
|
|
1136
|
+
row: "hover:bg-indigo-50",
|
|
1137
|
+
footer: "bg-slate-50 text-slate-500",
|
|
1138
|
+
pageButton: "rounded-full hover:bg-indigo-100",
|
|
1139
|
+
}}
|
|
1140
|
+
rowClassName={(row) => (row.status === "inactive" ? "opacity-50" : "")}
|
|
1141
|
+
/>
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
You do not need to add the library to Tailwind's `content` setting, because the class names live in your own code.
|
|
1145
|
+
|
|
1146
|
+
The grid's built-in CSS is written with `:where()`, which gives it zero specificity. That means any class you add, Tailwind utilities included, overrides it without `!important`.
|
|
1147
|
+
|
|
1148
|
+
#### Inline styles
|
|
1149
|
+
|
|
1150
|
+
```tsx
|
|
1151
|
+
<DataGrid
|
|
1152
|
+
style={{ border: "2px solid #111", borderRadius: 0 }}
|
|
1153
|
+
styles={{
|
|
1154
|
+
headerCell: { background: "#111", color: "#ffe66d" },
|
|
1155
|
+
footer: { background: "#ffe66d" },
|
|
1156
|
+
}}
|
|
1157
|
+
rowStyle={(row) => (row.status === "pending" ? { background: "#fff7cc" } : {})}
|
|
1158
|
+
/>
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
When both are given, `style` wins over `styles.root`.
|
|
1162
|
+
|
|
1163
|
+
#### Per column and per cell
|
|
1164
|
+
|
|
1165
|
+
`className` and `style` style a whole column (its header and every cell). The `header*` and `cell*` options target only one part:
|
|
1166
|
+
|
|
1167
|
+
```tsx
|
|
1168
|
+
{
|
|
1169
|
+
field: "salary",
|
|
1170
|
+
className: "font-mono text-slate-700", // header and every cell (Tailwind)
|
|
1171
|
+
style: { letterSpacing: 0.3 }, // header and every cell (inline)
|
|
1172
|
+
headerClassName: "text-right",
|
|
1173
|
+
headerStyle: { minWidth: 120 },
|
|
1174
|
+
cellClassName: ({ value }) => (Number(value) > 100000 ? "font-bold text-green-600" : ""),
|
|
1175
|
+
cellStyle: ({ value }) => (value === null ? { color: "#9ca3af" } : {}),
|
|
1176
|
+
}
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
#### Striped rows
|
|
1180
|
+
|
|
1181
|
+
```tsx
|
|
1182
|
+
<DataGrid striped /> // default shading
|
|
1183
|
+
<DataGrid stripeColors={["#fef3c7", "#e0f2fe"]} /> // your two colours (implies striped)
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
Rows alternate between the two colours, starting with the first. `stripeColors` sets `--rk-stripe-a` and `--rk-stripe-b` on the grid. You can also set those variables in your own CSS or theme, and turn on `striped`. Hovering a row still highlights it (`--rk-row-hover`).
|
|
1187
|
+
|
|
1188
|
+
#### Row animation
|
|
1189
|
+
|
|
1190
|
+
When rows render (the first load, a new page, a sort, a search or a filter), they slide up and fade in, one after another. It is on by default.
|
|
1191
|
+
|
|
1192
|
+
```tsx
|
|
1193
|
+
<DataGrid animateRows={false} /> // turn it off
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
Tune it with `--rk-row-duration` (default `0.38s`) and `--rk-row-stagger` (default `28ms` per row, up to 24 rows). Changing page or sort also scrolls the grid's own scroll area smoothly back to the top. Visitors who ask their system for reduced motion get no animation (`prefers-reduced-motion`).
|
|
1197
|
+
|
|
1198
|
+
#### Width
|
|
1199
|
+
|
|
1200
|
+
The grid is `width: 100%` of its container by default (it never overflows it: `max-width: 100%; min-width: 0`). To make it narrower, give it a width through `className` (`max-w-3xl`) or `style` (`{ maxWidth: 900 }`).
|
|
1201
|
+
|
|
1202
|
+
#### Plain CSS class reference
|
|
1203
|
+
|
|
1204
|
+
If you prefer writing CSS, these class names are stable.
|
|
1205
|
+
|
|
1206
|
+
| Class | Element |
|
|
1207
|
+
| --- | --- |
|
|
1208
|
+
| `.rk-grid` | Root |
|
|
1209
|
+
| `.rk-grid__toolbar`, `.rk-grid__search` | Toolbar and search box |
|
|
1210
|
+
| `.rk-grid__scroll`, `.rk-grid__table`, `.rk-grid__thead`, `.rk-grid__tbody` | Table structure |
|
|
1211
|
+
| `.rk-grid__tr` | Every row |
|
|
1212
|
+
| `.rk-grid__th`, `.rk-grid__th--sortable`, `.rk-grid__th-content`, `.rk-grid__sort` | Header cells and sort arrow |
|
|
1213
|
+
| `.rk-grid__filter-btn`, `.rk-grid__filter-btn--active`, `.rk-grid__filter-btn--open`, `.rk-grid__filter-dot` | Filter indicator button and its default dot |
|
|
1214
|
+
| `.rk-grid-popup`, `.rk-grid__filter-input`, `.rk-grid__filter-clear` | Filter popup (in `document.body`), its text box and Clear button |
|
|
1215
|
+
| `.rk-grid__row`, `.rk-grid__row--stripe-a`, `.rk-grid__row--stripe-b`, `.rk-grid__row--enter`, `.rk-grid__row--clickable` | Body rows |
|
|
1216
|
+
| `.rk-grid__td`, `.rk-grid__td--editable`, `.rk-grid__td--editing` | Body cells |
|
|
1217
|
+
| `.rk-grid__editor`, `.rk-grid__editor--invalid`, `.rk-grid__custom-editor` | Editors |
|
|
1218
|
+
| `.rk-grid__empty` | Empty state |
|
|
1219
|
+
| `.rk-grid__checkbox-cell` | Wrapper around a checkbox in a boolean column (the checkbox's own classes start with `.rk-checkbox`, see [Checkbox](#5-checkbox)) |
|
|
1220
|
+
| `.rk-grid__footer`, `.rk-grid__footer--top` | Pagination bar (`data-position="top"` or `"bottom"`) |
|
|
1221
|
+
| `.rk-grid__count`, `.rk-grid__pagination`, `.rk-grid__page-size-label`, `.rk-grid__page-size`, `.rk-grid__page-btn`, `.rk-grid__page-info` | Pagination parts |
|
|
1222
|
+
| `.rk-grid__toolbar-left`, `.rk-grid__toolbar-actions` | The two halves of the toolbar (search and selected count / buttons) |
|
|
1223
|
+
| `.rk-grid__tool-btn`, `.rk-grid__selected-count` | Toolbar buttons and the "N selected" label |
|
|
1224
|
+
| `.rk-grid__select-cell`, `.rk-grid__row--selected` | Row-selection checkbox cell and a selected row |
|
|
1225
|
+
| `.rk-grid__pinned`, `.rk-grid__pinned--left`, `.rk-grid__pinned--right`, `.rk-grid__pinned--edge` | Pinned cells (the edge class marks the last left / first right one) |
|
|
1226
|
+
| `.rk-grid__resizer` | The column resize handle |
|
|
1227
|
+
| `.rk-grid__sort-order` | The priority number next to a sort arrow in multi-column sort |
|
|
1228
|
+
| `.rk-grid--virtual`, `.rk-grid__spacer` | Virtualized grid and its spacer rows |
|
|
1229
|
+
| `.rk-grid__skeleton`, `.rk-grid__skeleton-row`, `.rk-grid__skeleton-cell` | Loading placeholder bar, row and (custom) cell |
|
|
1230
|
+
| `.rk-grid__error` | The error message cell (with the Retry button) |
|
|
1231
|
+
| `.rk-grid__filter-fields`, `.rk-grid__filter-field`, `.rk-grid__filter-options`, `.rk-grid__filter-empty` | Controls inside the filter popup (number, date, select) |
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
### 6.12 Nested fields
|
|
1235
|
+
|
|
1236
|
+
A column's `field` can point into nested objects with dot notation:
|
|
1237
|
+
|
|
1238
|
+
```tsx
|
|
1239
|
+
interface Person {
|
|
1240
|
+
id: number;
|
|
1241
|
+
name: string;
|
|
1242
|
+
contact: { email: string; phone: string };
|
|
1243
|
+
address: { city: string };
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const columns: ColumnDef<Person>[] = [
|
|
1247
|
+
{ field: "name" },
|
|
1248
|
+
{ field: "contact.email", headerName: "Email", filterable: true },
|
|
1249
|
+
{ field: "contact.phone", headerName: "Phone" },
|
|
1250
|
+
{ field: "address.city", headerName: "City" },
|
|
1251
|
+
];
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
- **TypeScript checks the path.** `field` is typed `FieldPath<Person>`: `"contact.phone"` is accepted, `"contact.fax"` is a type error. It follows up to 5 levels deep, and arrays and dates are treated as plain values.
|
|
1255
|
+
- **Everything uses the path:** display, global search, filters, sorting, editing, `valueFormatter`, `cellRenderer` (`value`, `field`), and boolean columns and select-all headers (`"prefs.newsletter"`).
|
|
1256
|
+
- **Editing never mutates your data.** Saving `contact.phone` copies `row` and `row.contact` and replaces the value, so the original objects are left alone. A missing level on the way is created. `onCellEdit` reports `field: "contact.phone"` and the updated row.
|
|
1257
|
+
- **A missing level reads as empty.** If `row.contact` is `undefined`, the cell is blank and sorts last. No error is thrown.
|
|
1258
|
+
- A key that really contains a dot (`{ "a.b": 1 }`) is read as that literal key first.
|
|
1259
|
+
- `__proto__`, `prototype` and `constructor` are never read or written through a path.
|
|
1260
|
+
- `getRowId`, `onRowClick` and other callbacks work as before.
|
|
1261
|
+
|
|
1262
|
+
### 6.13 Row selection
|
|
1263
|
+
|
|
1264
|
+
Off by default. `rowSelection` adds a checkbox column on the left.
|
|
1265
|
+
|
|
1266
|
+
```tsx
|
|
1267
|
+
<DataGrid columns={columns} rows={rows} getRowId={(r) => r.id} rowSelection />
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
| Value | Behaviour |
|
|
1271
|
+
| --- | --- |
|
|
1272
|
+
| `true` or `"multiple"` | Many rows. The header has a select-all checkbox. Shift+click selects a range |
|
|
1273
|
+
| `"single"` | One row at a time (selecting another replaces it). No select-all checkbox |
|
|
1274
|
+
|
|
1275
|
+
- Selected rows get the class `rk-grid__row--selected`, the colour `--rk-selected-bg` and `aria-selected="true"`.
|
|
1276
|
+
- Selection is kept **by row id**, so it survives paging, sorting, searching and filtering. Pass `getRowId` so ids are stable.
|
|
1277
|
+
- The **select-all checkbox** in the header acts on every selectable row that matches the current search and filters, **across all pages**. It shows a dash while only some of them are selected.
|
|
1278
|
+
- **Shift+click** on a row's checkbox selects (or clears) everything between it and the last row you clicked, as shown on screen. Disabled rows in between are skipped.
|
|
1279
|
+
- A "N selected" count appears in the toolbar (when a toolbar is shown) and in the pagination bar.
|
|
1280
|
+
|
|
1281
|
+
**Reading the selection** (`onSelectionChange` receives the ids and the row objects):
|
|
1282
|
+
|
|
1283
|
+
```tsx
|
|
1284
|
+
function Page() {
|
|
1285
|
+
const [selection, setSelection] = useState<RowSelection<Person>>({ ids: [], rows: [] });
|
|
1286
|
+
|
|
1287
|
+
return (
|
|
1288
|
+
<>
|
|
1289
|
+
<button disabled={!selection.ids.length} onClick={() => deletePeople(selection.ids)}>
|
|
1290
|
+
Delete {selection.ids.length} people
|
|
1291
|
+
</button>
|
|
1292
|
+
<DataGrid
|
|
1293
|
+
columns={columns}
|
|
1294
|
+
rows={rows}
|
|
1295
|
+
getRowId={(r) => r.id}
|
|
1296
|
+
rowSelection
|
|
1297
|
+
onSelectionChange={setSelection}
|
|
1298
|
+
/>
|
|
1299
|
+
</>
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
**Controlled selection.** Pass `selectedRowIds` and keep it up to date from `onSelectionChange`:
|
|
1305
|
+
|
|
1306
|
+
```tsx
|
|
1307
|
+
const [ids, setIds] = useState<(string | number)[]>([2, 4]);
|
|
1308
|
+
|
|
1309
|
+
<button onClick={() => setIds([])}>Clear</button>
|
|
1310
|
+
<DataGrid rowSelection selectedRowIds={ids} onSelectionChange={(s) => setIds(s.ids)} ... />
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
Or start with some rows selected and let the grid manage it: `defaultSelectedRowIds={[2, 4]}`.
|
|
1314
|
+
|
|
1315
|
+
**Rows that cannot be selected.** Return `false` from `isRowSelectable`. The row's checkbox is disabled, select-all skips it and Shift ranges pass over it:
|
|
1316
|
+
|
|
1317
|
+
```tsx
|
|
1318
|
+
<DataGrid rowSelection isRowSelectable={(row) => row.status !== "archived"} ... />
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
**Single selection**, for a master / detail layout:
|
|
1322
|
+
|
|
1323
|
+
```tsx
|
|
1324
|
+
const [current, setCurrent] = useState<Person | null>(null);
|
|
1325
|
+
|
|
1326
|
+
<DataGrid
|
|
1327
|
+
rowSelection="single"
|
|
1328
|
+
getRowId={(r) => r.id}
|
|
1329
|
+
onSelectionChange={(s) => setCurrent(s.rows[0] ?? null)}
|
|
1330
|
+
...
|
|
1331
|
+
/>
|
|
1332
|
+
{current && <PersonDetails person={current} />}
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
**From outside the grid**, with `apiRef` (see [6.19](#619-csv-export-and-the-api)):
|
|
1336
|
+
|
|
1337
|
+
```tsx
|
|
1338
|
+
const api = useRef<DataGridApi<Person> | null>(null);
|
|
1339
|
+
|
|
1340
|
+
<button onClick={() => api.current?.selectAll()}>Select all</button>
|
|
1341
|
+
<button onClick={() => api.current?.clearSelection()}>Clear</button>
|
|
1342
|
+
<button onClick={() => console.log(api.current?.getSelectedRows())}>Log selection</button>
|
|
1343
|
+
<DataGrid rowSelection apiRef={api} ... />
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
**Combined with other features.** Row selection works together with pinning (the checkbox column is pinned automatically when any column is pinned left), virtualization, keyboard navigation, and CSV export (`scope: "selected"`).
|
|
1347
|
+
|
|
1348
|
+
**In server-side mode** the grid only holds the current page, so it remembers every row it has seen. `onSelectionChange` and `getSelectedRows()` return those remembered rows, including ones selected on pages you have left. Select-all selects the rows of the page that is loaded.
|
|
1349
|
+
|
|
1350
|
+
### 6.14 Controlled state
|
|
1351
|
+
|
|
1352
|
+
Search, filters, sort, page and selection can each be **uncontrolled** (the grid keeps the state; the default) or **controlled** (you pass the value and the change callback).
|
|
1353
|
+
|
|
1354
|
+
| State | Value prop | Change callback | Initial value (uncontrolled) |
|
|
1355
|
+
| --- | --- | --- | --- |
|
|
1356
|
+
| Search text | `search` | `onSearchChange(text)` | |
|
|
1357
|
+
| Column filters | `filters` | `onFiltersChange(filters)` | |
|
|
1358
|
+
| Sort | `sort` | `onSortChange(sort)` | `defaultSort` |
|
|
1359
|
+
| Page (zero-based) | `page` | `onPageChange(page)` | page 0 |
|
|
1360
|
+
| Page size | | `onPageSizeChange(size)` | `pageSize` |
|
|
1361
|
+
| Selection | `selectedRowIds` | `onSelectionChange({ ids, rows })` | `defaultSelectedRowIds` |
|
|
1362
|
+
|
|
1363
|
+
**Rules.**
|
|
1364
|
+
|
|
1365
|
+
- Pass the value **and** its callback. A value without a callback makes that piece of state read-only.
|
|
1366
|
+
- You may control some pieces and leave the rest to the grid.
|
|
1367
|
+
- The page goes back to 0 automatically whenever the search, a filter, the sort or the page size changes. In a controlled grid you receive that through `onPageChange(0)`.
|
|
1368
|
+
|
|
1369
|
+
**Example: keep the grid in sync with the URL** so a page can be bookmarked and the Back button works:
|
|
1370
|
+
|
|
1371
|
+
```tsx
|
|
1372
|
+
import { useSearchParams } from "react-router-dom";
|
|
1373
|
+
|
|
1374
|
+
function PeopleGrid() {
|
|
1375
|
+
const [params, setParams] = useSearchParams();
|
|
1376
|
+
const page = Number(params.get("page") ?? 0);
|
|
1377
|
+
const search = params.get("q") ?? "";
|
|
1378
|
+
|
|
1379
|
+
const update = (changes: Record<string, string | null>) =>
|
|
1380
|
+
setParams((p) => {
|
|
1381
|
+
for (const [k, v] of Object.entries(changes)) v === null ? p.delete(k) : p.set(k, v);
|
|
1382
|
+
return p;
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
return (
|
|
1386
|
+
<DataGrid
|
|
1387
|
+
columns={columns}
|
|
1388
|
+
rows={rows}
|
|
1389
|
+
searchable
|
|
1390
|
+
pagination
|
|
1391
|
+
page={page}
|
|
1392
|
+
onPageChange={(p) => update({ page: String(p) })}
|
|
1393
|
+
search={search}
|
|
1394
|
+
onSearchChange={(q) => update({ q: q || null })}
|
|
1395
|
+
/>
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
**Example: buttons outside the grid**
|
|
1401
|
+
|
|
1402
|
+
```tsx
|
|
1403
|
+
const [page, setPage] = useState(0);
|
|
1404
|
+
const [search, setSearch] = useState("");
|
|
1405
|
+
|
|
1406
|
+
<button onClick={() => setPage(2)}>Go to page 3</button>
|
|
1407
|
+
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search outside the grid" />
|
|
1408
|
+
<DataGrid searchable pagination pageSize={5} page={page} onPageChange={setPage} search={search} onSearchChange={setSearch} ... />
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
**Example: saved views.** Remember a user's filters and sort between visits:
|
|
1412
|
+
|
|
1413
|
+
```tsx
|
|
1414
|
+
const [filters, setFilters] = useState<Record<string, FilterValue>>(() => JSON.parse(localStorage.getItem("filters") ?? "{}"));
|
|
1415
|
+
const [sort, setSort] = useState<SortModel[]>(() => JSON.parse(localStorage.getItem("sort") ?? "[]"));
|
|
1416
|
+
|
|
1417
|
+
useEffect(() => localStorage.setItem("filters", JSON.stringify(filters)), [filters]);
|
|
1418
|
+
useEffect(() => localStorage.setItem("sort", JSON.stringify(sort)), [sort]);
|
|
1419
|
+
|
|
1420
|
+
<DataGrid filters={filters} onFiltersChange={setFilters} sort={sort} onSortChange={setSort} ... />
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
### 6.15 Server-side mode
|
|
1424
|
+
|
|
1425
|
+
When your data lives on a server, switch on `serverSide`. The grid then **does not sort, filter, search or page the rows itself**. It shows the `rows` you give it (the current page) and tells you what to fetch.
|
|
1426
|
+
|
|
1427
|
+
```tsx
|
|
1428
|
+
<DataGrid
|
|
1429
|
+
serverSide
|
|
1430
|
+
columns={columns}
|
|
1431
|
+
rows={rows} // only the current page
|
|
1432
|
+
rowCount={total} // total rows on the server, for "1–10 of 237" and the page count
|
|
1433
|
+
pagination
|
|
1434
|
+
searchable
|
|
1435
|
+
onQueryChange={load}
|
|
1436
|
+
/>
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
`onQueryChange(query)` is called **on mount and whenever the query changes**: the sort, the search text (after `queryDebounce` ms), a filter, the page or the page size. The query is:
|
|
1440
|
+
|
|
1441
|
+
```ts
|
|
1442
|
+
interface GridQuery {
|
|
1443
|
+
sort: SortModel[]; // [{ field: "salary", direction: "desc" }]
|
|
1444
|
+
filters: Record<string, FilterValue>; // only active ones, by field
|
|
1445
|
+
search: string; // trimmed
|
|
1446
|
+
page: number; // zero-based
|
|
1447
|
+
pageSize: number;
|
|
1448
|
+
}
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
Filter values have the shapes from [6.5](#65-searching-and-filtering): a string for text / exact, `{ min, max }` for number, `{ from, to }` for date, an array for select. Only columns marked `sortable` appear in `sort`.
|
|
1452
|
+
|
|
1453
|
+
**Loading is caught from a function.** If `onQueryChange` returns a promise, the grid shows the [loading skeleton](#616-loading-skeleton) until the promise settles. If it rejects, the grid shows `errorMessage` with a **Retry** button that runs the same query again. You do not manage any `loading` state.
|
|
1454
|
+
|
|
1455
|
+
```tsx
|
|
1456
|
+
function PeopleGrid() {
|
|
1457
|
+
const [rows, setRows] = useState<Person[]>([]);
|
|
1458
|
+
const [total, setTotal] = useState(0);
|
|
1459
|
+
|
|
1460
|
+
const load = useCallback(async (q: GridQuery) => {
|
|
1461
|
+
const res = await fetch("/api/people?" + new URLSearchParams({
|
|
1462
|
+
page: String(q.page),
|
|
1463
|
+
pageSize: String(q.pageSize),
|
|
1464
|
+
search: q.search,
|
|
1465
|
+
sort: JSON.stringify(q.sort),
|
|
1466
|
+
filters: JSON.stringify(q.filters),
|
|
1467
|
+
}));
|
|
1468
|
+
if (!res.ok) throw new Error("Request failed"); // -> error row with Retry
|
|
1469
|
+
const body = await res.json();
|
|
1470
|
+
setRows(body.rows);
|
|
1471
|
+
setTotal(body.total);
|
|
1472
|
+
}, []);
|
|
1473
|
+
|
|
1474
|
+
return (
|
|
1475
|
+
<DataGrid
|
|
1476
|
+
serverSide
|
|
1477
|
+
columns={columns}
|
|
1478
|
+
rows={rows}
|
|
1479
|
+
rowCount={total}
|
|
1480
|
+
getRowId={(r) => r.id}
|
|
1481
|
+
pagination
|
|
1482
|
+
searchable
|
|
1483
|
+
onQueryChange={load}
|
|
1484
|
+
errorMessage="Could not reach the server."
|
|
1485
|
+
/>
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1490
|
+
**Ignoring stale answers.** The grid ignores the *loading state* of an older query when a newer one has started, but it cannot know which response **you** store. Cancel old requests with `AbortController`, or compare a counter:
|
|
1491
|
+
|
|
1492
|
+
```tsx
|
|
1493
|
+
const controller = useRef<AbortController>();
|
|
1494
|
+
|
|
1495
|
+
const load = useCallback(async (q: GridQuery) => {
|
|
1496
|
+
controller.current?.abort(); // cancel the previous request
|
|
1497
|
+
controller.current = new AbortController();
|
|
1498
|
+
const res = await fetch(url(q), { signal: controller.current.signal });
|
|
1499
|
+
const body = await res.json();
|
|
1500
|
+
setRows(body.rows);
|
|
1501
|
+
setTotal(body.total);
|
|
1502
|
+
}, []);
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
An aborted `fetch` rejects with an `AbortError`. If you do not want that to show the error row, catch it: `catch (e) { if ((e as Error).name !== "AbortError") throw e; }`.
|
|
1506
|
+
|
|
1507
|
+
**Using a data library.** Anything that returns a promise works:
|
|
1508
|
+
|
|
1509
|
+
```tsx
|
|
1510
|
+
// the grid calls your function; you call the library
|
|
1511
|
+
const load = (q: GridQuery) => queryClient.fetchQuery({ queryKey: ["people", q], queryFn: () => api.people(q) })
|
|
1512
|
+
.then((res) => { setRows(res.rows); setTotal(res.total); });
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
**Forcing the loading state.** If your loading flag lives somewhere else (a hook, a store), pass `loading` and it replaces the promise-based state:
|
|
1516
|
+
|
|
1517
|
+
```tsx
|
|
1518
|
+
const { data, isFetching } = useQuery(...);
|
|
1519
|
+
<DataGrid serverSide rows={data?.rows ?? []} rowCount={data?.total} loading={isFetching} onQueryChange={setQuery} />
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
**Refreshing** after your data changed (for example after deleting a row) runs the last query again:
|
|
1523
|
+
|
|
1524
|
+
```tsx
|
|
1525
|
+
api.current?.refresh();
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
**Search debounce.** Typing in the search box waits `queryDebounce` milliseconds (250 by default) after the last keystroke, so fast typing makes one request, not one per letter. Set `queryDebounce={0}` for instant queries.
|
|
1529
|
+
|
|
1530
|
+
**Server mode and the other features.**
|
|
1531
|
+
|
|
1532
|
+
- Selection, [CSV export of selected rows](#619-csv-export-and-the-api) and keyboard navigation work on the loaded page.
|
|
1533
|
+
- With `scope: "filtered"` the CSV contains the rows currently loaded (the current page), because the grid does not have the rest.
|
|
1534
|
+
- `defaultSort` is sent in the first query. `multiSort` lets users send several sort columns.
|
|
1535
|
+
- `onQueryChange` is the only place the grid asks for data. Without `serverSide`, the grid never calls it.
|
|
1536
|
+
|
|
1537
|
+
### 6.16 Loading skeleton
|
|
1538
|
+
|
|
1539
|
+
While loading, the body shows **placeholder rows** with shimmering bars instead of data. The header, toolbar and footer stay in place, and the grid root gets `aria-busy="true"`. When the promise from `onQueryChange` settles (or `loading` turns `false`), the real rows appear.
|
|
1540
|
+
|
|
1541
|
+
```tsx
|
|
1542
|
+
<DataGrid serverSide onQueryChange={load} loadingRows={8} ... />
|
|
1543
|
+
```
|
|
1544
|
+
|
|
1545
|
+
- `loadingRows` is how many placeholder rows to draw. Default: the page size (at most 20), or 5 without pagination.
|
|
1546
|
+
- With `rowSelection` the checkbox column shows a small bar too.
|
|
1547
|
+
- Bar widths vary slightly from row to row so the placeholder does not look like identical stripes.
|
|
1548
|
+
- Colours come from `--rk-skeleton-a` and `--rk-skeleton-b`. Each theme sets them. The shimmer stops when the user has "reduce motion" turned on.
|
|
1549
|
+
- Style the bars with `classNames.skeleton` / `styles.skeleton` (they are `<span class="rk-grid__skeleton">`).
|
|
1550
|
+
|
|
1551
|
+
**A custom skeleton for each row.** Pass a component as `skeletonRow`. It is drawn once per placeholder row, inside a single cell that spans the whole row, and receives `{ rowIndex, columns }`:
|
|
1552
|
+
|
|
1553
|
+
```tsx
|
|
1554
|
+
function PersonSkeleton({ rowIndex }: SkeletonRowProps<Person>) {
|
|
1555
|
+
return (
|
|
1556
|
+
<div style={{ display: "flex", gap: 12, alignItems: "center", padding: "4px 0" }}>
|
|
1557
|
+
<span className="avatar-skeleton" />
|
|
1558
|
+
<span style={{ display: "grid", gap: 6, flex: 1 }}>
|
|
1559
|
+
<span className="line-skeleton" style={{ width: `${40 + ((rowIndex * 17) % 30)}%` }} />
|
|
1560
|
+
<span className="line-skeleton" style={{ width: "25%" }} />
|
|
1561
|
+
</span>
|
|
1562
|
+
</div>
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
<DataGrid serverSide onQueryChange={load} skeletonRow={PersonSkeleton} loadingRows={6} ... />
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
You can reuse the built-in shimmer in your own placeholder by giving your elements the class `rk-grid__skeleton`:
|
|
1570
|
+
|
|
1571
|
+
```tsx
|
|
1572
|
+
function RowSkeleton({ columns }: SkeletonRowProps<Person>) {
|
|
1573
|
+
return (
|
|
1574
|
+
<div style={{ display: "grid", gridTemplateColumns: `repeat(${columns.length}, 1fr)`, gap: 16 }}>
|
|
1575
|
+
{columns.map((c) => <span key={c.field} className="rk-grid__skeleton" style={{ width: "70%" }} />)}
|
|
1576
|
+
</div>
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
```
|
|
1580
|
+
|
|
1581
|
+
Define the skeleton component at module level (not inline in render), like every other custom component.
|
|
1582
|
+
|
|
1583
|
+
**Showing it without a server.** `loading` works in a normal grid too, for example while a parent fetches `rows`:
|
|
1584
|
+
|
|
1585
|
+
```tsx
|
|
1586
|
+
<DataGrid columns={columns} rows={rows} loading={isLoading} />
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
**The error state.** If the promise from `onQueryChange` rejects, the body shows `errorMessage` (default "Could not load the data.") and a Retry button. Style it with `.rk-grid__error`:
|
|
1590
|
+
|
|
1591
|
+
```tsx
|
|
1592
|
+
<DataGrid serverSide onQueryChange={load} errorMessage={<strong>The server is not answering.</strong>} ... />
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
### 6.17 Resize, pin and hide columns
|
|
1596
|
+
|
|
1597
|
+
All three are opt-in. **There is no column reordering.**
|
|
1598
|
+
|
|
1599
|
+
**Resizing.** Add `resizable: true` to a column. A drag handle appears on its right edge. Drag it, or use a touch or pen: it uses pointer events.
|
|
1600
|
+
|
|
1601
|
+
```tsx
|
|
1602
|
+
const columns: ColumnDef<Employee>[] = [
|
|
1603
|
+
{ field: "name", resizable: true, minWidth: 100 },
|
|
1604
|
+
{ field: "email", resizable: true, minWidth: 140, maxWidth: 480 },
|
|
1605
|
+
{ field: "id", width: 70 }, // no handle: fixed
|
|
1606
|
+
];
|
|
1607
|
+
```
|
|
1608
|
+
|
|
1609
|
+
- On the first drag the grid fixes every column at the width it has at that moment (so nothing jumps) and switches the table to `table-layout: fixed`.
|
|
1610
|
+
- `minWidth` (at least 40px) and `maxWidth` limit the drag.
|
|
1611
|
+
- Widths live inside the grid and are not saved. To remember them, store the `width` you want per column yourself.
|
|
1612
|
+
|
|
1613
|
+
**Pinning.** Add `pinned: "left"` or `"right"` to keep a column visible while the rest scrolls sideways. Pinned columns are moved to the edges (left-pinned first, right-pinned last), keep their order among themselves, and have an opaque background.
|
|
1614
|
+
|
|
1615
|
+
```tsx
|
|
1616
|
+
const columns: ColumnDef<Employee>[] = [
|
|
1617
|
+
{ field: "id", width: 70, pinned: "left" },
|
|
1618
|
+
{ field: "name", width: 160, pinned: "left" }, // sits next to ID
|
|
1619
|
+
{ field: "email", width: 280 },
|
|
1620
|
+
{ field: "department", width: 200 },
|
|
1621
|
+
{ field: "salary", width: 160 },
|
|
1622
|
+
{ field: "status", width: 120, pinned: "right" }, // always at the right edge
|
|
1623
|
+
];
|
|
1624
|
+
|
|
1625
|
+
<DataGrid columns={columns} rows={rows} /> // scroll sideways: ID, Name and Status stay put
|
|
1626
|
+
```
|
|
1627
|
+
|
|
1628
|
+
The row-selection checkbox column is pinned too when any column is pinned left. A thin shadow line (`.rk-grid__pinned--edge`) marks where pinned columns end. Give pinned columns an explicit `width` for the cleanest look. Pinning needs the grid to be narrower than its columns, which happens when the columns' widths add up to more than the container.
|
|
1629
|
+
|
|
1630
|
+
Colours: `--rk-pinned-bg` and `--rk-pinned-header-bg`. Stripes, hover and selection are layered on top.
|
|
1631
|
+
|
|
1632
|
+
**Showing and hiding.** Switch on `columnMenu` for a "Columns" button with a checkbox per column:
|
|
1633
|
+
|
|
1634
|
+
```tsx
|
|
1635
|
+
<DataGrid
|
|
1636
|
+
columnMenu
|
|
1637
|
+
onColumnVisibilityChange={(hiddenFields) => localStorage.setItem("hidden", JSON.stringify(hiddenFields))}
|
|
1638
|
+
columns={[
|
|
1639
|
+
{ field: "id", hideable: false }, // always visible, not listed
|
|
1640
|
+
{ field: "name" },
|
|
1641
|
+
{ field: "email" },
|
|
1642
|
+
{ field: "notes", hidden: true }, // starts hidden
|
|
1643
|
+
]}
|
|
1644
|
+
rows={rows}
|
|
1645
|
+
/>
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
- `hidden: true` starts a column hidden. The menu shows it unchecked.
|
|
1649
|
+
- `hideable: false` keeps a column out of the menu, so it cannot be hidden.
|
|
1650
|
+
- The last visible column cannot be hidden.
|
|
1651
|
+
- Hidden columns are skipped by search, keyboard navigation and CSV export (unless `includeHidden`).
|
|
1652
|
+
|
|
1653
|
+
### 6.18 Keyboard
|
|
1654
|
+
|
|
1655
|
+
Switch on `keyboardNavigation` to move between cells with the keyboard. The grid has **one tab stop**: Tab enters the body at the last cell you used (or the first one), and the arrow keys move from there. Tab again leaves the grid.
|
|
1656
|
+
|
|
1657
|
+
| Key (on a cell) | Action |
|
|
1658
|
+
| --- | --- |
|
|
1659
|
+
| Arrow keys | Move one cell |
|
|
1660
|
+
| Home / End | First / last column of the row |
|
|
1661
|
+
| Ctrl+Home / Ctrl+End | First cell of the first row / last cell of the last row |
|
|
1662
|
+
| Page Up / Page Down | Move 10 rows |
|
|
1663
|
+
| Enter / F2 | Start editing (editable cells) |
|
|
1664
|
+
| Any character | Start editing and replace the value (text and number cells) |
|
|
1665
|
+
|
|
1666
|
+
Other keys, always available:
|
|
1667
|
+
|
|
1668
|
+
| Where | Key | Action |
|
|
1669
|
+
| --- | --- | --- |
|
|
1670
|
+
| Header | Enter / Space | Sort by this column (sortable columns) |
|
|
1671
|
+
| Editor | Enter | Save, edit the cell below |
|
|
1672
|
+
| Editor | Shift+Enter | Save, edit the cell above |
|
|
1673
|
+
| Editor | Tab / Shift+Tab | Save, edit the next / previous cell |
|
|
1674
|
+
| Editor | Esc | Cancel |
|
|
1675
|
+
| Filter popup | Esc | Close |
|
|
1676
|
+
| Text filter box | Enter | Close the popup |
|
|
1677
|
+
|
|
1678
|
+
With `virtualize`, moving past the visible window scrolls the target row into view before focusing it, so you can arrow through 20,000 rows. Combined with `maxHeight`, the focused cell is scrolled into view.
|
|
1679
|
+
|
|
1680
|
+
```tsx
|
|
1681
|
+
<DataGrid keyboardNavigation editable maxHeight={420} columns={columns} rows={rows} />
|
|
1682
|
+
```
|
|
1683
|
+
|
|
1684
|
+
Without `keyboardNavigation`, cells are reachable with Tab (editable cells) or the mouse, as before.
|
|
1685
|
+
|
|
1686
|
+
### 6.19 CSV export and the API
|
|
1687
|
+
|
|
1688
|
+
**The export button.** Switch on `exportable` for an "Export CSV" button in the toolbar. It downloads what the user sees: the visible columns, in order, for every row matching the search and filters (across all pages).
|
|
1689
|
+
|
|
1690
|
+
```tsx
|
|
1691
|
+
<DataGrid exportable ... /> // downloads export.csv
|
|
1692
|
+
<DataGrid exportable={{ filename: "employees" }} ... /> // employees.csv
|
|
1693
|
+
<DataGrid exportable={{ filename: "page-1", scope: "page" }} ... /> // only the current page
|
|
1694
|
+
<DataGrid exportable={{ scope: "selected" }} rowSelection ... /> // only the selected rows
|
|
1695
|
+
```
|
|
1696
|
+
|
|
1697
|
+
| `ExportOptions` | Default | Meaning |
|
|
1698
|
+
| --- | --- | --- |
|
|
1699
|
+
| `filename` | `"export"` | File name without `.csv` |
|
|
1700
|
+
| `scope` | `"filtered"` | `"filtered"` all rows matching search and filters, `"page"` the current page, `"selected"` the selected rows |
|
|
1701
|
+
| `includeHidden` | `false` | Also write hidden columns |
|
|
1702
|
+
| `sanitize` | `true` | Put an apostrophe in front of text starting with `=`, `+`, `-` or `@`, so Excel does not run it as a formula |
|
|
1703
|
+
|
|
1704
|
+
**What goes in the file.** The header row uses the column header names. Cell values are the displayed text (the result of `valueFormatter`, `Yes` / `No` for booleans), except that dates are written as `YYYY-MM-DD`. Numbers therefore export formatted (`$52,000`) unless you give an `exportValue`. To decide a column's value yourself, add `exportValue`:
|
|
1705
|
+
|
|
1706
|
+
```tsx
|
|
1707
|
+
{
|
|
1708
|
+
field: "status",
|
|
1709
|
+
exportValue: (value) => String(value).toUpperCase(), // ACTIVE
|
|
1710
|
+
}
|
|
1711
|
+
{
|
|
1712
|
+
field: "salary",
|
|
1713
|
+
valueFormatter: (v) => `$${v.toLocaleString()}`, // shown as $52,000
|
|
1714
|
+
exportValue: (v) => v, // exported as 52000, so a spreadsheet can add it up
|
|
1715
|
+
}
|
|
1716
|
+
```
|
|
1717
|
+
|
|
1718
|
+
The file starts with a byte-order mark so Excel reads accents and non-Latin text correctly. Commas, quotes and line breaks inside values are quoted.
|
|
1719
|
+
|
|
1720
|
+
**The API.** Pass a ref as `apiRef` to call methods from outside the grid:
|
|
1721
|
+
|
|
1722
|
+
```tsx
|
|
1723
|
+
const api = useRef<DataGridApi<Employee> | null>(null);
|
|
1724
|
+
|
|
1725
|
+
<DataGrid apiRef={api} rowSelection ... />
|
|
1726
|
+
|
|
1727
|
+
api.current?.exportCsv({ filename: "selected", scope: "selected" }); // download
|
|
1728
|
+
const text = api.current?.getCsv(); // the CSV as a string
|
|
1729
|
+
```
|
|
1730
|
+
|
|
1731
|
+
| Method | Does |
|
|
1732
|
+
| --- | --- |
|
|
1733
|
+
| `getCsv(options?)` | Returns the CSV text without downloading it |
|
|
1734
|
+
| `exportCsv(options?)` | Downloads a CSV file |
|
|
1735
|
+
| `getSelectedRows()` | The selected row objects |
|
|
1736
|
+
| `getSelectedRowIds()` | The selected ids |
|
|
1737
|
+
| `selectAll()` | Selects every selectable row matching the search and filters |
|
|
1738
|
+
| `clearSelection()` | Clears the selection |
|
|
1739
|
+
| `refresh()` | Runs the last query again (`serverSide` only) |
|
|
1740
|
+
|
|
1741
|
+
**Sending the CSV somewhere else** (upload, email) instead of downloading:
|
|
1742
|
+
|
|
1743
|
+
```tsx
|
|
1744
|
+
const csv = api.current!.getCsv({ scope: "selected" });
|
|
1745
|
+
await fetch("/api/reports", { method: "POST", headers: { "Content-Type": "text/csv" }, body: csv });
|
|
1746
|
+
```
|
|
1747
|
+
|
|
1748
|
+
**Your own button** next to the built-in ones:
|
|
1749
|
+
|
|
1750
|
+
```tsx
|
|
1751
|
+
<DataGrid
|
|
1752
|
+
apiRef={api}
|
|
1753
|
+
toolbar={<button className="rk-grid__tool-btn" onClick={() => api.current?.exportCsv({ filename: "all", includeHidden: true })}>Export everything</button>}
|
|
1754
|
+
...
|
|
1755
|
+
/>
|
|
1756
|
+
```
|
|
1757
|
+
|
|
1758
|
+
### 6.20 Virtualization
|
|
1759
|
+
|
|
1760
|
+
For tens of thousands of rows, switch on `virtualize`. Only the rows in (and just around) the scroll window are in the DOM; empty spacer rows stand in for the rest, so the scroll bar still reflects the full data.
|
|
1761
|
+
|
|
1762
|
+
```tsx
|
|
1763
|
+
<DataGrid
|
|
1764
|
+
virtualize
|
|
1765
|
+
rowHeight={40} // every row must be exactly this tall
|
|
1766
|
+
maxHeight={480} // the grid scrolls inside this height (400 if you give none)
|
|
1767
|
+
overscan={8} // extra rows drawn above and below
|
|
1768
|
+
columns={columns}
|
|
1769
|
+
rows={twentyThousandRows}
|
|
1770
|
+
getRowId={(r) => r.id}
|
|
1771
|
+
searchable
|
|
1772
|
+
/>
|
|
1773
|
+
```
|
|
1774
|
+
|
|
1775
|
+
- **Fixed row height.** Rows are laid out as `rowHeight` pixels each and cell text does not wrap (long text is cut off). Set `rowHeight` to fit your content.
|
|
1776
|
+
- Use it without `pagination` so everything scrolls in one list. Sorting, search and filters still work on the whole data set.
|
|
1777
|
+
- Row animation is skipped while virtualizing, because rows come and go as you scroll.
|
|
1778
|
+
- Keyboard navigation, selection and editing all work. Moving focus to a row outside the window scrolls it into view first.
|
|
1779
|
+
- Choose a `rowHeight` that suits your padding: 40 is right for the default cell padding.
|
|
1780
|
+
|
|
1781
|
+
```tsx
|
|
1782
|
+
// 50,000 rows, searchable, selectable, keyboard friendly
|
|
1783
|
+
<DataGrid virtualize rowHeight={36} maxHeight="70vh" searchable rowSelection keyboardNavigation theme="graphite" ... />
|
|
1784
|
+
```
|
|
1785
|
+
|
|
1786
|
+
### 6.21 Accessibility
|
|
1787
|
+
|
|
1788
|
+
- The table has `role="grid"`. Sortable headers are focusable, expose `aria-sort`, and work with Enter and Space. Columns that are not sortable are not focus stops.
|
|
1789
|
+
- Editable cells are focusable (`tabindex="0"`). With `keyboardNavigation` the body has a single tab stop and arrow keys move between cells ([6.18](#618-keyboard)).
|
|
1790
|
+
- The search box, filter indicators (`aria-haspopup="dialog"`, `aria-expanded`), filter popups (`role="dialog"`), the Columns menu, editors, page buttons and page-size menu have `aria-label`s. Filter inputs are named after their column ("Minimum Budget", "Due from"). Checkboxes in columns with an empty header are named after the field.
|
|
1791
|
+
- Selected rows have `aria-selected="true"`. Row checkboxes are named "Select row N", and the header one "Select all rows".
|
|
1792
|
+
- The "N selected" count is a status region (`role="status"`).
|
|
1793
|
+
- While loading, the grid has `aria-busy="true"` and the placeholder rows are hidden from assistive technology. The error message has `role="alert"`.
|
|
1794
|
+
- Invalid editors set `aria-invalid="true"`.
|
|
1795
|
+
- The shimmer and row animations stop when the system has "reduce motion" turned on.
|
|
1796
|
+
- Custom components you supply (renderers, editors, skeleton rows, indicators) are responsible for their own accessibility.
|
|
1797
|
+
|
|
1798
|
+
### 6.22 TypeScript
|
|
1799
|
+
|
|
1800
|
+
All types are exported:
|
|
1801
|
+
|
|
1802
|
+
```ts
|
|
1803
|
+
import type {
|
|
1804
|
+
// grid and columns
|
|
1805
|
+
DataGridProps, ColumnDef, DataGridApi, GridTheme, GridSlot, FieldPath,
|
|
1806
|
+
// cells, editors, headers
|
|
1807
|
+
CellParams, CellEditEvent, CellEditorProps, CellEditableParams, HeaderRendererProps, MoveDirection,
|
|
1808
|
+
// sorting, paging
|
|
1809
|
+
SortModel, SortDirection, PaginationPosition,
|
|
1810
|
+
// filters
|
|
1811
|
+
FilterType, FilterValue, FilterOption, NumberRangeFilter, DateRangeFilter, FilterRendererProps, FilterIndicatorProps,
|
|
1812
|
+
// selection, server, export, loading
|
|
1813
|
+
RowSelection, GridQuery, ExportOptions, SkeletonRowProps,
|
|
1814
|
+
// checkbox
|
|
1815
|
+
ColumnCheckboxOptions, CheckboxProps, CheckboxVariant, CheckboxSlot,
|
|
1816
|
+
} from "react-gridix";
|
|
1817
|
+
```
|
|
1818
|
+
|
|
1819
|
+
Typing tips:
|
|
1820
|
+
|
|
1821
|
+
```tsx
|
|
1822
|
+
const columns: ColumnDef<Employee>[] = [ ... ]; // field is checked against Employee (dot paths too)
|
|
1823
|
+
function Cell({ row }: CellParams<Employee>) { ... } // row is typed as Employee
|
|
1824
|
+
|
|
1825
|
+
const api = useRef<DataGridApi<Employee> | null>(null); // typed apiRef
|
|
1826
|
+
const [sel, setSel] = useState<RowSelection<Employee>>({ ids: [], rows: [] });
|
|
1827
|
+
const [filters, setFilters] = useState<Record<string, FilterValue>>({});
|
|
1828
|
+
|
|
1829
|
+
// narrowing a filter value yourself (for example when building a server query)
|
|
1830
|
+
function toParams(f: Record<string, FilterValue>) {
|
|
1831
|
+
return Object.entries(f).map(([field, v]) => {
|
|
1832
|
+
if (typeof v === "string") return `${field}~${v}`; // text / exact
|
|
1833
|
+
if (Array.isArray(v)) return `${field}=${v.join(",")}`; // select
|
|
1834
|
+
if ("min" in v || "max" in v) return `${field}:${(v as NumberRangeFilter).min ?? ""}..${(v as NumberRangeFilter).max ?? ""}`;
|
|
1835
|
+
return `${field}:${(v as DateRangeFilter).from ?? ""}..${(v as DateRangeFilter).to ?? ""}`; // date
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
```
|
|
1839
|
+
|
|
1840
|
+
### 6.23 Recipes
|
|
1841
|
+
|
|
1842
|
+
**Read-only grid**
|
|
1843
|
+
|
|
1844
|
+
```tsx
|
|
1845
|
+
<DataGrid columns={columns} rows={rows} /> // editable, sortable, searchable... all default to off
|
|
1846
|
+
```
|
|
1847
|
+
|
|
1848
|
+
**A typical admin table**
|
|
1849
|
+
|
|
1850
|
+
```tsx
|
|
1851
|
+
<DataGrid
|
|
1852
|
+
columns={columns} // sortable / filterable chosen per column
|
|
1853
|
+
rows={rows}
|
|
1854
|
+
getRowId={(r) => r.id}
|
|
1855
|
+
searchable
|
|
1856
|
+
pagination
|
|
1857
|
+
rowSelection
|
|
1858
|
+
columnMenu
|
|
1859
|
+
exportable={{ filename: "users" }}
|
|
1860
|
+
striped
|
|
1861
|
+
theme="lavender"
|
|
1862
|
+
/>
|
|
1863
|
+
```
|
|
1864
|
+
|
|
1865
|
+
**Currency column that accepts "$1,200" when typed**
|
|
1866
|
+
|
|
1867
|
+
```tsx
|
|
1868
|
+
{
|
|
1869
|
+
field: "salary",
|
|
1870
|
+
type: "number",
|
|
1871
|
+
align: "right",
|
|
1872
|
+
sortable: true,
|
|
1873
|
+
valueFormatter: (v) => (v == null ? "" : money.format(v)),
|
|
1874
|
+
valueParser: (text) => Number(text.replace(/[$,]/g, "")),
|
|
1875
|
+
validate: (v) => Number.isFinite(v),
|
|
1876
|
+
}
|
|
1877
|
+
```
|
|
1878
|
+
|
|
1879
|
+
(The editor opens with the raw number, such as `52000`. The parser only matters if the user types symbols.)
|
|
1880
|
+
|
|
1881
|
+
**Only one column editable**
|
|
1882
|
+
|
|
1883
|
+
```tsx
|
|
1884
|
+
<DataGrid columns={[{ field: "id" }, { field: "notes", editable: true }]} />
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
**Lock rows after approval**
|
|
1888
|
+
|
|
1889
|
+
```tsx
|
|
1890
|
+
{ field: "amount", editable: ({ row }) => !row.approved }
|
|
1891
|
+
```
|
|
1892
|
+
|
|
1893
|
+
**Scrolling grid with sticky header**
|
|
1894
|
+
|
|
1895
|
+
```tsx
|
|
1896
|
+
<DataGrid maxHeight="60vh" ... />
|
|
1897
|
+
```
|
|
1898
|
+
|
|
1899
|
+
**Highlight a row based on its data**
|
|
1900
|
+
|
|
1901
|
+
```tsx
|
|
1902
|
+
<DataGrid rowClassName={(row) => (row.overdue ? "bg-red-50" : "")} />
|
|
1903
|
+
```
|
|
1904
|
+
|
|
1905
|
+
**Overdue dates in red**
|
|
1906
|
+
|
|
1907
|
+
```tsx
|
|
1908
|
+
{
|
|
1909
|
+
field: "due",
|
|
1910
|
+
type: "date",
|
|
1911
|
+
cellStyle: ({ value }) => (value && String(value) < new Date().toISOString().slice(0, 10) ? { color: "#dc2626" } : {}),
|
|
1912
|
+
}
|
|
1913
|
+
```
|
|
1914
|
+
|
|
1915
|
+
**Status as a coloured badge, with an exact filter**
|
|
1916
|
+
|
|
1917
|
+
```tsx
|
|
1918
|
+
const Badge = ({ value }: CellParams<Task>) => <span className={`badge badge-${value}`}>{String(value)}</span>;
|
|
1919
|
+
|
|
1920
|
+
{ field: "status", sortable: true, filterable: true, filterType: "select", cellRenderer: Badge }
|
|
1921
|
+
```
|
|
1922
|
+
|
|
1923
|
+
**Delete the selected rows**
|
|
1924
|
+
|
|
1925
|
+
```tsx
|
|
1926
|
+
const [rows, setRows] = useState(initial);
|
|
1927
|
+
const [ids, setIds] = useState<(string | number)[]>([]);
|
|
1928
|
+
|
|
1929
|
+
<button disabled={!ids.length} onClick={() => { setRows((r) => r.filter((x) => !ids.includes(x.id))); setIds([]); }}>
|
|
1930
|
+
Delete {ids.length}
|
|
1931
|
+
</button>
|
|
1932
|
+
<DataGrid rows={rows} columns={columns} getRowId={(r) => r.id} rowSelection selectedRowIds={ids} onSelectionChange={(s) => setIds(s.ids)} />
|
|
1933
|
+
```
|
|
1934
|
+
|
|
1935
|
+
**Bulk edit the selected rows** (`onRowsChange` gives the whole array back):
|
|
1936
|
+
|
|
1937
|
+
```tsx
|
|
1938
|
+
const markDone = () => setRows((rs) => rs.map((r) => (ids.includes(r.id) ? { ...r, status: "done" } : r)));
|
|
1939
|
+
```
|
|
1940
|
+
|
|
1941
|
+
**Choose the page size once and remember it**
|
|
1942
|
+
|
|
1943
|
+
```tsx
|
|
1944
|
+
<DataGrid pagination pageSize={Number(localStorage.getItem("ps")) || 25} onPageSizeChange={(n) => localStorage.setItem("ps", String(n))} />
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
**A fast, big, read-only table**
|
|
1948
|
+
|
|
1949
|
+
```tsx
|
|
1950
|
+
<DataGrid virtualize rowHeight={32} maxHeight="80vh" animateRows={false} searchable columns={columns} rows={hugeArray} getRowId={(r) => r.id} />
|
|
1951
|
+
```
|
|
1952
|
+
|
|
1953
|
+
**An infinite-looking server table with a dark theme**
|
|
1954
|
+
|
|
1955
|
+
```tsx
|
|
1956
|
+
<DataGrid serverSide pagination searchable multiSort theme="aurora" columns={columns} rows={rows} rowCount={total} onQueryChange={load} />
|
|
1957
|
+
```
|
|
1958
|
+
|
|
1959
|
+
### 6.24 Known limitations
|
|
1960
|
+
|
|
1961
|
+
Not included:
|
|
1962
|
+
|
|
1963
|
+
- **Column reordering** (by design)
|
|
1964
|
+
- Row grouping, tree data, row detail panels and column spanning
|
|
1965
|
+
- Infinite scroll in server mode (paging is used instead)
|
|
1966
|
+
- Saving column widths or visibility between visits (use `onColumnVisibilityChange` and your own storage)
|
|
1967
|
+
- Variable row heights while virtualizing
|
|
1968
|
+
|
|
1969
|
+
Behaviours to be aware of:
|
|
1970
|
+
|
|
1971
|
+
- Text filters match by substring (`active` also matches `inactive`). Use `filterType: "exact"` or `"select"` for exact choices.
|
|
1972
|
+
- Only one filter popup is open at a time.
|
|
1973
|
+
- Stripes and the row animation restart on every page, sort, search or filter change.
|
|
1974
|
+
- `pageSize` is read once, on mount (use `onPageSizeChange`). `defaultSort` and `defaultSelectedRowIds` are read once too: to change them later, control the state.
|
|
1975
|
+
- `rows` must keep a stable identity between renders, otherwise edits are discarded (see [6.9](#69-editing)).
|
|
1976
|
+
- `onRowClick` also fires for clicks inside a cell that is being edited.
|
|
1977
|
+
- A `valueParser` that returns `NaN` stores `NaN`. Add `validate: (v) => Number.isFinite(v)` to reject it.
|
|
1978
|
+
- `virtualize` needs fixed-height rows. A cell with wrapped text will be cut off.
|
|
1979
|
+
- Pinned columns only stick when the table is wider than its container.
|
|
1980
|
+
- In `serverSide` mode the grid cannot export or select rows it has never loaded.
|
|
1981
|
+
- The browser's date picker looks different in each browser. Its language follows the browser, not `dateLocale` (which only formats the display).
|
|
1982
|
+
|
|
1983
|
+
---
|
|
1984
|
+
|
|
1985
|
+
## 7. Demos
|
|
1986
|
+
|
|
1987
|
+
The repository contains a playground with one page per feature:
|
|
1988
|
+
|
|
1989
|
+
| Demo (`#id`) | Shows |
|
|
1990
|
+
| --- | --- |
|
|
1991
|
+
| Basic (`#basic`) | Opt-in sorting, global search, per-column filter dots with popups, pagination, controlled `SortModel[]` |
|
|
1992
|
+
| Editing (`#editing`) | Enter-moves-down editing, Tab, validation, locked cells, `onCellEdit` |
|
|
1993
|
+
| Custom components (`#custom`) | `cellRenderer`, `cellEditor`, `headerRenderer`, a custom funnel `filterIndicator`, a per-column indicator style, a `filterRenderer` in the popup |
|
|
1994
|
+
| Checkboxes (`#checkboxes`) | Boolean columns with the three variants, an empty-label header, a custom `★` character, select-all headers, locking, styling from outside, and a standalone `Checkbox` |
|
|
1995
|
+
| Styling (`#styling`) | Tailwind classes and inline styles, column `className` / `style`, and `stripeColors` |
|
|
1996
|
+
| Pagination (`#pagination`) | Top, bottom and both, no pagination, the row animation toggle, and a **controlled page and search** grid driven by outside buttons |
|
|
1997
|
+
| Nested fields (`#nested`) | `contact.phone` style dot paths: editing, search, filter, sort, and proof the original data is not mutated |
|
|
1998
|
+
| Row selection (`#selection`) | Multiple and single modes, controlled ids, `isRowSelectable`, Shift+click ranges, select-all across pages, `apiRef.selectAll()`, a Delete button |
|
|
1999
|
+
| Server-side (`#server`) | A fake 237-row database with 700 ms latency: server sorting, multi-sort, search debounce, filters, paging, the loading skeleton (default and custom row), and a switch that makes the request fail so you can see the error and Retry |
|
|
2000
|
+
| Columns (`#columns`) | Resizing, pinned columns on both sides, the Columns menu, arrow-key navigation, CSV export with `exportValue`, multi-column sort |
|
|
2001
|
+
| Large data (`#large`) | 20,000 virtualized rows with search, sorting and a theme |
|
|
2002
|
+
| Dates and filters (`#dates`) | `type: "date"` with the date picker editor, `dateMin` / `dateMax`, a `Date`-object column, from/to, number range, select and exact filters |
|
|
2003
|
+
| Themes (`#themes`) | A switcher for none and all 8 themes |
|
|
2004
|
+
|
|
2005
|
+
Run it:
|
|
2006
|
+
|
|
2007
|
+
```bash
|
|
2008
|
+
npm install
|
|
2009
|
+
npm run dev
|
|
2010
|
+
```
|
|
2011
|
+
|
|
2012
|
+
Open the URL it prints (usually `http://localhost:5173`). Link straight to a demo with the `#id` in the table. The source for each demo is in `demo/examples/`, written as small, commented, copy-paste-able files.
|
|
2013
|
+
|
|
2014
|
+
The Styling demo loads Tailwind from its CDN, so it needs an internet connection. The rest works offline.
|
|
2015
|
+
|
|
2016
|
+
---
|
|
2017
|
+
|
|
2018
|
+
## 9. Troubleshooting
|
|
2019
|
+
|
|
2020
|
+
| Problem | Cause and fix |
|
|
2021
|
+
| --- | --- |
|
|
2022
|
+
| Everything looks unstyled | Import `react-gridix/style.css` once in your app |
|
|
2023
|
+
| `theme="ocean"` does nothing | The theme CSS ships in `style.css`: make sure it is imported. The attribute `data-rk-theme` should appear on the grid root |
|
|
2024
|
+
| Headers don't sort | Sorting is per column: add `sortable: true` to the column definition. There is no grid-level `sortable` prop any more |
|
|
2025
|
+
| There is no search box | Pass `searchable` |
|
|
2026
|
+
| There is no page info or page buttons | Pass `pagination`. Without it every row is shown and there is no footer |
|
|
2027
|
+
| I can't find the filter input | Filters are per column: set `filterable: true`, then click the dot in the header |
|
|
2028
|
+
| A date column shows `2026-11-05` instead of `Nov 5, 2026` | The column needs `type: "date"` |
|
|
2029
|
+
| The date picker never opens | The cell must be editable (`editable` on the grid or the column), then double-click it or press Enter / F2 |
|
|
2030
|
+
| The number filter popup asks for Min and Max, but I wanted text | Number columns default to a range filter. Set `filterType: "text"` |
|
|
2031
|
+
| Select filter lists the wrong labels | Pass `filterOptions` with `{ value, label }` pairs. `value` must equal what is stored in the row |
|
|
2032
|
+
| Selecting rows does nothing | Add `rowSelection`. Give the grid `getRowId` so selection survives sorting and paging |
|
|
2033
|
+
| The server grid asks for data twice on mount | In React StrictMode (development) effects run twice. The production build calls it once. Your handler should be safe to call repeatedly |
|
|
2034
|
+
| The server grid shows no skeleton | `onQueryChange` must **return** the promise (`return fetch(...)` or an `async` function). A function that starts a request and returns nothing cannot be tracked. Alternatively pass `loading` |
|
|
2035
|
+
| The server grid shows old rows after I changed a filter | You stored an older response last. Cancel the previous request with `AbortController` or compare a request counter |
|
|
2036
|
+
| Page 1 is shown after every search | By design: the page resets when search, filters, sort or page size change |
|
|
2037
|
+
| Resize handles do nothing | The column needs `resizable: true` |
|
|
2038
|
+
| Pinned columns scroll away | The columns must be wider than the container (give them `width`s), otherwise there is nothing to scroll. Give pinned columns an explicit `width` |
|
|
2039
|
+
| Virtualized rows look cut off or overlap | `rowHeight` is smaller than your content. Increase it, or shorten the content |
|
|
2040
|
+
| Virtualized grid is tiny | It scrolls inside `maxHeight` (400 if unset). Set `maxHeight` |
|
|
2041
|
+
| Arrow keys do nothing | Add `keyboardNavigation` and click or Tab into a cell first |
|
|
2042
|
+
| CSV opens with strange characters in Excel | The file has a BOM and is UTF-8. Use Excel's Data > From Text if your version ignores it |
|
|
2043
|
+
| CSV shows `$52,000` where I wanted `52000` | Add `exportValue: (v) => v` to the column |
|
|
2044
|
+
| Type error on `field: "contact.phone"` | The path must exist in your row type (optional objects are fine). If `rows` is typed loosely (`any`), type the columns as `ColumnDef<Person>[]` |
|
|
2045
|
+
| My checkbox column header is empty | By design: boolean columns default to no label. Set `headerName` to show one |
|
|
2046
|
+
| No row animation | `animateRows` is off, `virtualize` is on, or the system has "reduce motion" turned on |
|
|
2047
|
+
| My Tailwind class doesn't change the grid | Put it in the right slot (`classNames.headerCell`, and so on). The library CSS has zero specificity, so a class in the right place wins |
|
|
2048
|
+
| Edits disappear on the next render | `rows` gets a new identity every render. Keep it in `useState` or `useMemo` |
|
|
2049
|
+
| A custom editor, renderer or skeleton loses focus or resets while typing | The component is defined inline inside render. Move it to module level |
|
|
2050
|
+
| Typing in a custom editor column doesn't start editing | By design: custom editors open with double-click, Enter or F2 |
|
|
2051
|
+
| Enter doesn't move to the next row | The cell below is locked, you are on the last row of the page, or `enterMovesDown` is `false` |
|
|
2052
|
+
| Changing `pageSize` or `defaultSort` after mount does nothing | They are initial values only. Control `page` / `sort` instead |
|
|
2053
|
+
| A checkbox in a boolean column is greyed out and won't toggle | The cell isn't editable. Set `editable` on the grid, or `editable: true` on that column |
|
|
2054
|
+
| Type error: `field` not assignable | `field` must be a key of your row type. Check the spelling, and that `ColumnDef<T>` uses the same `T` as `rows` |
|
|
2055
|
+
|
|
2056
|
+
---
|
|
2057
|
+
|
|
2058
|
+
## 10. Migrating from the earlier version
|
|
2059
|
+
|
|
2060
|
+
Three defaults changed so that every feature is opt-in.
|
|
2061
|
+
|
|
2062
|
+
| Before | Now | What to do |
|
|
2063
|
+
| --- | --- | --- |
|
|
2064
|
+
| Grid prop `sortable` (default `true`), column `sortable` override | **Column** `sortable` only, default `false`. The grid-level prop is gone | Add `sortable: true` to each column that should sort. Remove `sortable` / `sortable={false}` from the grid |
|
|
2065
|
+
| `defaultSort` / `onSortChange` used one `SortModel` or `null` | `defaultSort` takes one `SortModel` **or an array**. `onSortChange` receives an **array** (empty when unsorted) | Change `(sort) => sort?.field` to `(sort) => sort[0]?.field` |
|
|
2066
|
+
| `searchable` default `true` | Default `false` | Add `searchable` to grids that should show the search box |
|
|
2067
|
+
| `pagination` default `true` | Default `false`. With it off there is no footer and no row count | Add `pagination` to grids that should page |
|
|
2068
|
+
| Filters were text only | Filter kinds: text, exact, number, date, select | Nothing to change. Number columns now get a Min / Max popup, boolean columns a checkbox list |
|
|
2069
|
+
| `FilterRendererProps` | Unchanged (`value: string`) | |
|
|
2070
|
+
|
|
2071
|
+
A quick way to get the old behaviour everywhere:
|
|
2072
|
+
|
|
2073
|
+
```tsx
|
|
2074
|
+
// before: <DataGrid columns={columns} rows={rows} />
|
|
2075
|
+
const withSorting = (cols: ColumnDef<Row>[]) => cols.map((c) => ({ sortable: true, ...c }));
|
|
2076
|
+
|
|
2077
|
+
<DataGrid columns={withSorting(columns)} rows={rows} searchable pagination />
|
|
2078
|
+
```
|
|
2079
|
+
|
|
2080
|
+
Everything else in this document is new and optional.
|