@jielga/tmdatagrid 2.1.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +38 -2
  3. package/dist/index.js +1329 -1246
  4. package/dist/index.js.map +1 -1
  5. package/docs/cell-selection.md +1 -1
  6. package/docs/column-layout.md +13 -8
  7. package/docs/columns.md +57 -2
  8. package/docs/export.md +2 -2
  9. package/docs/filtering.md +105 -15
  10. package/docs/getting-started.md +40 -0
  11. package/docs/grouping.md +9 -6
  12. package/docs/loading-and-empty.md +29 -9
  13. package/docs/localization.md +130 -3
  14. package/docs/menu.md +1 -1
  15. package/docs/pagination.md +26 -7
  16. package/docs/row-details.md +36 -4
  17. package/docs/row-interaction.md +1 -1
  18. package/docs/row-selection.md +6 -7
  19. package/docs/scrolling.md +1 -1
  20. package/docs/server-side.md +3 -1
  21. package/docs/testing.md +18 -0
  22. package/docs/use-tm-data-grid.md +69 -34
  23. package/package.json +2 -2
  24. package/skills/appearance/SKILL.md +6 -4
  25. package/skills/appearance/references/labels.md +126 -0
  26. package/skills/cell-selection/SKILL.md +2 -2
  27. package/skills/columns/SKILL.md +42 -41
  28. package/skills/columns/references/columns-api.md +15 -1
  29. package/skills/data/SKILL.md +77 -50
  30. package/skills/editing/SKILL.md +1 -1
  31. package/skills/filtering/SKILL.md +71 -49
  32. package/skills/filtering/references/custom-filters.md +118 -0
  33. package/skills/getting-started/SKILL.md +37 -2
  34. package/skills/grouping/SKILL.md +16 -9
  35. package/skills/migrating-to-2/SKILL.md +1 -1
  36. package/skills/options/SKILL.md +131 -51
  37. package/skills/rows/SKILL.md +53 -31
  38. package/skills/server-side/SKILL.md +1 -1
  39. package/skills/testing/SKILL.md +12 -1
  40. package/skills/testing-components/SKILL.md +1 -1
  41. package/skills/testing-editing/SKILL.md +1 -1
  42. package/src/components/TMDataGridTable.tsx +241 -41
  43. package/src/core/autosize.ts +19 -7
  44. package/src/core/cellRange.ts +11 -2
  45. package/src/core/colSpan.ts +78 -0
  46. package/src/core/columnUtils.ts +7 -0
  47. package/src/core/filterOperators.ts +16 -1
  48. package/src/index.ts +4 -0
  49. package/src/useTMDataGrid.tsx +38 -0
@@ -180,7 +180,7 @@ Unlike the Export cells item, `resolveRangeBounds` does not fall back to the foc
180
180
  | `cellSelection` | Option | `"none" \| "single" \| "range"` | `"none"` | Turns the cell cursor, and the rectangle, on. |
181
181
  | `TMDataGridCellSelectionMode` | Type | – | – | The type of `cellSelection`. |
182
182
  | `onFocusedCellChange` | Callback | `(cell \| null) => void` | – | Follows the cursor. |
183
- | `exportOptions` | Option | `TMDataGridExportOptions` | `DEFAULT_EXPORT_OPTIONS` | Format, file name and header row of the Export cells item. See [Export](/docs/export). |
183
+ | `exportOptions` | Option | `TMDataGridExportOptions` | `csvExcelFormat()`, `"export"`, headers on, `"visible"` columns | Format, file name and header row of the Export cells item. See [Export](/docs/export). |
184
184
  | `ui.state.focusedCell` | UI state | `{ rowId, columnId } \| null` | `null` | The cursor. |
185
185
  | `ui.state.cellRange` | UI state | `{ anchor, focus } \| null` | `null` | The rectangle's two corners. |
186
186
  | `ui.actions.setFocusedCell` · `setCellRange` | UI actions | – | – | Move either from your own code. |
@@ -14,7 +14,11 @@ hint: Drag a header to reorder · pin or hide from a column menu · drag a divid
14
14
  ## Hiding
15
15
 
16
16
  **Hide column** in any column menu, and the column chooser for the whole list at once: a search field, one checkbox per column, show/hide all, and **Reset layout**.
17
- The chooser is `TMDataGrid.Menu.Columns` in the [grid menu](/docs/menu), **Manage columns** in every column menu (a submenu of the same items), and `TMDataGrid.ColumnsPanel` as plain controls for a host that is not a menu.
17
+ The chooser comes in three forms:
18
+
19
+ - `TMDataGrid.Menu.Columns` in the [grid menu](/docs/menu).
20
+ - **Manage columns**, a submenu of every column menu with the same items.
21
+ - `TMDataGrid.ColumnsPanel`, plain controls for a host that is not a menu.
18
22
 
19
23
  `enableHiding: false` removes all of it; on a column it removes that column's
20
24
  menu item and leaves it out of the panel, which lists what can be hidden and
