@jielga/tmdatagrid 2.0.0-beta.9 → 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 (154) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1281 -768
  3. package/dist/index.js +4607 -3250
  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 +46 -47
  47. package/skills/columns/SKILL.md +90 -34
  48. package/skills/data/SKILL.md +86 -16
  49. package/skills/editing/SKILL.md +67 -40
  50. package/skills/editing/references/common-mistakes.md +77 -69
  51. package/skills/editing/references/editing-api.md +22 -19
  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 +7 -7
  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/components → components}/TMDataGrid.tsx +38 -21
  63. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
  64. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  65. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  66. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
  67. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  68. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
  69. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
  70. package/src/components/TMDataGridExportPicker.module.css +77 -0
  71. package/src/components/TMDataGridExportPicker.tsx +234 -0
  72. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  73. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  74. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  75. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  76. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  77. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  78. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
  79. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  80. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  81. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  82. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  83. package/src/components/TMDataGridMenu.tsx +357 -0
  84. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
  85. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
  86. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
  87. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  88. package/src/components/TMDataGridToolbar.tsx +181 -0
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  91. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  96. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  97. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  98. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  99. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  100. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  101. package/src/components/filters/controlLayout.ts +32 -0
  102. package/src/components/filters/filterControlFor.ts +65 -0
  103. package/src/components/generatedColumns.tsx +187 -0
  104. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  105. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  106. package/src/components/useHideableColumns.ts +52 -0
  107. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  108. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  109. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  110. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  111. package/src/core/controlledStateSync.ts +108 -0
  112. package/src/core/deletedRows.ts +34 -0
  113. package/src/core/dom.ts +74 -0
  114. package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
  115. package/src/core/export.ts +704 -0
  116. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  117. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  118. package/src/core/filterSurface.ts +99 -0
  119. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  120. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  121. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  122. package/src/core/pageReset.ts +120 -0
  123. package/src/core/pagination.ts +81 -0
  124. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  125. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  126. package/src/{tmdatagrid/index.ts → index.ts} +69 -35
  127. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
  128. package/src/useTMDataGridExport.ts +78 -0
  129. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  130. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  131. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
  134. /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
  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}/capabilities.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -0,0 +1,362 @@
