@jielga/tmdatagrid 2.0.0-beta.8 → 2.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 (155) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1323 -796
  3. package/dist/index.js +4719 -3193
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/adding-rows.md +132 -0
  7. package/docs/anatomy.md +119 -0
  8. package/docs/card-view.md +108 -0
  9. package/docs/cell-selection.md +194 -0
  10. package/docs/column-layout.md +182 -0
  11. package/docs/column-menu.md +66 -0
  12. package/docs/columns.md +268 -0
  13. package/docs/components.md +311 -0
  14. package/docs/draft-store.md +242 -0
  15. package/docs/editing.md +303 -0
  16. package/docs/editors.md +250 -0
  17. package/docs/export.md +319 -0
  18. package/docs/filtering.md +362 -0
  19. package/docs/getting-started.md +123 -0
  20. package/docs/grouping.md +165 -0
  21. package/docs/loading-and-empty.md +92 -0
  22. package/docs/localization.md +79 -0
  23. package/docs/menu.md +143 -0
  24. package/docs/migrating-to-2.md +163 -0
  25. package/docs/pagination.md +144 -0
  26. package/docs/persistence.md +114 -0
  27. package/docs/portfolio-rebalancer.md +94 -0
  28. package/docs/query-builder.md +179 -0
  29. package/docs/quick-search.md +84 -0
  30. package/docs/row-details.md +115 -0
  31. package/docs/row-interaction.md +149 -0
  32. package/docs/row-pinning.md +132 -0
  33. package/docs/row-selection.md +136 -0
  34. package/docs/row-styling.md +133 -0
  35. package/docs/scrolling.md +112 -0
  36. package/docs/server-query.md +246 -0
  37. package/docs/server-side.md +206 -0
  38. package/docs/sorting.md +101 -0
  39. package/docs/styling.md +126 -0
  40. package/docs/summary-row.md +76 -0
  41. package/docs/testing.md +744 -0
  42. package/docs/toolbar.md +161 -0
  43. package/docs/use-tm-data-grid.md +361 -0
  44. package/package.json +22 -46
  45. package/skills/appearance/SKILL.md +72 -19
  46. package/skills/cell-selection/SKILL.md +69 -78
  47. package/skills/columns/SKILL.md +90 -34
  48. package/skills/data/SKILL.md +86 -16
  49. package/skills/editing/SKILL.md +83 -50
  50. package/skills/editing/references/common-mistakes.md +77 -69
  51. package/skills/editing/references/editing-api.md +31 -23
  52. package/skills/editing/references/editors-and-validation.md +24 -17
  53. package/skills/filtering/SKILL.md +148 -40
  54. package/skills/getting-started/SKILL.md +17 -15
  55. package/skills/grouping/SKILL.md +31 -16
  56. package/skills/options/SKILL.md +8 -8
  57. package/skills/rows/SKILL.md +22 -18
  58. package/skills/server-side/SKILL.md +170 -17
  59. package/skills/testing/SKILL.md +150 -32
  60. package/skills/testing-components/SKILL.md +230 -0
  61. package/skills/testing-editing/SKILL.md +240 -0
  62. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  63. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  64. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  65. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
  66. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  67. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  68. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +9 -55
  69. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  70. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
  71. package/src/components/TMDataGridEntryRows.tsx +354 -0
  72. package/src/components/TMDataGridExportPicker.module.css +77 -0
  73. package/src/components/TMDataGridExportPicker.tsx +234 -0
  74. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  75. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  76. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  77. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  78. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  80. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +9 -72
  81. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  82. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  83. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  84. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  85. package/src/components/TMDataGridMenu.tsx +357 -0
  86. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +15 -53
  87. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
  88. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
  89. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  90. package/src/components/TMDataGridToolbar.tsx +181 -0
  91. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  96. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  97. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  98. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  99. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  100. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  101. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  102. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  103. package/src/components/filters/controlLayout.ts +32 -0
  104. package/src/components/filters/filterControlFor.ts +65 -0
  105. package/src/components/generatedColumns.tsx +187 -0
  106. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  107. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  108. package/src/components/useHideableColumns.ts +52 -0
  109. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  110. package/src/{tmdatagrid/core → core}/capabilities.ts +5 -5
  111. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  112. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  113. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  114. package/src/core/controlledStateSync.ts +108 -0
  115. package/src/core/deletedRows.ts +34 -0
  116. package/src/core/dom.ts +74 -0
  117. package/src/{tmdatagrid/core → core}/editEngine.ts +1172 -388
  118. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  119. package/src/core/export.ts +704 -0
  120. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  121. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  122. package/src/core/filterSurface.ts +99 -0
  123. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  124. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  125. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  126. package/src/core/pageReset.ts +120 -0
  127. package/src/core/pagination.ts +81 -0
  128. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  129. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  130. package/src/{tmdatagrid/index.ts → index.ts} +70 -36
  131. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
  132. package/src/useTMDataGridExport.ts +78 -0
  133. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
  134. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  135. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  136. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  137. package/src/tmdatagrid/core/cellExport.ts +0 -320
  138. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  141. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  142. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  143. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  144. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  155. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -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) |
