react-gridix 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1477 -252
- package/dist/index.d.ts +255 -24
- package/dist/react-gridix.cjs +2 -1
- package/dist/react-gridix.js +1460 -526
- package/dist/style.css +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# react-gridix
|
|
2
2
|
|
|
3
|
-
A React `DataGrid` with per-column filters, in-cell editing,
|
|
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
4
|
|
|
5
5
|
**Contents**
|
|
6
6
|
|
|
@@ -13,6 +13,7 @@ A React `DataGrid` with per-column filters, in-cell editing, nested fields, them
|
|
|
13
13
|
7. [Demos](#7-demos)
|
|
14
14
|
8. [Development, building and publishing](#8-development-building-and-publishing)
|
|
15
15
|
9. [Troubleshooting](#9-troubleshooting)
|
|
16
|
+
10. [Migrating from the earlier version](#10-migrating-from-the-earlier-version)
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
@@ -20,10 +21,29 @@ A React `DataGrid` with per-column filters, in-cell editing, nested fields, them
|
|
|
20
21
|
|
|
21
22
|
| Component | Purpose |
|
|
22
23
|
| --- | --- |
|
|
23
|
-
| `DataGrid` | Data table with sorting, search, per-column filters, pagination, in-cell editing, 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 you can use it on its own |
|
|
24
|
+
| `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 |
|
|
25
|
+
| `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
|
|
|
26
|
-
Everything is
|
|
27
|
+
**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:
|
|
28
|
+
|
|
29
|
+
| Feature | How you switch it on | Default |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| Sorting | `sortable: true` **on a column** | off |
|
|
32
|
+
| Multi-column sort | `multiSort` on the grid | off |
|
|
33
|
+
| Global search box | `searchable` | off |
|
|
34
|
+
| Pagination bar (row count, page controls) | `pagination` | off |
|
|
35
|
+
| Column filter | `filterable: true` on a column | off |
|
|
36
|
+
| Editing | `editable` | off |
|
|
37
|
+
| Row selection | `rowSelection` | off |
|
|
38
|
+
| Server-side mode | `serverSide` | off |
|
|
39
|
+
| Resize / pin | `resizable: true` / `pinned` on a column | off |
|
|
40
|
+
| Show / hide columns button | `columnMenu` | off |
|
|
41
|
+
| Arrow-key navigation | `keyboardNavigation` | off |
|
|
42
|
+
| CSV export button | `exportable` | off |
|
|
43
|
+
| Virtualization | `virtualize` | off |
|
|
44
|
+
| Theme | `theme` | none (plain look) |
|
|
45
|
+
|
|
46
|
+
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**.
|
|
27
47
|
|
|
28
48
|
---
|
|
29
49
|
|
|
@@ -47,6 +67,32 @@ The library does not inject CSS by itself. If the grid looks unstyled, this impo
|
|
|
47
67
|
|
|
48
68
|
## 3. Quick start
|
|
49
69
|
|
|
70
|
+
A read-only table, the smallest possible grid:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { DataGrid, type ColumnDef } from "react-gridix";
|
|
74
|
+
import "react-gridix/style.css";
|
|
75
|
+
|
|
76
|
+
interface Person { id: number; name: string; city: string }
|
|
77
|
+
|
|
78
|
+
const columns: ColumnDef<Person>[] = [
|
|
79
|
+
{ field: "id", headerName: "ID" },
|
|
80
|
+
{ field: "name", headerName: "Name" },
|
|
81
|
+
{ field: "city", headerName: "City" },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const rows: Person[] = [
|
|
85
|
+
{ id: 1, name: "Amina", city: "Kampala" },
|
|
86
|
+
{ id: 2, name: "Brian", city: "Nairobi" },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
export default function App() {
|
|
90
|
+
return <DataGrid columns={columns} rows={rows} />;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The same grid with the common features switched on:
|
|
95
|
+
|
|
50
96
|
```tsx
|
|
51
97
|
import { useState } from "react";
|
|
52
98
|
import { DataGrid, type ColumnDef } from "react-gridix";
|
|
@@ -55,20 +101,21 @@ import "react-gridix/style.css";
|
|
|
55
101
|
interface Person {
|
|
56
102
|
id: number;
|
|
57
103
|
name: string;
|
|
58
|
-
|
|
104
|
+
joined: string; // "2026-03-14"
|
|
105
|
+
contact: { email: string };
|
|
59
106
|
}
|
|
60
107
|
|
|
61
108
|
const columns: ColumnDef<Person>[] = [
|
|
62
|
-
{ field: "id", headerName: "ID", type: "number", editable: false },
|
|
63
|
-
{ field: "name", headerName: "Name", filterable: true },
|
|
109
|
+
{ field: "id", headerName: "ID", type: "number", editable: false, sortable: true },
|
|
110
|
+
{ field: "name", headerName: "Name", sortable: true, filterable: true },
|
|
111
|
+
{ field: "joined", headerName: "Joined", type: "date", sortable: true, filterable: true },
|
|
64
112
|
{ field: "contact.email", headerName: "Email" },
|
|
65
|
-
{ field: "contact.phone", headerName: "Phone" },
|
|
66
113
|
];
|
|
67
114
|
|
|
68
115
|
export default function App() {
|
|
69
116
|
const [rows, setRows] = useState<Person[]>([
|
|
70
|
-
{ id: 1, name: "Amina", contact: { email: "amina@example.com"
|
|
71
|
-
{ id: 2, name: "Brian", contact: { email: "brian@example.com"
|
|
117
|
+
{ id: 1, name: "Amina", joined: "2026-03-14", contact: { email: "amina@example.com" } },
|
|
118
|
+
{ id: 2, name: "Brian", joined: "2025-11-02", contact: { email: "brian@example.com" } },
|
|
72
119
|
]);
|
|
73
120
|
|
|
74
121
|
return (
|
|
@@ -78,6 +125,9 @@ export default function App() {
|
|
|
78
125
|
getRowId={(r) => r.id}
|
|
79
126
|
editable
|
|
80
127
|
onRowsChange={setRows}
|
|
128
|
+
searchable // search box
|
|
129
|
+
pagination // row count and page controls
|
|
130
|
+
rowSelection // checkbox column
|
|
81
131
|
theme="ocean"
|
|
82
132
|
/>
|
|
83
133
|
);
|
|
@@ -100,19 +150,51 @@ Pass a theme name to the grid. **Without a `theme` prop nothing changes**: the g
|
|
|
100
150
|
| --- | --- |
|
|
101
151
|
| `"ocean"` | Light, cool blues, blue gradient header, soft shadow |
|
|
102
152
|
| `"midnight"` | Dark slate, indigo accent, dark popups and inputs |
|
|
103
|
-
| `"sunset"` | Warm cream, orange-to-
|
|
153
|
+
| `"sunset"` | Warm cream, burnt-orange-to-raspberry gradient header, serif font, rounded corners |
|
|
104
154
|
| `"forest"` | Calm greens, solid dark-green header, sharper corners |
|
|
155
|
+
| `"lavender"` | Light purple, deep violet gradient header, rounded corners |
|
|
156
|
+
| `"coffee"` | Warm browns and cream, serif font, rich brown header |
|
|
157
|
+
| `"graphite"` | Dark neutral grey with a teal accent, flat and sharp |
|
|
158
|
+
| `"aurora"` | Deep night blue with an emerald-to-blue-to-violet gradient header and a soft glow |
|
|
159
|
+
|
|
160
|
+
A theme is only a set of CSS variables on `[data-rg-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.
|
|
161
|
+
|
|
162
|
+
**Switching themes at run time** is just a state change:
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
const [theme, setTheme] = useState<GridTheme>("ocean");
|
|
166
|
+
|
|
167
|
+
<select value={theme} onChange={(e) => setTheme(e.target.value as GridTheme)}>
|
|
168
|
+
{["ocean", "midnight", "sunset", "forest", "lavender", "coffee", "graphite", "aurora"].map((t) => (
|
|
169
|
+
<option key={t}>{t}</option>
|
|
170
|
+
))}
|
|
171
|
+
</select>
|
|
172
|
+
<DataGrid theme={theme} ... />
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Following the system dark mode.** Choose the theme from a media query:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
179
|
+
<DataGrid theme={dark ? "graphite" : "lavender"} ... />
|
|
180
|
+
```
|
|
105
181
|
|
|
106
|
-
|
|
182
|
+
**Tweaking a built-in theme.** Because themes have zero specificity, any variable or rule you write wins over them:
|
|
183
|
+
|
|
184
|
+
```css
|
|
185
|
+
/* make the aurora header a single colour */
|
|
186
|
+
[data-rg-theme="aurora"] { --rg-header-bg: #0f766e; }
|
|
187
|
+
```
|
|
107
188
|
|
|
108
189
|
**Making your own theme.** Any other string works as a theme name. Define the variables you want:
|
|
109
190
|
|
|
110
191
|
```css
|
|
111
|
-
[data-
|
|
112
|
-
--
|
|
113
|
-
--
|
|
114
|
-
--
|
|
115
|
-
--
|
|
192
|
+
[data-rg-theme="brand"] {
|
|
193
|
+
--rg-header-bg: #7c3aed;
|
|
194
|
+
--rg-header-text: #fff;
|
|
195
|
+
--rg-row-hover: #f5f3ff;
|
|
196
|
+
--rg-selected-bg: #ede9fe; /* selected rows */
|
|
197
|
+
--rg-primary: #7c3aed;
|
|
116
198
|
}
|
|
117
199
|
```
|
|
118
200
|
|
|
@@ -120,56 +202,70 @@ A theme is only a set of CSS variables on `[data-rk-theme="name"]`, written with
|
|
|
120
202
|
<DataGrid theme="brand" ... />
|
|
121
203
|
```
|
|
122
204
|
|
|
205
|
+
A dark custom theme should also set the colours that the light defaults assume, for example `--rg-surface`, `--rg-text`, `--rg-border`, `--rg-pinned-bg`, `--rg-skeleton-a` and `--rg-skeleton-b`. Copy the `graphite` block in `themes.css` as a starting point.
|
|
206
|
+
|
|
123
207
|
### 4.2 CSS variables
|
|
124
208
|
|
|
125
209
|
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`.
|
|
126
210
|
|
|
127
211
|
| Variable | Default | Used for |
|
|
128
212
|
| --- | --- | --- |
|
|
129
|
-
| `--
|
|
130
|
-
| `--
|
|
131
|
-
| `--
|
|
132
|
-
| `--
|
|
133
|
-
| `--
|
|
134
|
-
| `--
|
|
135
|
-
| `--
|
|
136
|
-
| `--
|
|
137
|
-
| `--
|
|
138
|
-
| `--
|
|
139
|
-
| `--
|
|
140
|
-
| `--
|
|
141
|
-
| `--
|
|
142
|
-
| `--
|
|
143
|
-
| `--
|
|
144
|
-
| `--
|
|
145
|
-
| `--
|
|
146
|
-
| `--
|
|
147
|
-
| `--
|
|
148
|
-
| `--
|
|
149
|
-
| `--
|
|
150
|
-
| `--
|
|
151
|
-
| `--
|
|
152
|
-
| `--
|
|
153
|
-
| `--
|
|
154
|
-
| `--
|
|
155
|
-
| `--
|
|
156
|
-
| `--
|
|
157
|
-
| `--
|
|
158
|
-
| `--
|
|
159
|
-
| `--
|
|
160
|
-
| `--
|
|
161
|
-
| `--
|
|
162
|
-
| `--
|
|
163
|
-
| `--
|
|
164
|
-
| `--
|
|
213
|
+
| `--rg-font` | system UI stack | Font family |
|
|
214
|
+
| `--rg-text` | `#111827` | Main text |
|
|
215
|
+
| `--rg-muted` | `#6b7280` | Secondary text, empty state |
|
|
216
|
+
| `--rg-border` | `#d1d5db` | Borders |
|
|
217
|
+
| `--rg-surface` | `#ffffff` | Background of the grid, inputs, buttons |
|
|
218
|
+
| `--rg-bg-subtle` | `#f3f4f6` | Header and footer background |
|
|
219
|
+
| `--rg-primary` | `#4f46e5` | Focus rings, active editor border |
|
|
220
|
+
| `--rg-danger` | `#dc2626` | Invalid editor border |
|
|
221
|
+
| `--rg-invalid-bg` | `#fef2f2` | Invalid editor background |
|
|
222
|
+
| `--rg-grid-bg` | `--rg-surface` | Grid background |
|
|
223
|
+
| `--rg-grid-radius` | `10px` | Grid corner radius |
|
|
224
|
+
| `--rg-grid-shadow` | `none` | Grid shadow |
|
|
225
|
+
| `--rg-toolbar-bg` | `transparent` | Toolbar background |
|
|
226
|
+
| `--rg-input-bg`, `--rg-input-border`, `--rg-input-text` | surface, border, inherit | Search box, filter inputs, editor, page controls |
|
|
227
|
+
| `--rg-header-bg` | `--rg-bg-subtle` | Header background. Any CSS background, so gradients work |
|
|
228
|
+
| `--rg-header-text` | inherit | Header text colour |
|
|
229
|
+
| `--rg-header-hover-bg` | `rgba(0,0,0,.06)` | Header hover |
|
|
230
|
+
| `--rg-header-border` | `--rg-border` | Line under the header |
|
|
231
|
+
| `--rg-cell-border` | `#eef0f3` | Lines between rows |
|
|
232
|
+
| `--rg-row-hover` | `#f3f4f6` | Row hover |
|
|
233
|
+
| `--rg-selected-bg` | `#eef2ff` | Background of selected rows |
|
|
234
|
+
| `--rg-on-primary` | `#ffffff` | Text colour on top of `--rg-primary` (the selected day in the calendar). Dark themes set a dark colour |
|
|
235
|
+
| `--rg-pinned-bg` | `--rg-grid-bg` | Opaque background behind pinned body cells (so scrolled cells do not show through) |
|
|
236
|
+
| `--rg-pinned-header-bg` | `--rg-bg-subtle` | Opaque background behind pinned header cells |
|
|
237
|
+
| `--rg-skeleton-a`, `--rg-skeleton-b` | `#e5e7eb`, `#f3f4f6` | The two colours of the shimmering loading bars |
|
|
238
|
+
| `--rg-stripe-a`, `--rg-stripe-b` | `transparent`, `#fafafa` | Alternating row colours (see [stripes](#striped-rows)) |
|
|
239
|
+
| `--rg-footer-bg`, `--rg-footer-text` | subtle, muted | Pagination bar |
|
|
240
|
+
| `--rg-btn-hover` | `#e5e7eb` | Page button, toolbar button and Clear button hover |
|
|
241
|
+
| `--rg-filter-active` | `--rg-primary` | Filter dot colour while a filter is set |
|
|
242
|
+
| `--rg-filter-glow` | `rgba(79,70,229,.25)` | Ring around an active filter dot |
|
|
243
|
+
| `--rg-popup-bg`, `--rg-popup-text`, `--rg-popup-border` | white, dark, grey | Filter popup and column menu |
|
|
244
|
+
| `--rg-popup-radius`, `--rg-popup-shadow` | `10px`, soft shadow | Popup shape |
|
|
245
|
+
| `--rg-row-duration` | `0.38s` | Row entrance animation length |
|
|
246
|
+
| `--rg-row-stagger` | `28ms` | Extra delay per row in the entrance animation |
|
|
247
|
+
| `--rg-checkbox-size` | `17px` | Checkbox width and height |
|
|
248
|
+
| `--rg-checkbox-border-width` | `1px` | Checkbox border width |
|
|
249
|
+
| `--rg-checkbox-radius` | `0` | Checkbox corner radius (square by default). The inner mark of the `"button"` variant is rounded half as much. The sunset and lavender themes use `2px` |
|
|
250
|
+
| `--rg-checkbox-color` | `#4f46e5` | Checked colour (also the focus ring and the native accent) |
|
|
251
|
+
| `--rg-checkbox-border` | `#9ca3af` | Border colour of an unchecked `"button"` / `"tick"` checkbox |
|
|
252
|
+
| `--rg-checkbox-bg` | `#ffffff` | Background of an unchecked `"button"` / `"tick"` checkbox |
|
|
253
|
+
| `--rg-checkbox-tick` | `#ffffff` | Colour of the tick, and of a custom character in the `"tick"` variant |
|
|
165
254
|
|
|
166
255
|
```css
|
|
167
256
|
:root {
|
|
168
|
-
--
|
|
169
|
-
--
|
|
257
|
+
--rg-primary: #0d9488;
|
|
258
|
+
--rg-row-hover: #f0fdfa;
|
|
259
|
+
--rg-selected-bg: #ccfbf1;
|
|
170
260
|
}
|
|
171
261
|
```
|
|
172
262
|
|
|
263
|
+
Scoped to one grid through its `style` prop:
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
<DataGrid style={{ "--rg-header-bg": "#0f172a", "--rg-header-text": "#fff" } as React.CSSProperties} ... />
|
|
267
|
+
```
|
|
268
|
+
|
|
173
269
|
---
|
|
174
270
|
|
|
175
271
|
## 5. Checkbox
|
|
@@ -221,16 +317,16 @@ A checkbox in three looks. Every variant toggles with **one click** (or the Spac
|
|
|
221
317
|
/>
|
|
222
318
|
```
|
|
223
319
|
|
|
224
|
-
(`aria-checked:` works for the `"button"` and `"tick"` variants. The native input has no `aria-checked`, so use the `--
|
|
225
|
-
2. **CSS variables**: `--
|
|
320
|
+
(`aria-checked:` works for the `"button"` and `"tick"` variants. The native input has no `aria-checked`, so use the `--rg-checkbox-color` variable for it.)
|
|
321
|
+
2. **CSS variables**: `--rg-checkbox-size`, `--rg-checkbox-border-width`, `--rg-checkbox-radius`, `--rg-checkbox-color`, `--rg-checkbox-border`, `--rg-checkbox-bg` and `--rg-checkbox-tick` (see [Theming](#42-css-variables)). Set them anywhere above the checkbox:
|
|
226
322
|
|
|
227
323
|
```tsx
|
|
228
|
-
<Checkbox styles={{ root: { "--
|
|
324
|
+
<Checkbox styles={{ root: { "--rg-checkbox-color": "#dc2626" } as CSSProperties }} ... />
|
|
229
325
|
```
|
|
230
|
-
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 `--
|
|
231
|
-
4. **Plain CSS** with these stable classes: `.
|
|
326
|
+
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 `--rg-checkbox-size`) and coloured with `--rg-checkbox-color` in the button variant and `--rg-checkbox-tick` in the tick variant. Restyle it through `classNames.indicator` / `styles.indicator`.
|
|
327
|
+
4. **Plain CSS** with these stable classes: `.rg-checkbox`, `.rg-checkbox--button`, `.rg-checkbox--tick`, `.rg-checkbox--native`, `.rg-checkbox--checked`, `.rg-checkbox--indeterminate`, `.rg-checkbox--disabled`, `.rg-checkbox__control`, `.rg-checkbox__indicator`, `.rg-checkbox__indicator--custom` and `.rg-checkbox__label`. State is also exposed as `data-state="checked"`, `"unchecked"` or `"indeterminate"` and, on the button variants, `aria-checked` (`"true"`, `"false"` or `"mixed"`).
|
|
232
328
|
|
|
233
|
-
**Defaults:** a 1px border and square corners (`--
|
|
329
|
+
**Defaults:** a 1px border and square corners (`--rg-checkbox-border-width: 1px`, `--rg-checkbox-radius: 0`). Round them with `--rg-checkbox-radius: 999px` or a Tailwind `rounded-*` class on `classNames.control`.
|
|
234
330
|
|
|
235
331
|
The checkbox CSS is written with `:where()` (zero specificity), so any class you add wins without `!important`.
|
|
236
332
|
|
|
@@ -250,44 +346,106 @@ The checkbox CSS is written with `:where()` (zero specificity), so any class you
|
|
|
250
346
|
|
|
251
347
|
- **`rows`** is an array of plain objects.
|
|
252
348
|
- **`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.
|
|
253
|
-
- **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.
|
|
349
|
+
- **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.
|
|
254
350
|
- **`rowIndex`** in callbacks and params is always the row's index in your original `rows` array, not its position on screen.
|
|
255
|
-
- **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.
|
|
351
|
+
- **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).
|
|
352
|
+
- **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).
|
|
353
|
+
- **Opt-in everywhere.** Features are off until you switch them on (see the table in the [overview](#1-overview)).
|
|
256
354
|
|
|
257
|
-
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](#
|
|
355
|
+
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)).
|
|
258
356
|
|
|
259
357
|
### 6.2 DataGrid props
|
|
260
358
|
|
|
359
|
+
**Data and editing**
|
|
360
|
+
|
|
261
361
|
| Prop | Type | Default | Description |
|
|
262
362
|
| --- | --- | --- | --- |
|
|
263
363
|
| `columns` | `ColumnDef<T>[]` | required | Column definitions |
|
|
264
|
-
| `rows` | `T[]` | required | Data |
|
|
364
|
+
| `rows` | `T[]` | required | Data. In `serverSide` mode: the current page |
|
|
265
365
|
| `getRowId` | `(row, index) => string \| number` | row index | Stable id for each row |
|
|
266
366
|
| `editable` | `boolean` | `false` | Allow editing cells. Columns can override this |
|
|
267
367
|
| `enterMovesDown` | `boolean` | `true` | After Enter saves a cell, edit the cell below |
|
|
268
368
|
| `onCellEdit` | `(event: CellEditEvent<T>) => void` | | Called after a cell edit is saved |
|
|
269
369
|
| `onRowsChange` | `(rows: T[]) => void` | | Called with the full updated array after an edit |
|
|
270
|
-
| `sortable` | `boolean` | `true` | Click headers to sort. Columns can override |
|
|
271
|
-
| `defaultSort` | `SortModel \| null` | `null` | Initial sort. Read once, on mount |
|
|
272
|
-
| `onSortChange` | `(sort: SortModel \| null) => void` | | Called when the sort changes |
|
|
273
|
-
| `searchable` | `boolean` | `true` | Show the global search box |
|
|
274
|
-
| `searchPlaceholder` | `string` | `"Search…"` | Placeholder of the search box |
|
|
275
|
-
| `pagination` | `boolean` | `true` | Split rows into pages |
|
|
276
|
-
| `paginationPosition` | `"bottom" \| "top" \| "both"` | `"bottom"` | Where the row count and page controls go |
|
|
277
|
-
| `pageSize` | `number` | `10` | Initial rows per page. Read once, on mount |
|
|
278
|
-
| `pageSizeOptions` | `number[]` | `[5, 10, 25, 50]` | Choices in the page size menu. The current page size is added if missing |
|
|
279
370
|
| `onRowClick` | `(row, rowIndex) => void` | | Called when a row is clicked |
|
|
371
|
+
|
|
372
|
+
**Row selection** (see [6.13](#613-row-selection))
|
|
373
|
+
|
|
374
|
+
| Prop | Type | Default | Description |
|
|
375
|
+
| --- | --- | --- | --- |
|
|
376
|
+
| `rowSelection` | `boolean \| "single" \| "multiple"` | `false` | Adds a checkbox column. `true` means `"multiple"` |
|
|
377
|
+
| `selectedRowIds` | `(string \| number)[]` | | Controlled selection |
|
|
378
|
+
| `defaultSelectedRowIds` | `(string \| number)[]` | `[]` | Initial selection when not controlled |
|
|
379
|
+
| `onSelectionChange` | `(selection: RowSelection<T>) => void` | | Called with `{ ids, rows }` |
|
|
380
|
+
| `isRowSelectable` | `(row) => boolean` | all rows | Return `false` to disable a row's checkbox |
|
|
381
|
+
|
|
382
|
+
**Sorting** (see [6.4](#64-sorting))
|
|
383
|
+
|
|
384
|
+
| Prop | Type | Default | Description |
|
|
385
|
+
| --- | --- | --- | --- |
|
|
386
|
+
| `multiSort` | `boolean` | `false` | Shift+click adds more sort columns |
|
|
387
|
+
| `sort` | `SortModel[]` | | Controlled sort |
|
|
388
|
+
| `defaultSort` | `SortModel \| SortModel[] \| null` | `null` | Initial sort when not controlled |
|
|
389
|
+
| `onSortChange` | `(sort: SortModel[]) => void` | | Called when the sort changes |
|
|
390
|
+
|
|
391
|
+
**Search, filters and paging** (see [6.5](#65-searching-and-filtering), [6.7](#67-pagination), [6.14](#614-controlled-state))
|
|
392
|
+
|
|
393
|
+
| Prop | Type | Default | Description |
|
|
394
|
+
| --- | --- | --- | --- |
|
|
395
|
+
| `searchable` | `boolean` | `false` | Show the global search box |
|
|
396
|
+
| `searchPlaceholder` | `string` | `"Search…"` | Placeholder of the search box |
|
|
397
|
+
| `search` / `onSearchChange` | `string` / `(s) => void` | | Controlled search text |
|
|
398
|
+
| `filters` / `onFiltersChange` | `Record<string, FilterValue>` / `(f) => void` | | Controlled column filters |
|
|
399
|
+
| `pagination` | `boolean` | `false` | Split rows into pages and show the bar with the row count and page controls |
|
|
400
|
+
| `paginationPosition` | `"bottom" \| "top" \| "both"` | `"bottom"` | Where the bar goes |
|
|
401
|
+
| `pageSize` | `number` | `10` | Rows per page (initial value) |
|
|
402
|
+
| `pageSizeOptions` | `number[]` | `[5, 10, 25, 50]` | Choices in the page size menu. The current size is added if missing |
|
|
403
|
+
| `page` / `onPageChange` | `number` / `(page) => void` | | Controlled page (zero-based) |
|
|
404
|
+
| `onPageSizeChange` | `(pageSize) => void` | | Called when the page size menu changes |
|
|
405
|
+
|
|
406
|
+
**Server-side mode, loading and errors** (see [6.15](#615-server-side-mode), [6.16](#616-loading-skeleton))
|
|
407
|
+
|
|
408
|
+
| Prop | Type | Default | Description |
|
|
409
|
+
| --- | --- | --- | --- |
|
|
410
|
+
| `serverSide` | `boolean` | `false` | Your server sorts, filters, searches and pages |
|
|
411
|
+
| `rowCount` | `number` | | Total rows on the server (for the page count) |
|
|
412
|
+
| `onQueryChange` | `(query: GridQuery) => void \| Promise<unknown>` | | Called on mount and whenever the query changes. A returned promise drives the loading state |
|
|
413
|
+
| `queryDebounce` | `number` | `250` | Milliseconds to wait after the last keystroke in the search box before querying |
|
|
414
|
+
| `filterDebounce` | `number` | `300` | Milliseconds to wait after the last keystroke in a typed column filter (text, exact, number) before it is applied. A column can override it. See [Debouncing typed filters](#debouncing-typed-filters) |
|
|
415
|
+
| `loading` | `boolean` | follows the promise | Force the loading state on or off |
|
|
416
|
+
| `loadingRows` | `number` | page size (max 20), or 5 | How many skeleton rows to draw |
|
|
417
|
+
| `skeletonRow` | `ComponentType<SkeletonRowProps<T>>` | shimmer bars | Your own component for each placeholder row |
|
|
418
|
+
| `errorMessage` | `ReactNode` | `"Could not load the data."` | Shown with a Retry button when the promise rejects |
|
|
419
|
+
|
|
420
|
+
**Columns, keyboard, big data and export**
|
|
421
|
+
|
|
422
|
+
| Prop | Type | Default | Description |
|
|
423
|
+
| --- | --- | --- | --- |
|
|
424
|
+
| `columnMenu` | `boolean` | `false` | A "Columns" button to show and hide columns |
|
|
425
|
+
| `onColumnVisibilityChange` | `(hiddenFields: string[]) => void` | | Called with the fields that are hidden |
|
|
426
|
+
| `keyboardNavigation` | `boolean` | `false` | Arrow keys, Home, End, Page Up and Page Down move between cells |
|
|
427
|
+
| `virtualize` | `boolean` | `false` | Render only the visible rows |
|
|
428
|
+
| `rowHeight` | `number` | `40` | Row height in px when virtualizing |
|
|
429
|
+
| `overscan` | `number` | `6` | Extra rows drawn above and below the window |
|
|
430
|
+
| `exportable` | `boolean \| ExportOptions` | `false` | Show an "Export CSV" button |
|
|
431
|
+
| `apiRef` | `MutableRefObject<DataGridApi<T> \| null>` | | Call `exportCsv()`, `getSelectedRows()` and more from outside |
|
|
432
|
+
|
|
433
|
+
**Look and feel**
|
|
434
|
+
|
|
435
|
+
| Prop | Type | Default | Description |
|
|
436
|
+
| --- | --- | --- | --- |
|
|
437
|
+
| `theme` | `GridTheme \| string` | | Applies a theme. Nothing changes without it. See [Theming](#4-theming) |
|
|
280
438
|
| `toolbar` | `ReactNode` | | Extra content on the right of the toolbar |
|
|
281
439
|
| `emptyMessage` | `ReactNode` | `"No rows to show"` | Shown when no rows match |
|
|
282
440
|
| `striped` | `boolean` | `false` | Alternate row shading |
|
|
283
441
|
| `stripeColors` | `[string, string]` | | Two colours for the alternating rows. Implies `striped`. See [striped rows](#striped-rows) |
|
|
284
442
|
| `animateRows` | `boolean` | `true` | Rows slide up and fade in when they render. See [row animation](#row-animation) |
|
|
285
|
-
| `
|
|
286
|
-
| `checkboxVariant` | `"button" \| "tick" \| "native"` | `"native"` | Checkbox look for
|
|
443
|
+
| `rowAnimationClassName` | `string \| (row, rowIndex) => string` | | Your own CSS class for the entrance animation. Replaces the built-in slide-up. See [custom row animation](#custom-row-animation) |
|
|
444
|
+
| `checkboxVariant` | `"button" \| "tick" \| "native"` | `"native"` | Checkbox look for boolean columns and row selection. Columns can override it |
|
|
287
445
|
| `maxHeight` | `number \| string` | | Makes the body scroll, with a sticky header. Example: `400` or `"60vh"` |
|
|
288
446
|
| `className` | `string` | | Classes for the root element (Tailwind works). The grid is `width: 100%` of its container by default |
|
|
289
447
|
| `style` | `CSSProperties` | | Inline styles for the root element |
|
|
290
|
-
| `classNames` | `Partial<Record<GridSlot, string>>` | | Classes for individual parts. See [6.
|
|
448
|
+
| `classNames` | `Partial<Record<GridSlot, string>>` | | Classes for individual parts. See [6.11](#611-styling) |
|
|
291
449
|
| `styles` | `Partial<Record<GridSlot, CSSProperties>>` | | Inline styles for individual parts |
|
|
292
450
|
| `rowClassName` | `string \| (row, rowIndex) => string` | | Classes for rows |
|
|
293
451
|
| `rowStyle` | `CSSProperties \| (row, rowIndex) => CSSProperties` | | Inline styles for rows |
|
|
@@ -296,8 +454,9 @@ The grid is generic: `DataGrid<T>` infers `T` from `rows`, and `ColumnDef<T>` ch
|
|
|
296
454
|
|
|
297
455
|
```ts
|
|
298
456
|
const columns: ColumnDef<Employee>[] = [
|
|
299
|
-
{ field: "name", headerName: "Name" },
|
|
300
|
-
{ field: "salary", headerName: "Salary", type: "number", align: "right" },
|
|
457
|
+
{ field: "name", headerName: "Name", sortable: true, filterable: true },
|
|
458
|
+
{ field: "salary", headerName: "Salary", type: "number", align: "right", sortable: true },
|
|
459
|
+
{ field: "hired", headerName: "Hired", type: "date", sortable: true, filterable: true },
|
|
301
460
|
];
|
|
302
461
|
```
|
|
303
462
|
|
|
@@ -305,57 +464,143 @@ const columns: ColumnDef<Employee>[] = [
|
|
|
305
464
|
| --- | --- | --- |
|
|
306
465
|
| `field` | `FieldPath<T>` | **Required.** Key of the row object this column reads and writes. Use dot notation for nested objects: `"contact.phone"` |
|
|
307
466
|
| `headerName` | `string` | Header text. Defaults to the field name, except in `type: "boolean"` columns, where it defaults to an empty string |
|
|
308
|
-
| `type` | `"text" \| "number" \| "boolean"` | `"number"` columns edit and parse as numbers
|
|
467
|
+
| `type` | `"text" \| "number" \| "boolean" \| "date" \| "sequence"` | `"number"` columns edit and parse as numbers, `"boolean"` columns show a checkbox, `"date"` columns have a calendar picker (see [6.6](#66-date-columns)), `"sequence"` columns show row numbers (see [row numbers](#row-numbers-sequence-column)). Default `"text"` |
|
|
468
|
+
| `sequenceStart` | `number` | Only for `type: "sequence"`: the number of the first row (default `1`) |
|
|
469
|
+
| `sequenceRestartEachPage` | `boolean` | Only for `type: "sequence"`: start again at `sequenceStart` on every page (default `false`: the count continues, 11, 12, ...) |
|
|
309
470
|
| `width` | `number \| string` | Header cell width |
|
|
310
|
-
| `minWidth` | `number \| string` |
|
|
471
|
+
| `minWidth` / `maxWidth` | `number \| string` / `number` | Smallest and largest width (`maxWidth` limits dragging when resizing) |
|
|
311
472
|
| `align` | `"left" \| "center" \| "right"` | Text alignment for the header, cells and editor |
|
|
312
|
-
| `sortable` | `boolean` |
|
|
473
|
+
| `sortable` | `boolean` | **Opt in to sorting this column** (default `false`) |
|
|
474
|
+
| `resizable` | `boolean` | Show a drag handle on the header's right edge (default `false`). See [6.17](#617-resize-pin-and-hide-columns) |
|
|
475
|
+
| `pinned` | `"left" \| "right"` | Stick the column to an edge while scrolling sideways |
|
|
476
|
+
| `hidden` | `boolean` | Start hidden (shown again from the Columns menu) |
|
|
477
|
+
| `hideable` | `boolean` | Set `false` to keep a column out of the Columns menu (default `true`) |
|
|
313
478
|
| `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) |
|
|
314
|
-
| `
|
|
479
|
+
| `filterType` | `"text" \| "exact" \| "number" \| "date" \| "select"` | Which filter controls the popup shows. Default follows `type` |
|
|
480
|
+
| `filterOptions` | `(string \| FilterOption)[]` | Choices for a `"select"` filter. Without it they are built from the data |
|
|
481
|
+
| `filterPlaceholder` | `string` | Placeholder of the text filter box |
|
|
482
|
+
| `filterDebounce` | `number` | Milliseconds to wait after typing in **this** column's text / exact / number filter before it is applied. Overrides the grid's `filterDebounce` (default 300). `0` applies on every keystroke |
|
|
315
483
|
| `filterIndicator` | `ComponentType<FilterIndicatorProps<T>>` | Your own component drawn instead of the default dot |
|
|
316
|
-
| `filterIndicatorClassName` | `string` | Classes for this column's indicator button |
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
319
|
-
| `filterPopupStyle` | `CSSProperties` | Inline styles for this column's filter popup |
|
|
484
|
+
| `filterIndicatorClassName` / `filterIndicatorStyle` | `string` / `CSSProperties` | Classes and inline styles for this column's indicator button |
|
|
485
|
+
| `filterPopupClassName` / `filterPopupStyle` | `string` / `CSSProperties` | Classes and inline styles for this column's filter popup |
|
|
486
|
+
| `filterRenderer` | `ComponentType<FilterRendererProps<T>>` | Component that replaces the filter controls inside the popup |
|
|
320
487
|
| `editable` | `boolean \| (params: CellEditableParams<T>) => boolean` | Overrides the grid-level `editable`. A function decides per cell |
|
|
321
|
-
| `valueFormatter` | `(value, row) => string` | Text used for display, search
|
|
488
|
+
| `valueFormatter` | `(value, row) => string` | Text used for display, search and text filters. Does not change the stored value |
|
|
322
489
|
| `valueParser` | `(text, row) => unknown` | Turns typed text into the value to store |
|
|
323
490
|
| `validate` | `(value, row) => boolean` | Return `false` to reject an edit |
|
|
324
491
|
| `comparator` | `(a, b, rowA, rowB) => number` | Custom sort order |
|
|
492
|
+
| `dateFormat` / `dateLocale` | `Intl.DateTimeFormatOptions` / `string` | How a `"date"` column is displayed |
|
|
493
|
+
| `dateMin` / `dateMax` | `string` (`"YYYY-MM-DD"`) | Earliest and latest day the date picker accepts |
|
|
494
|
+
| `exportValue` | `(value, row) => string \| number \| boolean \| null` | What goes in the CSV file for this column |
|
|
325
495
|
| `cellRenderer` | `ComponentType<CellParams<T>>` | Component that renders the cell |
|
|
326
|
-
| `cellEditor` | `ComponentType<CellEditorProps<T>>` | Component used instead of the
|
|
496
|
+
| `cellEditor` | `ComponentType<CellEditorProps<T>>` | Component used instead of the built-in editor |
|
|
327
497
|
| `headerRenderer` | `ComponentType<HeaderRendererProps<T>>` | Component that renders the header label |
|
|
328
|
-
| `
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
331
|
-
| `headerClassName` | `string` | Classes for this column's header cell only |
|
|
332
|
-
| `headerStyle` | `CSSProperties` | Inline styles for this column's header cell |
|
|
333
|
-
| `cellClassName` | `string \| (params) => string` | Classes for this column's cells |
|
|
334
|
-
| `cellStyle` | `CSSProperties \| (params) => CSSProperties` | Inline styles for this column's cells |
|
|
498
|
+
| `className` / `style` | `string` / `CSSProperties` | Classes and inline styles for the whole column: header cell and every body cell (Tailwind works) |
|
|
499
|
+
| `headerClassName` / `headerStyle` | `string` / `CSSProperties` | Header cell only |
|
|
500
|
+
| `cellClassName` / `cellStyle` | `string \| (params) => string` / `CSSProperties \| (params) => CSSProperties` | Body cells only |
|
|
335
501
|
| `checkbox` | `ColumnCheckboxOptions` | Look of the checkbox in a `type: "boolean"` column: `variant`, `indicator`, `className`, `style`, `classNames`, `styles` |
|
|
336
|
-
| `headerCheckbox` | `boolean` | On a `type: "boolean"` column, draws a select-all checkbox in the header
|
|
502
|
+
| `headerCheckbox` | `boolean` | On a `type: "boolean"` column, draws a select-all checkbox in the header. See [Select-all header checkbox](#select-all-header-checkbox) |
|
|
503
|
+
|
|
504
|
+
#### Row numbers (sequence column)
|
|
505
|
+
|
|
506
|
+
`type: "sequence"` adds a column of row numbers. It needs no data in your rows: give it a `field` that starts with `#` (so it can never clash with a real field) and a header.
|
|
507
|
+
|
|
508
|
+
```tsx
|
|
509
|
+
const columns: ColumnDef<Person>[] = [
|
|
510
|
+
{ field: "#", type: "sequence", headerName: "No.", width: 64, align: "center" },
|
|
511
|
+
{ field: "name", sortable: true, filterable: true },
|
|
512
|
+
{ field: "email" },
|
|
513
|
+
];
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
- The number is the row's **position in the list the user is looking at**. After a search or a filter the numbers start again at 1, and after sorting they follow the new order.
|
|
517
|
+
- With pagination the count **continues** on the next pages (page 2 starts at 11). Set `sequenceRestartEachPage: true` to start at `sequenceStart` on every page.
|
|
518
|
+
- `sequenceStart` changes the first number: `sequenceStart: 0` counts from 0.
|
|
519
|
+
- In server-side mode the number is `page * pageSize + position`, so page 3 of 10 rows starts at 21.
|
|
520
|
+
- The column is display-only: it cannot be sorted, filtered or edited, and it is skipped by the search. It can be pinned (`pinned: "left"`), resized and hidden like any other. The header defaults to `#`.
|
|
521
|
+
- `valueFormatter` works on it: `valueFormatter: (n) => String(n).padStart(3, "0")` gives `001`, `002`, ...
|
|
522
|
+
- CSV export includes it. A `scope: "page"` export numbers like the page does.
|
|
523
|
+
|
|
524
|
+
```tsx
|
|
525
|
+
// "No. 1", "No. 2", ... pinned on the left, starting after a header row of your own
|
|
526
|
+
{ field: "#", type: "sequence", sequenceStart: 1, pinned: "left", width: 70, valueFormatter: (n) => `No. ${n}` }
|
|
527
|
+
|
|
528
|
+
// restart on every page
|
|
529
|
+
{ field: "#", type: "sequence", sequenceRestartEachPage: true }
|
|
530
|
+
```
|
|
337
531
|
|
|
338
532
|
### 6.4 Sorting
|
|
339
533
|
|
|
534
|
+
**Sorting is chosen per column.** A column is sortable only when its definition says `sortable: true`. There is no grid-level switch.
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
const columns: ColumnDef<Employee>[] = [
|
|
538
|
+
{ field: "id", type: "number" }, // not sortable
|
|
539
|
+
{ field: "name", sortable: true }, // click to sort
|
|
540
|
+
{ field: "salary", type: "number", sortable: true },
|
|
541
|
+
];
|
|
542
|
+
```
|
|
543
|
+
|
|
340
544
|
- Click a header to sort ascending, click again for descending, click a third time to clear the sort.
|
|
341
|
-
-
|
|
342
|
-
- Numbers sort numerically, dates by time, booleans false before true. Everything else sorts as text with natural number ordering (`"item 2"` before `"item 10"`), ignoring case.
|
|
545
|
+
- 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.
|
|
343
546
|
- Empty values (`null`, `undefined`, `""`) always sort last, in both directions.
|
|
344
|
-
-
|
|
547
|
+
- 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.
|
|
548
|
+
|
|
549
|
+
**Custom order** with a `comparator`:
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
const order = ["low", "medium", "high", "urgent"];
|
|
553
|
+
{ field: "priority", sortable: true, comparator: (a, b) => order.indexOf(a) - order.indexOf(b) }
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**Starting sorted** with `defaultSort`:
|
|
557
|
+
|
|
558
|
+
```tsx
|
|
559
|
+
<DataGrid defaultSort={{ field: "salary", direction: "desc" }} ... />
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**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.
|
|
563
|
+
|
|
564
|
+
```tsx
|
|
565
|
+
<DataGrid
|
|
566
|
+
multiSort
|
|
567
|
+
defaultSort={[
|
|
568
|
+
{ field: "department", direction: "asc" },
|
|
569
|
+
{ field: "salary", direction: "desc" }, // inside each department, highest salary first
|
|
570
|
+
]}
|
|
571
|
+
columns={columns}
|
|
572
|
+
rows={rows}
|
|
573
|
+
/>
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**Reacting to the sort** (always an array, empty when unsorted):
|
|
345
577
|
|
|
346
578
|
```tsx
|
|
347
|
-
|
|
579
|
+
<DataGrid
|
|
580
|
+
onSortChange={(sort) => console.log(sort)}
|
|
581
|
+
// [{ field: "department", direction: "asc" }, { field: "salary", direction: "desc" }]
|
|
582
|
+
...
|
|
583
|
+
/>
|
|
348
584
|
```
|
|
349
585
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
586
|
+
**Controlling the sort yourself** (also see [6.14](#614-controlled-state)):
|
|
587
|
+
|
|
588
|
+
```tsx
|
|
589
|
+
const [sort, setSort] = useState<SortModel[]>([{ field: "name", direction: "asc" }]);
|
|
590
|
+
|
|
591
|
+
<button onClick={() => setSort([])}>Reset sort</button>
|
|
592
|
+
<DataGrid sort={sort} onSortChange={setSort} ... />
|
|
593
|
+
```
|
|
353
594
|
|
|
354
595
|
### 6.5 Searching and filtering
|
|
355
596
|
|
|
356
|
-
**Global search.**
|
|
597
|
+
**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.
|
|
598
|
+
|
|
599
|
+
```tsx
|
|
600
|
+
<DataGrid searchable searchPlaceholder="Search people…" ... />
|
|
601
|
+
```
|
|
357
602
|
|
|
358
|
-
**Column filters.** Filters are set per column
|
|
603
|
+
**Column filters.** Filters are set per column. Only columns with `filterable: true` get one:
|
|
359
604
|
|
|
360
605
|
```tsx
|
|
361
606
|
const columns: ColumnDef<Person>[] = [
|
|
@@ -366,13 +611,84 @@ const columns: ColumnDef<Person>[] = [
|
|
|
366
611
|
```
|
|
367
612
|
|
|
368
613
|
- 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.
|
|
369
|
-
- **Clicking the indicator** opens a popup right below it
|
|
614
|
+
- **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.
|
|
370
615
|
- The popup shows a **Clear** button while a filter is set.
|
|
371
|
-
-
|
|
616
|
+
- All filters and the global search combine (a row must pass every one).
|
|
372
617
|
- 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.
|
|
373
|
-
- There is no grid-level filter prop: a column without `filterable: true` has no filter, and a filter value is ignored if its column stops being filterable.
|
|
618
|
+
- 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.
|
|
619
|
+
|
|
620
|
+
#### Filter types
|
|
621
|
+
|
|
622
|
+
Each column's filter has a kind. By default it follows the column `type`; set `filterType` to choose another.
|
|
623
|
+
|
|
624
|
+
| `filterType` | Default for | Popup shows | Rows pass when |
|
|
625
|
+
| --- | --- | --- | --- |
|
|
626
|
+
| `"text"` | `type: "text"` | One text box | The displayed text **contains** the typed text (case-insensitive) |
|
|
627
|
+
| `"exact"` | | One text box | The displayed text **equals** the typed text (case-insensitive) |
|
|
628
|
+
| `"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 |
|
|
629
|
+
| `"date"` | `type: "date"` | From and To boxes with a range calendar | The day is between From and To (either may be left empty) |
|
|
630
|
+
| `"select"` | `type: "boolean"` | A checkbox per value | The raw value is one of the ticked values |
|
|
631
|
+
|
|
632
|
+
```tsx
|
|
633
|
+
const columns: ColumnDef<Task>[] = [
|
|
634
|
+
// "contains": typing "log" finds "Fix login" and "Catalogue"
|
|
635
|
+
{ field: "title", filterable: true },
|
|
636
|
+
|
|
637
|
+
// "exact": typing "active" finds "active" but NOT "inactive"
|
|
638
|
+
{ field: "status", filterable: true, filterType: "exact" },
|
|
639
|
+
|
|
640
|
+
// number range: type: "number" gives a Min / Max popup by default
|
|
641
|
+
{ field: "budget", type: "number", filterable: true },
|
|
642
|
+
|
|
643
|
+
// date range: type: "date" gives From / To boxes and a calendar by default
|
|
644
|
+
{ field: "due", type: "date", filterable: true },
|
|
645
|
+
|
|
646
|
+
// choose from a list: values are collected from the data...
|
|
647
|
+
{ field: "owner", filterable: true, filterType: "select" },
|
|
648
|
+
|
|
649
|
+
// ...or you decide the list and the labels
|
|
650
|
+
{
|
|
651
|
+
field: "priority",
|
|
652
|
+
filterable: true,
|
|
653
|
+
filterType: "select",
|
|
654
|
+
filterOptions: [
|
|
655
|
+
{ value: "p1", label: "Urgent" },
|
|
656
|
+
{ value: "p2", label: "Normal" },
|
|
657
|
+
"p3", // a plain string is its own label
|
|
658
|
+
],
|
|
659
|
+
},
|
|
660
|
+
];
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
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.
|
|
664
|
+
|
|
665
|
+
#### Reading and setting filters
|
|
666
|
+
|
|
667
|
+
The shape of a filter value depends on its kind:
|
|
668
|
+
|
|
669
|
+
```ts
|
|
670
|
+
// text / exact: "sales"
|
|
671
|
+
// number: { min: 1000, max: 5000 } (either key may be missing)
|
|
672
|
+
// date: { from: "2026-01-01", to: "2026-03-31" }
|
|
673
|
+
// select: ["active", "pending"]
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Listen with `onFiltersChange`, or control the filters completely:
|
|
677
|
+
|
|
678
|
+
```tsx
|
|
679
|
+
const [filters, setFilters] = useState<Record<string, FilterValue>>({
|
|
680
|
+
status: ["open", "blocked"], // select
|
|
681
|
+
budget: { min: 1000 }, // number
|
|
682
|
+
due: { from: "2026-06-01" }, // date
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
<button onClick={() => setFilters({})}>Clear all filters</button>
|
|
686
|
+
<DataGrid filters={filters} onFiltersChange={setFilters} ... />
|
|
687
|
+
```
|
|
374
688
|
|
|
375
|
-
|
|
689
|
+
#### Styling each indicator
|
|
690
|
+
|
|
691
|
+
Every column has its own options:
|
|
376
692
|
|
|
377
693
|
```tsx
|
|
378
694
|
{
|
|
@@ -384,9 +700,11 @@ const columns: ColumnDef<Person>[] = [
|
|
|
384
700
|
}
|
|
385
701
|
```
|
|
386
702
|
|
|
387
|
-
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 `--
|
|
703
|
+
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 `--rg-filter-active`. To style all indicators or popups at once, use `classNames.filterIndicator`, `classNames.filterPopup`, `classNames.filterInput` (or the matching `styles`).
|
|
704
|
+
|
|
705
|
+
#### A custom indicator component
|
|
388
706
|
|
|
389
|
-
|
|
707
|
+
Replace the dot with any component. It receives `{ column, active, open }`:
|
|
390
708
|
|
|
391
709
|
```tsx
|
|
392
710
|
function FunnelIndicator({ active }: FilterIndicatorProps<Employee>) {
|
|
@@ -402,49 +720,166 @@ function FunnelIndicator({ active }: FilterIndicatorProps<Employee>) {
|
|
|
402
720
|
|
|
403
721
|
The component is drawn inside the indicator `<button>`, so do not put another button in it.
|
|
404
722
|
|
|
405
|
-
|
|
723
|
+
#### A custom filter control
|
|
724
|
+
|
|
725
|
+
`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.
|
|
726
|
+
|
|
727
|
+
#### Debouncing typed filters
|
|
728
|
+
|
|
729
|
+
Typing in a text, exact or number filter does not apply the filter on every keystroke. The box updates at once, and the filter is applied once the user has stopped typing for a moment. This keeps a big grid responsive, and in [server-side mode](#615-server-side-mode) it means one request per pause instead of one per letter.
|
|
730
|
+
|
|
731
|
+
- The default wait is **300 ms**, set for the whole grid with `filterDebounce`.
|
|
732
|
+
- Any column can choose its own wait with `filterDebounce` in its definition (it wins over the grid's).
|
|
733
|
+
- `0` applies the filter on every keystroke.
|
|
734
|
+
- **Enter** applies a text or number filter immediately. Closing the popup also applies what is waiting, so typed text is never lost.
|
|
735
|
+
- **Clear**, or changing `filters` from outside, drops anything still waiting.
|
|
736
|
+
- Select checkboxes and date pickers are single clicks, so they apply at once. A custom `filterRenderer` also applies at once (debounce inside your component if you need it).
|
|
737
|
+
- The filter indicator lights up when the filter is applied, which is after the wait.
|
|
738
|
+
|
|
739
|
+
```tsx
|
|
740
|
+
// the whole grid waits 500 ms
|
|
741
|
+
<DataGrid filterDebounce={500} ... />
|
|
742
|
+
|
|
743
|
+
const columns: ColumnDef<Person>[] = [
|
|
744
|
+
{ field: "name", filterable: true }, // 500 ms (from the grid)
|
|
745
|
+
{ field: "email", filterable: true, filterDebounce: 800 }, // slow typers: 800 ms
|
|
746
|
+
{ field: "age", type: "number", filterable: true, filterDebounce: 0 }, // instant, a small local data set
|
|
747
|
+
{ field: "status", filterable: true, filterType: "select" }, // clicks: always instant
|
|
748
|
+
];
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
For a server grid you will often raise it, for a local grid you can lower it:
|
|
752
|
+
|
|
753
|
+
```tsx
|
|
754
|
+
<DataGrid serverSide filterDebounce={600} queryDebounce={400} onQueryChange={load} ... />
|
|
755
|
+
```
|
|
406
756
|
|
|
407
|
-
Notes
|
|
757
|
+
#### Notes
|
|
408
758
|
|
|
409
|
-
- Searching and
|
|
410
|
-
- Filtering is a substring match. Filtering a status column for `active` also matches `inactive`. For exact choices, use a `filterRenderer` and a distinct display text, or format the values so they don't overlap.
|
|
759
|
+
- 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.
|
|
411
760
|
- Changing the search, a filter, the sort or the page size sends the grid back to page 1.
|
|
412
|
-
-
|
|
761
|
+
- 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.
|
|
762
|
+
|
|
763
|
+
### 6.6 Date columns
|
|
764
|
+
|
|
765
|
+
`type: "date"` is a built-in column type with formatting, a date picker editor, chronological sorting and a From / To filter.
|
|
766
|
+
|
|
767
|
+
```tsx
|
|
768
|
+
interface Task { id: number; title: string; due: string; created: Date }
|
|
769
|
+
|
|
770
|
+
const columns: ColumnDef<Task>[] = [
|
|
771
|
+
{ field: "title", editable: true },
|
|
772
|
+
{
|
|
773
|
+
field: "due",
|
|
774
|
+
type: "date",
|
|
775
|
+
sortable: true,
|
|
776
|
+
filterable: true,
|
|
777
|
+
editable: true,
|
|
778
|
+
dateMin: "2026-01-01", // the picker will not accept earlier days
|
|
779
|
+
dateMax: "2027-12-31",
|
|
780
|
+
},
|
|
781
|
+
];
|
|
782
|
+
|
|
783
|
+
<DataGrid columns={columns} rows={tasks} editable onRowsChange={setTasks} />
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
**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.
|
|
787
|
+
|
|
788
|
+
**Editing.** Double-click a date cell (or press Enter / F2). A text box opens with a **calendar** under it:
|
|
789
|
+
|
|
790
|
+
- Click a day to save it at once. **Today** picks today and **Clear** saves an empty date (`null`).
|
|
791
|
+
- Or type the date as `YYYY-MM-DD`. The calendar follows what you type, and Enter (or a click elsewhere) saves it. Tab saves and moves on, Escape cancels.
|
|
792
|
+
- The arrows change month (`‹ ›`) and year (`« »`). Days before `dateMin` or after `dateMax` are dimmed and cannot be picked.
|
|
793
|
+
- Text that is not a real date (such as `2026-02-31`) marks the editor invalid and is not saved. Clicking away from it throws it away.
|
|
794
|
+
- Typing letters does not start an editor on a date cell: use double-click, Enter or F2.
|
|
795
|
+
- The calendar is drawn by the library, so it looks the same in every browser and follows your theme (`--rg-popup-*`, `--rg-primary`, `--rg-on-primary`). Month and weekday names follow `dateLocale`.
|
|
796
|
+
|
|
797
|
+
```tsx
|
|
798
|
+
{ field: "due", type: "date", editable: true, dateLocale: "fr-FR", dateMin: "2026-01-01" }
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**The saved value keeps the shape the row already had.**
|
|
413
802
|
|
|
414
|
-
|
|
803
|
+
| Row held | Saved value |
|
|
804
|
+
| --- | --- |
|
|
805
|
+
| `"2026-03-14"` (text) | `"2026-03-14"` |
|
|
806
|
+
| a `Date` object | a `Date` at local midnight |
|
|
807
|
+
| a timestamp number | a timestamp at local midnight |
|
|
808
|
+
| empty | the same shape as the other rows in the column (the ISO text when there are none) |
|
|
809
|
+
|
|
810
|
+
Dates are handled in the **user's local time zone**, so typing 5 November never turns into 4 November because of UTC.
|
|
811
|
+
|
|
812
|
+
**Display.** The default is the browser's medium date format (`Nov 5, 2026`). Change it with `dateLocale` and `dateFormat` (any `Intl.DateTimeFormat` options):
|
|
813
|
+
|
|
814
|
+
```tsx
|
|
815
|
+
{ field: "created", type: "date", dateLocale: "en-GB", dateFormat: { dateStyle: "long" } } // 5 November 2026
|
|
816
|
+
{ field: "created", type: "date", dateLocale: "de-DE", dateFormat: { day: "2-digit", month: "2-digit", year: "numeric" } } // 05.11.2026
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
A `valueFormatter` still wins over these two if you set one.
|
|
820
|
+
|
|
821
|
+
**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:
|
|
415
822
|
|
|
416
823
|
```tsx
|
|
417
|
-
<DataGrid
|
|
824
|
+
<DataGrid filters={{ due: { from: "2026-03-01", to: "2026-06-30" } }} onFiltersChange={setFilters} ... />
|
|
418
825
|
```
|
|
419
826
|
|
|
420
|
-
The
|
|
827
|
+
**The date filter calendar.** The filter popup of a date column has From and To boxes and a calendar. Type the dates, or click on the calendar: the first click sets **From**, the second sets **To** (swapped if it is earlier), and a third click starts a new range. Days in the range are highlighted. With the keyboard, Tab into the calendar, then arrows move a day or a week, Page Up / Page Down change month, Home / End go to the start / end of the week, Enter picks.
|
|
828
|
+
|
|
829
|
+
**CSV export** writes dates as `"YYYY-MM-DD"` unless you give `exportValue`.
|
|
830
|
+
|
|
831
|
+
### 6.7 Pagination
|
|
832
|
+
|
|
833
|
+
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.**
|
|
834
|
+
|
|
835
|
+
```tsx
|
|
836
|
+
<DataGrid pagination pageSize={20} pageSizeOptions={[10, 20, 50, 100]} paginationPosition="both" ... />
|
|
837
|
+
```
|
|
421
838
|
|
|
422
839
|
| `paginationPosition` | Result |
|
|
423
840
|
| --- | --- |
|
|
424
841
|
| `"bottom"` | Bar under the table (default) |
|
|
425
|
-
| `"top"` | Bar above the table, below the
|
|
842
|
+
| `"top"` | Bar above the table, below the toolbar |
|
|
426
843
|
| `"both"` | Both bars, kept in sync |
|
|
427
844
|
|
|
428
|
-
|
|
845
|
+
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:
|
|
429
846
|
|
|
430
847
|
```tsx
|
|
431
|
-
<DataGrid
|
|
848
|
+
<DataGrid maxHeight={400} ... />
|
|
432
849
|
```
|
|
433
850
|
|
|
434
|
-
`pageSize` is only read when the grid first mounts. The user's choice in the menu is then kept inside the grid.
|
|
851
|
+
`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).
|
|
852
|
+
|
|
853
|
+
**Reacting to paging:**
|
|
854
|
+
|
|
855
|
+
```tsx
|
|
856
|
+
<DataGrid
|
|
857
|
+
pagination
|
|
858
|
+
onPageChange={(page) => console.log("page", page)} // zero-based
|
|
859
|
+
onPageSizeChange={(size) => localStorage.setItem("size", String(size))}
|
|
860
|
+
pageSize={Number(localStorage.getItem("size")) || 10}
|
|
861
|
+
...
|
|
862
|
+
/>
|
|
863
|
+
```
|
|
435
864
|
|
|
436
|
-
### 6.
|
|
865
|
+
### 6.8 Row click, toolbar and empty state
|
|
437
866
|
|
|
438
867
|
```tsx
|
|
439
868
|
<DataGrid
|
|
440
869
|
onRowClick={(row, rowIndex) => navigate(`/people/${row.id}`)}
|
|
441
|
-
toolbar={<button onClick={
|
|
870
|
+
toolbar={<button onClick={openNewPersonDialog}>Add person</button>}
|
|
442
871
|
emptyMessage="Nothing matches your search"
|
|
443
872
|
striped
|
|
444
873
|
/>
|
|
445
874
|
```
|
|
446
875
|
|
|
447
|
-
|
|
876
|
+
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 `rg-grid__tool-btn`:
|
|
877
|
+
|
|
878
|
+
```tsx
|
|
879
|
+
toolbar={<button className="rg-grid__tool-btn" onClick={refetch}>Reload</button>}
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### 6.9 Editing
|
|
448
883
|
|
|
449
884
|
Turn editing on with `editable` on the grid, then lock or unlock columns:
|
|
450
885
|
|
|
@@ -491,7 +926,7 @@ Details:
|
|
|
491
926
|
2. Otherwise, in a `type: "number"` column, an empty text stores `null`, a valid number stores that number, and anything else is rejected.
|
|
492
927
|
3. Otherwise the text is stored as a string.
|
|
493
928
|
|
|
494
|
-
**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 `
|
|
929
|
+
**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 `rg-grid__editor--invalid` class and `aria-invalid="true"`. Pressing Esc or clicking away discards the invalid text.
|
|
495
930
|
|
|
496
931
|
```tsx
|
|
497
932
|
{ field: "email", validate: (v) => /^\S+@\S+\.\S+$/.test(String(v)) }
|
|
@@ -531,7 +966,7 @@ Important: the grid copies `rows` into its own state and re-syncs whenever the `
|
|
|
531
966
|
/>
|
|
532
967
|
```
|
|
533
968
|
|
|
534
|
-
### 6.
|
|
969
|
+
### 6.10 Custom components per column
|
|
535
970
|
|
|
536
971
|
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.
|
|
537
972
|
|
|
@@ -638,7 +1073,7 @@ function DepartmentFilter({ value, onChange }: FilterRendererProps<Employee>) {
|
|
|
638
1073
|
|
|
639
1074
|
#### Checkbox columns (`type: "boolean"`)
|
|
640
1075
|
|
|
641
|
-
Set `type: "boolean"` on a column and each cell shows a [Checkbox](#checkbox). One click saves the new `true` / `false` value.
|
|
1076
|
+
Set `type: "boolean"` on a column and each cell shows a [Checkbox](#5-checkbox). One click saves the new `true` / `false` value.
|
|
642
1077
|
|
|
643
1078
|
```tsx
|
|
644
1079
|
const columns: ColumnDef<Task>[] = [
|
|
@@ -660,7 +1095,7 @@ How it behaves:
|
|
|
660
1095
|
- **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`.
|
|
661
1096
|
- **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.
|
|
662
1097
|
- **Header label.** The header of a boolean column is empty unless you set `headerName`. Screen readers still get the field name.
|
|
663
|
-
- **Custom character.** `checkbox: { indicator: "★" }` draws that character instead of the default fill or tick when checked. Colour it with `--
|
|
1098
|
+
- **Custom character.** `checkbox: { indicator: "★" }` draws that character instead of the default fill or tick when checked. Colour it with `--rg-checkbox-color` (button variant) or `--rg-checkbox-tick` (tick variant), for example through `checkbox.styles.root`.
|
|
664
1099
|
- **Defaults.** 1px border, no rounded corners. See [Checkbox](#5-checkbox).
|
|
665
1100
|
- **A column with its own `cellEditor`** is treated as a normal editable column instead.
|
|
666
1101
|
|
|
@@ -672,7 +1107,7 @@ How it behaves:
|
|
|
672
1107
|
| One column | `checkbox: { variant: "tick" }` |
|
|
673
1108
|
| Whole grid, classes or inline styles | `classNames` / `styles` slots `checkbox`, `checkboxControl`, `checkboxIndicator` |
|
|
674
1109
|
| One column, classes or inline styles | `checkbox: { className, style, classNames, styles, indicator }` (same parts as the standalone `Checkbox`) |
|
|
675
|
-
| Colours and size | CSS variables such as `--
|
|
1110
|
+
| Colours and size | CSS variables such as `--rg-checkbox-color`, set on the grid's `className` / `style`, or per column in `checkbox.styles.root` |
|
|
676
1111
|
|
|
677
1112
|
```tsx
|
|
678
1113
|
<DataGrid
|
|
@@ -727,7 +1162,7 @@ Add `headerCheckbox: true` to a boolean column. The header label becomes a check
|
|
|
727
1162
|
- **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.
|
|
728
1163
|
- **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"`.
|
|
729
1164
|
|
|
730
|
-
### 6.
|
|
1165
|
+
### 6.11 Styling
|
|
731
1166
|
|
|
732
1167
|
#### Whole grid
|
|
733
1168
|
|
|
@@ -762,6 +1197,8 @@ Add `headerCheckbox: true` to a boolean column. The header label becomes a check
|
|
|
762
1197
|
| `pageInfo` | The "Page 1 of 20" text |
|
|
763
1198
|
| `pageSize` | The rows-per-page `<select>` |
|
|
764
1199
|
| `pagination` | Wrapper around the page-size menu and buttons |
|
|
1200
|
+
| `toolButton` | Toolbar buttons (Columns, Export CSV, Retry) |
|
|
1201
|
+
| `skeleton` | The shimmering bars of the loading skeleton |
|
|
765
1202
|
| `pageButton` | Each first/previous/next/last button |
|
|
766
1203
|
| `checkbox` | Root of each checkbox in a `type: "boolean"` column |
|
|
767
1204
|
| `checkboxControl` | The checkbox's button or input |
|
|
@@ -826,7 +1263,7 @@ When both are given, `style` wins over `styles.root`.
|
|
|
826
1263
|
<DataGrid stripeColors={["#fef3c7", "#e0f2fe"]} /> // your two colours (implies striped)
|
|
827
1264
|
```
|
|
828
1265
|
|
|
829
|
-
Rows alternate between the two colours, starting with the first. `stripeColors` sets `--
|
|
1266
|
+
Rows alternate between the two colours, starting with the first. `stripeColors` sets `--rg-stripe-a` and `--rg-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 (`--rg-row-hover`).
|
|
830
1267
|
|
|
831
1268
|
#### Row animation
|
|
832
1269
|
|
|
@@ -836,7 +1273,32 @@ When rows render (the first load, a new page, a sort, a search or a filter), the
|
|
|
836
1273
|
<DataGrid animateRows={false} /> // turn it off
|
|
837
1274
|
```
|
|
838
1275
|
|
|
839
|
-
Tune it with `--
|
|
1276
|
+
Tune it with `--rg-row-duration` (default `0.38s`) and `--rg-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`).
|
|
1277
|
+
|
|
1278
|
+
#### Custom row animation
|
|
1279
|
+
|
|
1280
|
+
Pass your own CSS class to replace the built-in slide-up. The class should set `animation`. Every row also gets the CSS variable `--rg-i` (its position on the page, 0 to 24), so a delay of `calc(var(--rg-i) * 40ms)` makes rows arrive one after another.
|
|
1281
|
+
|
|
1282
|
+
```css
|
|
1283
|
+
@keyframes fade-left { from { opacity: 0; transform: translateX(-24px); } to { opacity: 1; transform: none; } }
|
|
1284
|
+
.row-fade-left { animation: fade-left 0.45s ease-out both; animation-delay: calc(var(--rg-i, 0) * 45ms); }
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
```tsx
|
|
1288
|
+
<DataGrid rowAnimationClassName="row-fade-left" ... />
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
It can also be a function of the row, for example to animate new rows differently:
|
|
1292
|
+
|
|
1293
|
+
```tsx
|
|
1294
|
+
<DataGrid rowAnimationClassName={(row) => (row.isNew ? "row-flash" : "row-fade-left")} ... />
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
With Tailwind, define the keyframes in your config and use the utility, for example `rowAnimationClassName="animate-fade-in-left [animation-delay:calc(var(--rg-i)*40ms)]"`.
|
|
1298
|
+
|
|
1299
|
+
- It runs on the same moments as the built-in animation: first render, a new page, sort, search or filter.
|
|
1300
|
+
- `animateRows={false}` turns every row animation off, custom or not. It is also skipped while `virtualize` is on.
|
|
1301
|
+
- Reduced motion: the built-in animation stops for people who ask for it. Your own class is yours to handle: wrap it in `@media (prefers-reduced-motion: no-preference) { ... }`.
|
|
840
1302
|
|
|
841
1303
|
#### Width
|
|
842
1304
|
|
|
@@ -848,23 +1310,35 @@ If you prefer writing CSS, these class names are stable.
|
|
|
848
1310
|
|
|
849
1311
|
| Class | Element |
|
|
850
1312
|
| --- | --- |
|
|
851
|
-
| `.
|
|
852
|
-
| `.
|
|
853
|
-
| `.
|
|
854
|
-
| `.
|
|
855
|
-
| `.
|
|
856
|
-
| `.
|
|
857
|
-
| `.
|
|
858
|
-
| `.
|
|
859
|
-
| `.
|
|
860
|
-
| `.
|
|
861
|
-
| `.
|
|
862
|
-
| `.
|
|
863
|
-
| `.
|
|
864
|
-
| `.
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1313
|
+
| `.rg-grid` | Root |
|
|
1314
|
+
| `.rg-grid__toolbar`, `.rg-grid__search` | Toolbar and search box |
|
|
1315
|
+
| `.rg-grid__scroll`, `.rg-grid__table`, `.rg-grid__thead`, `.rg-grid__tbody` | Table structure |
|
|
1316
|
+
| `.rg-grid__tr` | Every row |
|
|
1317
|
+
| `.rg-grid__th`, `.rg-grid__th--sortable`, `.rg-grid__th-content`, `.rg-grid__sort` | Header cells and sort arrow |
|
|
1318
|
+
| `.rg-grid__filter-btn`, `.rg-grid__filter-btn--active`, `.rg-grid__filter-btn--open`, `.rg-grid__filter-dot` | Filter indicator button and its default dot |
|
|
1319
|
+
| `.rg-grid-popup`, `.rg-grid__filter-input`, `.rg-grid__filter-clear` | Filter popup (in `document.body`), its text box and Clear button |
|
|
1320
|
+
| `.rg-grid__row`, `.rg-grid__row--stripe-a`, `.rg-grid__row--stripe-b`, `.rg-grid__row--enter`, `.rg-grid__row--clickable` | Body rows |
|
|
1321
|
+
| `.rg-grid__td`, `.rg-grid__td--editable`, `.rg-grid__td--editing` | Body cells |
|
|
1322
|
+
| `.rg-grid__editor`, `.rg-grid__editor--invalid`, `.rg-grid__custom-editor` | Editors |
|
|
1323
|
+
| `.rg-grid__empty` | Empty state |
|
|
1324
|
+
| `.rg-grid__checkbox-cell` | Wrapper around a checkbox in a boolean column (the checkbox's own classes start with `.rg-checkbox`, see [Checkbox](#5-checkbox)) |
|
|
1325
|
+
| `.rg-grid__footer`, `.rg-grid__footer--top` | Pagination bar (`data-position="top"` or `"bottom"`) |
|
|
1326
|
+
| `.rg-grid__count`, `.rg-grid__pagination`, `.rg-grid__page-size-label`, `.rg-grid__page-size`, `.rg-grid__page-btn`, `.rg-grid__page-info` | Pagination parts |
|
|
1327
|
+
| `.rg-grid__toolbar-left`, `.rg-grid__toolbar-actions` | The two halves of the toolbar (search and selected count / buttons) |
|
|
1328
|
+
| `.rg-grid__tool-btn`, `.rg-grid__selected-count` | Toolbar buttons and the "N selected" label |
|
|
1329
|
+
| `.rg-grid__select-cell`, `.rg-grid__row--selected` | Row-selection checkbox cell and a selected row |
|
|
1330
|
+
| `.rg-grid__pinned`, `.rg-grid__pinned--left`, `.rg-grid__pinned--right`, `.rg-grid__pinned--edge` | Pinned cells (the edge class marks the last left / first right one) |
|
|
1331
|
+
| `.rg-grid__resizer` | The column resize handle |
|
|
1332
|
+
| `.rg-grid__sort-order` | The priority number next to a sort arrow in multi-column sort |
|
|
1333
|
+
| `.rg-grid--virtual`, `.rg-grid__spacer` | Virtualized grid and its spacer rows |
|
|
1334
|
+
| `.rg-grid__skeleton`, `.rg-grid__skeleton-row`, `.rg-grid__skeleton-cell` | Loading placeholder bar, row and (custom) cell |
|
|
1335
|
+
| `.rg-cal`, `.rg-cal__head`, `.rg-cal__nav`, `.rg-cal__title`, `.rg-cal__grid`, `.rg-cal__dow`, `.rg-cal__foot`, `.rg-cal__link` | The calendar: frame, month bar and arrows, weekday names, footer buttons |
|
|
1336
|
+
| `.rg-cal__day`, `.rg-cal__day--today`, `.rg-cal__day--selected`, `.rg-cal__day--in-range`, `.rg-cal__day--range-start`, `.rg-cal__day--range-end`, `.rg-cal__day--outside` | A day button and its states |
|
|
1337
|
+
| `.rg-grid__error` | The error message cell (with the Retry button) |
|
|
1338
|
+
| `.rg-grid__filter-fields`, `.rg-grid__filter-field`, `.rg-grid__filter-options`, `.rg-grid__filter-empty` | Controls inside the filter popup (number, date, select) |
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
### 6.12 Nested fields
|
|
868
1342
|
|
|
869
1343
|
A column's `field` can point into nested objects with dot notation:
|
|
870
1344
|
|
|
@@ -892,152 +1366,903 @@ const columns: ColumnDef<Person>[] = [
|
|
|
892
1366
|
- `__proto__`, `prototype` and `constructor` are never read or written through a path.
|
|
893
1367
|
- `getRowId`, `onRowClick` and other callbacks work as before.
|
|
894
1368
|
|
|
895
|
-
### 6.
|
|
1369
|
+
### 6.13 Row selection
|
|
896
1370
|
|
|
897
|
-
|
|
898
|
-
| --- | --- | --- |
|
|
899
|
-
| Header | Enter / Space | Sort by this column |
|
|
900
|
-
| Selected cell | Enter / F2 | Start editing |
|
|
901
|
-
| Selected cell | Any character | Start editing, replacing the value |
|
|
902
|
-
| Editor | Enter | Save, edit cell below |
|
|
903
|
-
| Editor | Shift+Enter | Save, edit cell above |
|
|
904
|
-
| Editor | Tab / Shift+Tab | Save, edit next / previous cell |
|
|
905
|
-
| Editor | Esc | Cancel |
|
|
1371
|
+
Off by default. `rowSelection` adds a checkbox column on the left.
|
|
906
1372
|
|
|
907
|
-
|
|
1373
|
+
```tsx
|
|
1374
|
+
<DataGrid columns={columns} rows={rows} getRowId={(r) => r.id} rowSelection />
|
|
1375
|
+
```
|
|
908
1376
|
|
|
909
|
-
|
|
1377
|
+
| Value | Behaviour |
|
|
1378
|
+
| --- | --- |
|
|
1379
|
+
| `true` or `"multiple"` | Many rows. The header has a select-all checkbox. Shift+click selects a range |
|
|
1380
|
+
| `"single"` | One row at a time (selecting another replaces it). No select-all checkbox |
|
|
910
1381
|
|
|
911
|
-
-
|
|
912
|
-
-
|
|
913
|
-
- The
|
|
914
|
-
-
|
|
915
|
-
-
|
|
1382
|
+
- Selected rows get the class `rg-grid__row--selected`, the colour `--rg-selected-bg` and `aria-selected="true"`.
|
|
1383
|
+
- Selection is kept **by row id**, so it survives paging, sorting, searching and filtering. Pass `getRowId` so ids are stable.
|
|
1384
|
+
- 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.
|
|
1385
|
+
- **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.
|
|
1386
|
+
- A "N selected" count appears in the toolbar (when a toolbar is shown) and in the pagination bar.
|
|
916
1387
|
|
|
917
|
-
|
|
1388
|
+
**Reading the selection** (`onSelectionChange` receives the ids and the row objects):
|
|
918
1389
|
|
|
919
|
-
|
|
1390
|
+
```tsx
|
|
1391
|
+
function Page() {
|
|
1392
|
+
const [selection, setSelection] = useState<RowSelection<Person>>({ ids: [], rows: [] });
|
|
920
1393
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
ColumnCheckboxOptions,
|
|
937
|
-
FilterIndicatorProps,
|
|
938
|
-
FieldPath,
|
|
939
|
-
GridTheme,
|
|
940
|
-
CheckboxProps,
|
|
941
|
-
CheckboxVariant,
|
|
942
|
-
CheckboxSlot,
|
|
943
|
-
} from "react-gridix";
|
|
1394
|
+
return (
|
|
1395
|
+
<>
|
|
1396
|
+
<button disabled={!selection.ids.length} onClick={() => deletePeople(selection.ids)}>
|
|
1397
|
+
Delete {selection.ids.length} people
|
|
1398
|
+
</button>
|
|
1399
|
+
<DataGrid
|
|
1400
|
+
columns={columns}
|
|
1401
|
+
rows={rows}
|
|
1402
|
+
getRowId={(r) => r.id}
|
|
1403
|
+
rowSelection
|
|
1404
|
+
onSelectionChange={setSelection}
|
|
1405
|
+
/>
|
|
1406
|
+
</>
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
944
1409
|
```
|
|
945
1410
|
|
|
946
|
-
|
|
1411
|
+
**Controlled selection.** Pass `selectedRowIds` and keep it up to date from `onSelectionChange`:
|
|
947
1412
|
|
|
948
1413
|
```tsx
|
|
949
|
-
const
|
|
950
|
-
|
|
1414
|
+
const [ids, setIds] = useState<(string | number)[]>([2, 4]);
|
|
1415
|
+
|
|
1416
|
+
<button onClick={() => setIds([])}>Clear</button>
|
|
1417
|
+
<DataGrid rowSelection selectedRowIds={ids} onSelectionChange={(s) => setIds(s.ids)} ... />
|
|
951
1418
|
```
|
|
952
1419
|
|
|
953
|
-
|
|
1420
|
+
Or start with some rows selected and let the grid manage it: `defaultSelectedRowIds={[2, 4]}`.
|
|
954
1421
|
|
|
955
|
-
**
|
|
1422
|
+
**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:
|
|
956
1423
|
|
|
957
1424
|
```tsx
|
|
958
|
-
<DataGrid
|
|
1425
|
+
<DataGrid rowSelection isRowSelectable={(row) => row.status !== "archived"} ... />
|
|
959
1426
|
```
|
|
960
1427
|
|
|
961
|
-
**
|
|
1428
|
+
**Single selection**, for a master / detail layout:
|
|
962
1429
|
|
|
963
1430
|
```tsx
|
|
964
|
-
|
|
965
|
-
field: "salary",
|
|
966
|
-
type: "number",
|
|
967
|
-
align: "right",
|
|
968
|
-
valueFormatter: (v) => (v == null ? "" : money.format(v)),
|
|
969
|
-
valueParser: (text) => Number(text.replace(/[$,]/g, "")),
|
|
970
|
-
validate: (v) => Number.isFinite(v),
|
|
971
|
-
}
|
|
972
|
-
```
|
|
973
|
-
|
|
974
|
-
(The editor opens with the raw number, such as `52000`. The parser only matters if the user types symbols.)
|
|
975
|
-
|
|
976
|
-
**Only one column editable**
|
|
1431
|
+
const [current, setCurrent] = useState<Person | null>(null);
|
|
977
1432
|
|
|
978
|
-
|
|
979
|
-
|
|
1433
|
+
<DataGrid
|
|
1434
|
+
rowSelection="single"
|
|
1435
|
+
getRowId={(r) => r.id}
|
|
1436
|
+
onSelectionChange={(s) => setCurrent(s.rows[0] ?? null)}
|
|
1437
|
+
...
|
|
1438
|
+
/>
|
|
1439
|
+
{current && <PersonDetails person={current} />}
|
|
980
1440
|
```
|
|
981
1441
|
|
|
982
|
-
**
|
|
1442
|
+
**From outside the grid**, with `apiRef` (see [6.19](#619-csv-export-and-the-api)):
|
|
983
1443
|
|
|
984
1444
|
```tsx
|
|
985
|
-
|
|
1445
|
+
const api = useRef<DataGridApi<Person> | null>(null);
|
|
1446
|
+
|
|
1447
|
+
<button onClick={() => api.current?.selectAll()}>Select all</button>
|
|
1448
|
+
<button onClick={() => api.current?.clearSelection()}>Clear</button>
|
|
1449
|
+
<button onClick={() => console.log(api.current?.getSelectedRows())}>Log selection</button>
|
|
1450
|
+
<DataGrid rowSelection apiRef={api} ... />
|
|
986
1451
|
```
|
|
987
1452
|
|
|
988
|
-
**
|
|
1453
|
+
**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"`).
|
|
989
1454
|
|
|
990
|
-
|
|
991
|
-
<DataGrid pagination={false} maxHeight="60vh" />
|
|
992
|
-
```
|
|
1455
|
+
**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.
|
|
993
1456
|
|
|
994
|
-
|
|
1457
|
+
### 6.14 Controlled state
|
|
995
1458
|
|
|
996
|
-
|
|
997
|
-
<DataGrid rowClassName={(row) => (row.overdue ? "bg-red-50" : "")} />
|
|
998
|
-
```
|
|
1459
|
+
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).
|
|
999
1460
|
|
|
1000
|
-
|
|
1461
|
+
| State | Value prop | Change callback | Initial value (uncontrolled) |
|
|
1462
|
+
| --- | --- | --- | --- |
|
|
1463
|
+
| Search text | `search` | `onSearchChange(text)` | |
|
|
1464
|
+
| Column filters | `filters` | `onFiltersChange(filters)` | |
|
|
1465
|
+
| Sort | `sort` | `onSortChange(sort)` | `defaultSort` |
|
|
1466
|
+
| Page (zero-based) | `page` | `onPageChange(page)` | page 0 |
|
|
1467
|
+
| Page size | | `onPageSizeChange(size)` | `pageSize` |
|
|
1468
|
+
| Selection | `selectedRowIds` | `onSelectionChange({ ids, rows })` | `defaultSelectedRowIds` |
|
|
1001
1469
|
|
|
1002
|
-
|
|
1470
|
+
**Rules.**
|
|
1003
1471
|
|
|
1004
|
-
-
|
|
1005
|
-
-
|
|
1006
|
-
-
|
|
1007
|
-
- Row virtualization (very large pages render every row, so keep `pageSize` moderate)
|
|
1008
|
-
- Server-side sorting, filtering and paging (the grid works on the array you give it)
|
|
1009
|
-
- Arrow-key navigation between cells
|
|
1010
|
-
- Controlled search, filter or page state (only `defaultSort` and `pageSize` initial values)
|
|
1472
|
+
- Pass the value **and** its callback. A value without a callback makes that piece of state read-only.
|
|
1473
|
+
- You may control some pieces and leave the rest to the grid.
|
|
1474
|
+
- 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)`.
|
|
1011
1475
|
|
|
1012
|
-
|
|
1476
|
+
**Example: keep the grid in sync with the URL** so a page can be bookmarked and the Back button works:
|
|
1013
1477
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
- Stripes and the row animation restart on every page, sort, search or filter change.
|
|
1017
|
-
- `defaultSort` and `pageSize` are read once, on mount. Changing them later has no effect.
|
|
1018
|
-
- `rows` must keep a stable identity between renders, otherwise edits are discarded (see [6.8](#68-editing)).
|
|
1019
|
-
- `onRowClick` also fires for clicks inside a cell that is being edited.
|
|
1020
|
-
- A `valueParser` that returns `NaN` stores `NaN`. Add `validate: (v) => Number.isFinite(v)` to reject it.
|
|
1021
|
-
- Date values display with `toLocaleDateString()`. Editing a `Date` cell shows its `String()` form, so store dates as ISO strings (`"2026-11-05"`) or provide a `cellEditor`.
|
|
1478
|
+
```tsx
|
|
1479
|
+
import { useSearchParams } from "react-router-dom";
|
|
1022
1480
|
|
|
1023
|
-
|
|
1481
|
+
function PeopleGrid() {
|
|
1482
|
+
const [params, setParams] = useSearchParams();
|
|
1483
|
+
const page = Number(params.get("page") ?? 0);
|
|
1484
|
+
const search = params.get("q") ?? "";
|
|
1024
1485
|
|
|
1025
|
-
|
|
1486
|
+
const update = (changes: Record<string, string | null>) =>
|
|
1487
|
+
setParams((p) => {
|
|
1488
|
+
for (const [k, v] of Object.entries(changes)) v === null ? p.delete(k) : p.set(k, v);
|
|
1489
|
+
return p;
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
return (
|
|
1493
|
+
<DataGrid
|
|
1494
|
+
columns={columns}
|
|
1495
|
+
rows={rows}
|
|
1496
|
+
searchable
|
|
1497
|
+
pagination
|
|
1498
|
+
page={page}
|
|
1499
|
+
onPageChange={(p) => update({ page: String(p) })}
|
|
1500
|
+
search={search}
|
|
1501
|
+
onSearchChange={(q) => update({ q: q || null })}
|
|
1502
|
+
/>
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
```
|
|
1506
|
+
|
|
1507
|
+
**Example: buttons outside the grid**
|
|
1508
|
+
|
|
1509
|
+
```tsx
|
|
1510
|
+
const [page, setPage] = useState(0);
|
|
1511
|
+
const [search, setSearch] = useState("");
|
|
1512
|
+
|
|
1513
|
+
<button onClick={() => setPage(2)}>Go to page 3</button>
|
|
1514
|
+
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search outside the grid" />
|
|
1515
|
+
<DataGrid searchable pagination pageSize={5} page={page} onPageChange={setPage} search={search} onSearchChange={setSearch} ... />
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
**Example: saved views.** Remember a user's filters and sort between visits:
|
|
1519
|
+
|
|
1520
|
+
```tsx
|
|
1521
|
+
const [filters, setFilters] = useState<Record<string, FilterValue>>(() => JSON.parse(localStorage.getItem("filters") ?? "{}"));
|
|
1522
|
+
const [sort, setSort] = useState<SortModel[]>(() => JSON.parse(localStorage.getItem("sort") ?? "[]"));
|
|
1523
|
+
|
|
1524
|
+
useEffect(() => localStorage.setItem("filters", JSON.stringify(filters)), [filters]);
|
|
1525
|
+
useEffect(() => localStorage.setItem("sort", JSON.stringify(sort)), [sort]);
|
|
1526
|
+
|
|
1527
|
+
<DataGrid filters={filters} onFiltersChange={setFilters} sort={sort} onSortChange={setSort} ... />
|
|
1528
|
+
```
|
|
1529
|
+
|
|
1530
|
+
### 6.15 Server-side mode
|
|
1531
|
+
|
|
1532
|
+
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.
|
|
1533
|
+
|
|
1534
|
+
```tsx
|
|
1535
|
+
<DataGrid
|
|
1536
|
+
serverSide
|
|
1537
|
+
columns={columns}
|
|
1538
|
+
rows={rows} // only the current page
|
|
1539
|
+
rowCount={total} // total rows on the server, for "1–10 of 237" and the page count
|
|
1540
|
+
pagination
|
|
1541
|
+
searchable
|
|
1542
|
+
onQueryChange={load}
|
|
1543
|
+
/>
|
|
1544
|
+
```
|
|
1545
|
+
|
|
1546
|
+
`onQueryChange(query)` is called **on mount and whenever the query changes**: the sort, the search text (after `queryDebounce` ms), a filter (after `filterDebounce` ms when typed), the page or the page size. The query is:
|
|
1547
|
+
|
|
1548
|
+
```ts
|
|
1549
|
+
interface GridQuery {
|
|
1550
|
+
sort: SortModel[]; // [{ field: "salary", direction: "desc" }]
|
|
1551
|
+
filters: Record<string, FilterValue>; // only active ones, by field
|
|
1552
|
+
search: string; // trimmed
|
|
1553
|
+
page: number; // zero-based
|
|
1554
|
+
pageSize: number;
|
|
1555
|
+
}
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
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`.
|
|
1559
|
+
|
|
1560
|
+
**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.
|
|
1561
|
+
|
|
1562
|
+
```tsx
|
|
1563
|
+
function PeopleGrid() {
|
|
1564
|
+
const [rows, setRows] = useState<Person[]>([]);
|
|
1565
|
+
const [total, setTotal] = useState(0);
|
|
1566
|
+
|
|
1567
|
+
const load = useCallback(async (q: GridQuery) => {
|
|
1568
|
+
const res = await fetch("/api/people?" + new URLSearchParams({
|
|
1569
|
+
page: String(q.page),
|
|
1570
|
+
pageSize: String(q.pageSize),
|
|
1571
|
+
search: q.search,
|
|
1572
|
+
sort: JSON.stringify(q.sort),
|
|
1573
|
+
filters: JSON.stringify(q.filters),
|
|
1574
|
+
}));
|
|
1575
|
+
if (!res.ok) throw new Error("Request failed"); // -> error row with Retry
|
|
1576
|
+
const body = await res.json();
|
|
1577
|
+
setRows(body.rows);
|
|
1578
|
+
setTotal(body.total);
|
|
1579
|
+
}, []);
|
|
1580
|
+
|
|
1581
|
+
return (
|
|
1582
|
+
<DataGrid
|
|
1583
|
+
serverSide
|
|
1584
|
+
columns={columns}
|
|
1585
|
+
rows={rows}
|
|
1586
|
+
rowCount={total}
|
|
1587
|
+
getRowId={(r) => r.id}
|
|
1588
|
+
pagination
|
|
1589
|
+
searchable
|
|
1590
|
+
onQueryChange={load}
|
|
1591
|
+
errorMessage="Could not reach the server."
|
|
1592
|
+
/>
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
```
|
|
1596
|
+
|
|
1597
|
+
**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:
|
|
1598
|
+
|
|
1599
|
+
```tsx
|
|
1600
|
+
const controller = useRef<AbortController>();
|
|
1601
|
+
|
|
1602
|
+
const load = useCallback(async (q: GridQuery) => {
|
|
1603
|
+
controller.current?.abort(); // cancel the previous request
|
|
1604
|
+
controller.current = new AbortController();
|
|
1605
|
+
const res = await fetch(url(q), { signal: controller.current.signal });
|
|
1606
|
+
const body = await res.json();
|
|
1607
|
+
setRows(body.rows);
|
|
1608
|
+
setTotal(body.total);
|
|
1609
|
+
}, []);
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
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; }`.
|
|
1613
|
+
|
|
1614
|
+
**Using a data library.** Anything that returns a promise works:
|
|
1615
|
+
|
|
1616
|
+
```tsx
|
|
1617
|
+
// the grid calls your function; you call the library
|
|
1618
|
+
const load = (q: GridQuery) => queryClient.fetchQuery({ queryKey: ["people", q], queryFn: () => api.people(q) })
|
|
1619
|
+
.then((res) => { setRows(res.rows); setTotal(res.total); });
|
|
1620
|
+
```
|
|
1621
|
+
|
|
1622
|
+
**Forcing the loading state.** If your loading flag lives somewhere else (a hook, a store), pass `loading` and it replaces the promise-based state:
|
|
1623
|
+
|
|
1624
|
+
```tsx
|
|
1625
|
+
const { data, isFetching } = useQuery(...);
|
|
1626
|
+
<DataGrid serverSide rows={data?.rows ?? []} rowCount={data?.total} loading={isFetching} onQueryChange={setQuery} />
|
|
1627
|
+
```
|
|
1628
|
+
|
|
1629
|
+
**Refreshing** after your data changed (for example after deleting a row) runs the last query again:
|
|
1630
|
+
|
|
1631
|
+
```tsx
|
|
1632
|
+
api.current?.refresh();
|
|
1633
|
+
```
|
|
1634
|
+
|
|
1635
|
+
**Debounce.** Typing in the search box waits `queryDebounce` milliseconds (250 by default) after the last keystroke, and typing in a column filter waits `filterDebounce` (300 by default, also settable per column), so fast typing makes one request, not one per letter. Set either to `0` for instant queries. Select checkboxes, date pickers, sorting and paging query immediately.
|
|
1636
|
+
|
|
1637
|
+
**Server mode and the other features.**
|
|
1638
|
+
|
|
1639
|
+
- Selection, [CSV export of selected rows](#619-csv-export-and-the-api) and keyboard navigation work on the loaded page.
|
|
1640
|
+
- With `scope: "filtered"` the CSV contains the rows currently loaded (the current page), because the grid does not have the rest.
|
|
1641
|
+
- `defaultSort` is sent in the first query. `multiSort` lets users send several sort columns.
|
|
1642
|
+
- `onQueryChange` is the only place the grid asks for data. Without `serverSide`, the grid never calls it.
|
|
1643
|
+
|
|
1644
|
+
### 6.16 Loading skeleton
|
|
1645
|
+
|
|
1646
|
+
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.
|
|
1647
|
+
|
|
1648
|
+
```tsx
|
|
1649
|
+
<DataGrid serverSide onQueryChange={load} loadingRows={8} ... />
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
- `loadingRows` is how many placeholder rows to draw. Default: the page size (at most 20), or 5 without pagination.
|
|
1653
|
+
- With `rowSelection` the checkbox column shows a small bar too.
|
|
1654
|
+
- Bar widths vary slightly from row to row so the placeholder does not look like identical stripes.
|
|
1655
|
+
- Colours come from `--rg-skeleton-a` and `--rg-skeleton-b`. Each theme sets them. The shimmer stops when the user has "reduce motion" turned on.
|
|
1656
|
+
- Style the bars with `classNames.skeleton` / `styles.skeleton` (they are `<span class="rg-grid__skeleton">`).
|
|
1657
|
+
|
|
1658
|
+
**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 }`:
|
|
1659
|
+
|
|
1660
|
+
```tsx
|
|
1661
|
+
function PersonSkeleton({ rowIndex }: SkeletonRowProps<Person>) {
|
|
1662
|
+
return (
|
|
1663
|
+
<div style={{ display: "flex", gap: 12, alignItems: "center", padding: "4px 0" }}>
|
|
1664
|
+
<span className="avatar-skeleton" />
|
|
1665
|
+
<span style={{ display: "grid", gap: 6, flex: 1 }}>
|
|
1666
|
+
<span className="line-skeleton" style={{ width: `${40 + ((rowIndex * 17) % 30)}%` }} />
|
|
1667
|
+
<span className="line-skeleton" style={{ width: "25%" }} />
|
|
1668
|
+
</span>
|
|
1669
|
+
</div>
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
<DataGrid serverSide onQueryChange={load} skeletonRow={PersonSkeleton} loadingRows={6} ... />
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
You can reuse the built-in shimmer in your own placeholder by giving your elements the class `rg-grid__skeleton`:
|
|
1677
|
+
|
|
1678
|
+
```tsx
|
|
1679
|
+
function RowSkeleton({ columns }: SkeletonRowProps<Person>) {
|
|
1680
|
+
return (
|
|
1681
|
+
<div style={{ display: "grid", gridTemplateColumns: `repeat(${columns.length}, 1fr)`, gap: 16 }}>
|
|
1682
|
+
{columns.map((c) => <span key={c.field} className="rg-grid__skeleton" style={{ width: "70%" }} />)}
|
|
1683
|
+
</div>
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
```
|
|
1687
|
+
|
|
1688
|
+
Define the skeleton component at module level (not inline in render), like every other custom component.
|
|
1689
|
+
|
|
1690
|
+
**Showing it without a server.** `loading` works in a normal grid too, for example while a parent fetches `rows`:
|
|
1691
|
+
|
|
1692
|
+
```tsx
|
|
1693
|
+
<DataGrid columns={columns} rows={rows} loading={isLoading} />
|
|
1694
|
+
```
|
|
1695
|
+
|
|
1696
|
+
**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 `.rg-grid__error`:
|
|
1697
|
+
|
|
1698
|
+
```tsx
|
|
1699
|
+
<DataGrid serverSide onQueryChange={load} errorMessage={<strong>The server is not answering.</strong>} ... />
|
|
1700
|
+
```
|
|
1701
|
+
|
|
1702
|
+
### 6.17 Resize, pin and hide columns
|
|
1703
|
+
|
|
1704
|
+
All three are opt-in. **There is no column reordering.**
|
|
1705
|
+
|
|
1706
|
+
**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.
|
|
1707
|
+
|
|
1708
|
+
```tsx
|
|
1709
|
+
const columns: ColumnDef<Employee>[] = [
|
|
1710
|
+
{ field: "name", resizable: true, minWidth: 100 },
|
|
1711
|
+
{ field: "email", resizable: true, minWidth: 140, maxWidth: 480 },
|
|
1712
|
+
{ field: "id", width: 70 }, // no handle: fixed
|
|
1713
|
+
];
|
|
1714
|
+
```
|
|
1715
|
+
|
|
1716
|
+
- 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`.
|
|
1717
|
+
- `minWidth` (at least 40px) and `maxWidth` limit the drag.
|
|
1718
|
+
- Widths live inside the grid and are not saved. To remember them, store the `width` you want per column yourself.
|
|
1719
|
+
|
|
1720
|
+
**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.
|
|
1721
|
+
|
|
1722
|
+
```tsx
|
|
1723
|
+
const columns: ColumnDef<Employee>[] = [
|
|
1724
|
+
{ field: "id", width: 70, pinned: "left" },
|
|
1725
|
+
{ field: "name", width: 160, pinned: "left" }, // sits next to ID
|
|
1726
|
+
{ field: "email", width: 280 },
|
|
1727
|
+
{ field: "department", width: 200 },
|
|
1728
|
+
{ field: "salary", width: 160 },
|
|
1729
|
+
{ field: "status", width: 120, pinned: "right" }, // always at the right edge
|
|
1730
|
+
];
|
|
1731
|
+
|
|
1732
|
+
<DataGrid columns={columns} rows={rows} /> // scroll sideways: ID, Name and Status stay put
|
|
1733
|
+
```
|
|
1734
|
+
|
|
1735
|
+
The row-selection checkbox column is pinned too when any column is pinned left. A thin shadow line (`.rg-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.
|
|
1736
|
+
|
|
1737
|
+
Colours: `--rg-pinned-bg` and `--rg-pinned-header-bg`. Stripes, hover and selection are layered on top.
|
|
1738
|
+
|
|
1739
|
+
**Showing and hiding.** Switch on `columnMenu` for a "Columns" button with a checkbox per column:
|
|
1740
|
+
|
|
1741
|
+
```tsx
|
|
1742
|
+
<DataGrid
|
|
1743
|
+
columnMenu
|
|
1744
|
+
onColumnVisibilityChange={(hiddenFields) => localStorage.setItem("hidden", JSON.stringify(hiddenFields))}
|
|
1745
|
+
columns={[
|
|
1746
|
+
{ field: "id", hideable: false }, // always visible, not listed
|
|
1747
|
+
{ field: "name" },
|
|
1748
|
+
{ field: "email" },
|
|
1749
|
+
{ field: "notes", hidden: true }, // starts hidden
|
|
1750
|
+
]}
|
|
1751
|
+
rows={rows}
|
|
1752
|
+
/>
|
|
1753
|
+
```
|
|
1754
|
+
|
|
1755
|
+
- `hidden: true` starts a column hidden. The menu shows it unchecked.
|
|
1756
|
+
- `hideable: false` keeps a column out of the menu, so it cannot be hidden.
|
|
1757
|
+
- The last visible column cannot be hidden.
|
|
1758
|
+
- Hidden columns are skipped by search, keyboard navigation and CSV export (unless `includeHidden`).
|
|
1759
|
+
|
|
1760
|
+
### 6.18 Keyboard
|
|
1761
|
+
|
|
1762
|
+
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.
|
|
1763
|
+
|
|
1764
|
+
| Key (on a cell) | Action |
|
|
1765
|
+
| --- | --- |
|
|
1766
|
+
| Arrow keys | Move one cell |
|
|
1767
|
+
| Home / End | First / last column of the row |
|
|
1768
|
+
| Ctrl+Home / Ctrl+End | First cell of the first row / last cell of the last row |
|
|
1769
|
+
| Page Up / Page Down | Move 10 rows |
|
|
1770
|
+
| Enter / F2 | Start editing (editable cells) |
|
|
1771
|
+
| Any character | Start editing and replace the value (text and number cells) |
|
|
1772
|
+
|
|
1773
|
+
Other keys, always available:
|
|
1774
|
+
|
|
1775
|
+
| Where | Key | Action |
|
|
1776
|
+
| --- | --- | --- |
|
|
1777
|
+
| Header | Enter / Space | Sort by this column (sortable columns) |
|
|
1778
|
+
| Editor | Enter | Save, edit the cell below |
|
|
1779
|
+
| Editor | Shift+Enter | Save, edit the cell above |
|
|
1780
|
+
| Editor | Tab / Shift+Tab | Save, edit the next / previous cell |
|
|
1781
|
+
| Editor | Esc | Cancel |
|
|
1782
|
+
| Filter popup | Esc | Close |
|
|
1783
|
+
| Text filter box | Enter | Close the popup |
|
|
1784
|
+
|
|
1785
|
+
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.
|
|
1786
|
+
|
|
1787
|
+
```tsx
|
|
1788
|
+
<DataGrid keyboardNavigation editable maxHeight={420} columns={columns} rows={rows} />
|
|
1789
|
+
```
|
|
1790
|
+
|
|
1791
|
+
Without `keyboardNavigation`, cells are reachable with Tab (editable cells) or the mouse, as before.
|
|
1792
|
+
|
|
1793
|
+
### 6.19 CSV export and the API
|
|
1794
|
+
|
|
1795
|
+
**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).
|
|
1796
|
+
|
|
1797
|
+
```tsx
|
|
1798
|
+
<DataGrid exportable ... /> // downloads export.csv
|
|
1799
|
+
<DataGrid exportable={{ filename: "employees" }} ... /> // employees.csv
|
|
1800
|
+
<DataGrid exportable={{ filename: "page-1", scope: "page" }} ... /> // only the current page
|
|
1801
|
+
<DataGrid exportable={{ scope: "selected" }} rowSelection ... /> // only the selected rows
|
|
1802
|
+
```
|
|
1803
|
+
|
|
1804
|
+
| `ExportOptions` | Default | Meaning |
|
|
1805
|
+
| --- | --- | --- |
|
|
1806
|
+
| `filename` | `"export"` | File name without `.csv` |
|
|
1807
|
+
| `scope` | `"filtered"` | `"filtered"` all rows matching search and filters, `"page"` the current page, `"selected"` the selected rows |
|
|
1808
|
+
| `includeHidden` | `false` | Also write hidden columns |
|
|
1809
|
+
| `sanitize` | `true` | Put an apostrophe in front of text starting with `=`, `+`, `-` or `@`, so Excel does not run it as a formula |
|
|
1810
|
+
|
|
1811
|
+
**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`:
|
|
1812
|
+
|
|
1813
|
+
```tsx
|
|
1814
|
+
{
|
|
1815
|
+
field: "status",
|
|
1816
|
+
exportValue: (value) => String(value).toUpperCase(), // ACTIVE
|
|
1817
|
+
}
|
|
1818
|
+
{
|
|
1819
|
+
field: "salary",
|
|
1820
|
+
valueFormatter: (v) => `$${v.toLocaleString()}`, // shown as $52,000
|
|
1821
|
+
exportValue: (v) => v, // exported as 52000, so a spreadsheet can add it up
|
|
1822
|
+
}
|
|
1823
|
+
```
|
|
1824
|
+
|
|
1825
|
+
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.
|
|
1826
|
+
|
|
1827
|
+
**The API.** Pass a ref as `apiRef` to call methods from outside the grid:
|
|
1828
|
+
|
|
1829
|
+
```tsx
|
|
1830
|
+
const api = useRef<DataGridApi<Employee> | null>(null);
|
|
1831
|
+
|
|
1832
|
+
<DataGrid apiRef={api} rowSelection ... />
|
|
1833
|
+
|
|
1834
|
+
api.current?.exportCsv({ filename: "selected", scope: "selected" }); // download
|
|
1835
|
+
const text = api.current?.getCsv(); // the CSV as a string
|
|
1836
|
+
```
|
|
1837
|
+
|
|
1838
|
+
| Method | Does |
|
|
1839
|
+
| --- | --- |
|
|
1840
|
+
| `getCsv(options?)` | Returns the CSV text without downloading it |
|
|
1841
|
+
| `exportCsv(options?)` | Downloads a CSV file |
|
|
1842
|
+
| `getSelectedRows()` | The selected row objects |
|
|
1843
|
+
| `getSelectedRowIds()` | The selected ids |
|
|
1844
|
+
| `selectAll()` | Selects every selectable row matching the search and filters |
|
|
1845
|
+
| `clearSelection()` | Clears the selection |
|
|
1846
|
+
| `refresh()` | Runs the last query again (`serverSide` only) |
|
|
1847
|
+
|
|
1848
|
+
**Sending the CSV somewhere else** (upload, email) instead of downloading:
|
|
1849
|
+
|
|
1850
|
+
```tsx
|
|
1851
|
+
const csv = api.current!.getCsv({ scope: "selected" });
|
|
1852
|
+
await fetch("/api/reports", { method: "POST", headers: { "Content-Type": "text/csv" }, body: csv });
|
|
1853
|
+
```
|
|
1854
|
+
|
|
1855
|
+
**Your own button** next to the built-in ones:
|
|
1856
|
+
|
|
1857
|
+
```tsx
|
|
1858
|
+
<DataGrid
|
|
1859
|
+
apiRef={api}
|
|
1860
|
+
toolbar={<button className="rg-grid__tool-btn" onClick={() => api.current?.exportCsv({ filename: "all", includeHidden: true })}>Export everything</button>}
|
|
1861
|
+
...
|
|
1862
|
+
/>
|
|
1863
|
+
```
|
|
1864
|
+
|
|
1865
|
+
### 6.20 Virtualization
|
|
1866
|
+
|
|
1867
|
+
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.
|
|
1868
|
+
|
|
1869
|
+
```tsx
|
|
1870
|
+
<DataGrid
|
|
1871
|
+
virtualize
|
|
1872
|
+
rowHeight={40} // every row must be exactly this tall
|
|
1873
|
+
maxHeight={480} // the grid scrolls inside this height (400 if you give none)
|
|
1874
|
+
overscan={8} // extra rows drawn above and below
|
|
1875
|
+
columns={columns}
|
|
1876
|
+
rows={twentyThousandRows}
|
|
1877
|
+
getRowId={(r) => r.id}
|
|
1878
|
+
searchable
|
|
1879
|
+
/>
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
- **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.
|
|
1883
|
+
- Use it without `pagination` so everything scrolls in one list. Sorting, search and filters still work on the whole data set.
|
|
1884
|
+
- Row animation is skipped while virtualizing, because rows come and go as you scroll.
|
|
1885
|
+
- Keyboard navigation, selection and editing all work. Moving focus to a row outside the window scrolls it into view first.
|
|
1886
|
+
- Choose a `rowHeight` that suits your padding: 40 is right for the default cell padding.
|
|
1887
|
+
|
|
1888
|
+
```tsx
|
|
1889
|
+
// 50,000 rows, searchable, selectable, keyboard friendly
|
|
1890
|
+
<DataGrid virtualize rowHeight={36} maxHeight="70vh" searchable rowSelection keyboardNavigation theme="graphite" ... />
|
|
1891
|
+
```
|
|
1892
|
+
|
|
1893
|
+
### 6.21 Accessibility
|
|
1894
|
+
|
|
1895
|
+
- 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.
|
|
1896
|
+
- 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)).
|
|
1897
|
+
- 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.
|
|
1898
|
+
- Selected rows have `aria-selected="true"`. Row checkboxes are named "Select row N", and the header one "Select all rows".
|
|
1899
|
+
- The "N selected" count is a status region (`role="status"`).
|
|
1900
|
+
- While loading, the grid has `aria-busy="true"` and the placeholder rows are hidden from assistive technology. The error message has `role="alert"`.
|
|
1901
|
+
- Invalid editors set `aria-invalid="true"`.
|
|
1902
|
+
- The shimmer and row animations stop when the system has "reduce motion" turned on.
|
|
1903
|
+
- Custom components you supply (renderers, editors, skeleton rows, indicators) are responsible for their own accessibility.
|
|
1904
|
+
|
|
1905
|
+
### 6.22 TypeScript
|
|
1906
|
+
|
|
1907
|
+
All types are exported:
|
|
1908
|
+
|
|
1909
|
+
```ts
|
|
1910
|
+
import type {
|
|
1911
|
+
// grid and columns
|
|
1912
|
+
DataGridProps, ColumnDef, DataGridApi, GridTheme, GridSlot, FieldPath,
|
|
1913
|
+
// cells, editors, headers
|
|
1914
|
+
CellParams, CellEditEvent, CellEditorProps, CellEditableParams, HeaderRendererProps, MoveDirection,
|
|
1915
|
+
// sorting, paging
|
|
1916
|
+
SortModel, SortDirection, PaginationPosition,
|
|
1917
|
+
// filters
|
|
1918
|
+
FilterType, FilterValue, FilterOption, NumberRangeFilter, DateRangeFilter, FilterRendererProps, FilterIndicatorProps,
|
|
1919
|
+
// selection, server, export, loading
|
|
1920
|
+
RowSelection, GridQuery, ExportOptions, SkeletonRowProps,
|
|
1921
|
+
// checkbox
|
|
1922
|
+
ColumnCheckboxOptions, CheckboxProps, CheckboxVariant, CheckboxSlot,
|
|
1923
|
+
} from "react-gridix";
|
|
1924
|
+
```
|
|
1925
|
+
|
|
1926
|
+
Typing tips:
|
|
1927
|
+
|
|
1928
|
+
```tsx
|
|
1929
|
+
const columns: ColumnDef<Employee>[] = [ ... ]; // field is checked against Employee (dot paths too)
|
|
1930
|
+
function Cell({ row }: CellParams<Employee>) { ... } // row is typed as Employee
|
|
1931
|
+
|
|
1932
|
+
const api = useRef<DataGridApi<Employee> | null>(null); // typed apiRef
|
|
1933
|
+
const [sel, setSel] = useState<RowSelection<Employee>>({ ids: [], rows: [] });
|
|
1934
|
+
const [filters, setFilters] = useState<Record<string, FilterValue>>({});
|
|
1935
|
+
|
|
1936
|
+
// narrowing a filter value yourself (for example when building a server query)
|
|
1937
|
+
function toParams(f: Record<string, FilterValue>) {
|
|
1938
|
+
return Object.entries(f).map(([field, v]) => {
|
|
1939
|
+
if (typeof v === "string") return `${field}~${v}`; // text / exact
|
|
1940
|
+
if (Array.isArray(v)) return `${field}=${v.join(",")}`; // select
|
|
1941
|
+
if ("min" in v || "max" in v) return `${field}:${(v as NumberRangeFilter).min ?? ""}..${(v as NumberRangeFilter).max ?? ""}`;
|
|
1942
|
+
return `${field}:${(v as DateRangeFilter).from ?? ""}..${(v as DateRangeFilter).to ?? ""}`; // date
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
### 6.23 Recipes
|
|
1948
|
+
|
|
1949
|
+
**Read-only grid**
|
|
1950
|
+
|
|
1951
|
+
```tsx
|
|
1952
|
+
<DataGrid columns={columns} rows={rows} /> // editable, sortable, searchable... all default to off
|
|
1953
|
+
```
|
|
1954
|
+
|
|
1955
|
+
**A typical admin table**
|
|
1956
|
+
|
|
1957
|
+
```tsx
|
|
1958
|
+
<DataGrid
|
|
1959
|
+
columns={columns} // sortable / filterable chosen per column
|
|
1960
|
+
rows={rows}
|
|
1961
|
+
getRowId={(r) => r.id}
|
|
1962
|
+
searchable
|
|
1963
|
+
pagination
|
|
1964
|
+
rowSelection
|
|
1965
|
+
columnMenu
|
|
1966
|
+
exportable={{ filename: "users" }}
|
|
1967
|
+
striped
|
|
1968
|
+
theme="lavender"
|
|
1969
|
+
/>
|
|
1970
|
+
```
|
|
1971
|
+
|
|
1972
|
+
**Currency column that accepts "$1,200" when typed**
|
|
1973
|
+
|
|
1974
|
+
```tsx
|
|
1975
|
+
{
|
|
1976
|
+
field: "salary",
|
|
1977
|
+
type: "number",
|
|
1978
|
+
align: "right",
|
|
1979
|
+
sortable: true,
|
|
1980
|
+
valueFormatter: (v) => (v == null ? "" : money.format(v)),
|
|
1981
|
+
valueParser: (text) => Number(text.replace(/[$,]/g, "")),
|
|
1982
|
+
validate: (v) => Number.isFinite(v),
|
|
1983
|
+
}
|
|
1984
|
+
```
|
|
1985
|
+
|
|
1986
|
+
(The editor opens with the raw number, such as `52000`. The parser only matters if the user types symbols.)
|
|
1987
|
+
|
|
1988
|
+
**Only one column editable**
|
|
1989
|
+
|
|
1990
|
+
```tsx
|
|
1991
|
+
<DataGrid columns={[{ field: "id" }, { field: "notes", editable: true }]} />
|
|
1992
|
+
```
|
|
1993
|
+
|
|
1994
|
+
**Lock rows after approval**
|
|
1995
|
+
|
|
1996
|
+
```tsx
|
|
1997
|
+
{ field: "amount", editable: ({ row }) => !row.approved }
|
|
1998
|
+
```
|
|
1999
|
+
|
|
2000
|
+
**Scrolling grid with sticky header**
|
|
2001
|
+
|
|
2002
|
+
```tsx
|
|
2003
|
+
<DataGrid maxHeight="60vh" ... />
|
|
2004
|
+
```
|
|
2005
|
+
|
|
2006
|
+
**Highlight a row based on its data**
|
|
2007
|
+
|
|
2008
|
+
```tsx
|
|
2009
|
+
<DataGrid rowClassName={(row) => (row.overdue ? "bg-red-50" : "")} />
|
|
2010
|
+
```
|
|
2011
|
+
|
|
2012
|
+
**Overdue dates in red**
|
|
2013
|
+
|
|
2014
|
+
```tsx
|
|
2015
|
+
{
|
|
2016
|
+
field: "due",
|
|
2017
|
+
type: "date",
|
|
2018
|
+
cellStyle: ({ value }) => (value && String(value) < new Date().toISOString().slice(0, 10) ? { color: "#dc2626" } : {}),
|
|
2019
|
+
}
|
|
2020
|
+
```
|
|
2021
|
+
|
|
2022
|
+
**Status as a coloured badge, with an exact filter**
|
|
2023
|
+
|
|
2024
|
+
```tsx
|
|
2025
|
+
const Badge = ({ value }: CellParams<Task>) => <span className={`badge badge-${value}`}>{String(value)}</span>;
|
|
2026
|
+
|
|
2027
|
+
{ field: "status", sortable: true, filterable: true, filterType: "select", cellRenderer: Badge }
|
|
2028
|
+
```
|
|
2029
|
+
|
|
2030
|
+
**Delete the selected rows**
|
|
2031
|
+
|
|
2032
|
+
```tsx
|
|
2033
|
+
const [rows, setRows] = useState(initial);
|
|
2034
|
+
const [ids, setIds] = useState<(string | number)[]>([]);
|
|
2035
|
+
|
|
2036
|
+
<button disabled={!ids.length} onClick={() => { setRows((r) => r.filter((x) => !ids.includes(x.id))); setIds([]); }}>
|
|
2037
|
+
Delete {ids.length}
|
|
2038
|
+
</button>
|
|
2039
|
+
<DataGrid rows={rows} columns={columns} getRowId={(r) => r.id} rowSelection selectedRowIds={ids} onSelectionChange={(s) => setIds(s.ids)} />
|
|
2040
|
+
```
|
|
2041
|
+
|
|
2042
|
+
**Bulk edit the selected rows** (`onRowsChange` gives the whole array back):
|
|
2043
|
+
|
|
2044
|
+
```tsx
|
|
2045
|
+
const markDone = () => setRows((rs) => rs.map((r) => (ids.includes(r.id) ? { ...r, status: "done" } : r)));
|
|
2046
|
+
```
|
|
2047
|
+
|
|
2048
|
+
**Choose the page size once and remember it**
|
|
2049
|
+
|
|
2050
|
+
```tsx
|
|
2051
|
+
<DataGrid pagination pageSize={Number(localStorage.getItem("ps")) || 25} onPageSizeChange={(n) => localStorage.setItem("ps", String(n))} />
|
|
2052
|
+
```
|
|
2053
|
+
|
|
2054
|
+
**A fast, big, read-only table**
|
|
2055
|
+
|
|
2056
|
+
```tsx
|
|
2057
|
+
<DataGrid virtualize rowHeight={32} maxHeight="80vh" animateRows={false} searchable columns={columns} rows={hugeArray} getRowId={(r) => r.id} />
|
|
2058
|
+
```
|
|
2059
|
+
|
|
2060
|
+
**An infinite-looking server table with a dark theme**
|
|
2061
|
+
|
|
2062
|
+
```tsx
|
|
2063
|
+
<DataGrid serverSide pagination searchable multiSort theme="aurora" columns={columns} rows={rows} rowCount={total} onQueryChange={load} />
|
|
2064
|
+
```
|
|
2065
|
+
|
|
2066
|
+
### 6.24 Known limitations
|
|
2067
|
+
|
|
2068
|
+
Not included:
|
|
2069
|
+
|
|
2070
|
+
- **Column reordering** (by design)
|
|
2071
|
+
- Row grouping, tree data, row detail panels and column spanning
|
|
2072
|
+
- Infinite scroll in server mode (paging is used instead)
|
|
2073
|
+
- Saving column widths or visibility between visits (use `onColumnVisibilityChange` and your own storage)
|
|
2074
|
+
- Variable row heights while virtualizing
|
|
2075
|
+
|
|
2076
|
+
Behaviours to be aware of:
|
|
2077
|
+
|
|
2078
|
+
- Text filters match by substring (`active` also matches `inactive`). Use `filterType: "exact"` or `"select"` for exact choices.
|
|
2079
|
+
- Only one filter popup is open at a time.
|
|
2080
|
+
- Stripes and the row animation restart on every page, sort, search or filter change.
|
|
2081
|
+
- `pageSize` is read once, on mount (use `onPageSizeChange`). `defaultSort` and `defaultSelectedRowIds` are read once too: to change them later, control the state.
|
|
2082
|
+
- `rows` must keep a stable identity between renders, otherwise edits are discarded (see [6.9](#69-editing)).
|
|
2083
|
+
- `onRowClick` also fires for clicks inside a cell that is being edited.
|
|
2084
|
+
- A `valueParser` that returns `NaN` stores `NaN`. Add `validate: (v) => Number.isFinite(v)` to reject it.
|
|
2085
|
+
- `virtualize` needs fixed-height rows. A cell with wrapped text will be cut off.
|
|
2086
|
+
- Pinned columns only stick when the table is wider than its container.
|
|
2087
|
+
- In `serverSide` mode the grid cannot export or select rows it has never loaded.
|
|
2088
|
+
- The date calendar starts weeks on Sunday and has no week numbers or time of day.
|
|
2089
|
+
|
|
2090
|
+
---
|
|
2091
|
+
|
|
2092
|
+
## 7. Demos
|
|
2093
|
+
|
|
2094
|
+
The repository contains a playground with one page per feature:
|
|
2095
|
+
|
|
2096
|
+
| Demo (`#id`) | Shows |
|
|
2097
|
+
| --- | --- |
|
|
2098
|
+
| Basic (`#basic`) | Opt-in sorting, global search, per-column filter dots with popups, pagination, controlled `SortModel[]` |
|
|
2099
|
+
| Editing (`#editing`) | Enter-moves-down editing, Tab, validation, locked cells, `onCellEdit` |
|
|
2100
|
+
| Custom components (`#custom`) | `cellRenderer`, `cellEditor`, `headerRenderer`, a custom funnel `filterIndicator`, a per-column indicator style, a `filterRenderer` in the popup |
|
|
2101
|
+
| 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` |
|
|
2102
|
+
| Styling (`#styling`) | Tailwind classes and inline styles, column `className` / `style`, and `stripeColors` |
|
|
2103
|
+
| Pagination (`#pagination`) | Top, bottom and both, no pagination, the row animation toggle with two **custom animation classes**, a **row-number (sequence) column**, and a **controlled page and search** grid driven by outside buttons |
|
|
2104
|
+
| Nested fields (`#nested`) | `contact.phone` style dot paths: editing, search, filter, sort, and proof the original data is not mutated |
|
|
2105
|
+
| Row selection (`#selection`) | Multiple and single modes, controlled ids, `isRowSelectable`, Shift+click ranges, select-all across pages, `apiRef.selectAll()`, a Delete button |
|
|
2106
|
+
| 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 |
|
|
2107
|
+
| Columns (`#columns`) | Resizing, pinned columns on both sides, the Columns menu, arrow-key navigation, CSV export with `exportValue`, multi-column sort |
|
|
2108
|
+
| Large data (`#large`) | 20,000 virtualized rows with search, sorting and a theme |
|
|
2109
|
+
| Dates and filters (`#dates`) | `type: "date"` with the calendar editor and range-calendar filter, `dateMin` / `dateMax`, a `Date`-object column, from/to, number range, select and exact filters |
|
|
2110
|
+
| Themes (`#themes`) | A switcher for none and all 8 themes |
|
|
2111
|
+
|
|
2112
|
+
Run it:
|
|
2113
|
+
|
|
2114
|
+
```bash
|
|
2115
|
+
npm install
|
|
2116
|
+
npm run dev
|
|
2117
|
+
```
|
|
2118
|
+
|
|
2119
|
+
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.
|
|
2120
|
+
|
|
2121
|
+
The Styling demo loads Tailwind from its CDN, so it needs an internet connection. The rest works offline.
|
|
2122
|
+
|
|
2123
|
+
---
|
|
2124
|
+
|
|
2125
|
+
## 8. Development, building and publishing
|
|
2126
|
+
|
|
2127
|
+
### Project layout
|
|
2128
|
+
|
|
2129
|
+
```
|
|
2130
|
+
src/
|
|
2131
|
+
index.ts Public exports
|
|
2132
|
+
components/Checkbox/ Checkbox.tsx, checkbox.css
|
|
2133
|
+
components/DataGrid/
|
|
2134
|
+
DataGrid.tsx The grid (state, rendering, keyboard, selection, server mode, virtualization)
|
|
2135
|
+
types.ts All public types, with doc comments
|
|
2136
|
+
ColumnFilter.tsx The filter indicator and its popup (text, number, date, select)
|
|
2137
|
+
ColumnMenu.tsx The "Columns" show / hide menu
|
|
2138
|
+
Popup.tsx Portal popup used by the two above
|
|
2139
|
+
filters.ts Filter kinds, matching and select options
|
|
2140
|
+
dates.ts Date parsing and formatting helpers
|
|
2141
|
+
csv.ts CSV building and download
|
|
2142
|
+
hooks.ts useControlled and a layout-effect helper
|
|
2143
|
+
paths.ts Dot notation: FieldPath, getByPath, setByPath
|
|
2144
|
+
utils.ts Small helpers
|
|
2145
|
+
datagrid.css Layout and component styles (zero specificity)
|
|
2146
|
+
themes.css The 8 themes
|
|
2147
|
+
demo/ Playground (not published)
|
|
2148
|
+
examples/ One file per demo page
|
|
2149
|
+
vite.config.ts Library build
|
|
2150
|
+
vite.demo.config.ts Playground dev server
|
|
2151
|
+
```
|
|
2152
|
+
|
|
2153
|
+
### Scripts
|
|
2154
|
+
|
|
2155
|
+
| Command | Does |
|
|
2156
|
+
| --- | --- |
|
|
2157
|
+
| `npm run dev` | Starts the demo playground using the library source |
|
|
2158
|
+
| `npm run typecheck` | Runs the TypeScript compiler without emitting files |
|
|
2159
|
+
| `npm run build` | Builds `dist/`: ES module, CommonJS, bundled type definitions and `style.css` |
|
|
2160
|
+
|
|
2161
|
+
### Testing the built package in another app
|
|
2162
|
+
|
|
2163
|
+
```bash
|
|
2164
|
+
npm run build
|
|
2165
|
+
npm pack # creates react-gridix-0.1.0.tgz
|
|
2166
|
+
# in another project:
|
|
2167
|
+
npm install /path/to/react-gridix-0.1.0.tgz
|
|
2168
|
+
```
|
|
2169
|
+
|
|
2170
|
+
### Publishing to npm
|
|
2171
|
+
|
|
2172
|
+
1. Create an npm account, enable two-factor authentication, and run `npm login`.
|
|
2173
|
+
2. Check the name is free with `npm view react-gridix`. If taken, rename the package, for example `@yourname/react-gridix` (scoped packages need `npm publish --access public`).
|
|
2174
|
+
3. In `package.json` fill in `author`, `repository`, `homepage` and `bugs`, and put your name in the `LICENSE` file (it says `YOUR NAME`).
|
|
2175
|
+
4. Run `npm run typecheck` and `npm run build`. `prepublishOnly` runs both automatically on `npm publish`. Confirm `dist/` contains `react-gridix.js`, `react-gridix.cjs`, `index.d.ts` and `style.css`.
|
|
2176
|
+
5. Run `npm publish --dry-run` and check that only `dist/`, `package.json`, `README.md` and `LICENSE` are listed.
|
|
2177
|
+
6. Run `npm publish`.
|
|
2178
|
+
7. For later releases bump the version first (`npm version patch`, `minor` or `major`). npm will not accept the same version twice. This release changes defaults (see [section 10](#10-migrating-from-the-earlier-version)), so a `minor` bump while below 1.0 (or `major` after) is appropriate.
|
|
2179
|
+
|
|
2180
|
+
---
|
|
2181
|
+
|
|
2182
|
+
## 9. Troubleshooting
|
|
1026
2183
|
|
|
1027
2184
|
| Problem | Cause and fix |
|
|
1028
2185
|
| --- | --- |
|
|
1029
2186
|
| Everything looks unstyled | Import `react-gridix/style.css` once in your app |
|
|
1030
|
-
| `theme="ocean"` does nothing | The theme CSS ships in `style.css`: make sure it is imported. The attribute `data-
|
|
1031
|
-
|
|
|
2187
|
+
| `theme="ocean"` does nothing | The theme CSS ships in `style.css`: make sure it is imported. The attribute `data-rg-theme` should appear on the grid root |
|
|
2188
|
+
| Headers don't sort | Sorting is per column: add `sortable: true` to the column definition. There is no grid-level `sortable` prop any more |
|
|
2189
|
+
| There is no search box | Pass `searchable` |
|
|
2190
|
+
| There is no page info or page buttons | Pass `pagination`. Without it every row is shown and there is no footer |
|
|
2191
|
+
| I can't find the filter input | Filters are per column: set `filterable: true`, then click the dot in the header |
|
|
2192
|
+
| A date column shows `2026-11-05` instead of `Nov 5, 2026` | The column needs `type: "date"` |
|
|
2193
|
+
| The date calendar never opens | The cell must be editable (`editable` on the grid or the column), then double-click it or press Enter / F2 |
|
|
2194
|
+
| A sequence column shows nothing or a type error | Give it `type: "sequence"` and a `field` that starts with `#`, for example `{ field: "#", type: "sequence" }` |
|
|
2195
|
+
| My custom row animation does nothing | `animateRows` is `false`, `virtualize` is on, or your class does not set `animation`. Check that the keyframes exist and that the class name is spelled the same in CSS |
|
|
2196
|
+
| The number filter popup asks for Min and Max, but I wanted text | Number columns default to a range filter. Set `filterType: "text"` |
|
|
2197
|
+
| A typed filter feels slow, or fires on every key | Change the wait with `filterDebounce` on the grid (default 300 ms) or on one column. Use `0` for no waiting |
|
|
2198
|
+
| Select filter lists the wrong labels | Pass `filterOptions` with `{ value, label }` pairs. `value` must equal what is stored in the row |
|
|
2199
|
+
| Selecting rows does nothing | Add `rowSelection`. Give the grid `getRowId` so selection survives sorting and paging |
|
|
2200
|
+
| 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 |
|
|
2201
|
+
| 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` |
|
|
2202
|
+
| 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 |
|
|
2203
|
+
| Page 1 is shown after every search | By design: the page resets when search, filters, sort or page size change |
|
|
2204
|
+
| Resize handles do nothing | The column needs `resizable: true` |
|
|
2205
|
+
| 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` |
|
|
2206
|
+
| Virtualized rows look cut off or overlap | `rowHeight` is smaller than your content. Increase it, or shorten the content |
|
|
2207
|
+
| Virtualized grid is tiny | It scrolls inside `maxHeight` (400 if unset). Set `maxHeight` |
|
|
2208
|
+
| Arrow keys do nothing | Add `keyboardNavigation` and click or Tab into a cell first |
|
|
2209
|
+
| 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 |
|
|
2210
|
+
| CSV shows `$52,000` where I wanted `52000` | Add `exportValue: (v) => v` to the column |
|
|
1032
2211
|
| 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>[]` |
|
|
1033
2212
|
| My checkbox column header is empty | By design: boolean columns default to no label. Set `headerName` to show one |
|
|
1034
|
-
| No row animation | `animateRows` is off, or the system has "reduce motion" turned on |
|
|
2213
|
+
| No row animation | `animateRows` is off, `virtualize` is on, or the system has "reduce motion" turned on |
|
|
1035
2214
|
| 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 |
|
|
1036
2215
|
| Edits disappear on the next render | `rows` gets a new identity every render. Keep it in `useState` or `useMemo` |
|
|
1037
|
-
| A custom editor or
|
|
2216
|
+
| A custom editor, renderer or skeleton loses focus or resets while typing | The component is defined inline inside render. Move it to module level |
|
|
1038
2217
|
| Typing in a custom editor column doesn't start editing | By design: custom editors open with double-click, Enter or F2 |
|
|
1039
2218
|
| 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` |
|
|
1040
|
-
|
|
|
1041
|
-
| Changing `pageSize` or `defaultSort` after mount does nothing | They are initial values only |
|
|
2219
|
+
| Changing `pageSize` or `defaultSort` after mount does nothing | They are initial values only. Control `page` / `sort` instead |
|
|
1042
2220
|
| 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 |
|
|
1043
2221
|
| 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` |
|
|
2222
|
+
|
|
2223
|
+
---
|
|
2224
|
+
|
|
2225
|
+
## 10. Migrating from the earlier version
|
|
2226
|
+
|
|
2227
|
+
### Renamed: react-gridix
|
|
2228
|
+
|
|
2229
|
+
The library is now **`react-gridix`**, and every CSS prefix changed from `rk` to `rg`.
|
|
2230
|
+
|
|
2231
|
+
| Before | Now |
|
|
2232
|
+
| --- | --- |
|
|
2233
|
+
| `npm install react-ui-kit` | `npm install react-gridix` |
|
|
2234
|
+
| `import ... from "react-ui-kit"` | `import ... from "react-gridix"` |
|
|
2235
|
+
| `import "react-ui-kit/style.css"` | `import "react-gridix/style.css"` |
|
|
2236
|
+
| CSS variables `--rk-header-bg`, `--rk-primary`, ... | `--rg-header-bg`, `--rg-primary`, ... |
|
|
2237
|
+
| Classes `.rk-grid__row`, `.rk-checkbox`, ... | `.rg-grid__row`, `.rg-checkbox`, ... |
|
|
2238
|
+
| Theme selector `[data-rk-theme="brand"]` | `[data-rg-theme="brand"]` |
|
|
2239
|
+
|
|
2240
|
+
Search your project's CSS for `rk-` and replace it with `rg-`. A one-line fix in a Unix shell: `sed -i 's/\brk-/rg-/g' src/**/*.css`.
|
|
2241
|
+
|
|
2242
|
+
### Other changes
|
|
2243
|
+
|
|
2244
|
+
Also new in this release: `filterDebounce`, a smaller default checkbox (17px instead of 20px, with a tighter gap and a narrower selection column), a calendar for date cells and date filters, the `sequence` column type, `rowAnimationClassName`, and re-tuned theme colours (stronger header contrast, clearer inputs in the dark themes).
|
|
2245
|
+
|
|
2246
|
+
### Changed defaults
|
|
2247
|
+
|
|
2248
|
+
Three defaults changed so that every feature is opt-in.
|
|
2249
|
+
|
|
2250
|
+
| Before | Now | What to do |
|
|
2251
|
+
| --- | --- | --- |
|
|
2252
|
+
| 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 |
|
|
2253
|
+
| `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` |
|
|
2254
|
+
| `searchable` default `true` | Default `false` | Add `searchable` to grids that should show the search box |
|
|
2255
|
+
| `pagination` default `true` | Default `false`. With it off there is no footer and no row count | Add `pagination` to grids that should page |
|
|
2256
|
+
| 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 |
|
|
2257
|
+
| `FilterRendererProps` | Unchanged (`value: string`) | |
|
|
2258
|
+
|
|
2259
|
+
A quick way to get the old behaviour everywhere:
|
|
2260
|
+
|
|
2261
|
+
```tsx
|
|
2262
|
+
// before: <DataGrid columns={columns} rows={rows} />
|
|
2263
|
+
const withSorting = (cols: ColumnDef<Row>[]) => cols.map((c) => ({ sortable: true, ...c }));
|
|
2264
|
+
|
|
2265
|
+
<DataGrid columns={withSorting(columns)} rows={rows} searchable pagination />
|
|
2266
|
+
```
|
|
2267
|
+
|
|
2268
|
+
Everything else in this document is new and optional.
|