@@ -42,8 +46,8 @@ computed from an `fr` value. The grid writes the column's rendered width into
42
46
  To start pinned, set `initialState.columnPinning`. The slice is TanStack's
43
47
  full `ColumnPinningState`, so a partial does not compile - name both sides.
44
48
  A column pinned at mount has no rendered width to store, so it takes its
45
- `size` - TanStack's default of `150` where none is set - and `minSize` does
46
- not apply. Give such a column an explicit `size`:
49
+ `size`, clamped between `minSize` and `maxSize`; with none of them set that is
50
+ TanStack's default of `150`. Give such a column an explicit `size`:
47
51
 
48
52
  ```tsx
49
53
  columnHelper.accessor("registration", { header: "Registration", size: 220 });
@@ -114,10 +118,11 @@ Columns are fluid by default. Each track is `minmax(minSize, flex fr)`.
114
118
 
115
119
  | Column option | Effect |
116
120
  | --- | --- |
117
- | `minSize` | Minimum width, and the column's contribution to the grid minimum width. Defaults to `80`. |
121
+ | `minSize` | Minimum width, and the column's contribution to the grid minimum width. Also the floor of a resize and of a pinned width. Defaults to `80`, through `defaultColumn.minSize`. |
122
+ | `maxSize` | Ceiling of a resize, an autosize and a pinned width. Does not cap a fluid track. No default. |
118
123
  | `meta.flex` | Share of the remaining width. Defaults to `1`. |
119
124
  | `minSize === maxSize` | Fixed width. The column is never fluid. |
120
- | `size` | Applied once the column becomes fixed by resizing or pinning. |
125
+ | `size` | Width of a column pinned at mount, clamped by `minSize` and `maxSize`. Ignored on a fluid column; a resize or an interactive pin stores the rendered width instead. Defaults to `150`. |
121
126
 
122
127
  Drag a divider to resize. A column switches to a fixed pixel width the moment
123
128
  it is resized or pinned, and the width is stored in `columnSizing`. The drag
@@ -148,8 +153,8 @@ the column's cells.
148
153
 
149
154
  ## Reset the layout
150
155
 
151
- `resetSettings()` from the hook clears visibility, order, pinning and widths in
152
- one call. The columns panel offers it as **Reset layout**.
156
+ `resetSettings()` from the hook clears visibility, order, pinning, widths and
157
+ grouping in one call. The columns panel offers it as **Reset layout**.
153
158
 
154
159
  ```tsx
155
160
  const { resetSettings } = useTMDataGrid({ data, columns });
@@ -169,7 +174,7 @@ const { resetSettings } = useTMDataGrid({ data, columns });
169
174
  | `meta.flex` | Column meta | `number` | `1` | Share of the remaining width. |
170
175
  | `meta.autoSize` | Column meta | `boolean` | `false` | Autosize once, on the render the column's first cells appear in. |
171
176
  | `minSize` / `maxSize` / `size` | Column options | `number` | `80` / – / – | Width bounds, and the fixed width once one applies. |
172
- | `resetSettings` | Hook return | `() => void` | – | Clears visibility, order, pinning and widths. |
177
+ | `resetSettings` | Hook return | `() => void` | – | Clears visibility, order, pinning, widths and grouping. |
173
178
  | `moveColumn` | Export | `({ table, columnId, targetId, side }) => void` | – | Moves a column beside another. |
174
179
  | `moveColumnByStep` | Export | `({ table, columnId, direction }) => void` | – | Moves it one place. |
175
180
  | `MoveColumnArgs` · `ColumnStepArgs` | Types | – | – | What `moveColumn` takes, and what `moveColumnByStep` and `getStepTargetColumn` take. |