@@ -0,0 +1,206 @@
1
+ # Server-side data
2
+
3
+ When the client holds one page and the server does the work, paging, sorting and
4
+ filtering all become round trips and the grid stops doing them itself.
5
+
6
+ The grid reads rows through `getPaginatedRowModel()` and totals through
7
+ `getRowCount()` and `getPageCount()`, all of which respect TanStack's manual
8
+ modes. A server-driven grid therefore requires only the standard `manual*`
9
+ configuration. `manualPagination: true` also switches the grid's pagination flag
10
+ on, so `TMDataGrid.Footer` renders its pager without `enablePagination`.
11
+
12
+ When the total is unknown, declare `pageCount: -1`: the next button stays
13
+ enabled and the `pagination` render prop receives `pageCount: -1`.
14
+
15
+ ## Usage
16
+
17
+ ```tsx
18
+ const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
19
+ const [sorting, setSorting] = useState([]);
20
+ const [columnFilters, setColumnFilters] = useState([]);
21
+
22
+ const { data, isFetching } = useQuery({
23
+ queryKey: ["employees", pagination, sorting, columnFilters],
24
+ queryFn: () => fetchEmployees({ pagination, sorting, columnFilters }),
25
+ placeholderData: keepPreviousData,
26
+ });
27
+
28
+ const grid = useTMDataGrid({
29
+ columns,
30
+ data: data?.rows ?? [],
31
+ getRowId: (row) => String(row.id),
32
+
33
+ manualPagination: true,
34
+ manualSorting: true,
35
+ manualFiltering: true,
36
+ rowCount: data?.total ?? 0,
37
+
38
+ state: { pagination, sorting, columnFilters },
39
+ onPaginationChange: setPagination,
40
+ onSortingChange: setSorting,
41
+ onColumnFiltersChange: setColumnFilters,
42
+
43
+ meta: { loading: isFetching, totalRowCount: data?.totalUnfiltered },
44
+ });
45
+ ```
46
+
47
+ ```demo
48
+ file: data/ServerSide.tsx
49
+ hint: Sorting, searching and paging are all round trips against a server with 500 ms of latency.
50
+ extraSources: data/orders.ts
51
+ ```
52
+
53
+ ## Differences from client-side data
54
+
55
+ | Concern | Client-side | Server-side |
56
+ | --- | --- | --- |
57
+ | Rendered rows | The current page, sliced locally | The rows returned by the server |
58
+ | Footer total | Pre-paginated row count | `options.rowCount` |
59
+ | `SummaryCount` total | Pre-filtered row count | `meta.totalRowCount`; without it, the count renders alone |
60
+ | Loading state | Not applicable | `meta.loading` |
61
+
62
+ No additional configuration is required. Column menus, the filter panel and the
63
+ column manager behave identically in both modes.
64
+
65
+ ## The page index
66
+
67
+ A column filter, the quick search or a sort changes what page 3 means.
68
+ The result set is a different one, and it may not have a page 3 at all - so the grid resets `pageIndex` to 0 whenever the query changes, on every grid.
69
+ The reset is applied in the same event as the change, so one request goes out, for the first page of the new query.
70
+
71
+ `resetPageOnQueryChange: false` switches it off.
72
+
73
+ TanStack's `autoResetPageIndex` is a different rule and does not cover this.
74
+ It fires on a change to `data` - which server-side is the response landing, after the request was sent, and under `editing.draft` every commit - so the grid switches it off everywhere and resets on the query change itself.
75
+
76
+ ## Column options
77
+
78
+ `meta.options: "faceted"` reads the distinct values present in `data`, which server-side is one page of them.
79
+ The dropdown then offers whatever happened to be on the page the user is looking at, and looks correct while being wrong.
80
+ Declare the set instead, as a list or a function.
81
+ The grid warns once per column when a faceted column resolves under `manualFiltering` or `manualPagination`.
82
+
83
+ ## Sending filters
84
+
85
+ Filter values are plain JSON:
86
+
87
+ ```json
88
+ [
89
+ { "id": "lastName", "value": { "operator": "contains", "value": "holm" } },
90
+ { "id": "age", "value": { "operator": "greaterThan", "value": "30" } },
91
+ { "id": "status", "value": { "operator": "isAnyOf", "value": ["Paid", "Pending"] } },
92
+ { "id": "hired", "value": { "operator": "onOrAfter", "value": "2026-01-01" } }
93
+ ]
94
+ ```
95
+
96
+ Forward `columnFilters` unchanged and translate it at the API boundary.
97
+ `activeColumnFilters` hands back the entries that are narrowing the grid, typed:
98
+ `ColumnFiltersState` types `value` as `unknown`, and an entry whose value is
99
+ still empty matches every row.
100
+
101
+ ```ts
102
+ import { activeColumnFilters } from "@jielga/tmdatagrid";
103
+
104
+ const active = activeColumnFilters(columnFilters);
105
+ // [{ id: "lastName", value: { operator: "contains", value: "holm" } }]
106
+ ```
107
+
108
+ It takes the `columnFilters` array, or the table where the grid owns the slice.
109
+ `isFilterActive(value)` is the single-value test it is built on.
110
+ Only the grid's own `{ operator, value }` shape is read: an entry holding some
111
+ other value - a custom filter control writing raw values - is dropped.
112
+
113
+ Debounce requests. The filter value input updates on every keystroke.
114
+
115
+ An endpoint that answers only some operators - `contains` and `equals` but no
116
+ `startsWith` - should not have the rest offered. `meta.filter.operators` on the
117
+ column narrows the panel and the header funnel to the operators the query can
118
+ express; see [Operators](/docs/filtering#operators).
119
+
120
+ For an endpoint that speaks its own query language rather than taking the
121
+ grid's filter model, see [A server-backed search](/docs/server-query): one
122
+ mapping layer turning filters, sorting and the page index into a request body,
123
+ and the response envelope back into rows.
124
+
125
+ ## Persistence
126
+
127
+ `persist` works unchanged. `dataKey` restores filters, sorting and pagination
128
+ before the first request, so reloading the page repeats the query the user last
129
+ ran.
130
+
131
+ If the same state is also held in your own `useState`, initialise it from the
132
+ same source or let the grid own it. Do not maintain both independently.
133
+
134
+ ## Row selection
135
+
136
+ Row selection is keyed by `getRowId`, so ids must be stable across pages. With
137
+ `manualPagination`, rows selected on an earlier page remain in `rowSelection`
138
+ even though they are no longer mounted. Read the state rather than the row
139
+ models:
140
+
141
+ ```ts
142
+ const selectedIds = Object.keys(grid.table.store.state.rowSelection);
143
+ ```
144
+
145
+ ## Infinite scroll
146
+
147
+ An alternative to the pager: keep every fetched row in `data` and load more as
148
+ the user scrolls. `onReachEnd` on `TMDataGrid.Table` fires as the scroll nears
149
+ the last row. Append the next page and the virtualizer keeps its position:
150
+
151
+ ```tsx
152
+ const [rows, setRows] = useState<Order[]>([]);
153
+
154
+ const grid = useTMDataGrid({
155
+ data: rows,
156
+ columns,
157
+ getRowId: (row) => String(row.id),
158
+ meta: { loading: isFetching, totalRowCount: total },
159
+ enableSorting: false,
160
+ enableColumnFilters: false,
161
+ });
162
+
163
+ <TMDataGrid.Table onReachEnd={() => void fetchNextPage()} />
164
+ ```
165
+
166
+ ```demo
167
+ file: data/InfiniteScroll.tsx
168
+ hint: Scroll to the bottom and keep going. 100 rows arrive at a time and the scroll position holds.
169
+ extraSources: data/orders.ts
170
+ height: 460
171
+ ```
172
+
173
+ Fetch page zero yourself on mount. `onReachEnd` does not fire on an empty grid.
174
+
175
+ `onReachEnd` fires once per row count, so a pending fetch is not requested again
176
+ until its rows land. `reachEndThreshold` (default 10) sets how many rows before
177
+ the end it fires. `TMDataGrid.LoadingIndicator` in the toolbar shows the fetch,
178
+ since the body keeps showing the rows it already has.
179
+
180
+ Two constraints apply:
181
+
182
+ - **Sorting and filtering must be server-side** (`manualSorting` /
183
+ `manualFiltering`) or disabled. The client only holds a prefix of the data, so
184
+ a client-side sort would order that prefix and present it as the whole. When a
185
+ server-side sort or filter changes, reset the accumulated rows and start from
186
+ page zero.
187
+ - **Not compatible with `enablePagination`.** The pager slices the same scroll
188
+ the callback watches, so the end reached is the page's rather than the data's.
189
+ The grid warns once if both are set.
190
+
191
+ ## Reference
192
+
193
+ | Name | Kind | Type | Default | What it does |
194
+ | --- | --- | --- | --- | --- |
195
+ | `manualPagination` | Table option | `boolean` | `false` | The server pages. Implies `enablePagination`. |
196
+ | `manualSorting` | Table option | `boolean` | `false` | The server sorts. |
197
+ | `manualFiltering` | Table option | `boolean` | `false` | The server filters, column filters and quick search alike. |
198
+ | `manualGrouping` | Table option | `boolean` | `false` | The rows arrive grouped. See [Grouping](/docs/grouping#server-side-grids). |
199
+ | `rowCount` | Table option | `number` | – | The true total. `pageCount: -1` when it is unknown. |
200
+ | `meta.loading` | Option | `boolean` | `false` | A fetch is in flight. See [Loading and empty states](/docs/loading-and-empty). |
201
+ | `meta.totalRowCount` | Option | `number` | – | The unfiltered total, for `SummaryCount`. |
202
+ | `resetPageOnQueryChange` | Option | `boolean` | `true` | Back to page 1 when a filter, the quick search, the sort or the grouping changes. |
203
+ | `onReachEnd` | Table prop | `() => void` | – | Fires as the scroll nears the last row. Latches per row count. |
204
+ | `reachEndThreshold` | Table prop | `number` | `10` | How many rows before the end it fires. |
205
+ | `activeColumnFilters` | Export | `(columnFilters \| table) => Array<{ id, value }>` | – | The filters in the grid's own value shape that narrow anything, typed. |
206
+ | `isFilterActive` | Export | `(value) => boolean` | – | Whether one filter value narrows anything. |
@@ -0,0 +1,101 @@
1
+ # Sorting
2
+
3
+ On by default. Click a header to sort it, click again to reverse, click a third
4
+ time to clear. The column menu has the same three actions.
5
+
6
+ ```tsx
7
+ const grid = useTMDataGrid({ data, columns });
8
+ ```
9
+
10
+ ```demo
11
+ file: columns/Sorting.tsx
12
+ hint: Click a header to sort, Shift+click a second to append - the badge beside the arrow is its priority.
13
+ ```
14
+
15
+ ## Sorting by more than one column
16
+
17
+ Shift+click a second header to **add** it to the sort rather than replace it.
18
+ While more than one column sorts, each sorted header shows its priority - 1, 2,
19
+ … - beside the arrow, so the order they are applied in is visible.
20
+
21
+ A plain click still replaces the whole sort, and the menu's Sort items do the
22
+ same.
23
+
24
+ This is TanStack's own `isMultiSortEvent`, so `enableMultiSort`,
25
+ `maxMultiSortColCount` and a custom `isMultiSortEvent` all pass straight
26
+ through:
27
+
28
+ ```tsx
29
+ const grid = useTMDataGrid({
30
+ data,
31
+ columns,
32
+ maxMultiSortColCount: 3,
33
+ // Ctrl rather than Shift, say.
34
+ isMultiSortEvent: (event) => event.ctrlKey,
35
+ });
36
+ ```
37
+
38
+ ## Turning it off
39
+
40
+ `enableSorting: false` on the table removes click-to-sort, the indicator and
41
+ the menu items everywhere; on a column it removes them for that column alone.
42
+
43
+ ```tsx
44
+ columnHelper.accessor("avatar", { header: "", enableSorting: false });
45
+ ```
46
+
47
+ A column whose menu has no remaining items renders no menu button and does not
48
+ handle right-click, so the browser's own menu opens instead.
49
+
50
+ ## Where the state lives
51
+
52
+ Sorting writes TanStack's `sorting` state, an array of `{ id, desc }` in
53
+ priority order. Seed it, control it, or read it like any other slice:
54
+
55
+ ```tsx
56
+ const grid = useTMDataGrid({
57
+ data,
58
+ columns,
59
+ initialState: { sorting: [{ id: "lastName", desc: false }] },
60
+ });
61
+ ```
62
+
63
+ It is a **data** slice: it names a column and a direction over the data itself,
64
+ so a [persisted](/docs/use-tm-data-grid#persist) grid comes back sorted the way
65
+ it was left, under `dataKey`. For a server that does the sorting, see
66
+ [Server-side data](/docs/server-side).
67
+
68
+ Grouping runs first, so a grouped grid sorts rows within each group and orders
69
+ the groups by their aggregated value. See
70
+ [Grouping](/docs/grouping#sorting-a-grouped-grid).
71
+
72
+ ## Custom comparators
73
+
74
+ `sortFn` on the column takes any of TanStack's registered names, or a function
75
+ of two rows and the column id. It is `sortFn` in v9, not v8's `sortingFn`.
76
+
77
+ ```tsx
78
+ columnHelper.accessor("priority", {
79
+ header: "Priority",
80
+ sortFn: (rowA, rowB) =>
81
+ RANK[rowA.original.priority] - RANK[rowB.original.priority],
82
+ });
83
+ ```
84
+
85
+ ## The header menu
86
+
87
+ The menu opens from the ⋮ button on the header, or from a right-click anywhere
88
+ on it. The items are the same either way; a right-click opens it at the
89
+ pointer.
90
+
91
+ ## Reference
92
+
93
+ | Name | Kind | Type | Default | What it does |
94
+ | --- | --- | --- | --- | --- |
95
+ | `enableSorting` | Table option | `boolean` | `true` | Also a column option. `false` removes indicator, menu items and click-to-sort. |
96
+ | `enableMultiSort` | Table option | `boolean` | `true` | Whether Shift+click appends instead of replacing. |
97
+ | `maxMultiSortColCount` | Table option | `number` | `Infinity` | How many columns may sort at once. |
98
+ | `isMultiSortEvent` | Table option | `(event) => boolean` | Shift held | What counts as "append to the sort". |
99
+ | `sortFn` | Column option | name \| `(rowA, rowB, columnId) => number` | `"auto"` | The comparator for one column. v9's name for v8's `sortingFn`. |
100
+ | `initialState.sorting` | Table option | `Array<{ id, desc }>` | `[]` | Sort at mount. A settings slice, so it persists. |
101
+ | `manualSorting` | Table option | `boolean` | `false` | The server sorts; the grid stops. |
@@ -0,0 +1,126 @@
1
+ # Size, styling and theming
2
+
3
+ The grid is themed through CSS custom properties, and sized through Mantine's
4
+ standard `size` scale. Both are set on the root element, so a grid can be
5
+ themed per instance without a provider.
6
+
7
+ ```tsx
8
+ <TMDataGrid {...grid} size="sm" style={{ "--dg-row-selected-bg": "color-mix(in srgb, var(--mantine-color-blue-6) 12%, transparent)" }} />
9
+ ```
10
+
11
+ ```demo
12
+ file: customization/Styling.tsx
13
+ hint: Every value the controls change is a CSS variable set on the grid element.
14
+ extraSources: data/employeeColumns.tsx
15
+ ```
16
+
17
+ ## The size scale
18
+
19
+ `size` drives row height, header height, font size and cell padding together,
20
+ and selects the size of every Mantine control the grid renders - the page-size
21
+ select, the filter inputs, the column checkboxes.
22
+
23
+ | `size` | Row height | Header height | Font size | Cell padding |
24
+ | --- | --- | --- | --- | --- |
25
+ | `xs` | 34px | 32px | `xs` | 6px |
26
+ | `sm` | 42px | 38px | `sm` | 8px |
27
+ | `md` (default) | 52px | 44px | `sm` | 10px |
28
+ | `lg` | 62px | 52px | `md` | 14px |
29
+ | `xl` | 72px | 60px | `lg` | 18px |
30
+
31
+ ```demo
32
+ file: getting-started/DensityAndLayout.tsx
33
+ ```
34
+
35
+ Row height is also required by the virtualizer **as a number**, so it cannot be
36
+ defined in CSS alone. `SIZE_ROW_HEIGHT` is the exported source of these values,
37
+ and the stylesheet mirrors them. To use a height outside the scale, set
38
+ `meta.rowHeight` rather than the variable.
39
+
40
+ ## CSS variables
41
+
42
+ `style` accepts custom properties, and `className` reaches the same element
43
+ from a stylesheet.
44
+ The wrapper components - `Toolbar`, `Spacer`, `Footer`, `FilterPanel`, `FilterPills` and `ColumnsPanel` - take Mantine's `BoxProps` on top of their own props, so `mb="sm"` or `hiddenFrom="sm"` on any of them sets the element itself; see [Toolbar](/docs/toolbar#style-props).
45
+
46
+ ### Metrics
47
+
48
+ | Variable | Default | Applies to |
49
+ | --- | --- | --- |
50
+ | `--dg-row-height` | From `size` | Row height. Prefer `meta.rowHeight` - the virtualizer needs the number. |
51
+ | `--dg-header-height` | From `size` | Header row height |
52
+ | `--dg-summary-height` | From `size` | [Summary row](/docs/summary-row) height |
53
+ | `--dg-entry-height` | From `size` | The sticky [entry block](/docs/adding-rows#adding-rows) |
54
+ | `--dg-font-size` | From `size` | Cell and header font size |
55
+ | `--dg-padding` | From `size` | Horizontal cell padding. The generated lanes are excluded: they are fixed 36px tracks that centre their control. |
56
+ | `--dg-radius` | `--mantine-radius-md` | The frame's corner radius. `0` squares the grid off. The root clips its overflow, so the header and the last row follow it. |
57
+
58
+ ### Colours
59
+
60
+ Both colour schemes are supported. The grid's own stylesheet resolves its
61
+ colours with `light-dark()`, so it follows Mantine's scheme without a prop or a
62
+ second import. A default of **Themed** below means exactly that: the variable
63
+ resolves to one value under the light scheme and another under the dark one.
64
+ Set such a variable and you take over both schemes, so give it a value that
65
+ reads in each - `light-dark()` works in your own value too.
66
+
67
+ | Variable | Default | Applies to |
68
+ | --- | --- | --- |
69
+ | `--row-bg` | – | One row's own background. Set this, never `background`. See [Row styling](/docs/row-styling#set-the-row-background). |
70
+ | `--dg-row-selected-bg` | `--mantine-primary-color-light` | [Selected](/docs/row-selection) rows |
71
+ | `--dg-row-highlight-bg` | Themed | The highlighted row |
72
+ | `--dg-row-striped-bg` | Themed | Every second row under `striped` |
73
+ | `--dg-row-group-bg` | Themed | [Group](/docs/grouping) rows |
74
+ | `--dg-row-new-bg` | Green tint | New rows entered into the [draft store](/docs/draft-store) |
75
+ | `--dg-match-highlight-bg` | Themed yellow | [Marked](/docs/quick-search#match-highlighting) text |
76
+ | `--dg-header-shadow-color` | Themed | The shadow under the sticky header |
77
+
78
+ ### Layout internals
79
+
80
+ | Variable | Default | Applies to |
81
+ | --- | --- | --- |
82
+ | `--dg-sticky-edge-range` | `20px` | How far the pinned-lane band takes to fade in |
83
+ | `--dg-edge-top` · `-bottom` · `-left` · `-right` | – | Set by the grid to mark [cell-range](/docs/cell-selection) borders |
84
+ | `--dg-z-header` · `-pinned-cell` · `-summary-row` · … | – | The stacking order. Change these only to place something of your own between two layers. |
85
+
86
+ ## The stylesheet
87
+
88
+ One import, once, anywhere in your app:
89
+
90
+ ```tsx
91
+ import "@jielga/tmdatagrid/styles.css";
92
+ ```
93
+
94
+ `@jielga/tmdatagrid/styles.layer.css` is the same stylesheet wrapped in a
95
+ `@layer`, for an application that orders its own layers and needs the grid to
96
+ sit at a known place in that order. Import **one** of the two, never both.
97
+
98
+ ## Layout
99
+
100
+ The grid fills the box you give it and scrolls inside it. It does not size
101
+ itself to its content: a virtualized grid has no content height to measure.
102
+
103
+ ```tsx
104
+ <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
105
+ <TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }}>
106
+ <TMDataGrid.Table />
107
+ </TMDataGrid>
108
+ </div>
109
+ ```
110
+
111
+ `minHeight: 0` is required. A flex item's default `min-height: auto` will not
112
+ shrink below its content, so without it the grid grows past the viewport instead
113
+ of scrolling.
114
+
115
+ ## Reference
116
+
117
+ | Name | Kind | Type | Default | What it does |
118
+ | --- | --- | --- | --- | --- |
119
+ | `size` | Prop | `MantineSize` | `"md"` | The whole density scale. |
120
+ | `className` | Prop | `string` | – | Added to the root element's classes. |
121
+ | `style` | Prop | `CSSProperties` + `--*` | – | Root element styles, including the variables above. |
122
+ | `id` | Prop | `string` | – | Set on the root element. |
123
+ | `meta.rowHeight` | Option | `number` | From `size` | A row height outside the scale. |
124
+ | `SIZE_ROW_HEIGHT` | Export | `Record<MantineSize, number>` | – | The row heights listed in the table above. |
125
+ | `SIZE_CONTROL_SIZE` | Export | `Record<MantineSize, MantineSize>` | – | Which control size each grid size uses. |
126
+ | `DEFAULT_TMDATAGRID_SIZE` | Export | `"md"` | – | The default. |