1
+ # Filtering
2
+
3
+ Per-column filters, built out of an operator and a value. Users add them from
4
+ the filter panel, the column headers or a column menu - `filters` decides
5
+ which. You control what each column offers through `meta.type`, and can
6
+ replace the value control when the default input is not the right one.
7
+
8
+ For one box across every column instead, see [Quick search](/docs/quick-search).
9
+
10
+ ```demo
11
+ file: columns/Filtering.tsx
12
+ hint: Every column type offers its own operators - Salary opens on “between” because its meta says so.
13
+ ```
14
+
15
+ ## How a filter is stored
16
+
17
+ All columns share one filter function. The operator lives *in the value*
18
+ rather than being selected through `filterFn`:
19
+
20
+ ```ts
21
+ type TMDataGridFilterValue = {
22
+ operator: TMDataGridFilterOperator;
23
+ value: string | ReadonlyArray<string>;
24
+ };
25
+ ```
26
+
27
+ So the filter model is plain JSON, and can be forwarded to a server without
28
+ transformation - dates as ISO strings, booleans as `"true"` / `"false"`, and an
29
+ array only under `isAnyOf` / `isNoneOf` (the set the cell is tested against) and
30
+ `between` (a `[min, max]` pair, an empty string leaving that end open). See
31
+ [Server-side data](/docs/server-side#sending-filters).
32
+
33
+ A filter with an empty value stays in state, so a panel row survives while the
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
37
+ state, and `activeColumnFilters` applies it across the whole slice, handing
38
+ back the entries that narrow the grid with their values typed as
39
+ `TMDataGridFilterValue` rather than as `unknown`. Entries in any other value
40
+ shape are dropped, so a custom control's raw filter values do not come back
41
+ from it.
42
+
43
+ ## Operators
44
+
45
+ `meta.type` selects which operators a column offers.
46
+
47
+ | Operator | Label | Column type |
48
+ | --- | --- | --- |
49
+ | `contains` | contains | `string` |
50
+ | `equals` | equals | `string`, `number`, `boolean`, `date` |
51
+ | `notEquals` | does not equal | `string`, `number`, `boolean`, `date` |
52
+ | `startsWith` | starts with | `string` |
53
+ | `endsWith` | ends with | `string` |
54
+ | `greaterThan` | is greater than | `number` |
55
+ | `greaterThanOrEqual` | is greater than or equal to | `number` |
56
+ | `lessThan` | is less than | `number` |
57
+ | `lessThanOrEqual` | is less than or equal to | `number` |
58
+ | `between` | is between | `number`, `date` |
59
+ | `before` | is before | `date` |
60
+ | `after` | is after | `date` |
61
+ | `onOrBefore` | is on or before | `date` |
62
+ | `onOrAfter` | is on or after | `date` |
63
+ | `isAnyOf` | is any of | `select`, `multiSelect` |
64
+ | `isNoneOf` | is none of | `select`, `multiSelect` |
65
+ | `isEmpty` | is empty | every type |
66
+ | `isNotEmpty` | is not empty | every type |
67
+
68
+ String comparisons are case-insensitive. Date comparisons are by calendar day,
69
+ so `equals` on a `date` column matches the same day. On a `multiSelect`
70
+ column, whose cells hold arrays, `isAnyOf` is an intersection test and
71
+ `isNoneOf` its complement, and an empty cell array counts as empty for
72
+ `isEmpty`. `between` is inclusive at both ends; the panel renders a From/To
73
+ pair and either end may stay empty to leave the interval open on that side.
74
+
75
+ `meta.filter.defaultOperator` sets which one a fresh filter opens on: a salary
76
+ column can start on `between` rather than `equals`. It must be one of the
77
+ operators that type offers.
78
+
79
+ ```tsx
80
+ columnHelper.accessor("salary", {
81
+ header: "Salary",
82
+ meta: { type: "number", filter: { defaultOperator: "between" } },
83
+ });
84
+ ```
85
+
86
+ `meta.filter.operators` narrows the list a column offers to a subset of its type's.
87
+ The panel's operator dropdown and the header row's funnel menu then show only those, in the type's order.
88
+ An operator the type does not offer is ignored, and a list that leaves nothing falls back to the type's full set.
89
+ Without `defaultOperator`, a fresh filter opens on the type's default when it is offered and on the first offered operator otherwise.
90
+ Declare it on a column whose backend answers only some operators, so the user is never offered one the query cannot express - see [A server-backed search](/docs/server-query#offering-only-what-the-endpoint-answers).
91
+
92
+ ```tsx
93
+ columnHelper.accessor("customer", {
94
+ header: "Customer",
95
+ meta: { filter: { operators: ["contains", "equals", "isEmpty", "isNotEmpty"] } },
96
+ });
97
+ ```
98
+
99
+ `getColumnOperators(column)` returns the resolved list, and `getColumnDefaultOperator(column)` the operator a fresh filter on it opens on.
100
+
101
+ ## The filters option
102
+
103
+ `filters` on `useTMDataGrid` decides where the filter controls render.
104
+
105
+ ```tsx
106
+ useTMDataGrid({ data, columns, filters: { surface: "sidebar", inHeader: true } });
107
+ ```
108
+
109
+ | Option | Type | Default | What it does |
110
+ | --- | --- | --- | --- |
111
+ | `surface` | `"popup" \| "sidebar" \| "none"` | `"popup"` | The surface `TMDataGrid.Table` renders and `TMDataGrid.FilterButton` toggles. |
112
+ | `sidebarSide` | `"left" \| "right"` | `"right"` | Which side the sidebar sits on. Ignored by the other surfaces. |
113
+ | `sidebarWidth` | `string` | `"280px"` | Width of the sidebar, any CSS length. Ignored by the other surfaces. |
114
+ | `defaultOpen` | `boolean` | `true` under `"sidebar"`, `false` otherwise | Whether the surface starts open. Read once, at mount. Under `"none"`, the starting value of `ui.state.filterPanelOpen`. |
115
+ | `inHeader` | `boolean` | `false` | A second header row of per-column controls. Independent of `surface`. |
116
+
117
+ `surface` and `inHeader` are two separate choices, not one list: `inHeader` composes with all three surfaces.
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
+
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.
121
+
122
+ ### surface: "popup"
123
+
124
+ The default.
125
+ The panel floats over the first body rows, anchored under the header.
126
+ A pointerdown outside closes it, so does Escape, and so does emptying it - by removing the last filter row or by **Clear all**.
127
+ `TMDataGrid.FilterButton` is exempt from the click-away, which is what keeps it a toggle.
128
+ Closing only hides the popup; the filters stay.
129
+
130
+ ### surface: "sidebar"
131
+
132
+ The same panel beside the rows, inside the grid frame and under the toolbar.
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
+ 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.
136
+
137
+ ```demo
138
+ file: columns/FilterSidebar.tsx
139
+ hint: The funnel button in the toolbar closes the sidebar and gives the width back to the rows.
140
+ ```
141
+
142
+ ### surface: "none"
143
+
144
+ The grid renders no surface of its own and `TMDataGrid.FilterButton` renders nothing.
145
+ Use it for header filters alone, or to place the panel yourself - see [TMDataGrid.FilterPanel](#tmdatagridfilterpanel).
146
+
147
+ ### inHeader
148
+
149
+ `inHeader: true` adds a second header row holding one value control per filterable column, always visible.
150
+ The row shares the column tracks and pinned lanes of the header above it, so resizing, reordering and pinning move each control with its column.
151
+
152
+ A header cell has room for a value and not much else, so the panel's column and operator dropdowns are not there: the column is the one the cell sits over, and the operator is a funnel button beside the input, tinted whenever the column is on anything but its default operator.
153
+ The control itself is the same one the panel would render, `meta.filter.control` included - it receives `layout: "header"` and drops its field label for an `aria-label`.
154
+
155
+ Two pieces of column chrome come off with header filters on: the column menu's **Filter** item and the funnel indicator on a filtered header.
156
+ Both existed only to reveal a control that is now always on screen; the filtered column's tinted title stays.
157
+
158
+ Clearing a header control removes the column's `columnFilters` entry rather than leaving an empty one behind - unless the user also picked a non-default operator, which is kept, being the part of the filter an empty control cannot show.
159
+ Panel rows still keep their empty filters; see [How a filter is stored](#how-a-filter-is-stored).
160
+
161
+ A narrow column clips its control.
162
+ Give a column that has to hold a date range or a multi-select a `minSize` wide enough for it.
163
+
164
+ `TMDataGrid.FilterButton` still toggles whatever `surface` names, and the panel it opens holds the same filters.
165
+ `openColumnFilter` does not: with `inHeader` on it always scrolls the column's header control into view and focuses it, leaving the surface closed.
166
+
167
+ ```demo
168
+ file: columns/HeaderFilters.tsx
169
+ hint: Department offers its faceted values; the funnel beside an input changes that column's operator.
170
+ ```
171
+
172
+ ## TMDataGrid.FilterButton
173
+
174
+ The toolbar button that toggles the filter surface, tinted with the count of active filters.
175
+ Opening an empty panel seeds a filter row on the first filterable column; with filters already in state it opens on those.
176
+ It renders nothing when no column can be filtered (`enableColumnFilters: false`), and nothing under `surface: "none"`, where there is no surface to toggle.
177
+ It is part of every demo on this page.
178
+
179
+ No props.
180
+
181
+ ## TMDataGrid.FilterPanel
182
+
183
+ The panel of filter rows - one column / operator / value triple per filter, over an "Add filter" / "Clear all" footer.
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.
186
+
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
+
189
+ ```demo
190
+ file: columns/FilterPanelPlaced.tsx
191
+ ```
192
+
193
+ | Prop | Type | Default | Description |
194
+ | --- | --- | --- | --- |
195
+ | `layout` | `"row" \| "stacked"` | `"row"` | `"row"` lays the three fields side by side and wants about 550px. `"stacked"` puts them one under the other, each filling the width, for a drawer or a narrow column. Passed to every value control as its `layout`, so a `meta.filter.control` sizes itself to the same decision. |
196
+ | Mantine `BoxProps` | | – | Style props (`p`, `w`, …), `className` and `style`, set on the panel block. |
197
+
198
+ ```tsx
199
+ <Drawer opened={open} onClose={close}>
200
+ <TMDataGrid.FilterPanel layout="stacked" />
201
+ </Drawer>
202
+ ```
203
+
204
+ ## TMDataGrid.FilterPills
205
+
206
+ One pill per active filter - `First name: Sofia ✕` - where the ✕ clears that filter and a click on the label sends the user to that column's control through [openColumnFilter](#opencolumnfilter).
207
+ Half-typed filters produce no pill.
208
+ The label spells the operator out unless it is the column type's default: `Age is greater than 30`, but `First name: Sofia`.
209
+
210
+ It takes the grid as an `api` prop rather than reading context, so active filters can live in a page header, above the toolbar, or anywhere else on the page.
211
+ It is also exported as `TMDataGridFilterPills`, for a file where nothing else touches `TMDataGrid`.
212
+
213
+ ```demo
214
+ file: columns/FilterPills.tsx
215
+ ```
216
+
217
+ | Prop | Type | Default | Description |
218
+ | --- | --- | --- | --- |
219
+ | `api` | `TMDataGridApi<TData>` | - | The object returned by `useTMDataGrid`. |
220
+ | `size` | `TMDataGridSize` | `"sm"` | Pill size. |
221
+ | `showClearAll` | `boolean` | `true` | "Clear all", shown once two filters are active. |
222
+ | `onPillClick` | `(columnId: string) => void` | - | Replaces the default click behaviour. |
223
+ | Mantine `BoxProps` | | - | Style props (`mb`, `hiddenFrom`, …), `className` and `style`, set on the wrapper. |
224
+
225
+ ## openColumnFilter
226
+
227
+ `openColumnFilter(api, columnId)` sends the user to a column's filter control - what a pill's label and the column menu's **Filter** item both call, available for a control of your own.
228
+ It seeds an empty filter on the column if it has none yet, then opens the surface on that column's panel row.
229
+ Under `filters.inHeader` it instead scrolls the column's header control into view and focuses it, leaving the surface closed.
230
+
231
+ ## Replacing the value control
232
+
233
+ The built-in control follows the type and operator: a text input for strings, a
234
+ number pair for `between`, a native date input for dates, a Yes/No dropdown for
235
+ booleans, a multi-select for option columns.
236
+
237
+ Four ready-made alternatives ship as named exports:
238
+
239
+ | Export | For | Renders |
240
+ | --- | --- | --- |
241
+ | `DgRangeSliderFilter` | `number` | A range slider seeded from the data's min/max, writing the `between` pair. |
242
+ | `DgDateRangeFilter` | `date` | A From/To pair of native date inputs, writing the `between` pair. |
243
+ | `DgAutocompleteFilter` | `string` | Free text with the faceted (or declared) values as suggestions. |
244
+ | `DgTriStateFilter` | `boolean` | All / Yes / No segments - All clears the filter. |
245
+
246
+ ```tsx
247
+ meta: {
248
+ type: "number",
249
+ filter: { control: DgRangeSliderFilter, defaultOperator: "between" },
250
+ }
251
+ ```
252
+
253
+ Pair the range-shaped ones with `defaultOperator: "between"` so the filter
254
+ opens on them. For an operator they do not cover, every built-in falls back to
255
+ `TMDataGridFilterValueInput`, the default control, which is exported so custom
256
+ controls can fall back the same way.
257
+
258
+ ```demo
259
+ file: columns/BuiltInFilterControls.tsx
260
+ hint: Open the filter panel and compare each row's control with the plain input the other demos show.
261
+ ```
262
+
263
+ ### Writing your own
264
+
265
+ `meta.filter.control` is a **component**, rendered as JSX, so hooks may be used
266
+ inside. It receives `TMDataGridFilterControlArgs` and handles the value only:
267
+ it reads `operator` to shape itself and writes the bare value through
268
+ `onChange`, and the grid stores the `{ operator, value }` pair around it. The
269
+ column and operator dropdowns remain the panel's.
270
+
271
+ ```tsx
272
+ const SalaryFilter: TMDataGridFilterControlComponent = ({
273
+ operator,
274
+ value,
275
+ onChange,
276
+ }) =>
277
+ operator === "between" ? (
278
+ <RangeSlider /* value is the [min, max] pair */ />
279
+ ) : (
280
+ <NumberInput /* a single bound */ />
281
+ );
282
+
283
+ meta: { filter: { control: SalaryFilter, defaultOperator: "between" } }
284
+ ```
285
+
286
+ Define controls at module scope so their identity is stable. `args.options`
287
+ arrives pre-resolved through `resolveColumnOptions` for a column that declares
288
+ `meta.options` or is select-shaped. `args.table` is available to a control that
289
+ needs more than that.
290
+
291
+ `args.layout` says how much room the control has and whether it has to name
292
+ itself:
293
+
294
+ | `layout` | Where | The field |
295
+ | --- | --- | --- |
296
+ | `"row"` | A panel row laid out side by side | Labelled, fixed width |
297
+ | `"stacked"` | A panel row in a narrow host - the sidebar | Labelled, fills the width |
298
+ | `"header"` | One header cell, under `filters.inHeader` | No label; `aria-label` instead, fills the column |
299
+
300
+ ```tsx
301
+ const SalaryFilter: TMDataGridFilterControlComponent = ({ layout, ...rest }) =>
302
+ layout === "header" ? <NumberInput {...} /> : <RangeSlider {...} />;
303
+ ```
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
307
+ control honours it.
308
+
309
+ ```demo
310
+ file: columns/CustomFilterControl.tsx
311
+ hint: Open the filter panel - Status offers chips, every other column the built-in control.
312
+ ```
313
+
314
+ ## Custom matching
315
+
316
+ 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.
319
+
320
+ ## Turning it off
321
+
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.
326
+
327
+ ## Reference
328
+
329
+ | Name | Kind | Type | Default | What it does |
330
+ | --- | --- | --- | --- | --- |
331
+ | `enableColumnFilters` | Table option | `boolean` | `true` | `false` removes the panel, the button, the header row and the menu item. |
332
+ | `filters.surface` | Table option | `"popup" \| "sidebar" \| "none"` | `"popup"` | Which surface the table renders. |
333
+ | `filters.sidebarSide` | Table option | `"left" \| "right"` | `"right"` | Which side the sidebar sits on. |
334
+ | `filters.sidebarWidth` | Table option | `string` | `"280px"` | Width of the sidebar. |
335
+ | `filters.defaultOpen` | Table option | `boolean` | `true` under `"sidebar"` | Whether the surface starts open. |
336
+ | `filters.inHeader` | Table option | `boolean` | `false` | A second header row of per-column controls. |
337
+ | `enableColumnFilter` | Column option | `boolean` | `true` | `false` takes one column out of filtering. |
338
+ | `meta.type` | Column meta | `"string" \| "number" \| "boolean" \| "date" \| "select" \| "multiSelect"` | `"string"` | Selects the operators and the value control. |
339
+ | `meta.filter.operators` | Column meta | `readonly TMDataGridFilterOperator[]` | The type's list | The operators this column offers, a subset of its type's. |
340
+ | `meta.filter.defaultOperator` | Column meta | `TMDataGridFilterOperator` | The type's default, else the first offered | The operator a fresh filter opens on. |
341
+ | `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. |
343
+ | `TMDataGrid.FilterPanel` | Component | `layout: "row" \| "stacked"` | `"row"` | The panel of filter rows, as a plain block. |
344
+ | `TMDataGrid.FilterButton` | Component | – | – | Toolbar button toggling the surface, with an active count. Seeds a filter row only when the panel is empty. |
345
+ | `TMDataGrid.FilterPills` | Component | takes `api` | – | Active filters as removable pills, renderable anywhere. |
346
+ | `TMDataGridFilterPillsProps` | Type | – | – | The props of `TMDataGrid.FilterPills`. |
347
+ | `openColumnFilter` | Export | `(api, columnId) => void` | – | Sends the user to a column's filter control, seeding an empty filter. |
348
+ | `TMDataGridFilterControlArgs` | Type | `layout: "row" \| "stacked" \| "header"` | – | What a value control is handed, `layout` saying how much room it has. |
349
+ | `TMDataGridFilterControlLayout` | Type | `"row" \| "stacked" \| "header"` | – | The type of `layout` on `TMDataGridFilterControlArgs`. |
350
+ | `isFilterActive` | Export | `(value) => boolean` | – | Whether a filter value narrows anything. |
351
+ | `activeColumnFilters` | Export | `(columnFilters \| table) => Array<{ id, value }>` | – | The filters in the grid's own value shape that narrow anything, typed. |
352
+ | `TMDataGridColumnFilter` | Type | `{ id, value }` | – | One entry of `columnFilters`, typed. What `activeColumnFilters` returns a list of. |
353
+ | `getOperatorsForType` | Export | `(type) => operators` | – | The operator list a type offers. |
354
+ | `getColumnOperators` · `getColumnDefaultOperator` | Exports | `(column) => operators` · `(column) => operator` | – | The list one column offers after `meta.filter.operators`, and the operator a fresh filter on it opens on. |
355
+ | `FILTER_OPERATOR_LABELS` | Export | record | – | The label shown for each operator. |
356
+ | `TMDataGridFilterValueInput` | Export | component | – | The default value control, for falling back to. |
357
+ | `formatFilterLabel` | Export | `({ label, type, filter }) => string` | – | The one-line description used on the pills. |
358
+ | `emptyValueForOperator` · `operatorNeedsValue` · `operatorTakesArrayValue` · `operatorTakesRangeValue` · `filterValueShape` | Exports | – | – | What shape of value an operator expects. |
359
+ | `TMDataGridFilterValueShape` | Type | `"scalar" \| "set" \| "range"` | – | What `filterValueShape` returns for an operator. |
360
+ | `TMDataGridFiltersOptions` · `TMDataGridFiltersSettings` · `TMDataGridFilterSurface` · `TMDataGridFilterSidebarSide` | Types | – | – | The `filters` option, and the resolved form of it on `api.filters`. |
361
+ | `TMDataGridFilterPanelProps` · `TMDataGridFilterPanelLayout` | Types | – | – | For wrapping `TMDataGrid.FilterPanel` in a component of your own. |
362
+ | `DgRangeSliderFilter` · `DgDateRangeFilter` · `DgAutocompleteFilter` · `DgTriStateFilter` | Exports | components | – | The four ready-made controls. |
@@ -0,0 +1,123 @@
1
+ # Getting started
2
+
3
+ A React data grid built on TanStack Table v9 and Mantine. Always virtualized,
4
+ with resizable, reorderable, sortable, filterable, hideable and pinnable
5
+ columns.
6
+
7
+ `useTMDataGrid` creates the table, `TMDataGrid` provides it through context, and
8
+ the parts you render inside read what they need from that context.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @jielga/tmdatagrid
14
+ ```
15
+
16
+ Peer dependencies: `react` and `react-dom` (19.1 or later), `@mantine/core`,
17
+ `@tanstack/react-table` (v9), `@tanstack/react-store`, `@tanstack/store`,
18
+ `@tanstack/react-virtual` and `@tabler/icons-react`. Editing adds
19
+ `@tanstack/react-form`.
20
+
21
+ The grid must be rendered inside a Mantine `MantineProvider`.
22
+
23
+ ```tsx
24
+ import "@jielga/tmdatagrid/styles.css";
25
+ ```
26
+
27
+ Import it once. A layered stylesheet is also published; see
28
+ [Styling](/docs/styling#the-stylesheet).
29
+
30
+ > **TanStack Table v9 is still in beta.** The grid is built against
31
+ > `^9.0.0-beta.21` and uses its feature-registry API, which beta releases may
32
+ > change without a major bump. Pin `@tanstack/react-table` and
33
+ > `@tanstack/table-core` to an exact version if you need reproducible installs.
34
+
35
+ ## Your first grid
36
+
37
+ ```demo
38
+ file: getting-started/Minimal.tsx
39
+ extraSources: data/employees.ts
40
+ ```
41
+
42
+ Rows are virtualized by default.
43
+
44
+ ```tsx
45
+ import {
46
+ createTMDataGridColumnHelper,
47
+ TMDataGrid,
48
+ useTMDataGrid,
49
+ } from "@jielga/tmdatagrid";
50
+
51
+ type Employee = {
52
+ id: number;
53
+ firstName: string;
54
+ lastName: string;
55
+ department: string;
56
+ salary: number;
57
+ };
58
+
59
+ const columnHelper = createTMDataGridColumnHelper<Employee>();
60
+
61
+ const columns = columnHelper.columns([
62
+ columnHelper.accessor("firstName", { header: "First name" }),
63
+ columnHelper.accessor("lastName", { header: "Last name" }),
64
+ columnHelper.accessor("department", { header: "Department" }),
65
+ columnHelper.accessor("salary", {
66
+ header: "Salary",
67
+ meta: { type: "number", align: "right" },
68
+ }),
69
+ ]);
70
+
71
+ export function Employees({ data }: { data: Employee[] }) {
72
+ const grid = useTMDataGrid({
73
+ data,
74
+ columns,
75
+ getRowId: (row) => String(row.id),
76
+ });
77
+
78
+ return (
79
+ <TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }}>
80
+ <TMDataGrid.Table<Employee> />
81
+ </TMDataGrid>
82
+ );
83
+ }
84
+ ```
85
+
86
+ Define `columns` at module scope. A new array on every render rebuilds the
87
+ column model and resets the user's column widths and order.
88
+
89
+ Give the grid a bounded height: `style={{ flex: 1, minHeight: 0 }}` inside a
90
+ flex parent, or a fixed height. See [Layout](/docs/styling#layout).
91
+
92
+ ## Adding a toolbar and footer
93
+
94
+ ```tsx
95
+ <TMDataGrid {...grid}>
96
+ <TMDataGrid.Toolbar>
97
+ <TMDataGrid.SummaryCount />
98
+ <TMDataGrid.Menu>
99
+ <TMDataGrid.Menu.Columns />
100
+ </TMDataGrid.Menu>
101
+ </TMDataGrid.Toolbar>
102
+ <TMDataGrid.Table />
103
+ <TMDataGrid.Footer pageSizeOptions={[10, 25, 50]} />
104
+ </TMDataGrid>
105
+ ```
106
+
107
+ ```demo
108
+ file: getting-started/ToolbarAndFooter.tsx
109
+ ```
110
+
111
+ `Toolbar` and `Footer` are ordinary composition. See
112
+ [Grid anatomy](/docs/anatomy) for what each part is, and
113
+ [Toolbar](/docs/toolbar) for adding your own buttons among them.
114
+
115
+ ## Where to go next
116
+
117
+ - **[Defining columns](/docs/columns)** - accessors, `meta.type`, and what each
118
+ type configures.
119
+ - **[Grid anatomy](/docs/anatomy)** - the hook's return value, and every
120
+ component you can render.
121
+ - **[Editing](/docs/editing)** - three modes, from single cells to a whole row.
122
+ - **[Server-side data](/docs/server-side)** - when the server does the work.
123
+ - **[The playground](/playground)** - every feature at once, behind switches.
@@ -0,0 +1,165 @@
1
+ # Grouping
2
+
3
+ Grouping collapses the rows into a tree: one row per distinct value, with the
4
+ records that share it folded underneath.
5
+
6
+ It is on by default. Nothing changes until a column is grouped, which users do
7
+ from **Group by …** in any column menu.
8
+
9
+ ```tsx
10
+ const grid = useTMDataGrid({ data, columns });
11
+ ```
12
+
13
+ To open already grouped, seed the state:
14
+
15
+ ```tsx
16
+ const grid = useTMDataGrid({
17
+ data,
18
+ columns,
19
+ initialState: { grouping: ["department"] },
20
+ });
21
+ ```
22
+
23
+ ```demo
24
+ file: rows/Grouping.tsx
25
+ hint: “Group by …” lives in every column menu. Group by Location as well and the tree nests.
26
+ ```
27
+
28
+ ## Grouping is not hierarchical data
29
+
30
+ The tree here is built from column values. It is not the same thing as data
31
+ that is already a tree, which in TanStack is `getSubRows`.
32
+
33
+ `getSubRows` is TanStack's own option and the grid passes it through: the
34
+ nested rows reach the row model, render with `data-depth`, and count in the
35
+ [summary row](/docs/summary-row) alongside their parents. What the grid does
36
+ not add is any UI for them. There is no expander control on a parent row, and
37
+ `expanded` is the state [row details](/docs/row-details) uses, so a grid with
38
+ `renderDetails` set opens a panel where you would want children. Drive the
39
+ expansion from your own control and your own `expanded` state, or flatten the
40
+ data before it reaches the grid.
41
+
42
+ ## What grouping does to the grid
43
+
44
+ Grouping a column **removes it**, since its values have moved into the tree
45
+ lane, and a generated **Group** column appears at the front, pinned beside the
46
+ checkbox lane. Each group row shows its value, how many records are under it,
47
+ and a chevron. Group again from a second column's menu to nest.
48
+
49
+ Because a grouped column is no longer in the grid, **Ungroup** lives on the
50
+ tree column's menu, one item per grouped column. **Expand all groups** and
51
+ **Collapse all groups** are in every column menu while a grouping is active.
52
+
53
+ To keep a grouped column in the grid instead of removing it, pass
54
+ `groupedColumnMode: "reorder"`, which is TanStack's own default and moves
55
+ grouped columns to the front. Note that the kept column's data cells render as
56
+ TanStack's grouped-cell placeholder - blank - with the value only on group
57
+ rows, so it repeats what the tree lane already shows and cannot be typed into.
58
+ To set the grouped field on a new row, seed it through `edit.addRow(values)` -
59
+ see [Editing](/docs/adding-rows#adding-rows).
60
+
61
+ ## Aggregation
62
+
63
+ Off by default. A group row leaves every cell blank except the tree lane. Give
64
+ a column an `aggregationFn` and its group cells fill in.
65
+
66
+ ```tsx
67
+ columnHelper.accessor("salary", {
68
+ header: "Salary",
69
+ aggregationFn: "sum",
70
+ meta: { type: "number", align: "right" },
71
+ });
72
+ ```
73
+
74
+ `"sum"`, `"min"`, `"max"`, `"extent"`, `"mean"`, `"median"`, `"unique"`,
75
+ `"uniqueCount"` and `"count"` are registered, as is `"auto"` - which picks
76
+ `sum` for numbers and `extent` for dates. A function is accepted too, with
77
+ TanStack's signature `(columnId, leafRows, childRows)`; `leafRows` are the
78
+ group's data rows, each record on `row.original`. Pass
79
+ `aggregatedCell` to render the group row's value differently from the data
80
+ rows.
81
+
82
+ > TanStack's grouping feature defaults every column to `aggregationFn: "auto"`.
83
+ > The grid clears that default so grouping does not silently start summing
84
+ > numeric columns. Setting `aggregationFn: "auto"` yourself restores it.
85
+
86
+ For a total across the whole grid rather than per group, give the column a
87
+ `footer` instead. See [Summary row](/docs/summary-row).
88
+
89
+ ### Sorting a grouped grid
90
+
91
+ Grouping runs before sorting, so sorting sorts the rows *inside* each group and
92
+ orders the groups by their aggregated value. A column with no aggregation has
93
+ no value on a group row, so sorting on it reorders the rows within each group
94
+ but leaves the groups where they are.
95
+
96
+ ## Selection
97
+
98
+ A group row's checkbox selects every record under it, at any depth, including
99
+ records inside collapsed sub-groups. It shows a tick once all of them are
100
+ selected and a dash while only some are.
101
+
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.
105
+
106
+ Under `enableMultiRowSelection: false` group rows carry no checkbox.
107
+
108
+ ## Group rows
109
+
110
+ A group row is built on its first child's record rather than on one of its own,
111
+ so it does not fire `onRowClick`, cannot be highlighted, cannot be
112
+ [pinned](/docs/row-pinning) and has no details panel.
113
+
114
+ `rowStyle` and `rowClassName` are the exception: they are called for group rows too, with that same child's record as `original`.
115
+ Guard a callback that reads `original` with `row.getIsGrouped()`, and set `--dg-row-group-bg` to colour the group rows themselves.
116
+
117
+ `data-grouped` is present, with the value `"true"`, only on group rows, so
118
+ `[data-grouped]` and `[data-grouped="true"]` are equivalent. `data-depth`
119
+ carries the nesting level, and `--dg-row-group-bg` sets a group row's
120
+ background.
121
+
122
+ ## Grouping and pagination
123
+
124
+ While a column is grouped the grid renders the whole tree and relies on
125
+ virtualization. `TMDataGrid.Footer` greys its pager out and replaces the range
126
+ with `Grouped · all N rows`. Ungroup and paging resumes where it left off.
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`.
130
+
131
+ `isPagingActive(table, features)` is exported, so a custom pager can grey itself
132
+ out the same way:
133
+
134
+ ```tsx
135
+ <TMDataGrid.Footer
136
+ renderPagination={({ state, actions }) => (
137
+ <MyPager {...state} {...actions} disabled={!state.isPagingActive} />
138
+ )}
139
+ />
140
+ ```
141
+
142
+ ## Server-side grids
143
+
144
+ `manualPagination: true` turns grouping off: the client holds one page, and
145
+ grouping it would build groups out of an arbitrary slice. A grid that groups
146
+ server-side can set `enableGrouping: true` alongside `manualGrouping: true`. See
147
+ [Server-side data](/docs/server-side).
148
+
149
+ ## Reference
150
+
151
+ | Name | Kind | Type | Default | What it does |
152
+ | --- | --- | --- | --- | --- |
153
+ | `enableGrouping` | Table option | `boolean` | `true` | Group by and Ungroup menu items. Also a column option. |
154
+ | `groupedColumnMode` | Table option | `"reorder" \| "remove" \| false` | `"remove"` | Whether a grouped column leaves the grid or moves to the front. |
155
+ | `manualGrouping` | Table option | `boolean` | `false` | The rows arrive grouped. Required to group a server-paged grid. |
156
+ | `initialState.grouping` | Table option | `string[]` | `[]` | Column ids to group on at mount. A settings slice, so it persists. |
157
+ | `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. |
159
+ | `GROUP_COLUMN_ID` | Export | `"__group__"` | – | Id of the generated tree column. |
160
+ | `formatGroupValue` | Export | `(value) => string` | – | How the tree lane renders a group's value. |
161
+ | `getGroupDataRows` | Export | `(row) => Row[]` | – | Every record under a group row, at any depth. |
162
+ | `isPagingActive` | Export | `(table, features) => boolean` | – | Whether the pager is slicing anything. `false` while grouped. |
163
+ | `--dg-row-group-bg` | CSS variable | colour | Themed | Group row background. |
164
+ | `data-grouped` | Data attribute | `"true"` | – | `"true"` on group rows. Absent on the rest. |
165
+ | `data-depth` | Data attribute | `number` | – | Nesting level, on every row. |