package/docs/columns.md CHANGED
@@ -88,6 +88,7 @@ Its type is `TMDataGridColumnMeta`.
88
88
  | `options` | `TMDataGridOptionsSource` | - | The choices of a `select` / `multiSelect` column. See [Options](#options). |
89
89
  | `flex` | `number` | `1` | Share of the remaining width. |
90
90
  | `align` | `"left" \| "right" \| "center"` | `"left"` | Alignment, applied to both header and cells. |
91
+ | `colSpan` | `number \| ({ row, table }) => number` | `1` | Columns the cell covers on a row, counting itself. See [Spanning columns](#spanning-columns). |
91
92
  | `autoSize` | `boolean` | `false` | Fit the column to its content once, on the render its first cells appear in. |
92
93
  | `enableOrdering` | `boolean` | `true` | `false` keeps the column where it is. |
93
94
  | `filter` | `TMDataGridColumnFilterOptions` | - | How this column filters. See [meta.filter](#metafilter). |
@@ -185,6 +186,57 @@ A select column that declares no options still filters: the panel falls back to
185
186
  the faceted values. Mantine's dropdowns are not virtualized, so use the function
186
187
  form for very large sets.
187
188
 
189
+ ## Spanning columns
190
+
191
+ `meta.colSpan` makes a column's cell cover the columns to its right.
192
+ A cell spanning `n` is one cell `n` columns wide, with one background and one
193
+ border, and the `n - 1` cells it covers do not render on that row.
194
+ A function decides per row:
195
+
196
+ ```tsx
197
+ columnHelper.accessor("name", {
198
+ header: "Name",
199
+ meta: {
200
+ colSpan: ({ row }) => (row.getIsExpanded() ? Infinity : 1),
201
+ },
202
+ });
203
+ ```
204
+
205
+ ```demo
206
+ file: columns/SpanningCells.tsx
207
+ hint: Open a row with its chevron - its name takes the rest of the row, and the panel opens below.
208
+ ```
209
+
210
+ - The span is counted over the displayed columns. Hiding or moving a column
211
+ changes what a span covers, so `Infinity` - "to the last of my columns" - is
212
+ usually safer than a count.
213
+ - A span stops at the last of your columns. It never covers the checkbox,
214
+ tree, details or edit lane.
215
+ - A span stops at the edge of its pinned block.
216
+ - A column covered by a span ignores its own `colSpan` on that row.
217
+ - Group rows ignore `colSpan`, and so does a row open in
218
+ [row edit mode](/docs/editing), where every column holds an editor.
219
+ - Sorting, filtering, copy and export still see every column. Autosize leaves
220
+ a spanning cell out of its column's measurement, and a column whose every
221
+ mounted cell spans fits its header.
222
+ - Under [cell selection](/docs/cell-selection) the spanning cell is one cell:
223
+ the arrow keys step over it, and a move onto a column it covers lands on it.
224
+ It is addressed by its first column: a range holds it when it holds that
225
+ column, and a range over only the columns it covers leaves it out. Copy and
226
+ export take every column of the range either way. When the column the focus
227
+ is on becomes covered, the focus moves to the spanning cell.
228
+ - Under [cell editing](/docs/editing) a covered column has no cell to open,
229
+ and Tab from the spanning cell's editor moves past the columns it covers.
230
+ A row keeps its cells while an editor is open in a column a span would
231
+ cover, and the span lands when the edit ends.
232
+
233
+ A cell renderer that reads `row.getIsExpanded()`, as the demo's does, has to
234
+ subscribe to the store - see
235
+ [Opening a row from elsewhere](/docs/row-details#opening-a-row-from-elsewhere).
236
+
237
+ The cell carries `aria-colspan` and `data-col-span` with the number of columns
238
+ it covers.
239
+
188
240
  ## Header groups
189
241
 
190
242
  `columnHelper.group` nests columns under a shared header. The group is a header
@@ -231,8 +283,9 @@ Sizing, pinning, ordering and visibility are covered on
231
283
  ## The generated columns
232
284
 
233
285
  Five lanes the grid adds when a feature needs them. All are **system lanes**:
234
- fixed width, no column menu, no resize handle, never exported, never hidden and
235
- never listed in the columns panel.
286
+ pinned, with no column menu, never exported and never listed in the columns
287
+ panel. Four of them are fixed width with no resize handle; the tree lane can be
288
+ resized, and it is shown only while a grouping is active.
236
289
 
237
290
  | Id | Appears when | Where |
238
291
  | --- | --- | --- |
@@ -259,6 +312,8 @@ column can be placed in front of it.
259
312
  | `meta.options` | Column meta | array \| `"faceted"` \| `(args) => …` | – | The choices of an option column. |
260
313
  | `TMDataGridOptionsArgs` | Type | – | – | What a `meta.options` function receives: `{ table, column, row? }`. `row` is absent when the filter panel asks. |
261
314
  | `meta.align` | Column meta | `"left" \| "right" \| "center"` | `"left"` | Header and cell alignment. |
315
+ | `meta.colSpan` | Column meta | `number \| (args) => number` | `1` | Columns the cell covers on a row. See [Spanning columns](#spanning-columns). |
316
+ | `TMDataGridColSpanArgs` | Type | `{ row, table }` | – | What a `meta.colSpan` function receives. |
262
317
  | `meta.filter` | Column meta | `TMDataGridColumnFilterOptions` | – | How the column filters: `defaultOperator`, `control`. |
263
318
  | `meta.edit` | Column meta | `TMDataGridColumnEditOptions` | – | How the column edits: `enabled`, `field`, `editor`, `validate`, `mapValue`. |
264
319
  | `resolveColumnOptions` | Export | `({ table, column, row? }) => options` | – | Normalises all three `meta.options` forms. |
package/docs/export.md CHANGED
@@ -80,7 +80,7 @@ const grid = useTMDataGrid({
80
80
  | `includeHeaders` | `boolean` | `true` | Determines whether the column labels are written as the first row. |
81
81
  | `columns` | `"visible" \| "all" \| Array<string>` | `"visible"` | Which columns are written. See [What is written](#what-is-written). |
82
82
 
83
- The option is read field by field, so a literal is fine.
83
+ The option is read field by field, so an inline object literal needs no `useMemo`.
84
84
  Every export of the grid reads it: the menu items, the hook, and the export item of the cell-range menu.
85
85
 
86
86
  ## Formats
@@ -290,7 +290,7 @@ See [Testing](/docs/testing).
290
290
 
291
291
  | Name | Kind | Type | Default | What it does |
292
292
  | --- | --- | --- | --- | --- |
293
- | `exportOptions` | Option | `TMDataGridExportOptions` | `DEFAULT_EXPORT_OPTIONS` | Format, file name and header row for every export of the grid. |
293
+ | `exportOptions` | Option | `TMDataGridExportOptions` | `csvExcelFormat()`, `"export"`, headers on, `"visible"` columns | Format, file name and header row for every export of the grid. |
294
294
  | `meta.enableExport` | Column meta | `boolean` | `true` | `false` leaves the column out of every export and of Ctrl+C. |
295
295
  | `meta.exportValue` | Column meta | `({ value, row, column }) => unknown` | – | The value written in place of `row.getValue`. |
296
296
  | `TMDataGridExportValueGetter` | Type | – | – | The type of `meta.exportValue`. |
package/docs/filtering.md CHANGED
@@ -32,8 +32,8 @@ array only under `isAnyOf` / `isNoneOf` (the set the cell is tested against) and
32
32
 
33
33
  A filter with an empty value stays in state, so a panel row survives while the
34
34
  user types. (A header filter control has no row to keep alive and drops its
35
- entry instead - see [inHeader](#inheader).) It matches every row, does not set the header's filter
36
- indicator, and produces no pill. `isFilterActive(value)` tests for that
35
+ entry instead - see [inHeader](#inheader).) The built-in matcher lets every row through, and
36
+ the filter does not set the header's filter indicator and produces no pill. `isFilterActive(value)` tests for that
37
37
  state, and `activeColumnFilters` applies it across the whole slice, handing
38
38
  back the entries that narrow the grid with their values typed as
39
39
  `TMDataGridFilterValue` rather than as `unknown`. Entries in any other value
@@ -117,7 +117,7 @@ useTMDataGrid({ data, columns, filters: { surface: "sidebar", inHeader: true } }
117
117
  `surface` and `inHeader` are two separate choices, not one list: `inHeader` composes with all three surfaces.
118
118
  `{ surface: "none", inHeader: true }` is header filters and nothing else; `{ inHeader: true }` keeps the popup as well, for the multi-column work a header row has no room for.
119
119
 
120
- The option is read field by field, so a literal is fine - unlike `labels` and `persist`, it does not have to be referentially stable.
120
+ The option is read field by field, so an inline object literal needs no `useMemo` - unlike `labels` and `persist`, it does not have to be referentially stable.
121
121
 
122
122
  ### surface: "popup"
123
123
 
@@ -132,7 +132,7 @@ Closing only hides the popup; the filters stay.
132
132
  The same panel beside the rows, inside the grid frame and under the toolbar.
133
133
  It is a column of the frame rather than a layer over it: the rows give up the width instead of being covered, a click in the table does not dismiss it, and clearing the filters leaves it standing with its **Add filter** button.
134
134
  Escape closes it.
135
- It starts open (`defaultOpen` defaults to `true` here) and renders the panel with `layout="stacked"`, because 280px has no room for the side-by-side triple.
135
+ It starts open (`defaultOpen` defaults to `true` here) and renders the panel with `layout="stacked"`, because the default 280px cannot fit the column, operator and value fields side by side.
136
136
 
137
137
  ```demo
138
138
  file: columns/FilterSidebar.tsx
@@ -182,7 +182,7 @@ No props.
182
182
 
183
183
  The panel of filter rows - one column / operator / value triple per filter, over an "Add filter" / "Clear all" footer.
184
184
  It is a plain block with no title, no close button and no open state: it renders wherever it is mounted, and the popup and sidebar surfaces are wrappers around it.
185
- It must be inside `<TMDataGrid>`, since it reads the grid from context, and it only reads and writes the table's `columnFilters` state, so a `manualFiltering` grid gets the same panel for free.
185
+ It must be inside `<TMDataGrid>`, since it reads the grid from context, and it only reads and writes the table's `columnFilters` state, so the same panel works unchanged under `manualFiltering`.
186
186
 
187
187
  Pair it with `surface: "none"` so it is the only panel on the page, and drive it off `ui.state.filterPanelOpen` if it belongs behind a control of your own - `defaultOpen` sets where that state starts.
188
188
 
@@ -302,8 +302,8 @@ const SalaryFilter: TMDataGridFilterControlComponent = ({ layout, ...rest }) =>
302
302
  layout === "header" ? <NumberInput {...} /> : <RangeSlider {...} />;
303
303
  ```
304
304
 
305
- A control that ignores `layout` still works - it will simply look the same
306
- everywhere, which is fine until it has to fit a header cell. Every built-in
305
+ A control that ignores `layout` still works and looks the same in every
306
+ surface, including a header cell, where the room is smallest. Every built-in
307
307
  control honours it.
308
308
 
309
309
  ```demo
@@ -311,24 +311,111 @@ file: columns/CustomFilterControl.tsx
311
311
  hint: Open the filter panel - Status offers chips, every other column the built-in control.
312
312
  ```
313
313
 
314
+ ## Writing filters from your own interface
315
+
316
+ The grid's controls write plain TanStack state, so a control of your own does
317
+ the same. Write an envelope with `column.setFilterValue` or
318
+ `table.setColumnFilters`; the matcher, the pills, the header indicator and
319
+ `activeColumnFilters` all read it:
320
+
321
+ ```tsx
322
+ import { emptyValueForOperator, type TMDataGridFilterValue } from "@jielga/tmdatagrid";
323
+
324
+ const status = grid.table.getColumn("status")!;
325
+
326
+ // A set filter, as the panel would write it.
327
+ status.setFilterValue({
328
+ operator: "isAnyOf",
329
+ value: ["active", "paused"],
330
+ } satisfies TMDataGridFilterValue);
331
+
332
+ // An empty envelope stays in state and matches every row.
333
+ status.setFilterValue({ operator: "isAnyOf", value: emptyValueForOperator("isAnyOf") });
334
+
335
+ // `undefined` removes the entry.
336
+ status.setFilterValue(undefined);
337
+ ```
338
+
339
+ `getColumnOperators(column)` returns the operators a column offers and
340
+ `getColumnDefaultOperator(column)` the one a fresh filter opens on;
341
+ `emptyValueForOperator(operator)` returns the empty value of the right shape.
342
+
343
+ To keep the grid's panel and place it yourself, set `filters: { surface: "none" }`
344
+ and render [TMDataGrid.FilterPanel](#tmdatagridfilterpanel) where it belongs.
345
+ To keep filtering on and drop only the column menu's Filter item, pass
346
+ `renderColumnMenuItems` to `TMDataGrid.Table` and leave out the item keyed
347
+ `"filter"` (see [Column menu](/docs/column-menu)). To replace every filter
348
+ control, set `enableColumnFilters: false` and write the envelope yourself;
349
+ filters in state still apply.
350
+
314
351
  ## Custom matching
315
352
 
316
353
  To give a column its own matching logic instead of its own control, set
317
- `filterFn` on the column definition. The grid provides only `"tmDataGrid"`,
318
- which is the default.
354
+ `filterFn` on the column definition. A column `filterFn` replaces the shared
355
+ matcher for that column, so its third argument is the stored
356
+ `{ operator, value }` envelope, not the unwrapped value. Type it as
357
+ `TMDataGridFilterFn<TData>` and delegate to `tmDataGridFilterFn` for the
358
+ operators it does not handle itself. A column `filterFn` is also called with
359
+ half-typed envelopes, which the built-in matcher treats as matching every row,
360
+ so return `true` for `!isFilterActive(filter)` first:
361
+
362
+ ```ts
363
+ import {
364
+ isFilterActive,
365
+ tmDataGridFilterFn,
366
+ type TMDataGridFilterFn,
367
+ } from "@jielga/tmdatagrid";
368
+
369
+ // Reads "contains" as "has this tag", so "ops" matches the tag "ops" and not
370
+ // "devops"; every other operator keeps the built-in matching.
371
+ const matchesTags: TMDataGridFilterFn<Employee> = (row, columnId, filter) => {
372
+ if (!isFilterActive(filter)) return true;
373
+ if (filter.operator === "contains") {
374
+ const wanted = String(filter.value).toLowerCase();
375
+ return row.original.tags.some((tag) => tag.toLowerCase() === wanted);
376
+ }
377
+ return tmDataGridFilterFn(row, columnId, filter);
378
+ };
379
+
380
+ columnHelper.accessor((row) => row.tags.join(", "), {
381
+ id: "tags",
382
+ header: "Tags",
383
+ filterFn: matchesTags,
384
+ meta: { type: "string" },
385
+ });
386
+ ```
387
+
388
+ `filter.value` is a string under the scalar operators, a `ReadonlyArray<string>`
389
+ under `isAnyOf` / `isNoneOf` and a `[min, max]` pair under `between`;
390
+ `filterValueShape(operator)` tells which. `matchesFilter(cellValue, filter)`
391
+ applies the built-in matching to a value derived by the function itself.
392
+
393
+ The column's filter controls do not change: the operators still come from
394
+ `meta.type` and `meta.filter.operators`, and the panel, the pills and the header
395
+ indicator keep reading the envelope. A filter function named by string also
396
+ receives the envelope and does not unwrap it, so TanStack's built-ins such as
397
+ `"includesString"` do not work as a column `filterFn`. Write custom matching as
398
+ a function.
319
399
 
320
400
  ## Turning it off
321
401
 
322
- `enableColumnFilters: false` removes the filter menu item, the `FilterButton`,
323
- the panel and the header filter row. `enableColumnFilter: false` on a column
324
- removes that column's menu item, its entry in the panel's column list and its
325
- header control - the header cell stays, empty.
402
+ `enableColumnFilters: false` removes the Filter item from the column menu, the
403
+ `FilterButton`, the header filter row and the panel's column list, and disables
404
+ the panel's "Add filter" button. It does not clear `columnFilters`: an entry already in
405
+ state, or one written with `setFilterValue`, still narrows the rows, still shows
406
+ as a pill and still sets the header indicator. Call `table.resetColumnFilters()`
407
+ when switching the interface off. Under `filters.surface: "sidebar"`, set
408
+ `surface: "none"` as well, as the sidebar does not follow the switch.
409
+
410
+ `enableColumnFilter: false` on a column removes that column's menu item, its
411
+ entry in the panel's column list and its header control - the header cell
412
+ stays, empty.
326
413
 
327
414
  ## Reference
328
415
 
329
416
  | Name | Kind | Type | Default | What it does |
330
417
  | --- | --- | --- | --- | --- |
331
- | `enableColumnFilters` | Table option | `boolean` | `true` | `false` removes the panel, the button, the header row and the menu item. |
418
+ | `enableColumnFilters` | Table option | `boolean` | `true` | `false` removes the menu item, the button, the header row and the panel's column list, and disables the panel's "Add filter" button. Filters already in state still apply. |
332
419
  | `filters.surface` | Table option | `"popup" \| "sidebar" \| "none"` | `"popup"` | Which surface the table renders. |
333
420
  | `filters.sidebarSide` | Table option | `"left" \| "right"` | `"right"` | Which side the sidebar sits on. |
334
421
  | `filters.sidebarWidth` | Table option | `string` | `"280px"` | Width of the sidebar. |
@@ -339,7 +426,10 @@ header control - the header cell stays, empty.
339
426
  | `meta.filter.operators` | Column meta | `readonly TMDataGridFilterOperator[]` | The type's list | The operators this column offers, a subset of its type's. |
340
427
  | `meta.filter.defaultOperator` | Column meta | `TMDataGridFilterOperator` | The type's default, else the first offered | The operator a fresh filter opens on. |
341
428
  | `meta.filter.control` | Column meta | `TMDataGridFilterControlComponent` | By type and operator | Replaces the value control. |
342
- | `filterFn` | Column option | name \| fn | `"tmDataGrid"` | Custom matching for one column. |
429
+ | `filterFn` | Column option | `TMDataGridFilterFn<TData>` \| name | `"tmDataGrid"` | Custom matching for one column. Receives the `{ operator, value }` envelope. |
430
+ | `TMDataGridFilterFn<TData>` | Type | `(row, columnId, filter: TMDataGridFilterValue, addMeta?) => boolean` | – | The signature of a column's own `filterFn`. |
431
+ | `tmDataGridFilterFn` | Export | `(row, columnId, filterValue: unknown) => boolean`, assignable to `TMDataGridFilterFn<TData>` | – | The shared matcher, for a custom `filterFn` to delegate to. |
432
+ | `matchesFilter` | Export | `(cellValue, filter: TMDataGridFilterValue) => boolean` | – | The built-in matching applied to one value. |
343
433
  | `TMDataGrid.FilterPanel` | Component | `layout: "row" \| "stacked"` | `"row"` | The panel of filter rows, as a plain block. |
344
434
  | `TMDataGrid.FilterButton` | Component | – | – | Toolbar button toggling the surface, with an active count. Seeds a filter row only when the panel is empty. |
345
435
  | `TMDataGrid.FilterPills` | Component | takes `api` | – | Active filters as removable pills, renderable anywhere. |
@@ -112,6 +112,46 @@ file: getting-started/ToolbarAndFooter.tsx
112
112
  [Grid anatomy](/docs/anatomy) for what each part is, and
113
113
  [Toolbar](/docs/toolbar) for adding your own buttons among them.
114
114
 
115
+ ## Coming from TanStack Table
116
+
117
+ `useTMDataGrid` takes TanStack's `TableOptions`, so a page built on
118
+ `useReactTable` keeps its `data`, `columns`, `state` and `onXChange` callbacks.
119
+ Go through these differences:
120
+
121
+ - **`getRowId`** - return the entity id. The index default points at another
122
+ record after a sort or a refetch, which breaks selection, expansion and
123
+ editing. Required once `editing` is set.
124
+ - **Stable `data` and `columns`** - define `columns` outside the component and
125
+ keep `data` referentially stable; a fresh `[]` for the empty case belongs in
126
+ a module-scope constant. Under `editing`, a transient `[]` drops every open
127
+ draft.
128
+ - **Sizing** - columns are fluid, `minmax(minSize, flex fr)`. `size` applies
129
+ only to a column pinned at mount, `maxSize` only to resizing and autosizing,
130
+ and `minSize` defaults to `80`. See
131
+ [Visibility, pinning, ordering and size](/docs/column-layout#sizing).
132
+ - **Defaults the grid changes** - `aggregationFn` is cleared, so grouping does
133
+ not sum anything on its own; `groupedColumnMode` is `"remove"`;
134
+ `autoResetExpanded` and `autoResetPageIndex` are off; `globalFilterFn` is the
135
+ fuzzy matcher. The full list is on
136
+ [useTMDataGrid](/docs/use-tm-data-grid#options).
137
+ - **Filters are envelopes** - `columnFilters` holds `{ operator, value }`, and
138
+ a column's own `filterFn` receives that envelope. See
139
+ [Filtering](/docs/filtering#custom-matching).
140
+ - **Interface the page already owns** - `enableGlobalFilter: false`, or no
141
+ `TMDataGrid.Search`, for the search; `enableColumnFilters: false`, or
142
+ `filters: { surface: "none" }` to keep filtering without the grid's panel.
143
+ See [Filtering](/docs/filtering#turning-it-off).
144
+ - **Page reset** - `resetPageOnQueryChange` (default `true`) returns to the
145
+ first page when a filter, the search, the sorting or the grouping changes
146
+ through the table. A data source re-filtered outside table state does not
147
+ trigger it; call `table.setPageIndex(0)` yourself, or use `manualFiltering`.
148
+ See [Server-side data](/docs/server-side).
149
+ - **State reads** - there is no `table.getState()`; read slices with
150
+ `useSelector(table.store, ...)`. See
151
+ [useTMDataGrid](/docs/use-tm-data-grid#reading-state).
152
+ - **Tests** - the grid publishes `data-dg-part`, `data-row-id` and
153
+ `data-column-id`, and no `data-testid`. See [Testing](/docs/testing).
154
+
115
155
  ## Where to go next
116
156
 
117
157
  - **[Defining columns](/docs/columns)** - accessors, `meta.type`, and what each
package/docs/grouping.md CHANGED
@@ -100,8 +100,8 @@ records inside collapsed sub-groups. It shows a tick once all of them are
100
100
  selected and a dash while only some are.
101
101
 
102
102
  Only the records are written to `rowSelection`. A group row is never in it, so
103
- `getSelectedRowModel()` and the toolbar count do not depend on how the tree is
104
- arranged.
103
+ `getSelectedRowModel()` and `onRowSelectionChange` do not depend on how the tree
104
+ is arranged.
105
105
 
106
106
  Under `enableMultiRowSelection: false` group rows carry no checkbox.
107
107
 
@@ -125,8 +125,11 @@ While a column is grouped the grid renders the whole tree and relies on
125
125
  virtualization. `TMDataGrid.Footer` greys its pager out and replaces the range
126
126
  with `Grouped · all N rows`. Ungroup and paging resumes where it left off.
127
127
 
128
- To page a grouped grid, group and page on the server: feed the grid one page of
129
- a tree at a time with `manualPagination` and `manualGrouping`.
128
+ There is no option to keep the built-in pager active while grouped, and
129
+ `manualGrouping` does not change that: the pager greys out whenever `grouping`
130
+ is non-empty. To page a grouped grid, group and page on the server with
131
+ `manualPagination` and `manualGrouping`, and render the pager through
132
+ `renderPagination`, ignoring `isPagingActive`.
130
133
 
131
134
  `isPagingActive(table, features)` is exported, so a custom pager can grey itself
132
135
  out the same way:
@@ -150,12 +153,12 @@ server-side can set `enableGrouping: true` alongside `manualGrouping: true`. See
150
153
 
151
154
  | Name | Kind | Type | Default | What it does |
152
155
  | --- | --- | --- | --- | --- |
153
- | `enableGrouping` | Table option | `boolean` | `true` | Group by and Ungroup menu items. Also a column option. |
156
+ | `enableGrouping` | Table option | `boolean` | `true`; `false` under `manualPagination` | Group by and Ungroup menu items, and the tree lane. Also a column option. |
154
157
  | `groupedColumnMode` | Table option | `"reorder" \| "remove" \| false` | `"remove"` | Whether a grouped column leaves the grid or moves to the front. |
155
158
  | `manualGrouping` | Table option | `boolean` | `false` | The rows arrive grouped. Required to group a server-paged grid. |
156
159
  | `initialState.grouping` | Table option | `string[]` | `[]` | Column ids to group on at mount. A settings slice, so it persists. |
157
160
  | `aggregationFn` | Column option | `TMDataGridAggregationName \| fn` | – | How a column fills in its group cells. Unset leaves them blank. |
158
- | `aggregatedCell` | Column option | `(ctx) => ReactNode` | The `cell` renderer | Renders a group row's value differently from a data row's. |
161
+ | `aggregatedCell` | Column option | `(ctx) => ReactNode` | `cell` over the aggregate when `aggregationFn` is set, else blank | Renders a group row's value differently from a data row's. |
159
162
  | `GROUP_COLUMN_ID` | Export | `"__group__"` | – | Id of the generated tree column. |
160
163
  | `formatGroupValue` | Export | `(value) => string` | – | How the tree lane renders a group's value. |
161
164
  | `getGroupDataRows` | Export | `(row) => Row[]` | – | Every record under a group row, at any depth. |
@@ -1,6 +1,6 @@
1
1
  # Loading and empty states
2
2
 
3
- A grid can have nothing to show for four different reasons, and the body says
3
+ A grid can have nothing to show for five different reasons, and the body says
4
4
  which one it is: still fetching, emptied by a filter, or holding no data.
5
5
 
6
6
  ```demo
@@ -18,9 +18,29 @@ An empty body shows exactly one thing, decided in this order:
18
18
  2. **Entry rows** - an open entry row from `edit.addRow()`: the entry block
19
19
  only, with no message beside the form.
20
20
  3. **`renderEmptyState`** - your node, centred where the message would be.
21
- 4. **Filtered-empty** - a filter or search is active: a search icon and
22
- `labels.noResults` ("No rows match your filters").
23
- 5. **Truly-empty** - no data at all: `labels.noRows` ("No rows to show").
21
+ 4. **Filtered-empty** - a column filter is active (`isFilterActive`) or the
22
+ quick search is not blank: a search icon and `labels.noResults` ("No rows
23
+ match your filters").
24
+ 5. **Truly-empty** - no column filter is active and the quick search is blank:
25
+ `labels.noRows` ("No rows to show").
26
+
27
+ "Empty" means no displayed row and no pinned row: the current page under
28
+ pagination, otherwise every filtered row. States 4 and 5 are decided by the
29
+ filter state alone, so an empty `data` array shows `noResults` while a filter is
30
+ active. A half-typed filter, such as `{ operator: "contains", value: "" }`, is
31
+ not active, so it does not count.
32
+
33
+ ## While data loads
34
+
35
+ The loader shows only while `meta.loading` is `true` and the view is empty. It
36
+ is the only "no data yet" signal the grid has, so set `meta.loading` during the
37
+ first fetch as well; without it the empty message shows until the rows arrive.
38
+
39
+ `renderEmptyState` is called only once `meta.loading` is not `true`. During a
40
+ refetch that returns no rows, the loader mounts when `loading` turns `true`,
41
+ `renderEmptyState` mounts when it turns `false`, and it unmounts again on the
42
+ next refetch; the two branches never render together. Rows on screen stay on
43
+ screen until `data` changes, whatever `loading` says.
24
44
 
25
45
  ## Replacing the message
26
46
 
@@ -70,8 +90,8 @@ typically after `Spacer`.
70
90
 
71
91
  `TMDataGrid.SummaryCount` shows visible rows out of the total. The total is
72
92
  `meta.totalRowCount` when you provide it, and the pre-filtered row count
73
- otherwise. A [server-side](/docs/server-side) grid must provide it, because the
74
- client only holds one page.
93
+ otherwise. A [server-side](/docs/server-side) grid holds one page, so without
94
+ `meta.totalRowCount` it shows the shown count alone, with no total.
75
95
 
76
96
  ```tsx
77
97
  <TMDataGrid.SummaryCount>
@@ -83,10 +103,10 @@ client only holds one page.
83
103
 
84
104
  | Name | Kind | Type | Default | What it does |
85
105
  | --- | --- | --- | --- | --- |
86
- | `meta.loading` | Option | `boolean` | `false` | A fetch is in flight. Takes precedence over every empty message. |
106
+ | `meta.loading` | Option | `boolean` | `false` | A fetch is in flight. While `true` and the view is empty, the loader replaces every empty message and `renderEmptyState` is not called. |
87
107
  | `meta.noResultsLabel` | Option | `string` | `labels.noResults` | The filtered-empty message, without a render prop. |
88
- | `meta.totalRowCount` | Option | `number` | Pre-filtered count | The total `SummaryCount` reports. |
89
- | `renderEmptyState` | Table prop | `({ hasActiveFilters, table }) => ReactNode` | – | Replaces both built-in empty messages. |
108
+ | `meta.totalRowCount` | Option | `number` | Pre-filtered count; none on a server-driven grid | The total `SummaryCount` reports. |
109
+ | `renderEmptyState` | Table prop | `({ hasActiveFilters, table }) => ReactNode` | – | Replaces both built-in empty messages. `hasActiveFilters` is `true` when a column filter is active (`isFilterActive`) or the quick search is not blank. |
90
110
  | `TMDataGrid.LoadingIndicator` | Component | – | – | Spinner while `meta.loading`, for when the body has rows. |
91
111
  | `TMDataGrid.SummaryCount` | Component | `children` replaces the text | – | Visible rows out of total. |
92
112
  | `labels.noRows` · `labels.noResults` | Labels | `string` | English defaults | The two built-in messages. See [Localization](/docs/localization). |