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