@jielga/tmdatagrid 2.1.4 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +38 -2
- package/dist/index.js +1329 -1246
- package/dist/index.js.map +1 -1
- package/docs/cell-selection.md +1 -1
- package/docs/column-layout.md +13 -8
- package/docs/columns.md +57 -2
- package/docs/export.md +2 -2
- package/docs/filtering.md +105 -15
- package/docs/getting-started.md +40 -0
- package/docs/grouping.md +9 -6
- package/docs/loading-and-empty.md +29 -9
- package/docs/localization.md +130 -3
- package/docs/menu.md +1 -1
- package/docs/pagination.md +26 -7
- package/docs/row-details.md +36 -4
- package/docs/row-interaction.md +1 -1
- package/docs/row-selection.md +6 -7
- package/docs/scrolling.md +1 -1
- package/docs/server-side.md +3 -1
- package/docs/testing.md +18 -0
- package/docs/use-tm-data-grid.md +69 -34
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +6 -4
- package/skills/appearance/references/labels.md +126 -0
- package/skills/cell-selection/SKILL.md +2 -2
- package/skills/columns/SKILL.md +42 -41
- package/skills/columns/references/columns-api.md +15 -1
- package/skills/data/SKILL.md +77 -50
- package/skills/editing/SKILL.md +1 -1
- package/skills/filtering/SKILL.md +71 -49
- package/skills/filtering/references/custom-filters.md +118 -0
- package/skills/getting-started/SKILL.md +37 -2
- package/skills/grouping/SKILL.md +16 -9
- package/skills/migrating-to-2/SKILL.md +1 -1
- package/skills/options/SKILL.md +131 -51
- package/skills/rows/SKILL.md +53 -31
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +12 -1
- package/skills/testing-components/SKILL.md +1 -1
- package/skills/testing-editing/SKILL.md +1 -1
- package/src/components/TMDataGridTable.tsx +241 -41
- package/src/core/autosize.ts +19 -7
- package/src/core/cellRange.ts +11 -2
- package/src/core/colSpan.ts +78 -0
- package/src/core/columnUtils.ts +7 -0
- package/src/core/filterOperators.ts +16 -1
- package/src/index.ts +4 -0
- package/src/useTMDataGrid.tsx +38 -0
package/skills/data/SKILL.md
CHANGED
|
@@ -8,7 +8,7 @@ description: >
|
|
|
8
8
|
canPaginate, always-on virtualization with overscan and meta.rowHeight,
|
|
9
9
|
scrollToRow, the edge callbacks onScrollToBottom / onScrollToRight and why
|
|
10
10
|
onReachEnd is better for loading more, the header and pinned-lane depth
|
|
11
|
-
shadows, and the
|
|
11
|
+
shadows, and the five empty states in precedence order with meta.loading,
|
|
12
12
|
renderEmptyState, hasActiveFilters, TMDataGrid.LoadingIndicator and
|
|
13
13
|
TMDataGrid.SummaryCount, and export: TMDataGrid.Menu.Export and
|
|
14
14
|
Menu.ExportSelected, useTMDataGridExport, exportGrid, exportOptions, the
|
|
@@ -18,7 +18,7 @@ description: >
|
|
|
18
18
|
metadata:
|
|
19
19
|
type: core
|
|
20
20
|
library: '@jielga/tmdatagrid'
|
|
21
|
-
library_version: '2.
|
|
21
|
+
library_version: '2.2.0'
|
|
22
22
|
sources:
|
|
23
23
|
- 'Jielga/react:packages/tmdatagrid/docs/pagination.md'
|
|
24
24
|
- 'Jielga/react:packages/tmdatagrid/docs/scrolling.md'
|
|
@@ -56,8 +56,14 @@ const grid = useTMDataGrid({
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Initial page size is 25, through `initialState.pagination`. `enablePagination`
|
|
59
|
-
is defined by the grid rather than by TanStack
|
|
60
|
-
|
|
59
|
+
is defined by the grid rather than by TanStack.
|
|
60
|
+
|
|
61
|
+
The Footer renders a rows-per-page select, the range (`1–25 of 300`, from
|
|
62
|
+
`table.getRowCount()`) and previous and next buttons; there are no numbered page
|
|
63
|
+
buttons. The select offers `pageSizeOptions`, by default `[10, 25, 50, 100]`,
|
|
64
|
+
with the current size folded in when it is not in the list. Pass a module-scope
|
|
65
|
+
array: the select is memoized on it, and an inline literal remounts it on every
|
|
66
|
+
render.
|
|
61
67
|
|
|
62
68
|
`TMDataGrid.Footer` takes a `renderPagination` slot handed three things: the
|
|
63
69
|
`state` the pager shows, the `actions` it can take, and `Controls`, the built-in
|
|
@@ -82,22 +88,26 @@ pieces, already wired.
|
|
|
82
88
|
|
|
83
89
|
`Controls.PageSize`, `Controls.Range` and `Controls.Pager` are what the default
|
|
84
90
|
footer renders, in that order, so a custom layout can keep the parts it wants
|
|
85
|
-
instead of rebuilding them. They behave
|
|
86
|
-
|
|
87
|
-
server-paged grid usually shows in place of
|
|
88
|
-
not in the default footer;
|
|
89
|
-
hand.
|
|
91
|
+
instead of rebuilding them. They behave as in the default footer, including
|
|
92
|
+
greying out while paging is suspended. `Controls.PageNumber` renders
|
|
93
|
+
"Page 3 of 200", the label a server-paged grid usually shows in place of the
|
|
94
|
+
row range. It is not in the default footer; add it through the slot.
|
|
90
95
|
|
|
91
96
|
`state` carries `pageIndex`, `pageSize`, `pageCount`, `rowCount`,
|
|
92
97
|
`canPreviousPage`, `canNextPage`, `from`, `to` and `isPagingActive`. `actions`
|
|
93
98
|
carries `setPageIndex`, `setPageSize`, `previousPage`, `nextPage`, `firstPage`
|
|
94
|
-
and `lastPage`.
|
|
95
|
-
`
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
and `lastPage`. Outside the Footer,
|
|
100
|
+
`getTMDataGridPaginationApi(table, isPagingActive(table, grid.features))`
|
|
101
|
+
returns the same `{ state, actions }`. Without the second argument
|
|
102
|
+
`state.isPagingActive` is `true`, also while the rows are grouped. `Controls`
|
|
103
|
+
is bound to the grid context, so those components work only inside the slot.
|
|
104
|
+
|
|
105
|
+
Grouping suspends the pager: the built-in controls grey out and the range
|
|
106
|
+
becomes `Grouped · all N rows`. The default footer also carries
|
|
107
|
+
`data-paging-suspended` on its bar and shows `labels.pagingSuspendedHint` as a
|
|
108
|
+
tooltip; a footer with `renderPagination` has neither. There is no option to
|
|
109
|
+
keep the built-in pager active while grouped. `isPagingActive(table, features)`
|
|
110
|
+
is the live state, whether the pager is slicing anything right now, while
|
|
101
111
|
`getGridCapabilities(...).canPaginate` is the configuration. The two differ
|
|
102
112
|
while a grouping is active.
|
|
103
113
|
|
|
@@ -141,14 +151,13 @@ arrival rather than on every scroll event. For loading more rows use
|
|
|
141
151
|
`onReachEnd` (the `server-side` skill) instead: it fires a number of rows before
|
|
142
152
|
the bottom, and latches per row count so a pending fetch is not requested twice.
|
|
143
153
|
|
|
144
|
-
Two depth cues
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
content.
|
|
154
|
+
Two depth cues run as scroll-driven CSS animations, with no scroll listener and
|
|
155
|
+
no React render. A shadow appears under the sticky header once rows scroll
|
|
156
|
+
beneath it. A band appears beside a pinned lane while it covers content.
|
|
148
157
|
|
|
149
158
|
## Empty states
|
|
150
159
|
|
|
151
|
-
There are
|
|
160
|
+
There are five ways to have nothing to show, and an empty body shows exactly one
|
|
152
161
|
of them, in this order:
|
|
153
162
|
|
|
154
163
|
1. **Loading** - `meta.loading` is true: a centred loader. A grid that is
|
|
@@ -156,9 +165,23 @@ of them, in this order:
|
|
|
156
165
|
2. **Entry rows** - an open entry row from `edit.addRow()`: the entry block
|
|
157
166
|
only, with no message beside the form.
|
|
158
167
|
3. **`renderEmptyState`** - your node, centred where the message would be.
|
|
159
|
-
4. **Filtered-empty** - a filter
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
4. **Filtered-empty** - a column filter is active (`isFilterActive`) or the
|
|
169
|
+
quick search is not blank: `labels.noResults`, since clearing the filter
|
|
170
|
+
will bring rows back.
|
|
171
|
+
5. **Truly-empty** - no column filter is active and the quick search is blank:
|
|
172
|
+
`labels.noRows`.
|
|
173
|
+
|
|
174
|
+
"Empty" means no displayed row and no pinned row. States 4 and 5 are decided by
|
|
175
|
+
the filter state alone, so an empty `data` array shows `noResults` while a
|
|
176
|
+
filter is active. A half-typed filter, such as
|
|
177
|
+
`{ operator: "contains", value: "" }`, is not active, so it does not count.
|
|
178
|
+
|
|
179
|
+
The loader is the only "no data yet" signal, so set `meta.loading` during the
|
|
180
|
+
first fetch too; without it the empty message shows until the rows arrive.
|
|
181
|
+
`renderEmptyState` is called only once `meta.loading` is not `true`: during a
|
|
182
|
+
refetch that returns no rows the loader mounts when `loading` turns `true`,
|
|
183
|
+
`renderEmptyState` mounts when it turns `false`, and unmounts again on the next
|
|
184
|
+
refetch.
|
|
162
185
|
|
|
163
186
|
`renderEmptyState` replaces the last two with one render prop, and
|
|
164
187
|
`hasActiveFilters` says which it is standing in for:
|
|
@@ -177,15 +200,14 @@ of them, in this order:
|
|
|
177
200
|
/>
|
|
178
201
|
```
|
|
179
202
|
|
|
180
|
-
An empty grid is a good place to offer the action that fills it, such as
|
|
181
|
-
clearing the filters or creating the first record.
|
|
182
|
-
|
|
183
203
|
The body's loading state only appears while the grid is **empty**. A
|
|
184
204
|
server-driven grid refetching with rows still on screen keeps showing them.
|
|
185
205
|
`TMDataGrid.LoadingIndicator` covers that case: a small spinner while
|
|
186
206
|
`meta.loading` is true. `TMDataGrid.SummaryCount` shows visible rows out of
|
|
187
|
-
total
|
|
188
|
-
count
|
|
207
|
+
total. The total is `meta.totalRowCount` when provided, otherwise the
|
|
208
|
+
pre-filtered count on a client-side grid; a server-driven grid
|
|
209
|
+
(`manualFiltering` or `manualPagination`) without `meta.totalRowCount` shows the
|
|
210
|
+
visible count alone, with no total.
|
|
189
211
|
|
|
190
212
|
## Export
|
|
191
213
|
|
|
@@ -214,15 +236,19 @@ function ExportButton() {
|
|
|
214
236
|
}
|
|
215
237
|
```
|
|
216
238
|
|
|
217
|
-
What is written:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
239
|
+
What is written:
|
|
240
|
+
|
|
241
|
+
- **Columns** - the data columns in render order. Never the generated lanes,
|
|
242
|
+
never a column with `meta.enableExport: false`.
|
|
243
|
+
- **Rows** - every row after filtering and sorting. A grouped grid writes the
|
|
244
|
+
records under every group, never the group rows.
|
|
245
|
+
- **Values** - each cell's **value** rather than what it renders.
|
|
246
|
+
`meta.exportValue: ({ value, row, column }) => unknown` substitutes one.
|
|
247
|
+
|
|
222
248
|
`columns` on `exportOptions`, the items and the functions is `"visible"` (the
|
|
223
|
-
default), `"all"` (hidden columns too) or a list of ids
|
|
224
|
-
|
|
225
|
-
ticked, Export and Cancel
|
|
249
|
+
default), `"all"` (hidden columns too) or a list of ids. `columns="custom"` on a
|
|
250
|
+
menu item opens a picker instead: every exportable column, the visible ones
|
|
251
|
+
ticked, Export and Cancel. The picker is driven by `ui.state.exportPicker` and
|
|
226
252
|
`ui.actions.openExportPicker`. `TMDataGrid.Menu.ExportSelected` counts and
|
|
227
253
|
writes the ticked rows of the current view, in grid order; it renders nothing
|
|
228
254
|
when row selection is off.
|
|
@@ -303,10 +329,11 @@ a pending fetch is not requested twice.
|
|
|
303
329
|
|
|
304
330
|
Source: `packages/tmdatagrid/docs/scrolling.md` (Edge callbacks).
|
|
305
331
|
|
|
306
|
-
### HIGH `SummaryCount`
|
|
332
|
+
### HIGH `SummaryCount` without a total under manual pagination
|
|
307
333
|
|
|
308
|
-
The client
|
|
309
|
-
|
|
334
|
+
The client holds one page, so a server-driven grid has no total of its own.
|
|
335
|
+
Without `meta.totalRowCount`, `SummaryCount` shows the visible count alone, with
|
|
336
|
+
no total, over a table of thousands.
|
|
310
337
|
|
|
311
338
|
Correct:
|
|
312
339
|
|
|
@@ -334,7 +361,7 @@ Correct:
|
|
|
334
361
|
useTMDataGrid({ data, columns, meta: { loading: isFetching } });
|
|
335
362
|
```
|
|
336
363
|
|
|
337
|
-
Source: `packages/tmdatagrid/docs/loading-and-empty.md` (
|
|
364
|
+
Source: `packages/tmdatagrid/docs/loading-and-empty.md` (Precedence).
|
|
338
365
|
|
|
339
366
|
### MEDIUM Trusting the pager while grouped
|
|
340
367
|
|
|
@@ -342,7 +369,7 @@ Source: `packages/tmdatagrid/docs/loading-and-empty.md` (What wins).
|
|
|
342
369
|
grid is rendering the whole tree. A custom pager must read
|
|
343
370
|
`isPagingActive(table, features)` rather than the page count.
|
|
344
371
|
|
|
345
|
-
Source: `packages/tmdatagrid/docs/pagination.md` (Grouping
|
|
372
|
+
Source: `packages/tmdatagrid/docs/pagination.md` (Grouping).
|
|
346
373
|
|
|
347
374
|
## Reference
|
|
348
375
|
|
|
@@ -351,31 +378,31 @@ Source: `packages/tmdatagrid/docs/pagination.md` (Grouping suspends it).
|
|
|
351
378
|
| `enablePagination` | Option | `boolean` | `false` | Client-side paging and the Footer's pager. Grid-defined. |
|
|
352
379
|
| `manualPagination` | Table option | `boolean` | `false` | The server pages. Implies `enablePagination`. |
|
|
353
380
|
| `rowCount` | Table option | `number` | – | The true total, required under `manualPagination`. |
|
|
354
|
-
| `initialState.pagination` | Table option | `{ pageIndex, pageSize }` | `{ 0, 25 }` | Where paging starts. A `data` slice, so it persists. |
|
|
381
|
+
| `initialState.pagination` | Table option | `{ pageIndex, pageSize }` | `{ pageIndex: 0, pageSize: 25 }` | Where paging starts. A `data` slice, so it persists. |
|
|
355
382
|
| `onPaginationChange` | Table option | `OnChangeFn` | – | Controls the pagination state. |
|
|
356
|
-
| `TMDataGrid.Footer` | Component | `pageSizeOptions`, `renderPagination`, Mantine `BoxProps` | `[10, 25, 50, 100]` | The footer bar. Renders nothing when paging is off. Style props set on the bar. |
|
|
383
|
+
| `TMDataGrid.Footer` | Component | `pageSizeOptions`, `renderPagination`, Mantine `BoxProps` | `pageSizeOptions`: `[10, 25, 50, 100]` | The footer bar. Renders nothing when paging is off. Style props set on the bar. |
|
|
357
384
|
| `TMDataGridFooterProps` | Type | – | – | The props of `TMDataGrid.Footer`. |
|
|
358
385
|
| `Footer` `renderPagination` | Slot | `(args: TMDataGridPaginationSlotArgs) => ReactNode` | Built-in pager | Replaces the pager, and hands over its pieces. |
|
|
359
386
|
| `TMDataGridPaginationSlotArgs` | Type | `TMDataGridPaginationApi & { Controls }` | – | What `renderPagination` receives: `{ state, actions, Controls }`. |
|
|
360
|
-
| `getTMDataGridPaginationApi` | Export | `(table, isPaging?) => TMDataGridPaginationApi` | `isPaging`: `true` | The pager API, outside the Footer. |
|
|
387
|
+
| `getTMDataGridPaginationApi` | Export | `(table, isPaging?) => TMDataGridPaginationApi` | `isPaging`: `true` | The pager API, outside the Footer. Pass `isPagingActive(table, features)` as `isPaging`. |
|
|
361
388
|
| `TMDataGridPaginationApi` | Type | `{ state, actions }` | – | What `getTMDataGridPaginationApi` returns. |
|
|
362
389
|
| `TMDataGridPaginationState` · `TMDataGridPaginationActions` · `TMDataGridPaginationControls` | Exports | types | – | The three parts of the slot argument. |
|
|
363
390
|
| `isPagingActive` | Export | `(table, features) => boolean` | – | Whether the pager is slicing anything right now. |
|
|
364
391
|
| `overscan` | Option | `number` | `6` | Rows kept mounted beyond each edge of the viewport. |
|
|
365
|
-
| `meta.rowHeight` | Option | `number` |
|
|
392
|
+
| `meta.rowHeight` | Option | `number` | `52` at the default `md` size; see the `size` scale in the `appearance` skill | Row height in pixels. The virtualizer needs a number. |
|
|
366
393
|
| `scrollToRow` | Hook return | `({ rowId, align? }) => boolean` | `align: "auto"` | Scrolls a row into view, mounted or not. |
|
|
367
394
|
| `TMDataGridScrollToRowArgs` | Export | `{ rowId, align? }` | – | What `scrollToRow` takes. |
|
|
368
395
|
| `onScrollToTop` · `onScrollToBottom` · `onScrollToLeft` · `onScrollToRight` | Table props | `() => void` | – | Fire once on arriving at that edge. |
|
|
369
396
|
| `TMDataGridScrollAlign` | Export | `"start" \| "center" \| "end" \| "auto"` | – | The `align` argument. |
|
|
370
|
-
| `meta.loading` | Option | `boolean` | `false` | A fetch is in flight.
|
|
397
|
+
| `meta.loading` | Option | `boolean` | `false` | A fetch is in flight. While `true` and the view is empty, the loader replaces every empty message and `renderEmptyState` is not called. |
|
|
371
398
|
| `meta.noResultsLabel` | Option | `string` | `labels.noResults` | The filtered-empty message, without a render prop. |
|
|
372
|
-
| `meta.totalRowCount` | Option | `number` | Pre-filtered count | The total `SummaryCount` reports. |
|
|
373
|
-
| `renderEmptyState` | Table prop | `({ hasActiveFilters, table }) => ReactNode` | – | Replaces both built-in empty messages. |
|
|
399
|
+
| `meta.totalRowCount` | Option | `number` | Pre-filtered count; none on a server-driven grid | The total `SummaryCount` reports. |
|
|
400
|
+
| `renderEmptyState` | Table prop | `({ hasActiveFilters, table }) => ReactNode` | – | Replaces both built-in empty messages. `hasActiveFilters` is `true` when a column filter is active (`isFilterActive`) or the quick search is not blank. |
|
|
374
401
|
| `TMDataGrid.LoadingIndicator` | Component | – | – | Spinner while `meta.loading`, for when the body has rows. |
|
|
375
402
|
| `TMDataGrid.SummaryCount` | Component | `children` replaces the text | – | Visible rows out of total. |
|
|
376
403
|
| `--dg-header-shadow-color` | CSS variable | colour | Themed | The shadow under the sticky header. |
|
|
377
404
|
| `--dg-sticky-edge-range` | CSS variable | length | `20px` | How far the pinned-lane band takes to fade in. |
|
|
378
|
-
| `exportOptions` | Option | `TMDataGridExportOptions` | `
|
|
405
|
+
| `exportOptions` | Option | `TMDataGridExportOptions` | `csvExcelFormat()`, `"export"`, headers on, `"visible"` columns | Format, file name and header row for every export of the grid. |
|
|
379
406
|
| `meta.enableExport` | Column meta | `boolean` | `true` | `false` leaves the column out of every export and of Ctrl+C. |
|
|
380
407
|
| `meta.exportValue` | Column meta | `TMDataGridExportValueGetter`: `({ value, row, column }) => unknown` | – | The value written in place of `row.getValue`. |
|
|
381
408
|
| `TMDataGrid.Menu.Export` · `TMDataGrid.Menu.ExportSelected` | Components | `TMDataGridMenuExportProps` | – | Menu items: every filtered row, and the selected rows. `columns="custom"` opens the picker. |
|
package/skills/editing/SKILL.md
CHANGED
|
@@ -18,7 +18,7 @@ description: >
|
|
|
18
18
|
metadata:
|
|
19
19
|
type: core
|
|
20
20
|
library: '@jielga/tmdatagrid'
|
|
21
|
-
library_version: '2.
|
|
21
|
+
library_version: '2.2.0'
|
|
22
22
|
sources:
|
|
23
23
|
- 'Jielga/react:packages/tmdatagrid/docs/filtering.md'
|
|
24
24
|
- 'Jielga/react:packages/tmdatagrid/docs/quick-search.md'
|
|
@@ -50,11 +50,12 @@ cell is tested against) and `between` (a `[min, max]` pair, an empty string
|
|
|
50
50
|
leaving that end open).
|
|
51
51
|
|
|
52
52
|
A filter with an empty value **stays in state** so the panel keeps its row while
|
|
53
|
-
the user types.
|
|
54
|
-
|
|
55
|
-
`
|
|
56
|
-
|
|
57
|
-
`columnFilters`
|
|
53
|
+
the user types. The built-in matcher lets every row through, and the filter
|
|
54
|
+
does not set the header indicator and produces no pill.
|
|
55
|
+
`isFilterActive(value)` tells whether a value narrows anything.
|
|
56
|
+
`activeColumnFilters(columnFilters | table)` returns the active entries, typed,
|
|
57
|
+
and drops entries in any other value shape. An entry in `columnFilters` is not
|
|
58
|
+
necessarily active.
|
|
58
59
|
|
|
59
60
|
## Operators
|
|
60
61
|
|
|
@@ -106,8 +107,8 @@ columnHelper.accessor("customer", {
|
|
|
106
107
|
|
|
107
108
|
## The filters option
|
|
108
109
|
|
|
109
|
-
`filters` on `useTMDataGrid` picks the surface. It is read field by field, so
|
|
110
|
-
literal
|
|
110
|
+
`filters` on `useTMDataGrid` picks the surface. It is read field by field, so an
|
|
111
|
+
inline object literal needs no `useMemo`.
|
|
111
112
|
|
|
112
113
|
| Option | Type | Default | What it does |
|
|
113
114
|
| --- | --- | --- | --- |
|
|
@@ -132,13 +133,14 @@ close it; `FilterButton` is exempt from the click-away so it stays a toggle.
|
|
|
132
133
|
the toolbar. The rows give up the width rather than being covered, a click in
|
|
133
134
|
the table does not dismiss it, and clearing the filters leaves it standing.
|
|
134
135
|
Escape closes it. It starts open, being a layout choice; its rows are
|
|
135
|
-
`layout="stacked"`, since
|
|
136
|
+
`layout="stacked"`, since the default 280px cannot fit the column, operator and
|
|
137
|
+
value fields side by side.
|
|
136
138
|
|
|
137
139
|
**Header filters** - `inHeader: true` adds a header row of value controls, one
|
|
138
140
|
per filterable column, on the same column tracks as everything else. The
|
|
139
141
|
column and operator dropdowns are not there: the column is the one the cell
|
|
140
|
-
sits over, and the operator is a funnel button beside the input, tinted
|
|
141
|
-
default. The column menu's Filter item and the funnel indicator both come off,
|
|
142
|
+
sits over, and the operator is a funnel button beside the input, tinted when
|
|
143
|
+
the operator is not the type's default. The column menu's Filter item and the funnel indicator both come off,
|
|
142
144
|
having nothing left to reveal; the filtered column's tinted title stays. A
|
|
143
145
|
narrow column clips its control - give it a `minSize`.
|
|
144
146
|
|
|
@@ -158,7 +160,7 @@ hand-placed `TMDataGrid.FilterPanel` is the only one. Read
|
|
|
158
160
|
|
|
159
161
|
The panel of filter rows - one column / operator / value triple per filter, over "Add filter" and "Clear all".
|
|
160
162
|
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.
|
|
161
|
-
It must be inside `<TMDataGrid>` (it reads the grid from context) and it only reads and writes `columnFilters`, so
|
|
163
|
+
It must be inside `<TMDataGrid>` (it reads the grid from context) and it only reads and writes `columnFilters`, so it works unchanged under `manualFiltering`.
|
|
162
164
|
Closing a surface only hides it; the filters stay.
|
|
163
165
|
**Clear all** drops every filter, half-typed ones included.
|
|
164
166
|
|
|
@@ -201,7 +203,8 @@ import { TMDataGridFilterPills } from "@jielga/tmdatagrid";
|
|
|
201
203
|
|
|
202
204
|
## openColumnFilter
|
|
203
205
|
|
|
204
|
-
`openColumnFilter(api, columnId)` seeds an empty filter if the column has none, then opens the surface on that column's panel row
|
|
206
|
+
`openColumnFilter(api, columnId)` seeds an empty filter if the column has none, then opens the surface on that column's panel row.
|
|
207
|
+
Under `inHeader` it instead scrolls the column's header control into view and focuses it, leaving the surface closed.
|
|
205
208
|
It is what a pill's label and the column menu's Filter item both call.
|
|
206
209
|
|
|
207
210
|
## Replacing the value control
|
|
@@ -232,41 +235,47 @@ the same way.
|
|
|
232
235
|
inside. It handles the value only: it reads `operator` to shape itself and
|
|
233
236
|
writes the bare value through `onChange`, and the grid stores the
|
|
234
237
|
`{ operator, value }` pair around it. The column and operator dropdowns remain
|
|
235
|
-
the panel's.
|
|
236
|
-
|
|
237
|
-
```tsx
|
|
238
|
-
import type { TMDataGridFilterControlComponent } from "@jielga/tmdatagrid";
|
|
239
|
-
|
|
240
|
-
// Module scope: a new identity per render remounts the control mid-typing.
|
|
241
|
-
const StatusFilter: TMDataGridFilterControlComponent = ({
|
|
242
|
-
operator,
|
|
243
|
-
value,
|
|
244
|
-
options,
|
|
245
|
-
onChange,
|
|
246
|
-
}) => (
|
|
247
|
-
<Chip.Group
|
|
248
|
-
multiple
|
|
249
|
-
value={Array.isArray(value) ? [...value] : []}
|
|
250
|
-
onChange={onChange}
|
|
251
|
-
>
|
|
252
|
-
{options.map((option) => (
|
|
253
|
-
<Chip key={option.value} value={option.value}>
|
|
254
|
-
{option.label}
|
|
255
|
-
</Chip>
|
|
256
|
-
))}
|
|
257
|
-
</Chip.Group>
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
meta: { type: "select", filter: { control: StatusFilter } }
|
|
261
|
-
```
|
|
238
|
+
the panel's. Worked example: `references/custom-filters.md` (A value control of
|
|
239
|
+
your own).
|
|
262
240
|
|
|
263
241
|
`args.options` arrives pre-resolved for a column that declares `meta.options` or
|
|
264
242
|
is select-shaped. `args.table` is available to a control that needs more than
|
|
265
243
|
that.
|
|
266
244
|
|
|
245
|
+
## Writing filters from your own interface
|
|
246
|
+
|
|
247
|
+
The grid's controls write plain TanStack state, so a control of your own does
|
|
248
|
+
the same: `column.setFilterValue({ operator, value })` or
|
|
249
|
+
`table.setColumnFilters([...])` with an envelope, and the matcher, the pills,
|
|
250
|
+
the header indicator and `activeColumnFilters` all read it.
|
|
251
|
+
`setFilterValue(undefined)` removes the entry; an empty envelope stays in state
|
|
252
|
+
and matches every row. `getColumnOperators(column)`,
|
|
253
|
+
`getColumnDefaultOperator(column)` and `emptyValueForOperator(operator)` build
|
|
254
|
+
a valid envelope. To keep the grid's panel and place it yourself, set
|
|
255
|
+
`filters: { surface: "none" }` and render `TMDataGrid.FilterPanel`. To drop
|
|
256
|
+
only the column menu's Filter item, pass `renderColumnMenuItems` to
|
|
257
|
+
`TMDataGrid.Table` and leave out the item keyed `"filter"`. To replace every
|
|
258
|
+
filter control, set `enableColumnFilters: false` and write the envelope
|
|
259
|
+
yourself; filters in state still apply. Worked examples:
|
|
260
|
+
`references/custom-filters.md`.
|
|
261
|
+
|
|
262
|
+
## Custom matching
|
|
263
|
+
|
|
267
264
|
To give a column its own *matching* instead of its own control, set `filterFn`
|
|
268
|
-
on the column definition.
|
|
269
|
-
|
|
265
|
+
on the column definition. A column `filterFn` **replaces the shared matcher**
|
|
266
|
+
for that column, so its third argument is the stored `{ operator, value }`
|
|
267
|
+
envelope, not the unwrapped value. Type it as `TMDataGridFilterFn<TData>` and
|
|
268
|
+
delegate to `tmDataGridFilterFn(row, columnId, filter)` for the operators it
|
|
269
|
+
does not handle; `matchesFilter(cellValue, filter)` applies the built-in
|
|
270
|
+
matching to a derived value, and `filterValueShape(operator)` tells whether
|
|
271
|
+
`value` is a string, a `ReadonlyArray<string>` (`isAnyOf` / `isNoneOf`) or a
|
|
272
|
+
`[min, max]` pair (`between`). A column `filterFn` is also called with
|
|
273
|
+
half-typed envelopes, which the built-in matcher treats as matching every row,
|
|
274
|
+
so return `true` for `!isFilterActive(filter)` first. The column's controls do
|
|
275
|
+
not change and keep reading the envelope. A filter function named by string
|
|
276
|
+
also receives the envelope and does not unwrap it, so TanStack's built-ins such
|
|
277
|
+
as `"includesString"` do not work as a column `filterFn`. Write custom matching
|
|
278
|
+
as a function. Worked example: `references/custom-filters.md`.
|
|
270
279
|
|
|
271
280
|
## Quick search
|
|
272
281
|
|
|
@@ -298,10 +307,20 @@ excluded.
|
|
|
298
307
|
|
|
299
308
|
## Turning it off
|
|
300
309
|
|
|
301
|
-
`enableColumnFilters: false` removes the
|
|
302
|
-
`
|
|
303
|
-
|
|
304
|
-
|
|
310
|
+
`enableColumnFilters: false` removes the Filter item from the column menu, the
|
|
311
|
+
`FilterButton`, the header filter row and the panel's column list, and disables
|
|
312
|
+
the panel's "Add filter" button. It does **not** clear `columnFilters`: an entry already in
|
|
313
|
+
state, or one written with `setFilterValue`, still narrows the rows, still shows
|
|
314
|
+
as a pill and still sets the header indicator. Call `table.resetColumnFilters()`
|
|
315
|
+
when switching the interface off. Under `filters.surface: "sidebar"`, set
|
|
316
|
+
`surface: "none"` as well, as the sidebar does not follow the switch.
|
|
317
|
+
`TMDataGrid.Menu` has no filter items, so the switch changes nothing there.
|
|
318
|
+
|
|
319
|
+
`enableColumnFilter: false` on a column removes that column's menu item, its
|
|
320
|
+
entry in the panel's column list and its header control - the header cell
|
|
321
|
+
stays, empty. `enableGlobalFilter: false` removes the search input, or on a
|
|
322
|
+
column removes its participation in the search. The generated lanes are already
|
|
323
|
+
excluded.
|
|
305
324
|
|
|
306
325
|
## Common mistakes
|
|
307
326
|
|
|
@@ -417,17 +436,20 @@ Source: `packages/tmdatagrid/docs/quick-search.md` (Fuzzy by default).
|
|
|
417
436
|
|
|
418
437
|
| Name | Kind | Type | Default | What it does |
|
|
419
438
|
| --- | --- | --- | --- | --- |
|
|
420
|
-
| `enableColumnFilters` | Table option | `boolean` | `true` | `false` removes the
|
|
439
|
+
| `enableColumnFilters` | Table option | `boolean` | `true` | `false` removes the menu item, the button, the header row and the panel's column list, and disables the panel's "Add filter" button. Filters already in state still apply. |
|
|
421
440
|
| `enableColumnFilter` | Column option | `boolean` | `true` | `false` takes one column out of filtering. |
|
|
422
441
|
| `meta.type` | Column meta | `TMDataGridColumnType` | `"string"` | Selects the operators and the value control. |
|
|
423
442
|
| `meta.filter.operators` | Column meta | `readonly TMDataGridFilterOperator[]` | The type's list | The operators this column offers, a subset of its type's. |
|
|
424
443
|
| `meta.filter.defaultOperator` | Column meta | `TMDataGridFilterOperator` | The type's default, else the first offered | The operator a fresh filter opens on. |
|
|
425
444
|
| `meta.filter.control` | Column meta | `TMDataGridFilterControlComponent` | By type and operator | Replaces the value control. Module scope. |
|
|
426
|
-
| `filterFn` | Column option |
|
|
445
|
+
| `filterFn` | Column option | `TMDataGridFilterFn<TData>` or name | `"tmDataGrid"` | Custom matching for one column. Receives the `{ operator, value }` envelope. |
|
|
446
|
+
| `TMDataGridFilterFn<TData>` | Type | `(row, columnId, filter: TMDataGridFilterValue, addMeta?) => boolean` | – | The signature of a column's own `filterFn`. |
|
|
447
|
+
| `tmDataGridFilterFn` | Export | `(row, columnId, filterValue: unknown) => boolean`, assignable to `TMDataGridFilterFn<TData>` | – | The shared matcher, for a custom `filterFn` to delegate to. |
|
|
448
|
+
| `matchesFilter` | Export | `(cellValue, filter: TMDataGridFilterValue) => boolean` | – | The built-in matching applied to one value. |
|
|
427
449
|
| `quickSearchMode` | Option | `TMDataGridQuickSearchMode`: `"fuzzy" \| "contains"` | `"fuzzy"` | How the quick search matches. |
|
|
428
450
|
| `enableMatchHighlighting` | Option | `boolean` | `false` | Mark matched text in default-rendered cells. |
|
|
429
451
|
| `enableGlobalFilter` | Table option | `boolean` | `true` | Also a column option. Removes the input, or one column's participation. |
|
|
430
|
-
| `globalFilterFn` | Table option | filter fn |
|
|
452
|
+
| `globalFilterFn` | Table option | filter fn or name | `"tmDataGridFuzzy"`; `"includesString"` under `quickSearchMode: "contains"` | Overrides the matching, and the ranking with it. |
|
|
431
453
|
| `TMDataGrid.FilterPanel` | Component | `layout: "row" \| "stacked"`, Mantine `BoxProps` | `"row"` | The panel of filter rows, as a plain block. Style props set on it. |
|
|
432
454
|
| `TMDataGridFilterPanelProps` · `TMDataGridFilterPanelLayout` | Types | – · `"row" \| "stacked"` | – | The props of `TMDataGrid.FilterPanel`, and the type of its `layout`. For wrapping the panel in a component of your own. |
|
|
433
455
|
| `filters` | Table option | `TMDataGridFiltersOptions` | `{ surface: "popup" }` | Which surface holds the filter controls. |
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Custom filters
|
|
2
|
+
|
|
3
|
+
Worked examples for the three ways to take over part of TMDataGrid's filtering: a value control of your own, writing the filter state from an interface of your own, and matching a column with a function of your own.
|
|
4
|
+
All three keep the `{ operator, value }` envelope, so the panel, the pills, the header indicator and `activeColumnFilters` keep working.
|
|
5
|
+
|
|
6
|
+
## A value control of your own
|
|
7
|
+
|
|
8
|
+
`meta.filter.control` is a component that handles the value only: it reads `operator` to shape itself and writes the bare value through `onChange`.
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import type { TMDataGridFilterControlComponent } from "@jielga/tmdatagrid";
|
|
12
|
+
|
|
13
|
+
// Module scope: a new identity per render remounts the control mid-typing.
|
|
14
|
+
const StatusFilter: TMDataGridFilterControlComponent = ({
|
|
15
|
+
operator,
|
|
16
|
+
value,
|
|
17
|
+
options,
|
|
18
|
+
onChange,
|
|
19
|
+
}) => (
|
|
20
|
+
<Chip.Group
|
|
21
|
+
multiple
|
|
22
|
+
value={Array.isArray(value) ? [...value] : []}
|
|
23
|
+
onChange={onChange}
|
|
24
|
+
>
|
|
25
|
+
{options.map((option) => (
|
|
26
|
+
<Chip key={option.value} value={option.value}>
|
|
27
|
+
{option.label}
|
|
28
|
+
</Chip>
|
|
29
|
+
))}
|
|
30
|
+
</Chip.Group>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
meta: { type: "select", filter: { control: StatusFilter } }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Writing filters from your own interface
|
|
37
|
+
|
|
38
|
+
The grid's controls write plain TanStack state, so a control of your own does the same:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { emptyValueForOperator, type TMDataGridFilterValue } from "@jielga/tmdatagrid";
|
|
42
|
+
|
|
43
|
+
const status = grid.table.getColumn("status")!;
|
|
44
|
+
|
|
45
|
+
// A set filter, as the panel would write it.
|
|
46
|
+
status.setFilterValue({
|
|
47
|
+
operator: "isAnyOf",
|
|
48
|
+
value: ["active", "paused"],
|
|
49
|
+
} satisfies TMDataGridFilterValue);
|
|
50
|
+
|
|
51
|
+
// An empty envelope stays in state and matches every row.
|
|
52
|
+
status.setFilterValue({ operator: "isAnyOf", value: emptyValueForOperator("isAnyOf") });
|
|
53
|
+
|
|
54
|
+
// `undefined` removes the entry.
|
|
55
|
+
status.setFilterValue(undefined);
|
|
56
|
+
|
|
57
|
+
// Several columns at once.
|
|
58
|
+
grid.table.setColumnFilters([
|
|
59
|
+
{ id: "status", value: { operator: "equals", value: "active" } },
|
|
60
|
+
{ id: "salary", value: { operator: "between", value: ["40000", ""] } },
|
|
61
|
+
]);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`getColumnOperators(column)` returns the operators a column offers and `getColumnDefaultOperator(column)` the one a fresh filter opens on.
|
|
65
|
+
`emptyValueForOperator(operator)` returns the empty value of the right shape.
|
|
66
|
+
|
|
67
|
+
To keep the grid's panel and place it yourself, set `filters: { surface: "none" }` and render `TMDataGrid.FilterPanel` where it belongs.
|
|
68
|
+
To keep filtering on and drop only the column menu's Filter item, pass `renderColumnMenuItems` to `TMDataGrid.Table` and leave out the item keyed `"filter"`.
|
|
69
|
+
To replace every filter control, set `enableColumnFilters: false` and write the envelope yourself; filters in state still apply.
|
|
70
|
+
|
|
71
|
+
## Custom matching
|
|
72
|
+
|
|
73
|
+
A column `filterFn` replaces the shared matcher for that column, so its third argument is the stored envelope, not the unwrapped value.
|
|
74
|
+
Type it as `TMDataGridFilterFn<TData>` and delegate to `tmDataGridFilterFn` for the operators it does not handle itself.
|
|
75
|
+
A column `filterFn` is also called with half-typed envelopes, which the built-in matcher treats as matching every row, so return `true` for `!isFilterActive(filter)` first:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import {
|
|
79
|
+
isFilterActive,
|
|
80
|
+
tmDataGridFilterFn,
|
|
81
|
+
type TMDataGridFilterFn,
|
|
82
|
+
} from "@jielga/tmdatagrid";
|
|
83
|
+
|
|
84
|
+
// Reads "contains" as "has this tag", so "ops" matches the tag "ops" and not
|
|
85
|
+
// "devops"; every other operator keeps the built-in matching.
|
|
86
|
+
const matchesTags: TMDataGridFilterFn<Employee> = (row, columnId, filter) => {
|
|
87
|
+
if (!isFilterActive(filter)) return true;
|
|
88
|
+
if (filter.operator === "contains") {
|
|
89
|
+
const wanted = String(filter.value).toLowerCase();
|
|
90
|
+
return row.original.tags.some((tag) => tag.toLowerCase() === wanted);
|
|
91
|
+
}
|
|
92
|
+
return tmDataGridFilterFn(row, columnId, filter);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
columnHelper.accessor((row) => row.tags.join(", "), {
|
|
96
|
+
id: "tags",
|
|
97
|
+
header: "Tags",
|
|
98
|
+
filterFn: matchesTags,
|
|
99
|
+
meta: { type: "string" },
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`filter.value` is a string under the scalar operators, a `ReadonlyArray<string>` under `isAnyOf` / `isNoneOf` and a `[min, max]` pair under `between`; `filterValueShape(operator)` tells which.
|
|
104
|
+
`matchesFilter(cellValue, filter)` applies the built-in matching to a value derived by the function itself:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { isFilterActive, matchesFilter, type TMDataGridFilterFn } from "@jielga/tmdatagrid";
|
|
108
|
+
|
|
109
|
+
// Matches the filter against the owner's full name instead of the stored id.
|
|
110
|
+
const matchesOwner: TMDataGridFilterFn<Task> = (row, _columnId, filter) => {
|
|
111
|
+
if (!isFilterActive(filter)) return true;
|
|
112
|
+
return matchesFilter(ownerName(row.original.ownerId), filter);
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The column's filter controls do not change: the operators still come from `meta.type` and `meta.filter.operators`, and the panel, the pills and the header indicator keep reading the envelope.
|
|
117
|
+
A filter function named by string also receives the envelope and does not unwrap it, so TanStack's built-ins such as `"includesString"` do not work as a column `filterFn`.
|
|
118
|
+
Write custom matching as a function.
|
|
@@ -13,7 +13,7 @@ description: >
|
|
|
13
13
|
metadata:
|
|
14
14
|
type: core
|
|
15
15
|
library: '@jielga/tmdatagrid'
|
|
16
|
-
library_version: '2.
|
|
16
|
+
library_version: '2.2.0'
|
|
17
17
|
sources:
|
|
18
18
|
- 'Jielga/react:packages/tmdatagrid/docs/getting-started.md'
|
|
19
19
|
- 'Jielga/react:packages/tmdatagrid/docs/anatomy.md'
|
|
@@ -157,7 +157,7 @@ flex column.
|
|
|
157
157
|
| --- | --- | --- |
|
|
158
158
|
| `TMDataGrid` | `table`, `ui`, `features`, `size`, `className`, `style` | Root. Provides context. `style` also takes CSS variables: `--dg-row-selected-bg`, `--dg-row-height`, `--dg-header-height`, `--dg-font-size`, `--dg-padding`. |
|
|
159
159
|
| `TMDataGrid.Table` | `onRowClick(row)`, `renderRowContextMenu`, `renderColumnMenuItems`, `rowContextMenuProps` | Header, virtualized body, and whichever filter surface the `filters` option asks for. `onRowClick` runs in addition to selection under `selectionMode: "row"`. |
|
|
160
|
-
| `TMDataGrid.Footer` | `pageSizeOptions` (default `[10, 25, 50, 100]
|
|
160
|
+
| `TMDataGrid.Footer` | `pageSizeOptions` (default `[10, 25, 50, 100]`, keep the array stable), `renderPagination` slot | Rows-per-page select, range and previous/next buttons. Renders nothing unless pagination is enabled. |
|
|
161
161
|
| `TMDataGrid.Toolbar` | `children` | Flex row above the grid. |
|
|
162
162
|
| `TMDataGrid.Spacer` | - | Pushes later toolbar items right. |
|
|
163
163
|
| `TMDataGrid.SummaryCount` | `children` | Visible rows out of total. |
|
|
@@ -360,6 +360,41 @@ Source: `packages/tmdatagrid/docs/card-view.md`,
|
|
|
360
360
|
| `moveColumnByStep({ table, columnId, direction })` | Moves a column one position within its region. |
|
|
361
361
|
| `SELECT_COLUMN_ID` | Id of the generated checkbox column. |
|
|
362
362
|
|
|
363
|
+
## Coming from TanStack Table
|
|
364
|
+
|
|
365
|
+
`useTMDataGrid` takes TanStack's `TableOptions`, so a page built on
|
|
366
|
+
`useReactTable` keeps its `data`, `columns`, `state` and `onXChange` callbacks.
|
|
367
|
+
Go through these differences:
|
|
368
|
+
|
|
369
|
+
- **`getRowId`** - return the entity id. The index default points at another
|
|
370
|
+
record after a sort or a refetch, which breaks selection, expansion and
|
|
371
|
+
editing. Required once `editing` is set.
|
|
372
|
+
- **Stable `data` and `columns`** - define `columns` outside the component and
|
|
373
|
+
keep `data` referentially stable; a fresh `[]` for the empty case belongs in
|
|
374
|
+
a module-scope constant. Under `editing`, a transient `[]` drops every open
|
|
375
|
+
draft.
|
|
376
|
+
- **Sizing** - columns are fluid, `minmax(minSize, flex fr)`. `size` applies
|
|
377
|
+
only to a column pinned at mount, `maxSize` only to resizing and autosizing,
|
|
378
|
+
and `minSize` defaults to `80`. See the `columns` skill.
|
|
379
|
+
- **Defaults the grid changes** - `aggregationFn` is cleared, so grouping does
|
|
380
|
+
not sum anything on its own; `groupedColumnMode` is `"remove"`;
|
|
381
|
+
`autoResetExpanded` and `autoResetPageIndex` are off; `globalFilterFn` is the
|
|
382
|
+
fuzzy matcher. The full list is in the
|
|
383
|
+
`options` skill.
|
|
384
|
+
- **Filters are envelopes** - `columnFilters` holds `{ operator, value }`, and
|
|
385
|
+
a column's own `filterFn` receives that envelope. See the `filtering` skill.
|
|
386
|
+
- **Interface the page already owns** - `enableGlobalFilter: false`, or no
|
|
387
|
+
`TMDataGrid.Search`, for the search; `enableColumnFilters: false`, or
|
|
388
|
+
`filters: { surface: "none" }` to keep filtering without the grid's panel.
|
|
389
|
+
- **Page reset** - `resetPageOnQueryChange` (default `true`) returns to the
|
|
390
|
+
first page when a filter, the search, the sorting or the grouping changes
|
|
391
|
+
through the table. A data source re-filtered outside table state does not
|
|
392
|
+
trigger it; call `table.setPageIndex(0)` yourself, or use `manualFiltering`.
|
|
393
|
+
- **State reads** - there is no `table.getState()`; read slices with
|
|
394
|
+
`useSelector(table.store, ...)`. See the `options` skill.
|
|
395
|
+
- **Tests** - the grid publishes `data-dg-part`, `data-row-id` and
|
|
396
|
+
`data-column-id`, and no `data-testid`. See the `testing` skill.
|
|
397
|
+
|
|
363
398
|
## Common mistakes
|
|
364
399
|
|
|
365
400
|
### Columns or data defined during render
|