react-gridix 0.2.2 → 0.2.4

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 CHANGED
@@ -1,6 +1,6 @@
1
- # react-gridix
1
+ # react-gridix Documentation
2
2
 
3
- A React `DataGrid` with row selection, server-side mode, typed per-column filters, date columns, in-cell editing, resizing and pinning, virtualization, CSV export and 8 themes, plus the `Checkbox` it uses.
3
+ A React `DataGrid` with row selection, server-side mode, typed per-column filters, date columns, in-cell editing, resizing and pinning, virtualization, export to CSV / Excel / JSON / PDF, 12 row animations, filter comparisons and 8 themes, plus the `Checkbox` it uses.
4
4
 
5
5
  **Contents**
6
6
 
@@ -11,10 +11,7 @@ A React `DataGrid` with row selection, server-side mode, typed per-column filter
11
11
  5. [Checkbox](#5-checkbox)
12
12
  6. [DataGrid](#6-datagrid)
13
13
  7. [Demos](#7-demos)
14
- 8. [Development, building and publishing](#8-development-building-and-publishing)
15
- 9. [Troubleshooting](#9-troubleshooting)
16
- 10. [Migrating from the earlier version](#10-migrating-from-the-earlier-version)
17
-
14
+ 8. [Troubleshooting](#8-troubleshooting)
18
15
  ---
19
16
 
20
17
  ## 1. Overview
@@ -46,6 +43,11 @@ A React `DataGrid` with row selection, server-side mode, typed per-column filter
46
43
  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**.
47
44
 
48
45
  ---
46
+ ![react-gridix ui](images/data-smaple-ui.PNG)
47
+
48
+ ![react-gridix ui](images/Capture.PNG)
49
+
50
+ ![react-gridix ui](images/Capture1.PNG)
49
51
 
50
52
  ## 2. Installation
51
53
 
@@ -63,6 +65,15 @@ import "react-gridix/style.css";
63
65
 
64
66
  The library does not inject CSS by itself. If the grid looks unstyled, this import is missing.
65
67
 
68
+ **Optional: PDF export.** CSV, Excel (`.xlsx`) and JSON export need nothing extra. PDF export uses three small libraries that are loaded only when someone exports a PDF, so install them only if you use it:
69
+
70
+ ```bash
71
+ npm install jspdf jspdf-autotable # the PDF itself and its table
72
+ npm install html2canvas # only if the PDF header / footer is a React element (it is drawn to a picture)
73
+ ```
74
+
75
+ Without them, a PDF export reports an error that names the missing package (see `onExportError`) and everything else keeps working.
76
+
66
77
  ---
67
78
 
68
79
  ## 3. Quick start
@@ -398,6 +409,7 @@ The grid is generic: `DataGrid<T>` infers `T` from `rows`, and `ColumnDef<T>` ch
398
409
  | `filters` / `onFiltersChange` | `Record<string, FilterValue>` / `(f) => void` | | Controlled column filters |
399
410
  | `pagination` | `boolean` | `false` | Split rows into pages and show the bar with the row count and page controls |
400
411
  | `paginationPosition` | `"bottom" \| "top" \| "both"` | `"bottom"` | Where the bar goes |
412
+ | `showFooter` | `boolean` | `true` | Set `false` to remove the footer completely (both bars), even with `pagination` on. Rows are still paged, so control the page with `page` / `onPageChange`. See [removing the toolbar and footer](#removing-the-toolbar-and-footer) |
401
413
  | `pageSize` | `number` | `10` | Rows per page (initial value) |
402
414
  | `pageSizeOptions` | `number[]` | `[5, 10, 25, 50]` | Choices in the page size menu. The current size is added if missing |
403
415
  | `page` / `onPageChange` | `number` / `(page) => void` | | Controlled page (zero-based) |
@@ -427,7 +439,8 @@ The grid is generic: `DataGrid<T>` infers `T` from `rows`, and `ColumnDef<T>` ch
427
439
  | `virtualize` | `boolean` | `false` | Render only the visible rows |
428
440
  | `rowHeight` | `number` | `40` | Row height in px when virtualizing |
429
441
  | `overscan` | `number` | `6` | Extra rows drawn above and below the window |
430
- | `exportable` | `boolean \| ExportOptions` | `false` | Show an "Export CSV" button |
442
+ | `exportable` | `boolean \| ExportOptions` | `false` | Show an export button in the toolbar. `true` gives "Export CSV"; `{ formats: ["csv", "xlsx", "json", "pdf"] }` gives an "Export" menu. See [6.19](#619-export-and-the-api) |
443
+ | `onExportError` | `(error: Error, format: ExportFormat) => void` | | Called when an export fails (for example `jspdf` is not installed). Without it the error is logged to the console |
431
444
  | `apiRef` | `MutableRefObject<DataGridApi<T> \| null>` | | Call `exportCsv()`, `getSelectedRows()` and more from outside |
432
445
 
433
446
  **Look and feel**
@@ -436,11 +449,18 @@ The grid is generic: `DataGrid<T>` infers `T` from `rows`, and `ColumnDef<T>` ch
436
449
  | --- | --- | --- | --- |
437
450
  | `theme` | `GridTheme \| string` | | Applies a theme. Nothing changes without it. See [Theming](#4-theming) |
438
451
  | `toolbar` | `ReactNode` | | Extra content on the right of the toolbar |
452
+ | `showToolbar` | `boolean` | `true` | Set `false` to remove the toolbar row completely (search box, Columns, Export, selected count and your `toolbar`), even when those features are on |
439
453
  | `emptyMessage` | `ReactNode` | `"No rows to show"` | Shown when no rows match |
454
+ | `emptyComponent` | `ComponentType<EmptyStateProps<T>>` | | Your own component shown instead of `emptyMessage`. Receives `{ reason, search, filters, columns, clearAll }`. See [6.26](#626-custom-empty-state) |
455
+ | `sortIndicator` | `"default" \| "arrow" \| ComponentType<SortIndicatorProps<T>>` | `"default"` | The arrow in sortable headers. See [6.25](#625-sort-indicator) |
440
456
  | `striped` | `boolean` | `false` | Alternate row shading |
441
457
  | `stripeColors` | `[string, string]` | | Two colours for the alternating rows. Implies `striped`. See [striped rows](#striped-rows) |
442
- | `animateRows` | `boolean` | `true` | Rows slide up and fade in when they render. See [row animation](#row-animation) |
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) |
458
+ | `animateRows` | `boolean` | `true` | Rows animate in when they appear and out when they leave. See [row animation](#row-animation) |
459
+ | `rowAnimation` | `RowAnimationName \| { enter?, exit? }` | `"slide-up"` | Which of the 12 built-in animations to use (`"fade"`, `"zoom"`, `"flip-x"`, ...), for both directions or `{ enter, exit }` |
460
+ | `rowAnimationDuration` | `number` | `380` | Milliseconds an entrance takes (an exit takes 80% of it) |
461
+ | `rowAnimationStagger` | `number` | `28` | Milliseconds between rows that appear together. `0` starts them all at once |
462
+ | `rowExitAnimationClassName` | `string \| (row, rowIndex) => string` | | Your own CSS class for rows that leave, like `rowAnimationClassName` is for rows that arrive |
463
+ | `rowAnimationClassName` | `string \| (row, rowIndex) => string` | | Your own CSS class for the entrance animation. Replaces the built-in one. See [custom row animation](#custom-row-animation) |
444
464
  | `checkboxVariant` | `"button" \| "tick" \| "native"` | `"native"` | Checkbox look for boolean columns and row selection. Columns can override it |
445
465
  | `maxHeight` | `number \| string` | | Makes the body scroll, with a sticky header. Example: `400` or `"60vh"` |
446
466
  | `className` | `string` | | Classes for the root element (Tailwind works). The grid is `width: 100%` of its container by default |
@@ -479,6 +499,9 @@ const columns: ColumnDef<Employee>[] = [
479
499
  | `filterType` | `"text" \| "exact" \| "number" \| "date" \| "select"` | Which filter controls the popup shows. Default follows `type` |
480
500
  | `filterOptions` | `(string \| FilterOption)[]` | Choices for a `"select"` filter. Without it they are built from the data |
481
501
  | `filterPlaceholder` | `string` | Placeholder of the text filter box |
502
+ | `filterOperators` | `boolean \| TextFilterOperator[]` | Adds a drop-down to the text filter popup: contains, does not contain, equals, not equal, starts with, ... Off by default (the column then always means "contains"). See [6.24](#624-filter-comparisons) |
503
+ | `filterOperator` | `TextFilterOperator` | The comparison selected first when `filterOperators` is on. Default `"contains"` |
504
+ | `sortIndicator` | `"default" \| "arrow" \| Component` | Overrides the grid's `sortIndicator` for this column |
482
505
  | `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 |
483
506
  | `filterIndicator` | `ComponentType<FilterIndicatorProps<T>>` | Your own component drawn instead of the default dot |
484
507
  | `filterIndicatorClassName` / `filterIndicatorStyle` | `string` / `CSSProperties` | Classes and inline styles for this column's indicator button |
@@ -789,6 +812,7 @@ const columns: ColumnDef<Task>[] = [
789
812
 
790
813
  - Click a day to save it at once. **Today** picks today and **Clear** saves an empty date (`null`).
791
814
  - 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.
815
+ - **Keys while the text box has focus:** Up / Down move the date a week, Ctrl (or Alt) + Left / Right move it a day, Page Up / Page Down move it a month. The text box and the calendar update together, and Enter saves. Plain Left / Right still move the text cursor. (Arrow-key movement *between cells* is a separate opt-in: `keyboardNavigation`.)
792
816
  - The arrows change month (`‹ ›`) and year (`« »`). Days before `dateMin` or after `dateMax` are dimmed and cannot be picked.
793
817
  - 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
818
  - Typing letters does not start an editor on a date cell: use double-click, Enter or F2.
@@ -879,6 +903,41 @@ The toolbar row is drawn when at least one of `searchable`, `columnMenu`, `expor
879
903
  toolbar={<button className="rg-grid__tool-btn" onClick={refetch}>Reload</button>}
880
904
  ```
881
905
 
906
+ #### Removing the toolbar and footer
907
+
908
+ Both bars are drawn automatically: the **toolbar** when `searchable`, `columnMenu`, `exportable` or `toolbar` is used, and the **footer** when `pagination` is on. To remove one completely, even though its feature is on, switch it off:
909
+
910
+ ```tsx
911
+ <DataGrid showToolbar={false} showFooter={false} ... />
912
+ ```
913
+
914
+ The features keep working, so you can drive them yourself:
915
+
916
+ ```tsx
917
+ const api = useRef<DataGridApi<Person> | null>(null);
918
+ const [search, setSearch] = useState("");
919
+ const [page, setPage] = useState(0);
920
+
921
+ // your own search box and pager, with the grid's bars removed
922
+ <input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search…" />
923
+ <button onClick={() => api.current?.exportCsv({ filename: "people" })}>Export</button>
924
+ <button disabled={page === 0} onClick={() => setPage(page - 1)}>Previous</button>
925
+ <button onClick={() => setPage(page + 1)}>Next</button>
926
+
927
+ <DataGrid
928
+ apiRef={api}
929
+ searchable showToolbar={false} // the search still filters, from `search`
930
+ pagination pageSize={10} showFooter={false} // still 10 rows per page, from `page`
931
+ search={search} onSearchChange={setSearch}
932
+ page={page} onPageChange={setPage}
933
+ exportable showToolbar={false}
934
+ columns={columns}
935
+ rows={rows}
936
+ />
937
+ ```
938
+
939
+ Remember the "N selected" count lives in the toolbar and the footer, so read the selection from `onSelectionChange` instead when both are removed. Each bar can be removed on its own: `showToolbar={false}` leaves the footer, `showFooter={false}` leaves the toolbar.
940
+
882
941
  ### 6.9 Editing
883
942
 
884
943
  Turn editing on with `editable` on the grid, then lock or unlock columns:
@@ -1197,7 +1256,9 @@ Add `headerCheckbox: true` to a boolean column. The header label becomes a check
1197
1256
  | `pageInfo` | The "Page 1 of 20" text |
1198
1257
  | `pageSize` | The rows-per-page `<select>` |
1199
1258
  | `pagination` | Wrapper around the page-size menu and buttons |
1200
- | `toolButton` | Toolbar buttons (Columns, Export CSV, Retry) |
1259
+ | `toolButton` | Toolbar buttons (Columns, Export, Retry) |
1260
+ | `exportMenu` | The "Export" button when several formats are offered (in addition to `toolButton`) |
1261
+ | `sortIndicator` | The wrapper of the arrow in a sortable header |
1201
1262
  | `skeleton` | The shimmering bars of the loading skeleton |
1202
1263
  | `pageButton` | Each first/previous/next/last button |
1203
1264
  | `checkbox` | Root of each checkbox in a `type: "boolean"` column |
@@ -1267,28 +1328,59 @@ Rows alternate between the two colours, starting with the first. `stripeColors`
1267
1328
 
1268
1329
  #### Row animation
1269
1330
 
1270
- When rows render (the first load, a new page, a sort, a search or a filter), they slide up and fade in, one after another. It is on by default.
1331
+ Rows animate when they appear and when they leave. It is on by default (a slide up, and a slide up and fade out). The important part is that **rows are matched by their id (`getRowId`)**, so the grid never replays the animation on rows that stay:
1332
+
1333
+ - A **new row** adds itself to the list with the "in" animation, while every other row stays exactly where it is.
1334
+ - A **removed row** (deleted, or hidden by a search or filter) stays on screen for a moment, plays the "out" animation where it was, then goes. The rows around it do not move until it is gone.
1335
+ - Changing the **page** (or the page size) replaces the rows at once with the "in" animation: it would be odd to show the old page leaving.
1336
+ - Sorting moves rows without animating them.
1271
1337
 
1272
1338
  ```tsx
1273
- <DataGrid animateRows={false} /> // turn it off
1339
+ <DataGrid animateRows={false} /> // turn it off
1340
+ <DataGrid rowAnimation="zoom" /> // zoom in, zoom out
1341
+ <DataGrid rowAnimation={{ enter: "slide-left", exit: "fade" }} /> // mix the two directions
1342
+ <DataGrid rowAnimation={{ enter: "bounce", exit: "none" }} /> // "none": rows just disappear
1343
+ <DataGrid rowAnimation="flip-x" rowAnimationDuration={600} rowAnimationStagger={60} />
1274
1344
  ```
1275
1345
 
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`).
1346
+ The built-in animations (each has an "in" and an "out" version):
1347
+
1348
+ | Name | In | Out |
1349
+ | --- | --- | --- |
1350
+ | `"fade"` | fades in | fades out |
1351
+ | `"slide-up"` (default) | rises from below | floats up |
1352
+ | `"slide-down"` | drops from above | sinks down |
1353
+ | `"slide-left"` | slides in from the left | slides out to the left |
1354
+ | `"slide-right"` | slides in from the right | slides out to the right |
1355
+ | `"zoom"` | grows from small | shrinks away |
1356
+ | `"flip-x"` | flips up around its horizontal axis | flips away |
1357
+ | `"flip-y"` | swings in around its vertical axis | swings out |
1358
+ | `"rotate"` | turns and scales in | turns and scales out |
1359
+ | `"skew"` | slides in leaning over | slides out leaning over |
1360
+ | `"blur"` | comes into focus | blurs away |
1361
+ | `"bounce"` | drops in and bounces | hops and jumps out |
1362
+ | `"none"` | no animation | removed at once |
1363
+
1364
+ Use a row id that does not change: `getRowId={(row) => row.id}`. Without it the grid uses the row's position as its id, and removing a row from the middle makes every row after it look different, so the wrong row animates out.
1365
+
1366
+ Tuning: `rowAnimationDuration` (default 380 ms; exits take 80%), `rowAnimationStagger` (default 28 ms between rows that appear together, up to 24 rows). The same values exist as CSS variables on the grid: `--rg-row-duration`, `--rg-row-exit-duration` and `--rg-row-stagger`, so you can also set them in your own CSS. Visitors who ask their system for reduced motion get no animation, and rows leave at once. Animation is also off while `virtualize` is on.
1277
1367
 
1278
1368
  #### Custom row animation
1279
1369
 
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.
1370
+ Pass your own CSS class to replace the built-in animation: `rowAnimationClassName` for rows that arrive and `rowExitAnimationClassName` for rows that leave. The class should set `animation`; an exit class should end in the hidden state and keep it (`forwards`). Every row also gets the CSS variable `--rg-i` (its position among the rows that appear or leave together, 0 to 24), so a delay of `calc(var(--rg-i) * 40ms)` makes rows come one after another.
1281
1371
 
1282
1372
  ```css
1283
1373
  @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); }
1374
+ @keyframes fade-right { to { opacity: 0; transform: translateX(24px); } }
1375
+ .row-fade-left { animation: fade-left 0.45s ease-out both; animation-delay: calc(var(--rg-i, 0) * 45ms); }
1376
+ .row-fade-right { animation: fade-right 0.3s ease-in forwards; }
1285
1377
  ```
1286
1378
 
1287
1379
  ```tsx
1288
- <DataGrid rowAnimationClassName="row-fade-left" ... />
1380
+ <DataGrid rowAnimationClassName="row-fade-left" rowExitAnimationClassName="row-fade-right" ... />
1289
1381
  ```
1290
1382
 
1291
- It can also be a function of the row, for example to animate new rows differently:
1383
+ Both can be functions of the row, for example to flash new rows:
1292
1384
 
1293
1385
  ```tsx
1294
1386
  <DataGrid rowAnimationClassName={(row) => (row.isNew ? "row-flash" : "row-fade-left")} ... />
@@ -1296,9 +1388,41 @@ It can also be a function of the row, for example to animate new rows differentl
1296
1388
 
1297
1389
  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
1390
 
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) { ... }`.
1391
+ - A custom class replaces the built-in animation of its direction only: you can customise just the entrance and keep the built-in exit, or the reverse.
1392
+ - A leaving row is removed when its animation ends (or after 2.5 seconds if your class has none).
1393
+ - `animateRows={false}` turns every row animation off, custom or not.
1394
+ - Reduced motion: the built-in animations stop for people who ask for it. Your own class is yours to handle: wrap it in `@media (prefers-reduced-motion: no-preference) { ... }`.
1395
+
1396
+ #### What does `classNames={{ cell: "border border-teal-400 py-2" }}` do?
1397
+
1398
+ `classNames` adds your own CSS classes to named parts of the grid. The key is the part (a **slot**), and the value is the class string. `cell` is the slot for **every body cell** (`<td>`) in every column. So this:
1399
+
1400
+ ```tsx
1401
+ <DataGrid classNames={{ cell: "border border-teal-400 py-2" }} ... />
1402
+ ```
1403
+
1404
+ adds those three Tailwind classes to each body cell:
1405
+
1406
+ | Class | Effect |
1407
+ | --- | --- |
1408
+ | `border` | A 1px border on all four sides of every cell, drawn on top of the grid's normal line under each row (`border-collapse` merges neighbouring borders into a grid of lines) |
1409
+ | `border-teal-400` | Makes that border teal |
1410
+ | `py-2` | Vertical padding of 0.5rem (8px) top and bottom, replacing the default 9px. The horizontal padding (12px) is unchanged |
1411
+
1412
+ It works with no `!important` because the library's own CSS has zero specificity, so any class you add wins. The header cells are not affected (use `classNames.headerCell`), and neither is a single column (use that column's `cellClassName`, see below). The inline-style twin is `styles`: `styles={{ cell: { border: "1px solid #2dd4bf", paddingBlock: 8 } }}`.
1413
+
1414
+ Other slots work the same way: `root`, `toolbar`, `search`, `table`, `thead`, `headerRow`, `headerCell`, `tbody`, `row`, `cell`, `editor`, `empty`, `footer`, and so on (the full list is in the slot table below).
1415
+
1416
+ **Where each kind of class goes**
1417
+
1418
+ | You want to style | Use |
1419
+ | --- | --- |
1420
+ | Every body cell | `classNames.cell` / `styles.cell` |
1421
+ | Every header cell | `classNames.headerCell` / `styles.headerCell` |
1422
+ | Whole rows, or some rows | grid `rowClassName` / `rowStyle` (a string or a function of the row) |
1423
+ | One column (header and cells) | column `className` / `style` |
1424
+ | One column's body cells | column `cellClassName` / `cellStyle` (a string or a function of the cell) |
1425
+ | One column's header | column `headerClassName` / `headerStyle` |
1302
1426
 
1303
1427
  #### Width
1304
1428
 
@@ -1439,7 +1563,7 @@ const [current, setCurrent] = useState<Person | null>(null);
1439
1563
  {current && <PersonDetails person={current} />}
1440
1564
  ```
1441
1565
 
1442
- **From outside the grid**, with `apiRef` (see [6.19](#619-csv-export-and-the-api)):
1566
+ **From outside the grid**, with `apiRef` (see [6.19](#619-export-and-the-api)):
1443
1567
 
1444
1568
  ```tsx
1445
1569
  const api = useRef<DataGridApi<Person> | null>(null);
@@ -1636,7 +1760,7 @@ api.current?.refresh();
1636
1760
 
1637
1761
  **Server mode and the other features.**
1638
1762
 
1639
- - Selection, [CSV export of selected rows](#619-csv-export-and-the-api) and keyboard navigation work on the loaded page.
1763
+ - Selection, [CSV export of selected rows](#619-export-and-the-api) and keyboard navigation work on the loaded page.
1640
1764
  - With `scope: "filtered"` the CSV contains the rows currently loaded (the current page), because the grid does not have the rest.
1641
1765
  - `defaultSort` is sent in the first query. `multiSort` lets users send several sort columns.
1642
1766
  - `onQueryChange` is the only place the grid asks for data. Without `serverSide`, the grid never calls it.
@@ -1790,7 +1914,7 @@ With `virtualize`, moving past the visible window scrolls the target row into vi
1790
1914
 
1791
1915
  Without `keyboardNavigation`, cells are reachable with Tab (editable cells) or the mouse, as before.
1792
1916
 
1793
- ### 6.19 CSV export and the API
1917
+ ### 6.19 Export and the API
1794
1918
 
1795
1919
  **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
1920
 
@@ -1801,12 +1925,146 @@ Without `keyboardNavigation`, cells are reachable with Tab (editable cells) or t
1801
1925
  <DataGrid exportable={{ scope: "selected" }} rowSelection ... /> // only the selected rows
1802
1926
  ```
1803
1927
 
1928
+ **More formats: Excel, JSON and PDF.** List the formats you want and the button becomes an "Export" menu:
1929
+
1930
+ ```tsx
1931
+ <DataGrid
1932
+ rowSelection
1933
+ pagination
1934
+ exportable={{ filename: "employees", formats: ["csv", "xlsx", "json", "pdf"] }}
1935
+ columns={columns} rows={rows} getRowId={(r) => r.id}
1936
+ />
1937
+ ```
1938
+
1939
+ The menu has two parts. **Rows to export** lets the user pick *Visible rows* (what is on screen: the current page), *All matching rows* (every page; only offered with `pagination`) or *Selected rows* (only offered with `rowSelection`, and disabled while nothing is selected). Each choice shows how many rows it covers. **Download as** has one button per format. The menu remembers the last choice. With a single format (the default `["csv"]`) you get the plain "Export CSV" / "Export Excel" / "Export JSON" / "Export PDF" button instead, using `scope` from `exportable`.
1940
+
1941
+ | Format | What you get | Needs |
1942
+ | --- | --- | --- |
1943
+ | `"csv"` | Text of the displayed values, opens in any spreadsheet | nothing |
1944
+ | `"xlsx"` | A real Excel workbook: bold coloured header row that stays in place while you scroll, auto-filter, sized columns, real numbers, real dates and real booleans | nothing (written by the library itself) |
1945
+ | `"json"` | An array of objects, one per row, with real numbers and booleans and ISO dates (`"2026-11-05"`) | nothing |
1946
+ | `"pdf"` | A paged table with an optional title, header, footer and "Page 1 of 3" | `jspdf` and `jspdf-autotable` (and `html2canvas` for a header / footer element) |
1947
+
1948
+ The same exports are available from code, for your own buttons:
1949
+
1950
+ ```tsx
1951
+ const api = useRef<DataGridApi<Employee> | null>(null);
1952
+ <DataGrid apiRef={api} rowSelection ... />
1953
+
1954
+ api.current?.exportXlsx({ scope: "selected", filename: "picked", sheetName: "Picked" });
1955
+ api.current?.exportJson({ scope: "visible", jsonKeys: "header", jsonIndent: 0 });
1956
+ await api.current?.exportPdf({ scope: "filtered", pdf: { title: "All employees" } });
1957
+ await api.current?.exportAs("csv", { scope: "page" }); // any format by name
1958
+
1959
+ const json: string = api.current!.getJson({ scope: "filtered" }); // build without downloading
1960
+ const blob: Blob = await api.current!.getXlsx(); // e.g. upload it to your server
1961
+ const pdf: Blob = await api.current!.getPdf();
1962
+ const csv: string = api.current!.getCsv();
1963
+ ```
1964
+
1965
+ `exportXlsx` and `exportPdf` return promises. They **reject** when something goes wrong (a missing library, for instance), so `try / catch` them in your own code. The toolbar menu catches errors for you and sends them to `onExportError`:
1966
+
1967
+ ```tsx
1968
+ <DataGrid
1969
+ onExportError={(error, format) => toast.error(`${format} export failed: ${error.message}`)}
1970
+ exportable={{ formats: ["csv", "pdf"] }}
1971
+ />
1972
+ ```
1973
+
1974
+ **Which rows.** `scope` accepts `"filtered"` (default), `"visible"` or `"page"` (the same: the rows on screen) and `"selected"`. A page export continues the row numbers of a `sequence` column (page 2 starts at 11) unless the column has `sequenceRestartEachPage`.
1975
+
1976
+ **What each format writes.** CSV and PDF write the displayed text (`valueFormatter` applied, `Yes` / `No` for booleans, dates as `YYYY-MM-DD`). Excel and JSON write real values: a number column stays a number (`52000`, not `"$52,000"`), a boolean column stays `true` / `false`, a date column is a real Excel date, a column with a `valueFormatter` that changes text (for example codes shown as names) writes the shown text. A column's `exportValue(value, row)` overrides all of this in every format.
1977
+
1978
+ #### PDF header and footer
1979
+
1980
+ `pdf.header` and `pdf.footer` accept **a string or a React element**, and appear on every page.
1981
+
1982
+ ```tsx
1983
+ // A string. {page}, {pages} and {date} are replaced on each page.
1984
+ <DataGrid
1985
+ exportable={{
1986
+ formats: ["pdf"],
1987
+ pdf: {
1988
+ title: "Employee list",
1989
+ header: "Acme Corporation, People report ({date})",
1990
+ footer: "Confidential, page {page} of {pages}",
1991
+ },
1992
+ }}
1993
+ />
1994
+ ```
1995
+
1996
+ ```tsx
1997
+ // A component: a logo, colours, borders, anything you can style. Images and inline styles work.
1998
+ import logo from "./logo.png";
1999
+
2000
+ <DataGrid
2001
+ exportable={{
2002
+ formats: ["pdf"],
2003
+ pdf: {
2004
+ header: (
2005
+ <div style={{ display: "flex", alignItems: "center", gap: 12, borderBottom: "3px solid #4f46e5", paddingBottom: 6 }}>
2006
+ <img src={logo} alt="" width={40} height={40} style={{ borderRadius: 8 }} />
2007
+ <div>
2008
+ <div style={{ fontSize: 20, fontWeight: 700, color: "#312e81" }}>Acme Corporation</div>
2009
+ <div style={{ fontSize: 12, color: "#6b7280" }}>People &amp; Culture report</div>
2010
+ </div>
2011
+ </div>
2012
+ ),
2013
+ footer: <div style={{ textAlign: "center", fontSize: 11, color: "#6b7280" }}>Confidential. Do not share.</div>,
2014
+ },
2015
+ }}
2016
+ />
2017
+ ```
2018
+
2019
+ An element is drawn off-screen, turned into a picture with `html2canvas`, and placed on each page, shrunk to fit the page width and `maxImageHeight` (default 90 pt) with its proportions kept. A few things to know about elements:
2020
+
2021
+ - They fill the page width, so lay them out with `display: flex` and `justify-content` inside (the `headerAlign` / `footerAlign` options move strings, not elements).
2022
+ - They are drawn once, so they cannot show the page number. Use a string footer for "page 2 of 5", or both: an element header and a string footer.
2023
+ - Images must be loadable from the page: your own assets, data URLs, or servers that allow cross-origin use (CORS). Otherwise the browser blocks the picture.
2024
+ - Inline styles and plain CSS classes work. Very new CSS features may not be supported by `html2canvas`.
2025
+
2026
+ Other `pdf` options:
2027
+
2028
+ | Option | Default | Meaning |
2029
+ | --- | --- | --- |
2030
+ | `title` | | A heading above the table on the first page |
2031
+ | `header`, `footer` | | A string or element, on every page |
2032
+ | `headerAlign`, `footerAlign` | `"left"`, `"center"` | Where a **string** goes: `"left"`, `"center"`, `"right"` |
2033
+ | `orientation` | `"auto"` | `"portrait"`, `"landscape"`, or `"auto"` (landscape when there are more than 6 columns) |
2034
+ | `pageSize` | `"a4"` | `"a3"`, `"a4"`, `"a5"`, `"letter"`, `"legal"` |
2035
+ | `margin` | `36` | Points around the page (72 points = 1 inch) |
2036
+ | `fontSize` | `9` | Table text size in points |
2037
+ | `tableTheme` | `"striped"` | `"striped"`, `"grid"` or `"plain"` |
2038
+ | `headColor`, `headTextColor` | `"#4f46e5"`, `"#ffffff"` | Colours of the table's header row, as hex |
2039
+ | `pageNumbers` | `true` | "Page 1 of 3" in the bottom right |
2040
+ | `maxImageHeight` | `90` | Tallest header / footer picture, in points |
2041
+ | `fontName`, `beforeRender` | | Use another font (see below) |
2042
+
2043
+ jsPDF's built-in fonts cover Western European letters. For other alphabets (Arabic, Chinese, ...) register a font in `beforeRender` and name it in `fontName`:
2044
+
2045
+ ```tsx
2046
+ pdf: {
2047
+ fontName: "NotoSans",
2048
+ beforeRender: (doc) => {
2049
+ doc.addFileToVFS("NotoSans.ttf", notoSansBase64); // your font file as base64
2050
+ doc.addFont("NotoSans.ttf", "NotoSans", "normal");
2051
+ },
2052
+ }
2053
+ ```
2054
+
2055
+ #### Export options
2056
+
1804
2057
  | `ExportOptions` | Default | Meaning |
1805
2058
  | --- | --- | --- |
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 |
2059
+ | `filename` | `"export"` | File name without the extension |
2060
+ | `scope` | `"filtered"` | `"filtered"` all rows matching search and filters, `"visible"` / `"page"` the rows on screen, `"selected"` the selected rows |
1808
2061
  | `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 |
2062
+ | `sanitize` | `true` | CSV only: put an apostrophe in front of text starting with `=`, `+`, `-` or `@`, so Excel does not run it as a formula |
2063
+ | `formats` | `["csv"]` | The formats the toolbar offers: any of `"csv"`, `"xlsx"`, `"json"`, `"pdf"` |
2064
+ | `sheetName` | `"Sheet1"` | Excel only: the worksheet's name |
2065
+ | `jsonKeys` | `"field"` | JSON only: use each column's `"field"` or its `"header"` text as the key |
2066
+ | `jsonIndent` | `2` | JSON only: spaces of indentation (`0` for one line) |
2067
+ | `pdf` | | PDF only: see above |
1810
2068
 
1811
2069
  **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
2070
 
@@ -2063,7 +2321,132 @@ const markDone = () => setRows((rs) => rs.map((r) => (ids.includes(r.id) ? { ...
2063
2321
  <DataGrid serverSide pagination searchable multiSort theme="aurora" columns={columns} rows={rows} rowCount={total} onQueryChange={load} />
2064
2322
  ```
2065
2323
 
2066
- ### 6.24 Known limitations
2324
+ ### 6.24 Filter comparisons
2325
+
2326
+ A text filter normally means "contains". Switch on `filterOperators` on a column and its popup gets a drop-down on top, so the user chooses **how** to compare:
2327
+
2328
+ | Choice | A row passes when its text... |
2329
+ | --- | --- |
2330
+ | Contains | has the typed text anywhere |
2331
+ | Does not contain | does not have it anywhere |
2332
+ | Equals | is exactly the typed text |
2333
+ | Not equal | is anything else |
2334
+ | Starts with | begins with it |
2335
+ | Does not start with | does not begin with it |
2336
+ | Ends with | ends with it |
2337
+ | Does not end with | does not end with it |
2338
+
2339
+ All comparisons ignore upper / lower case. Changing the choice filters straight away; typing waits for `filterDebounce` as usual.
2340
+
2341
+ ```tsx
2342
+ const columns: ColumnDef<User>[] = [
2343
+ { field: "name", filterable: true, filterOperators: true }, // all eight
2344
+ { field: "email", filterable: true,
2345
+ filterOperators: ["contains", "startsWith", "endsWith", "equals"], // only these four
2346
+ filterOperator: "startsWith" }, // selected first
2347
+ { field: "department", filterable: true }, // no drop-down: always "contains"
2348
+ ];
2349
+ ```
2350
+
2351
+ - It is **off by default**. A column without `filterOperators` shows just the box and always means "contains" (or "equals" for `filterType: "exact"`).
2352
+ - It applies to text columns (`filterType` `"text"` or `"exact"`). It does nothing for number, date or select filters, nor when you supply your own `filterRenderer`.
2353
+ - Pass `true` for all eight, or a list to offer fewer. The list keeps the order shown above. `filterOperator` sets the first choice (default `"contains"`).
2354
+ - Keyboard: focus the drop-down, then Up / Down open it and move, Enter or Space choose, Esc closes just the list.
2355
+
2356
+ **The filter value.** With `filterOperators` on, the column's filter is an object, not a string: `{ operator: "startsWith", value: "ann" }` (type `TextFilter`). You see it in `filters` / `onFiltersChange` (controlled mode) and in the `serverSide` query, where your API applies it:
2357
+
2358
+ ```tsx
2359
+ onQueryChange={async (query) => {
2360
+ const nameFilter = query.filters.name; // string, or { operator, value } when filterOperators is on
2361
+ const { operator, value } = typeof nameFilter === "string" ? { operator: "contains", value: nameFilter } : nameFilter;
2362
+ // ... translate to your database: operator "startsWith" -> LIKE 'value%'
2363
+ }}
2364
+ ```
2365
+
2366
+ Controlled filters work the same way, and a plain string is still accepted (it is read as "contains"):
2367
+
2368
+ ```tsx
2369
+ <DataGrid filters={{ name: { operator: "notContains", value: "test" } }} onFiltersChange={setFilters} ... />
2370
+ ```
2371
+
2372
+ ### 6.25 Sort indicator
2373
+
2374
+ The arrow in sortable headers can be changed. The default is the small text arrow (▲ ▼ ↕). Two other choices:
2375
+
2376
+ ```tsx
2377
+ <DataGrid sortIndicator="arrow" ... /> // a smooth arrow that turns around
2378
+ <DataGrid sortIndicator={MyIndicator} ... /> // your own component
2379
+ ```
2380
+
2381
+ **`"arrow"`** is an SVG arrow. Ascending points up, descending points down, and "not sorted" is a dim up arrow. When the sort changes it **rotates smoothly** (a CSS transition of `--rg-sort-turn`, default `0.28s`, off for reduced motion). It always keeps turning the same way (up, down, back up), so each click is one clean half-turn. With several sorted columns it shows each column's priority number.
2382
+
2383
+ **Your own component** receives `{ column, direction, priority }`: `direction` is `"asc"`, `"desc"` or `null`, and `priority` is 1, 2, ... when several columns are sorted. The grid keeps the component mounted in the same place, so a CSS `transition` on it animates between states. Define it outside any component:
2384
+
2385
+ ```tsx
2386
+ import { SortArrow, type SortIndicatorProps } from "react-gridix";
2387
+
2388
+ // Reuse the rotating arrow with your own size and colour
2389
+ function BigArrow({ direction }: SortIndicatorProps<User>) {
2390
+ return <SortArrow direction={direction} size={18} style={{ color: "tomato" }} />;
2391
+ }
2392
+
2393
+ // Or draw anything, turned with CSS
2394
+ function Chevron({ direction }: SortIndicatorProps<User>) {
2395
+ return (
2396
+ <span style={{ display: "inline-block", transition: "transform .3s", transform: direction === "desc" ? "rotate(180deg)" : "none", opacity: direction ? 1 : 0.3 }}>
2397
+ ⌃
2398
+ </span>
2399
+ );
2400
+ }
2401
+
2402
+ <DataGrid sortIndicator={BigArrow} columns={columns} rows={rows} />
2403
+ ```
2404
+
2405
+ A single column can use its own: `{ field: "name", sortable: true, sortIndicator: "arrow" }`. The wrapper `<span>` has the `sortIndicator` slot (`classNames={{ sortIndicator: "..." }}`) and a `data-direction` attribute (`asc`, `desc` or `none`) you can style.
2406
+
2407
+ ### 6.26 Custom empty state
2408
+
2409
+ When there are no rows to show, the grid draws `emptyMessage` (default "No rows to show"). For more than a sentence, pass a component with `emptyComponent`. It replaces the message and receives:
2410
+
2411
+ | Prop | Meaning |
2412
+ | --- | --- |
2413
+ | `reason` | `"no-data"` when the grid was given no rows, `"no-results"` when the search or filters hide them all |
2414
+ | `search` | The search text (empty when none) |
2415
+ | `filters` | The active filters, by field |
2416
+ | `columns` | The visible columns |
2417
+ | `clearAll()` | Clears the search and all filters, which brings the rows back |
2418
+
2419
+ ```tsx
2420
+ import type { EmptyStateProps } from "react-gridix";
2421
+
2422
+ function NothingHere({ reason, clearAll, search }: EmptyStateProps<User>) {
2423
+ if (reason === "no-data") {
2424
+ return (
2425
+ <div style={{ textAlign: "center" }}>
2426
+ <img src="/empty-box.svg" alt="" width={96} />
2427
+ <h3>No users yet</h3>
2428
+ <button onClick={openAddUserDialog}>Add the first user</button>
2429
+ </div>
2430
+ );
2431
+ }
2432
+ return (
2433
+ <div style={{ textAlign: "center" }}>
2434
+ <h3>Nothing matches{search ? ` "${search}"` : ""}</h3>
2435
+ <button onClick={clearAll}>Clear search and filters</button>
2436
+ </div>
2437
+ );
2438
+ }
2439
+
2440
+ <DataGrid emptyComponent={NothingHere} columns={columns} rows={users} />
2441
+ ```
2442
+
2443
+ - It is drawn inside one full-width table cell (class `rg-grid__empty rg-grid__empty--custom`, slot `empty`), so `classNames={{ empty: "py-10" }}` still works.
2444
+ - Define the component outside render so it keeps its identity (and its state).
2445
+ - While loading, the skeleton is shown, not this. After a failed load, the error message and Retry button are shown.
2446
+ - When rows are removed with an exit animation, the component appears once the last row has left.
2447
+ - Keep using `emptyMessage` for a plain sentence. `emptyComponent` wins when both are set.
2448
+
2449
+ ### 6.27 Known limitations
2067
2450
 
2068
2451
  Not included:
2069
2452
 
@@ -2086,6 +2469,9 @@ Behaviours to be aware of:
2086
2469
  - Pinned columns only stick when the table is wider than its container.
2087
2470
  - In `serverSide` mode the grid cannot export or select rows it has never loaded.
2088
2471
  - The date calendar starts weeks on Sunday and has no week numbers or time of day.
2472
+ - Excel export writes one worksheet. It does not carry the grid's colours or fonts, only a styled header row.
2473
+ - PDF text uses jsPDF's built-in Western European fonts unless you register another one (`pdf.fontName`). A PDF header / footer element is a picture: its text cannot be selected.
2474
+ - Rows that leave play their exit animation as a copy in place; the table closes the gap when the animation ends instead of sliding the rows below up.
2089
2475
 
2090
2476
  ---
2091
2477
 
@@ -2108,6 +2494,7 @@ The repository contains a playground with one page per feature:
2108
2494
  | Large data (`#large`) | 20,000 virtualized rows with search, sorting and a theme |
2109
2495
  | 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
2496
  | Themes (`#themes`) | A switcher for none and all 8 themes |
2497
+ | Animations and export (`#features`) | Pick the "in" and "out" animation from 12, add and remove rows (top, end, selected, all) and watch the other rows stay put; the rotating sort arrow, the default arrow and a custom indicator component; the Export menu (CSV, Excel, JSON, PDF) with a visible / selected / all choice and a PDF header and footer given as a string or as a styled component with a logo; filter comparison drop-downs (all eight, a short list, and none); a custom empty-state component |
2111
2498
 
2112
2499
  Run it:
2113
2500
 
@@ -2122,6 +2509,7 @@ The Styling demo loads Tailwind from its CDN, so it needs an internet connection
2122
2509
 
2123
2510
  ---
2124
2511
 
2512
+
2125
2513
  ## 8. Troubleshooting
2126
2514
 
2127
2515
  | Problem | Cause and fix |
@@ -2136,6 +2524,12 @@ The Styling demo loads Tailwind from its CDN, so it needs an internet connection
2136
2524
  | 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 |
2137
2525
  | A sequence column shows nothing or a type error | Give it `type: "sequence"` and a `field` that starts with `#`, for example `{ field: "#", type: "sequence" }` |
2138
2526
  | 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 |
2527
+ | PDF export fails with `needs the package "jspdf"` | Install the optional packages: `npm install jspdf jspdf-autotable` (and `html2canvas` for a header / footer element). Handle the error with `onExportError` |
2528
+ | The PDF header / footer element has no image | The image must be loadable by the browser without being blocked: a bundled asset, a data URL, or a server that sends CORS headers |
2529
+ | The comparison drop-down does not show in the filter popup | Add `filterOperators: true` (or a list) to that column. It applies to text columns only, and not with a custom `filterRenderer` |
2530
+ | The wrong row animates out when I delete one | Give the grid `getRowId={(row) => row.id}`. Without it rows are identified by position |
2531
+ | Removed rows do not animate | `animateRows` is off, `virtualize` is on, the exit animation is `"none"`, or the system asks for reduced motion. Rows are also replaced at once when the page changes |
2532
+ | "Selected rows" is greyed out in the export menu | Nothing is selected yet, or `rowSelection` is off |
2139
2533
  | The number filter popup asks for Min and Max, but I wanted text | Number columns default to a range filter. Set `filterType: "text"` |
2140
2534
  | 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 |
2141
2535
  | Select filter lists the wrong labels | Pass `filterOptions` with `{ value, label }` pairs. `value` must equal what is stored in the row |
@@ -2163,28 +2557,4 @@ The Styling demo loads Tailwind from its CDN, so it needs an internet connection
2163
2557
  | 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 |
2164
2558
  | 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` |
2165
2559
 
2166
- ---
2167
-
2168
- ### Changed defaults
2169
-
2170
- Three defaults changed so that every feature is opt-in.
2171
-
2172
- | Before | Now | What to do |
2173
- | --- | --- | --- |
2174
- | 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 |
2175
- | `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` |
2176
- | `searchable` default `true` | Default `false` | Add `searchable` to grids that should show the search box |
2177
- | `pagination` default `true` | Default `false`. With it off there is no footer and no row count | Add `pagination` to grids that should page |
2178
- | 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 |
2179
- | `FilterRendererProps` | Unchanged (`value: string`) | |
2180
-
2181
- A quick way to get the old behaviour everywhere:
2182
-
2183
- ```tsx
2184
- // before: <DataGrid columns={columns} rows={rows} />
2185
- const withSorting = (cols: ColumnDef<Row>[]) => cols.map((c) => ({ sortable: true, ...c }));
2186
-
2187
- <DataGrid columns={withSorting(columns)} rows={rows} searchable pagination />
2188
- ```
2189
-
2190
- Everything else in this document is new and optional.
2560
+ ---