@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21

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 (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
@@ -0,0 +1,134 @@
1
+ # Row selection
2
+
3
+ Picking rows out of the grid, and reading back what was picked. The grid
4
+ separates two things: a set of rows for a bulk action, and a single highlighted
5
+ row. `selectionMode` sets which of them a click drives, and defaults to
6
+ `"checkbox"`.
7
+
8
+ ```tsx
9
+ const grid = useTMDataGrid({ data, columns }); // "checkbox"
10
+ const rows = useTMDataGrid({ data, columns, selectionMode: "row" });
11
+ const master = useTMDataGrid({ data, columns, selectionMode: "highlight" });
12
+ ```
13
+
14
+ ```demo
15
+ file: rows/SelectionModes.tsx
16
+ ```
17
+
18
+ ## The four modes
19
+
20
+ `selectionMode` sets what selecting looks like and what a bare row click does.
21
+ A click can toggle a multi-selection or move a highlight, not both, so the two
22
+ are one option rather than two.
23
+
24
+ | Mode | Checkbox column | Row click |
25
+ | ---- | --------------- | --------- |
26
+ | `"checkbox"` | yes, multi-select | nothing |
27
+ | `"row"` | no | selects, with the usual modifiers |
28
+ | `"checkboxAndHighlight"` | yes, multi-select | highlights one row |
29
+ | `"highlight"` | no | highlights one row - no selection at all |
30
+
31
+ The first two write to TanStack's `rowSelection`, so the toolbar count,
32
+ `getSelectedRowModel()` and persistence all behave the same either way.
33
+
34
+ Under `"row"` the click follows the usual desktop-list conventions: a plain
35
+ click replaces the selection with this row, Ctrl/Cmd toggles it and leaves the
36
+ rest, Shift selects the range from the anchor, Ctrl+Shift adds that range. Rows are
37
+ focusable in this mode, and Space or Enter toggles the focused row.
38
+
39
+ `enableRowSelection: false` removes the checkbox column and row-click
40
+ selection in any mode. It has no effect under `"highlight"`, which selects
41
+ nothing.
42
+
43
+ ## The highlighted row
44
+
45
+ The highlighted row is state of its own rather than a slice of `rowSelection`,
46
+ so `"checkboxAndHighlight"` runs both at once: tick rows for a bulk action,
47
+ click one to open its detail panel beside the grid.
48
+
49
+ `defaultHighlightedRowId` seeds it and `onHighlightedRowChange` follows it,
50
+ typically into a route for a master–detail view:
51
+
52
+ ```tsx
53
+ const grid = useTMDataGrid({
54
+ data,
55
+ columns,
56
+ selectionMode: "highlight",
57
+ onHighlightedRowChange: (rowId) =>
58
+ navigate({ to: "/employees/$id", params: { id: rowId ?? "" } }),
59
+ });
60
+ ```
61
+
62
+ For a panel that opens under the row instead of beside the grid, see
63
+ [Row details](/docs/row-details). A grid can use both.
64
+
65
+ ## Acting on a selection
66
+
67
+ Read the selection through the table store with TanStack Store's
68
+ [`useSelector`](https://tanstack.com/store/latest/docs/framework/react/reference), rather
69
+ than by calling a method directly. The table identity is stable across renders,
70
+ so the React Compiler caches a bare `getSelectedRowModel()` call and the
71
+ toolbar stops updating.
72
+
73
+ ```tsx
74
+ const selected = useSelector(
75
+ grid.table.store,
76
+ () => grid.table.getSelectedRowModel().rows,
77
+ );
78
+ ```
79
+
80
+ ```demo
81
+ file: rows/SelectionState.tsx
82
+ hint: Tick a few rows - the toolbar turns into a bulk-action bar and back.
83
+ ```
84
+
85
+ ## Highlighting selected rows
86
+
87
+ `showSelectedBackground` follows the mode: on for `"row"`, off for
88
+ `"checkbox"`. Set it explicitly for checkboxes and a background, or for row
89
+ selection with no background change.
90
+
91
+ ```tsx
92
+ const grid = useTMDataGrid({ data, columns, showSelectedBackground: true });
93
+ ```
94
+
95
+ The colour is `--dg-row-selected-bg`, which defaults to
96
+ `--mantine-primary-color-light`. Change it on the grid element rather than
97
+ touching the flag:
98
+
99
+ ```tsx
100
+ <TMDataGrid
101
+ {...grid}
102
+ style={{ "--dg-row-selected-bg": "color-mix(in srgb, var(--mantine-color-blue-6) 12%, transparent)" }}
103
+ />
104
+ ```
105
+
106
+ Rows carry `data-selected` whenever they are selected, `data-selected-bg` when
107
+ they also take the background, and `data-highlighted` on the highlighted row.
108
+ [Custom styling](/docs/row-styling) can key off any of them.
109
+
110
+ ## Group rows
111
+
112
+ A group row's checkbox selects every record under it, at any depth, including
113
+ records inside collapsed sub-groups. It shows a tick once all of them are
114
+ selected and a dash while only some are. Only the records are written to
115
+ `rowSelection`. See [Grouping](/docs/grouping#selection).
116
+
117
+ ## Reference
118
+
119
+ | Name | Kind | Type | Default | What it does |
120
+ | --- | --- | --- | --- | --- |
121
+ | `selectionMode` | Option | `"checkbox" \| "row" \| "checkboxAndHighlight" \| "highlight"` | `"checkbox"` | What selecting looks like and what a row click does. |
122
+ | `enableRowSelection` | Table option | `boolean \| (row) => boolean` | `true` | `false` removes the checkbox column and row-click selection. |
123
+ | `enableMultiRowSelection` | Table option | `boolean` | `true` | `false` limits the selection to one row and drops group checkboxes. |
124
+ | `showSelectedBackground` | Option | `boolean` | Follows the mode | Whether selected rows take a background tint. |
125
+ | `defaultHighlightedRowId` | Option | `string \| null` | `null` | Row highlighted at mount. |
126
+ | `onHighlightedRowChange` | Callback | `(rowId: string \| null) => void` | – | Fires when the highlight moves. |
127
+ | `SELECT_COLUMN_ID` | Export | `"__select__"` | – | Id of the generated checkbox column. |
128
+ | `getSelectableRowIds` | Export | `(table) => string[]` | – | Ids the header checkbox would select. |
129
+ | `resolveRowSelectionClick` | Export | `(args) => ResolvedRowSelection` | – | The desktop-list click rules, for a custom surface. |
130
+ | `--dg-row-selected-bg` | CSS variable | colour | `--mantine-primary-color-light` | Selected row background. |
131
+ | `--dg-row-highlight-bg` | CSS variable | colour | Themed | Highlighted row background. |
132
+ | `data-selected` | Data attribute | – | – | On every selected row. |
133
+ | `data-selected-bg` | Data attribute | – | – | On selected rows that also take the background. |
134
+ | `data-highlighted` | Data attribute | – | – | On the highlighted row. |
@@ -0,0 +1,133 @@
1
+ # Row styling
2
+
3
+ Colouring rows by their contents, such as an overdue invoice in red.
4
+
5
+ ```tsx
6
+ <TMDataGrid.Table<Employee>
7
+ rowStyle={(row) =>
8
+ !row.getIsGrouped() && row.original.status === "Terminated"
9
+ ? { "--row-bg": "color-mix(in srgb, var(--mantine-color-red-6) 12%, transparent)" }
10
+ : undefined
11
+ }
12
+ />
13
+ ```
14
+
15
+ Group rows are handed to `rowStyle` and `rowClassName` too, and a group row's `original` is an arbitrary child's record.
16
+ Guard a callback that reads `original` with `row.getIsGrouped()`, or match `data-grouped="true"` to style the group rows themselves.
17
+
18
+ ```demo
19
+ file: rows/RowStyling.tsx
20
+ hint: Select and hover a coloured row - both still show through the row background.
21
+ ```
22
+
23
+ ## Set the row background
24
+
25
+ Set `--row-bg` rather than `background`. Hover, selection, the highlight, the
26
+ cell range and striping each add a background over the row and are composed
27
+ against `--row-bg`; `background` overrides all of them, so a coloured row stops
28
+ responding to hover and selection.
29
+
30
+ ```tsx
31
+ rowStyle={() => ({ background: "pink" })} // hover and selection hidden
32
+ rowStyle={() => ({ "--row-bg": "pink" })} // both still show
33
+ ```
34
+
35
+ `rowStyle` accepts `CSSProperties` or an object of custom properties. The type
36
+ is a union, so a callback returning either compiles.
37
+
38
+ Pick a colour that reads under both colour schemes. A `-0` Mantine shade
39
+ (`red-0`) is near-white, which turns unreadable behind light text in the dark
40
+ scheme; mixing a mid shade into transparency tints both schemes evenly:
41
+ `color-mix(in srgb, var(--mantine-color-red-6) 12%, transparent)`.
42
+
43
+ The grid composes `--row-bg` over the theme's body colour, so a translucent value tints the row and stays opaque under the pinned columns and the pinned rows.
44
+
45
+ ## One cell, not the row
46
+
47
+ There is no `cellStyle` or `cellClassName`. Style a single cell from the
48
+ column's own `cell` renderer, which owns the element you want to colour:
49
+
50
+ ```tsx
51
+ columnHelper.accessor("age", {
52
+ cell: ({ getValue }) => {
53
+ const age = getValue();
54
+ return <span style={{ color: age > 60 ? "var(--mantine-color-red-6)" : undefined }}>{age}</span>;
55
+ },
56
+ });
57
+ ```
58
+
59
+ From a stylesheet, match the cell's `data-column-id` under a `rowClassName`.
60
+ Body cells carry no `data-dg-part`; the coordinate attributes identify them:
61
+
62
+ ```css
63
+ .overdue [data-column-id="dueDate"] {
64
+ font-weight: 600;
65
+ }
66
+ ```
67
+
68
+ ## Classes instead
69
+
70
+ `rowClassName` takes the same shape and adds to the grid's own classes, for
71
+ when the styling belongs in a stylesheet:
72
+
73
+ ```tsx
74
+ <TMDataGrid.Table<Invoice>
75
+ rowClassName={(row) => (row.original.overdue ? classes.overdue : undefined)}
76
+ />
77
+ ```
78
+
79
+ ```css
80
+ .overdue {
81
+ --row-bg: color-mix(in srgb, var(--mantine-color-red-6) 12%, transparent);
82
+ font-weight: 600;
83
+ }
84
+ ```
85
+
86
+ ## Striping
87
+
88
+ `striped` gives every second row `--dg-row-striped-bg`. Striping follows the
89
+ row's **position in the view**, so it survives sorting, filtering and
90
+ virtualization rather than sticking to particular records.
91
+
92
+ ```tsx
93
+ <TMDataGrid.Table striped />
94
+ ```
95
+
96
+ Pinned rows are not striped: they have left the scrolling order.
97
+
98
+ ## Styling by state
99
+
100
+ Rows carry data attributes for their state, so a stylesheet can target any of
101
+ it without a callback:
102
+
103
+ | Attribute | On |
104
+ | --- | --- |
105
+ | `data-selected` | Selected rows |
106
+ | `data-selected-bg` | Selected rows that also take the background |
107
+ | `data-highlighted` | The highlighted row |
108
+ | `data-grouped` | Every row: `"true"` on group rows, `"false"` on the rest |
109
+ | `data-depth` | Every row. The nesting level |
110
+ | `data-context-menu` | The row whose context menu is open |
111
+ | `data-row-id` | Every row. Its id, which [tests](/docs/testing) key off |
112
+
113
+ The state attributes are published as `"true"` or `"false"`, so match the
114
+ value; the bare attribute selector matches every row:
115
+
116
+ ```css
117
+ [data-dg-part="row"][data-grouped="true"] {
118
+ --row-bg: color-mix(in srgb, var(--mantine-color-gray-6) 12%, transparent);
119
+ font-weight: 600;
120
+ }
121
+ ```
122
+
123
+ ## Reference
124
+
125
+ | Name | Kind | Type | Default | What it does |
126
+ | --- | --- | --- | --- | --- |
127
+ | `rowStyle` | Table prop | `TMDataGridRowStyle \| (row) => TMDataGridRowStyle` | – | Inline style for a body row. |
128
+ | `rowClassName` | Table prop | `string \| (row) => string \| undefined` | – | Class for a body row, added after the grid's own. |
129
+ | `striped` | Table prop | `boolean` | `false` | Every second row takes `--dg-row-striped-bg`. |
130
+ | `TMDataGridRowStyle` | Export | type | – | `CSSProperties` or an object of `--*` custom properties. |
131
+ | `--row-bg` | CSS variable | colour | – | The row's own background, composed over the body colour and under hover, selection and range. |
132
+ | `--dg-row-striped-bg` | CSS variable | colour | Themed | The stripe colour. |
133
+ | `--dg-row-height` | CSS variable | length | From `size` | Row height. `meta.rowHeight` is the supported way to change it. |
@@ -0,0 +1,111 @@
1
+ # Scrolling and virtualization
2
+
3
+ Virtualization is **always on**. There is no flag and no threshold: only the
4
+ rows within the viewport, plus a small overscan, are mounted, at any row
5
+ count.
6
+
7
+ ```tsx
8
+ const grid = useTMDataGrid({ data, columns }); // 200 rows or 200 000
9
+ ```
10
+
11
+ Rows only. Columns are not virtualized: every column that is visible is in the
12
+ DOM, header and body alike, however far off-screen it sits. A grid with a few
13
+ dozen columns is fine; hide the ones a user does not need rather than relying
14
+ on the viewport to do it.
15
+
16
+ ## Overscan
17
+
18
+ How many rows stay mounted on each side of the viewport. Defaults to `6`.
19
+
20
+ ```tsx
21
+ const grid = useTMDataGrid({ data, columns, overscan: 12 });
22
+ ```
23
+
24
+ Raise it if a fast scroll flashes blank rows; lower it when rows are expensive
25
+ to render.
26
+
27
+ ## Row height
28
+
29
+ Taken from `meta.rowHeight`, or from the `size` prop when that is not set. Rows
30
+ are **fixed height**, so the virtualizer's estimate is exact and the scrollbar
31
+ does not drift as you scroll.
32
+
33
+ ```tsx
34
+ const grid = useTMDataGrid({ data, columns, meta: { rowHeight: 64 } });
35
+ ```
36
+
37
+ [Row details](/docs/row-details) are the exception: a row showing a panel is as
38
+ tall as the panel, so those rows are measured after they mount.
39
+ `renderDetailsEstHeight` is what the virtualizer assumes for one it has not
40
+ measured yet.
41
+
42
+ ## Scrolling to a row
43
+
44
+ ```tsx
45
+ const { scrollToRow } = useTMDataGrid({ data, columns, getRowId });
46
+
47
+ scrollToRow({ rowId: "42", align: "center" });
48
+ ```
49
+
50
+ Under virtualization the target row may not be mounted, so
51
+ `element.scrollIntoView()` cannot find it. `align` is `"start"`, `"center"`,
52
+ `"end"` or `"auto"`, which scrolls only if the row is out of view.
53
+
54
+ It answers whether the row could be reached. `false` means the row is not in
55
+ the current view - filtered out, on another page, or an id matching no row -
56
+ and nothing scrolled. A pinned row answers `true` without scrolling.
57
+
58
+ The hook reaches the virtualizer through `scrollerRef`, which `TMDataGrid.Table`
59
+ fills in. That is internal wiring rather than an API: spread the whole grid
60
+ object onto `<TMDataGrid>`, because a hand-assembled prop list that leaves it
61
+ out has no scrolling.
62
+
63
+ While the draft store is running, `TMDataGrid.DraftActions` hands its
64
+ `renderActions` slot an `actions.scrollToFirstOpenRow(align?)` that goes to the
65
+ first row [left open](/docs/editing#the-draft-store), without your having to
66
+ track the ids.
67
+
68
+ ## Edge callbacks
69
+
70
+ `TMDataGrid.Table` reports arrivals at each edge, firing **once** when the
71
+ scroll reaches it rather than on every scroll event:
72
+
73
+ ```tsx
74
+ <TMDataGrid.Table
75
+ onScrollToBottom={() => console.log("at the end")}
76
+ onScrollToRight={() => console.log("at the last column")}
77
+ />
78
+ ```
79
+
80
+ For loading more rows, use [`onReachEnd`](/docs/server-side#infinite-scroll)
81
+ instead. It fires a number of rows before the bottom, and latches per row count
82
+ so a pending fetch is not requested twice.
83
+
84
+ ## Scroll shadows
85
+
86
+ Two soft shadows, both driven by `animation-timeline: scroll(…)` rather than by
87
+ a scroll listener.
88
+
89
+ **Under the header.** Once body rows scroll beneath the sticky header, a shadow
90
+ appears along its bottom edge, indicating rows above the viewport. A grid with
91
+ nothing to scroll shows none. `--dg-header-shadow-color` recolours it.
92
+
93
+ **Beside a pinned lane.** A [pinned column](/docs/column-layout#pinning) shows a
94
+ band over the data next to it, and only while it is covering content: it fades
95
+ in over the first 20px of horizontal scroll and fades out again at the far
96
+ end.
97
+
98
+ Where `animation-timeline` is unsupported, the header's own border draws the
99
+ boundary and the pinned band is always on.
100
+
101
+ ## Reference
102
+
103
+ | Name | Kind | Type | Default | What it does |
104
+ | --- | --- | --- | --- | --- |
105
+ | `overscan` | Option | `number` | `6` | Rows kept mounted beyond each edge of the viewport. |
106
+ | `meta.rowHeight` | Option | `number` | From `size` | Row height, in pixels. The virtualizer needs a number. |
107
+ | `scrollToRow` | Hook return | `({ rowId, align? }) => boolean` | `align: "auto"` | Scrolls a row into view, mounted or not. Answers whether it could be reached. |
108
+ | `onScrollToTop` · `onScrollToBottom` · `onScrollToLeft` · `onScrollToRight` | Table props | `() => void` | – | Fire once on arriving at that edge. |
109
+ | `TMDataGridScrollAlign` | Export | `"start" \| "center" \| "end" \| "auto"` | – | The `align` argument. |
110
+ | `--dg-header-shadow-color` | CSS variable | colour | Themed | The shadow under the sticky header. |
111
+ | `--dg-sticky-edge-range` | CSS variable | length | `20px` | How far the pinned-lane band takes to fade in. |
@@ -0,0 +1,246 @@
1
+ # A server-backed search
2
+
3
+ The grid's state is filters, sorting and a page index.
4
+ An API takes a request body of its own: its own field names, its own operator set, its own status codes, and pages counted from 1.
5
+ This recipe is the layer between the two, and the grid state that goes into it is shown as the request that comes out.
6
+
7
+ ```demo
8
+ file: recipes/ServerQuery.tsx
9
+ hint: Filter Amount or City and watch the request body under the grid change, then the result set follow it.
10
+ extraSources: data/orderSearchApi.ts
11
+ height: 700
12
+ ```
13
+
14
+ The grid does no filtering, sorting or paging here.
15
+ See [Server-side data](/docs/server-side) for the `manual*` options themselves; this page is about what to send them.
16
+
17
+ ## What the endpoint takes
18
+
19
+ The demo's API is deliberately not grid-shaped:
20
+
21
+ ```ts
22
+ type OrderSearchRequest = {
23
+ filter: { and: Array<Predicate> };
24
+ orderBy: Array<{ field: string; direction: "ASC" | "DESC" }>;
25
+ page: { number: number; size: number };
26
+ };
27
+
28
+ type Predicate =
29
+ | { field: string; op: "in" | "notIn"; values: Array<string | number> }
30
+ | { field: string; op: "range"; from?: string | number; to?: string | number }
31
+ | { field: string; op: "isNull" | "isNotNull" }
32
+ // …and the scalar form, for every remaining operator:
33
+ | { field: string; op: "eq" | "like" | "lt" | "gte"; value: string | number };
34
+ ```
35
+
36
+ The response is a page envelope, not an array:
37
+
38
+ ```ts
39
+ type OrderSearchResponse = {
40
+ items: Array<OrderRecord>;
41
+ page: { number: number; size: number; totalPages: number; totalItems: number };
42
+ };
43
+ ```
44
+
45
+ Forwarding `columnFilters` unchanged, as [Server-side data](/docs/server-side#sending-filters) describes, works when you own the endpoint.
46
+ When you do not, the translation has to live somewhere, and one module that both directions pass through is easier to keep correct than a translation spread across the fetch, the columns and the cells.
47
+
48
+ ## Two tables, three functions
49
+
50
+ The whole layer is `toSearchRequest` over two lookup tables, plus `toRow` on the way back.
51
+
52
+ `QUERY_FIELDS` maps a column id onto an API field, together with the cast from what the filter control writes to what the field holds.
53
+ Every filter control writes strings; `totalAmount` is a number and `status` an enum, so neither end is the right place for the conversion.
54
+
55
+ ```ts
56
+ const QUERY_FIELDS: Record<string, { field: string; cast: (raw: string) => string | number }> = {
57
+ id: { field: "orderRef", cast: Number },
58
+ amount: { field: "totalAmount", cast: Number },
59
+ status: { field: "status", cast: (raw) => STATUS_CODES[raw] ?? raw },
60
+ };
61
+ ```
62
+
63
+ A column missing from the table is one the API cannot query.
64
+ `toPredicate` returns `undefined` for it and the filter is dropped, rather than a field the endpoint would reject being sent.
65
+
66
+ `PREDICATE_OPS` maps the grid's operators onto the endpoint's.
67
+ Declare it as a `Record` over `TMDataGridFilterOperator` and not a `Partial`: an operator added by a later version of the grid then fails the build here, where it can be answered, instead of arriving at the server unmapped.
68
+
69
+ ```ts
70
+ const PREDICATE_OPS: Record<TMDataGridFilterOperator, PredicateOp> = {
71
+ contains: "like",
72
+ between: "range",
73
+ before: "lt",
74
+ lessThan: "lt",
75
+ isAnyOf: "in",
76
+ isEmpty: "isNull",
77
+ // …and one line for every remaining operator.
78
+ };
79
+ ```
80
+
81
+ Several grid operators collapse onto one API operator.
82
+ A date `before` and a number `lessThan` are both `lt` once the value has been cast.
83
+
84
+ ## Offering only what the endpoint answers
85
+
86
+ The demo's endpoint answers every operator the grid has, which is the exception.
87
+ An endpoint that has `like` and `eq` but no prefix match should not offer `startsWith` in the panel, because the only honest thing to do with it there is drop it, and a filter that silently does nothing looks like a bug.
88
+ `meta.filter.operators` narrows a column to the operators the query can express, and the mapping table is then declared over exactly that list:
89
+
90
+ ```ts
91
+ const TEXT_OPERATORS = [
92
+ "contains",
93
+ "equals",
94
+ "isEmpty",
95
+ "isNotEmpty",
96
+ ] as const satisfies readonly TMDataGridFilterOperator[];
97
+
98
+ const TEXT_OPS: Record<(typeof TEXT_OPERATORS)[number], PredicateOp> = {
99
+ contains: "like",
100
+ equals: "eq",
101
+ isEmpty: "isNull",
102
+ isNotEmpty: "isNotNull",
103
+ };
104
+
105
+ columnHelper.accessor("customer", {
106
+ header: "Customer",
107
+ meta: { filter: { operators: TEXT_OPERATORS } },
108
+ });
109
+ ```
110
+
111
+ One list feeds both the column and the type of the table, so an operator cannot be offered without a mapping, or mapped without being offered.
112
+ A fresh filter on the column opens on `meta.filter.defaultOperator` when that is set, else on the type's default when the list holds it - `contains` here - else on the list's first entry.
113
+
114
+ The lookup at the boundary still returns `undefined` for an operator it has no entry for.
115
+ A filter restored by `persist` from before the list was narrowed can carry one, and dropping it is the same rule as dropping a column the API cannot query.
116
+
117
+ ## The three value shapes
118
+
119
+ `TMDataGridFilterValue` is `{ operator, value }`, and the operator decides what `value` holds.
120
+ A mapping function has to branch on all four cases, in this order:
121
+
122
+ | Operator | `value` | Sent as |
123
+ | --- | --- | --- |
124
+ | `isEmpty`, `isNotEmpty` | Not used | `{ field, op }` |
125
+ | `isAnyOf`, `isNoneOf` | `ReadonlyArray<string>` | `{ field, op, values }` |
126
+ | `between` | `[min, max]`, either end possibly `""` | `{ field, op, from?, to? }` |
127
+ | Everything else | `string` | `{ field, op, value }` |
128
+
129
+ An empty end of a `between` pair leaves that side of the interval open, so it becomes an absent bound rather than an empty string.
130
+
131
+ ## What not to send
132
+
133
+ A filter whose value is still empty stays in the grid's state so the panel keeps its row while the user types.
134
+ It matches every row, so sending it as a predicate would narrow the result set to nothing.
135
+ `activeColumnFilters` is the test, applied across the slice: it hands back the entries that narrow the grid, with their values typed as `TMDataGridFilterValue` rather than as `unknown`.
136
+
137
+ ```ts
138
+ activeColumnFilters(state.columnFilters)
139
+ .map((filter) => toPredicate(filter.id, filter.value))
140
+ .filter((predicate): predicate is Predicate => predicate !== undefined);
141
+ ```
142
+
143
+ ## Keying the fetch on the request
144
+
145
+ The request is JSON, so the JSON is both what you send and what the fetch can key on:
146
+
147
+ ```tsx
148
+ const requestJson = useMemo(
149
+ () => JSON.stringify(toSearchRequest({ columnFilters, sorting, pagination }), null, 2),
150
+ [columnFilters, sorting, pagination],
151
+ );
152
+
153
+ const request = useMemo(() => JSON.parse(requestJson) as OrderSearchRequest, [requestJson]);
154
+ ```
155
+
156
+ `request` then changes identity only when the query changes.
157
+ Opening the filter panel and adding an empty row moves `columnFilters` and leaves the request alone, so no request is sent.
158
+ With TanStack Query, the same string is the `queryKey`.
159
+
160
+ Two things the effect owes the server:
161
+
162
+ - **Debounce.** The filter value input updates on every keystroke, so a request per keystroke is what you get without it.
163
+ - **Cancel.** A response that arrived after the query moved on is not this query's. A `cancelled` flag in the cleanup is enough; an `AbortController` on a real `fetch` is better.
164
+
165
+ ```tsx
166
+ useEffect(() => {
167
+ let cancelled = false;
168
+ setLoading(true);
169
+
170
+ const timer = setTimeout(() => {
171
+ void searchOrders(request).then((response) => {
172
+ if (cancelled) return;
173
+ setRows(response.items.map(toRow));
174
+ setPage(response.page);
175
+ setLoading(false);
176
+ });
177
+ }, 300);
178
+
179
+ return () => {
180
+ cancelled = true;
181
+ clearTimeout(timer);
182
+ };
183
+ }, [request]);
184
+ ```
185
+
186
+ ## Paging against a page envelope
187
+
188
+ Three numbers, in three places:
189
+
190
+ | The API's | The grid's | Written as |
191
+ | --- | --- | --- |
192
+ | `page.number`, counted from 1 | `pagination.pageIndex`, counted from 0 | `number: pageIndex + 1` |
193
+ | `page.totalItems`, the matched count | `rowCount` | `rowCount: page?.totalItems ?? 0` |
194
+ | `page.totalPages` | `state.pageCount`, derived from `rowCount / pageSize` | Nothing; the grid computes it |
195
+
196
+ `pageCount` follows from `rowCount`, so a response's `totalPages` needs forwarding only when the server pages by something other than the size the grid asked for.
197
+ Set `pageCount: -1` when the total is unknown, as an endpoint returning a cursor rather than a count leaves it.
198
+
199
+ The footer shows the page number through the `renderPagination` slot, keeping the built-in page-size select and pager on either side of it:
200
+
201
+ ```tsx
202
+ <TMDataGrid.Footer
203
+ renderPagination={({ Controls }) => (
204
+ <>
205
+ <Controls.PageSize />
206
+ <Controls.PageNumber />
207
+ <Controls.Pager />
208
+ </>
209
+ )}
210
+ />
211
+ ```
212
+
213
+ A filter or a sort changes what page 3 means, and under `manualPagination` the grid takes itself back to page 1 when it does - see [the page index](/docs/server-side#the-page-index).
214
+ The change callbacks are therefore the plain setters.
215
+
216
+ `meta.totalRowCount` is the unfiltered total, which no filtered response carries.
217
+ Take it from a separate count call, or from the one the page was opened with.
218
+ Without it, `SummaryCount` shows the matched count alone rather than comparing it against the rows of one page.
219
+
220
+ ## What the client no longer knows
221
+
222
+ Holding one page costs the grid the two things it derives from holding all of them.
223
+
224
+ **Faceted options.** `meta.options: "faceted"` reads the distinct values present in `data`, which is now one page of them.
225
+ The grid warns once per column about it.
226
+ A select column declares its own set instead:
227
+
228
+ ```tsx
229
+ columnHelper.accessor("city", {
230
+ meta: { type: "select", options: CITIES },
231
+ });
232
+ ```
233
+
234
+ **Rows off the page.** Row selection is keyed by `getRowId`, so ids selected on an earlier page stay in `rowSelection` while their rows are unmounted.
235
+ Read the state rather than the row models, as [Server-side data](/docs/server-side#row-selection) describes.
236
+
237
+ ## Reference
238
+
239
+ The pieces this recipe composes:
240
+
241
+ | Piece | Documented on |
242
+ | --- | --- |
243
+ | `manualFiltering`, `manualSorting`, `manualPagination`, `rowCount`, `meta.loading`, `meta.totalRowCount` | [Server-side data](/docs/server-side) |
244
+ | `TMDataGridFilterValue`, `TMDataGridFilterOperator`, `activeColumnFilters`, `meta.filter.operators`, `meta.filter.defaultOperator` | [Filtering](/docs/filtering) |
245
+ | `meta.options` | [Defining columns](/docs/columns) |
246
+ | `Footer` `renderPagination`, `Controls.PageSize`, `Controls.PageNumber`, `Controls.Pager` | [Pagination](/docs/pagination) |