@jielga/tmdatagrid 2.0.0-beta.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -212
- package/dist/index.d.ts +1323 -796
- package/dist/index.js +4719 -3193
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/adding-rows.md +132 -0
- package/docs/anatomy.md +119 -0
- package/docs/card-view.md +108 -0
- package/docs/cell-selection.md +194 -0
- package/docs/column-layout.md +182 -0
- package/docs/column-menu.md +66 -0
- package/docs/columns.md +268 -0
- package/docs/components.md +311 -0
- package/docs/draft-store.md +242 -0
- package/docs/editing.md +303 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +319 -0
- package/docs/filtering.md +362 -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/migrating-to-2.md +163 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +114 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +179 -0
- package/docs/quick-search.md +84 -0
- package/docs/row-details.md +115 -0
- package/docs/row-interaction.md +149 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +136 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +112 -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 +744 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +22 -46
- package/skills/appearance/SKILL.md +72 -19
- package/skills/cell-selection/SKILL.md +69 -78
- package/skills/columns/SKILL.md +90 -34
- package/skills/data/SKILL.md +86 -16
- package/skills/editing/SKILL.md +83 -50
- package/skills/editing/references/common-mistakes.md +77 -69
- package/skills/editing/references/editing-api.md +31 -23
- package/skills/editing/references/editors-and-validation.md +24 -17
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +17 -15
- package/skills/grouping/SKILL.md +31 -16
- package/skills/options/SKILL.md +8 -8
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +150 -32
- package/skills/testing-components/SKILL.md +230 -0
- package/skills/testing-editing/SKILL.md +240 -0
- 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 +38 -21
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +73 -7
- 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 +9 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +23 -63
- package/src/components/TMDataGridEntryRows.tsx +354 -0
- 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 +15 -8
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +9 -72
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +357 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +15 -53
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +88 -65
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +579 -165
- package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -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/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
- 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 +16 -2
- 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/components/generatedColumns.tsx +187 -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 +5 -5
- package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/{tmdatagrid/core → core}/editEngine.ts +1172 -388
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +704 -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}/grouping.ts +21 -0
- package/src/{tmdatagrid/core → core}/labels.ts +51 -6
- package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
- package/src/core/pageReset.ts +120 -0
- package/src/core/pagination.ts +81 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/{tmdatagrid/core → core}/summary.ts +20 -4
- package/src/{tmdatagrid/index.ts → index.ts} +70 -36
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +534 -123
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +0 -298
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- /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}/controlledState.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}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/resizePreview.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
- /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Persistence
|
|
2
|
+
|
|
3
|
+
Restores table state on mount and writes it back on every change, so users
|
|
4
|
+
return to the grid as they left it.
|
|
5
|
+
|
|
6
|
+
State is split across **two keys**: `settingsKey` for the column layout, which
|
|
7
|
+
stays valid indefinitely, and `dataKey` for filters, sorting and pagination,
|
|
8
|
+
which go stale as the data underneath them changes. Either can be cleared
|
|
9
|
+
without touching the other.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// Module scope: the object is a dependency of the write subscription.
|
|
13
|
+
const persist = {
|
|
14
|
+
dataKey: "employees.data",
|
|
15
|
+
settingsKey: "employees.settings",
|
|
16
|
+
} satisfies TMDataGridPersistence;
|
|
17
|
+
|
|
18
|
+
const grid = useTMDataGrid({ data, columns, persist });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Both keys are optional - pass only `settingsKey` to remember the layout but
|
|
22
|
+
never the filters.
|
|
23
|
+
|
|
24
|
+
```demo
|
|
25
|
+
file: data/Persistence.tsx
|
|
26
|
+
hint: Sort, filter and hide a column, then reload the page. It all comes back; the page index does not.
|
|
27
|
+
extraSources: data/employeeColumns.tsx
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## The two groups
|
|
31
|
+
|
|
32
|
+
| Group | Slices | Lifetime |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `dataKey` | `columnFilters`, `globalFilter`, `sorting`, `pagination`, `expanded` | As long as the data means the same thing |
|
|
35
|
+
| `settingsKey` | `columnVisibility`, `columnSizing`, `columnOrder`, `columnPinning`, `grouping` | Indefinite. This is the user's layout |
|
|
36
|
+
|
|
37
|
+
`DATA_STATE_SLICES` and `SETTINGS_STATE_SLICES` are exported with the same
|
|
38
|
+
values. Slice names are typed per group, so only valid names compile.
|
|
39
|
+
|
|
40
|
+
Settings saved by 1.x with the old `columnPinning` keys `left` and `right` are
|
|
41
|
+
read and migrated to `start` and `end`.
|
|
42
|
+
|
|
43
|
+
## Persisting only some of it
|
|
44
|
+
|
|
45
|
+
A key on its own persists every slice in its group. Pass a tuple to narrow it:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
const persist = {
|
|
49
|
+
// Restore filters and sorting, but always start on the first page.
|
|
50
|
+
dataKey: ["employees.data", ["columnFilters", "sorting"]],
|
|
51
|
+
// Restore column layout but not widths.
|
|
52
|
+
settingsKey: ["employees.settings", ["columnVisibility", "columnOrder"]],
|
|
53
|
+
storageMode: "sessionStorage",
|
|
54
|
+
} satisfies TMDataGridPersistence;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Only the selected slices are read back, so a payload written before you narrowed
|
|
58
|
+
the selection cannot reintroduce slices you have since opted out of.
|
|
59
|
+
|
|
60
|
+
## Behaviour
|
|
61
|
+
|
|
62
|
+
**Restoring happens once**, on mount, through `initialState`. Writing is a
|
|
63
|
+
subscription to the table store, so state changed directly through the table
|
|
64
|
+
API is persisted too.
|
|
65
|
+
|
|
66
|
+
**A payload from another version is dropped whole**, not migrated. Payloads
|
|
67
|
+
carry the exported `PERSIST_PAYLOAD_VERSION`, and anything else, including
|
|
68
|
+
everything written by a 0.x build, which had no stamp, is discarded.
|
|
69
|
+
|
|
70
|
+
**Restored state is realigned against the columns that exist.** Entries naming a
|
|
71
|
+
column removed between deploys are dropped: a stale id in the order, a width for
|
|
72
|
+
a column that no longer exists, or a sort or filter that would be active with no
|
|
73
|
+
column to show it. New columns need no handling, since TanStack appends columns
|
|
74
|
+
missing from `columnOrder` in definition order.
|
|
75
|
+
|
|
76
|
+
**Storage access is guarded.** If storage is unavailable, disabled or full,
|
|
77
|
+
persistence is skipped rather than throwing.
|
|
78
|
+
|
|
79
|
+
**Keys are not namespaced.** Include a tenant or user identifier if several
|
|
80
|
+
people can share a browser profile.
|
|
81
|
+
|
|
82
|
+
## Resetting
|
|
83
|
+
|
|
84
|
+
`resetSettings()` puts the settings state back to what a first visit with clean
|
|
85
|
+
storage would have shown (your `initialState` plus the structural lanes), and,
|
|
86
|
+
with persistence configured, writes through to storage like any other change.
|
|
87
|
+
The columns panel's **Reset layout** button calls it.
|
|
88
|
+
|
|
89
|
+
TanStack's own `resetColumnX()` family cannot do it on a persisted grid: those
|
|
90
|
+
reset to `initialState`, which the mount built *from* the restored payload.
|
|
91
|
+
|
|
92
|
+
## Relation to Mantine
|
|
93
|
+
|
|
94
|
+
Persistence does not use Mantine's `useLocalStorage`; the table owns the state
|
|
95
|
+
and storage only mirrors it. The option names follow Mantine's
|
|
96
|
+
`UseStorageOptions` where they apply, and `storageMode` takes the same values as
|
|
97
|
+
its `StorageType`.
|
|
98
|
+
|
|
99
|
+
## Reference
|
|
100
|
+
|
|
101
|
+
| Name | Kind | Type | Default | What it does |
|
|
102
|
+
| --- | --- | --- | --- | --- |
|
|
103
|
+
| `persist` | Option | `TMDataGridPersistence` | – | The whole configuration. Keep it referentially stable. |
|
|
104
|
+
| `dataKey` | persist field | `string \| [string, DataSlice[]]` | – | Storage key for the data group. |
|
|
105
|
+
| `settingsKey` | persist field | `string \| [string, SettingsSlice[]]` | – | Storage key for the settings group. |
|
|
106
|
+
| `TMDataGridPersistKey` | Type | `string \| [string, slices]` | – | The type of `dataKey` and `settingsKey`. |
|
|
107
|
+
| `storageMode` | persist field | `"localStorage" \| "sessionStorage"` | `"localStorage"` | Storage area. `"sessionStorage"` is per tab. |
|
|
108
|
+
| `TMDataGridStorageMode` | Type | `"localStorage" \| "sessionStorage"` | – | The type of `storageMode`. |
|
|
109
|
+
| `serialize` | persist field | `(value) => string` | `JSON.stringify` | Serializes a payload before storing. |
|
|
110
|
+
| `deserialize` | persist field | `(value: string) => unknown` | `JSON.parse` | Parses a stored payload. |
|
|
111
|
+
| `resetSettings` | Hook return | `() => void` | – | Back to a clean first visit, written through to storage. |
|
|
112
|
+
| `DATA_STATE_SLICES` · `SETTINGS_STATE_SLICES` | Exports | `string[]` | – | The slice names of each group. |
|
|
113
|
+
| `TMDataGridDataSlice` · `TMDataGridSettingsSlice` | Types | – | – | One slice name of each group. |
|
|
114
|
+
| `PERSIST_PAYLOAD_VERSION` | Export | `number` | – | The stamp. A payload from another version is dropped. |
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# A portfolio rebalancer
|
|
2
|
+
|
|
3
|
+
A holdings book where one column is a decision and the rest are consequences.
|
|
4
|
+
The user edits **Target**; the grid recomputes drift and the trade to place, totals each sector, and refuses an edit that would allocate more than the whole book.
|
|
5
|
+
|
|
6
|
+
```demo
|
|
7
|
+
file: recipes/PortfolioRebalancer.tsx
|
|
8
|
+
hint: Type 40 into a Target cell - the commit is refused with the total it would have produced.
|
|
9
|
+
height: 620
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Deriving the rows
|
|
13
|
+
|
|
14
|
+
`accessorFn` is handed one row, so a column whose value depends on the other rows - a weight as a share of the portfolio - cannot be written as one.
|
|
15
|
+
Derive the whole collection once and hand the grid the finished shape:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const positions = useMemo(() => {
|
|
19
|
+
const valued = holdings.map((h) => ({ ...h, marketValue: h.price * h.shares }));
|
|
20
|
+
const total = valued.reduce((sum, h) => sum + h.marketValue, 0);
|
|
21
|
+
|
|
22
|
+
return valued.map((h) => ({
|
|
23
|
+
...h,
|
|
24
|
+
currentPct: (h.marketValue / total) * 100,
|
|
25
|
+
drift: h.targetPct - (h.marketValue / total) * 100,
|
|
26
|
+
}));
|
|
27
|
+
}, [holdings]);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`editing.onCommit` writes back to `holdings`, the source array, and the derived rows arrive through `data` on the next render.
|
|
31
|
+
Under `editing.mode: "cell"` with no draft store, every dependent column follows the keystroke that committed.
|
|
32
|
+
|
|
33
|
+
## Gating the one editable column
|
|
34
|
+
|
|
35
|
+
`editing.columns` lists what takes edits. Everything else is market data and stays read-only whatever its own meta says.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
editing: {
|
|
39
|
+
mode: "cell",
|
|
40
|
+
columns: ["targetPct"],
|
|
41
|
+
onCommit: ({ rowId, value }) => save(rowId, value.targetPct),
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## The rule that needs the other rows
|
|
46
|
+
|
|
47
|
+
A target weight is only valid against the rest of the book, so the rule is `editing.tableValidators`, not `meta.edit.validate`.
|
|
48
|
+
`rows` is the collection as it would stand if the commit landed, so the committing row's drafted value is already in it.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
tableValidators: {
|
|
52
|
+
onSubmit: ({ rows }) => {
|
|
53
|
+
const total = rows.reduce((sum, r) => sum + Number(r.value.targetPct ?? 0), 0);
|
|
54
|
+
|
|
55
|
+
return total > 100.005
|
|
56
|
+
? { fields: { targetPct: `Targets would total ${pct(total)}` } }
|
|
57
|
+
: undefined;
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The bound on a single cell - between 0 and 100 - stays on the column as [`meta.edit.validate`](/docs/editors), because it needs nothing but the value.
|
|
63
|
+
|
|
64
|
+
## Totals in two places
|
|
65
|
+
|
|
66
|
+
Sector totals come from `aggregationFn` on each column; the portfolio total comes from a `footer` and [`aggregateColumn`](/docs/summary-row), which follows the filters.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
columnHelper.accessor("marketValue", {
|
|
70
|
+
aggregationFn: "sum",
|
|
71
|
+
footer: ({ table }) =>
|
|
72
|
+
money.format(Number(aggregateColumn({ table, columnId: "marketValue" }))),
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Summing `targetPct` is meaningful only because the values are shares of one whole: a footer reading `100.0%` says the book is fully allocated.
|
|
77
|
+
|
|
78
|
+
## Reading drift
|
|
79
|
+
|
|
80
|
+
Drift is signed, so the cell renders a tint whose strength follows the size of the miss and whose colour follows its direction.
|
|
81
|
+
Rows more than three points out take `--row-bg` as well, which composes under hover and selection instead of replacing them.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<TMDataGrid.Table<Position>
|
|
85
|
+
rowStyle={(row) =>
|
|
86
|
+
!row.getIsGrouped() && Math.abs(row.original.drift) >= 3
|
|
87
|
+
? { "--row-bg": "color-mix(in srgb, var(--mantine-color-yellow-6) 10%, transparent)" }
|
|
88
|
+
: undefined
|
|
89
|
+
}
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A group row is handed to `rowStyle` too, and its `original` is an arbitrary child's record, so the callback guards with `row.getIsGrouped()`.
|
|
94
|
+
[Row styling](/docs/row-styling) covers the rest of the vocabulary.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# A query builder form
|
|
2
|
+
|
|
3
|
+
A search form holds dates, a title filter and a list of extra conditions. The
|
|
4
|
+
conditions are rows, so the condition builder is a grid, wrapped as a form
|
|
5
|
+
field: rows come in through `value`, and approved changes go back out through
|
|
6
|
+
`onChange`.
|
|
7
|
+
|
|
8
|
+
```demo
|
|
9
|
+
file: recipes/QueryBuilderForm.tsx
|
|
10
|
+
hint: Add a condition and pick Status - the value editor becomes a select. Delete every condition, or repeat one, and Search says why it will not.
|
|
11
|
+
height: 560
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The form holds the array; the grid holds the row being edited. The grid never
|
|
15
|
+
mutates `data`, so the form's state is only ever what the user has approved.
|
|
16
|
+
|
|
17
|
+
## Wrapping the grid as a field
|
|
18
|
+
|
|
19
|
+
The grid is wrapped once, as an ordinary controlled component:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
function ConditionsGrid({ value, onChange }: {
|
|
23
|
+
value: Array<QueryCondition>;
|
|
24
|
+
onChange: (next: Array<QueryCondition>) => void;
|
|
25
|
+
}) {
|
|
26
|
+
const grid = useTMDataGrid({
|
|
27
|
+
data: value,
|
|
28
|
+
columns,
|
|
29
|
+
getRowId: (row) => String(row.id),
|
|
30
|
+
editing: {
|
|
31
|
+
mode: "row",
|
|
32
|
+
rowValidators,
|
|
33
|
+
onCommit: ({ rowId, value: row }) =>
|
|
34
|
+
onChange(value.map((c) => (String(c.id) === rowId ? row : c))),
|
|
35
|
+
onRowAdd: ({ value: row }) =>
|
|
36
|
+
onChange([...value, { ...row, id: Math.min(0, ...value.map((c) => c.id)) - 1 }]),
|
|
37
|
+
onRowDelete: ({ rowId }) =>
|
|
38
|
+
onChange(value.filter((c) => String(c.id) !== rowId)),
|
|
39
|
+
newRowDefaults: () => ({ id: 0, field: "title", operator: "contains", value: "" }),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
return (
|
|
43
|
+
<TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }}>
|
|
44
|
+
<TMDataGrid.Table<QueryCondition> />
|
|
45
|
+
</TMDataGrid>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
And handed to the form as a field:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<form.Field
|
|
54
|
+
name="conditions"
|
|
55
|
+
validators={{
|
|
56
|
+
onChange: ({ value }) =>
|
|
57
|
+
value.length === 0 ? "Add at least one condition" : undefined,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{(field) => (
|
|
61
|
+
<ConditionsGrid value={field.state.value} onChange={field.handleChange} />
|
|
62
|
+
)}
|
|
63
|
+
</form.Field>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`field.handleChange` is the `onChange`: it writes the array into the form and
|
|
67
|
+
runs the field's validators. `onChange` carries the whole next array rather than
|
|
68
|
+
a diff.
|
|
69
|
+
|
|
70
|
+
- **Map by row id, never by index.** A sort or a delete moves the index; the
|
|
71
|
+
draft is keyed by the id.
|
|
72
|
+
- **`data` identity stays stable.** `field.state.value` changes identity only
|
|
73
|
+
when a row is written, which is what `data` requires.
|
|
74
|
+
See [useTMDataGrid](/docs/use-tm-data-grid).
|
|
75
|
+
- **New rows count down from `-1`.** `Math.min(0, ...ids) - 1`, so an emptied
|
|
76
|
+
grid starts at `-1` rather than `-Infinity` and existing negative ids keep
|
|
77
|
+
descending. The grid's `tempId` never leaves the grid: the id you assign in
|
|
78
|
+
`editing.onRowAdd` is the one the form sees.
|
|
79
|
+
|
|
80
|
+
## Where validation belongs
|
|
81
|
+
|
|
82
|
+
A rule that can be decided from one row belongs to the grid. A rule that needs
|
|
83
|
+
the other rows, or the collection as a whole, belongs to the form.
|
|
84
|
+
|
|
85
|
+
| Rule | Owner | Written as |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| "The condition needs a value" | Grid | `editing.rowValidators.onSubmit` |
|
|
88
|
+
| "A date, for a date field" | Grid | `meta.edit.editor` per column |
|
|
89
|
+
| "At least one condition" | Form | the `conditions` field's `onChange` validator |
|
|
90
|
+
| "No two conditions repeat a field and operator" | Form | the same place |
|
|
91
|
+
|
|
92
|
+
A row's form is seeded with that row's values only, so it cannot see the array,
|
|
93
|
+
and the grid's `edit.store` publishes field names but not values, so the form
|
|
94
|
+
cannot see a draft.
|
|
95
|
+
|
|
96
|
+
## Editors that follow the row
|
|
97
|
+
|
|
98
|
+
In a `[field, operator, value]` builder the value cell means something
|
|
99
|
+
different on every row. `meta.edit.editor` receives the row's live form, so one
|
|
100
|
+
component can switch on the *draft* field rather than the committed one, and
|
|
101
|
+
picking a new field resets its dependents:
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
const FieldEditor: TMDataGridEditorComponent = ({ field, form, size }) => (
|
|
105
|
+
<Select
|
|
106
|
+
size={size}
|
|
107
|
+
data={[...FIELDS]}
|
|
108
|
+
value={String(field.state.value)}
|
|
109
|
+
onChange={(next) => {
|
|
110
|
+
if (next === null) return;
|
|
111
|
+
field.handleChange(next);
|
|
112
|
+
form.setFieldValue("operator", OPERATORS[next as QueryField][0]);
|
|
113
|
+
form.setFieldValue("value", "");
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The operator and value editors read the sibling with TanStack Store's
|
|
120
|
+
[`useSelector`](https://tanstack.com/store/latest/docs/framework/react/reference):
|
|
121
|
+
`useSelector(form.store, (state) => state.values.field)`, so switching Field
|
|
122
|
+
mid-edit switches them immediately. See
|
|
123
|
+
[Editors and validation](/docs/editors) for the editor's full API.
|
|
124
|
+
|
|
125
|
+
## Which mode
|
|
126
|
+
|
|
127
|
+
`editing.mode: "row"`. A condition is approved as a unit: the pencil opens the
|
|
128
|
+
whole row, Save commits it, and `editing.onCommit` hands the form one finished
|
|
129
|
+
condition. The form's rules run at that point.
|
|
130
|
+
|
|
131
|
+
**Not `draft: true`.** A draft store holds every commit inside the grid and
|
|
132
|
+
calls nothing until `edit.saveDrafts()`, so the form's array goes stale the
|
|
133
|
+
moment typing starts: "at least one condition" counts rows the user may have
|
|
134
|
+
marked for deletion, a duplicate sitting in a draft passes unseen, and the
|
|
135
|
+
form's submit saves an array missing every pending edit.
|
|
136
|
+
|
|
137
|
+
To use the store anyway, invert where the save happens: `grid.edit.saveDrafts()`
|
|
138
|
+
becomes the only way rows reach the form, and the form's submit is gated on the
|
|
139
|
+
grid holding no draft -
|
|
140
|
+
`useSelector(grid.edit.store, (s) => s.openRowIds.length === 0)`.
|
|
141
|
+
|
|
142
|
+
## Submitting
|
|
143
|
+
|
|
144
|
+
The form submits only while the grid holds no draft.
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
const hasOpenDraft = useSelector(grid.edit.store, (s) => s.openRowIds.length > 0);
|
|
148
|
+
|
|
149
|
+
<Button type="submit" disabled={!canSubmit || hasOpenDraft}>
|
|
150
|
+
{hasOpenDraft ? "Finish the open condition" : "Search"}
|
|
151
|
+
</Button>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The alternative is to flush instead of block.
|
|
155
|
+
`await grid.edit.commitAll()` before `form.handleSubmit()` submits every open row through the normal `editing.onCommit` path, in every mode; with `draft: true` follow it with `grid.edit.saveDrafts()`.
|
|
156
|
+
Both resolve `ok: false` when a row stayed open or was reopened with an error, so submit the form only on `ok`:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
const committed = await grid.edit.commitAll();
|
|
160
|
+
const saved = committed.ok ? await grid.edit.saveDrafts() : undefined;
|
|
161
|
+
if (saved?.ok) await form.handleSubmit();
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Two constraints apply to a native `<form>` around a grid:
|
|
165
|
+
|
|
166
|
+
- The grid's buttons cannot submit it. Mantine buttons default to
|
|
167
|
+
`type="button"`, so only your own `type="submit"` button submits.
|
|
168
|
+
- Enter inside a cell editor commits the cell, not the form. The editor
|
|
169
|
+
prevents the default, so implicit form submission never fires.
|
|
170
|
+
|
|
171
|
+
## Reference
|
|
172
|
+
|
|
173
|
+
The pieces this recipe composes:
|
|
174
|
+
|
|
175
|
+
| Piece | Documented on |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `data`, `getRowId`, `editing.mode`, `editing.onCommit`, `editing.onRowAdd`, `editing.onRowDelete`, `editing.newRowDefaults`, `edit.store` | [Editing](/docs/editing) |
|
|
178
|
+
| `editing.rowValidators`, `meta.edit.editor`, the editor contract | [Editors and validation](/docs/editors) |
|
|
179
|
+
| `useForm`, `form.Field`, field validators | TanStack Form's own docs |
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Quick search
|
|
2
|
+
|
|
3
|
+
One box over every column. `TMDataGrid.Search` is a debounced input writing
|
|
4
|
+
TanStack's `globalFilter` state. There is no option to turn it on: render the
|
|
5
|
+
component, or do not.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<TMDataGrid.Toolbar>
|
|
9
|
+
<TMDataGrid.Search />
|
|
10
|
+
</TMDataGrid.Toolbar>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```demo
|
|
14
|
+
file: data/QuickSearch.tsx
|
|
15
|
+
hint: Try “Stckholm”. Fuzzy finds it; contains does not.
|
|
16
|
+
extraSources: data/employeeColumns.tsx
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Fuzzy by default
|
|
20
|
+
|
|
21
|
+
Typos and skipped characters still match, and while the search is the only thing
|
|
22
|
+
narrowing the grid (no sort, no grouping) the rows are ordered by **match
|
|
23
|
+
quality**, best first.
|
|
24
|
+
|
|
25
|
+
That ordering is derived and never written into `sorting`: no column takes
|
|
26
|
+
`aria-sort`, nothing is written to the persisted slices, and the next sort click
|
|
27
|
+
replaces it.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
const grid = useTMDataGrid({ data, columns, quickSearchMode: "contains" });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`"contains"` restores plain substring matching. An explicit `globalFilterFn`
|
|
34
|
+
overrides both, and switches the rank ordering off with it.
|
|
35
|
+
|
|
36
|
+
`fuzzyGlobalFilterFn` is exported for building your own input over the same
|
|
37
|
+
matching.
|
|
38
|
+
|
|
39
|
+
## Match highlighting
|
|
40
|
+
|
|
41
|
+
Opt in with `enableMatchHighlighting: true` and cells mark the matched part of
|
|
42
|
+
their text, while the quick search is active or a `contains` / `starts with` /
|
|
43
|
+
`ends with` column filter is.
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
const grid = useTMDataGrid({ data, columns, enableMatchHighlighting: true });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
What is marked is the **contiguous, case-insensitive** occurrence of the search
|
|
50
|
+
term, so a fuzzy typo-match with no contiguous occurrence is not highlighted.
|
|
51
|
+
Equality operators highlight nothing.
|
|
52
|
+
|
|
53
|
+
**Default-rendered cells only.** A column with its own `cell` renderer is
|
|
54
|
+
excluded: the grid reproduces the default value-to-string render with the marks
|
|
55
|
+
added, and does not modify a custom renderer's output.
|
|
56
|
+
|
|
57
|
+
The mark colour is `--dg-match-highlight-bg`, a yellow that follows the Mantine
|
|
58
|
+
colour scheme.
|
|
59
|
+
|
|
60
|
+
## Opting columns out
|
|
61
|
+
|
|
62
|
+
`enableGlobalFilter: false` on a column takes it out of the search; on the table
|
|
63
|
+
it removes the input entirely. The generated lanes are already excluded.
|
|
64
|
+
|
|
65
|
+
A search input of your own can skip the component and call
|
|
66
|
+
`table.setGlobalFilter` directly. The state is TanStack's own, so a
|
|
67
|
+
[`manualFiltering`](/docs/server-side) grid forwards it to the server, and it is
|
|
68
|
+
one of the persisted `data` slices.
|
|
69
|
+
|
|
70
|
+
## Reference
|
|
71
|
+
|
|
72
|
+
| Name | Kind | Type | Default | What it does |
|
|
73
|
+
| --- | --- | --- | --- | --- |
|
|
74
|
+
| `TMDataGrid.Search` | Component | – | – | The debounced quick-search input. |
|
|
75
|
+
| `placeholder` | Search prop | `string` | `labels.searchPlaceholder` | Input placeholder. |
|
|
76
|
+
| `debounce` | Search prop | `number` | `250` | Pause before the filter applies, in ms. `0` filters per keystroke. |
|
|
77
|
+
| `w` | Search prop | `number \| string` | `220` | Input width. |
|
|
78
|
+
| `quickSearchMode` | Option | `"fuzzy" \| "contains"` | `"fuzzy"` | How the search matches. |
|
|
79
|
+
| `TMDataGridQuickSearchMode` | Type | `"fuzzy" \| "contains"` | – | The type of `quickSearchMode`. |
|
|
80
|
+
| `enableMatchHighlighting` | Option | `boolean` | `false` | Mark the matched text in default-rendered cells. |
|
|
81
|
+
| `enableGlobalFilter` | Table option | `boolean` | `true` | Also a column option. `false` removes the input, or one column's participation. |
|
|
82
|
+
| `globalFilterFn` | Table option | filter fn | `fuzzy` | Overrides the matching, and the rank ordering with it. |
|
|
83
|
+
| `fuzzyGlobalFilterFn` | Export | filter fn | – | The default matcher, for a custom input. |
|
|
84
|
+
| `--dg-match-highlight-bg` | CSS variable | colour | Themed yellow | The mark colour. |
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Row details
|
|
2
|
+
|
|
3
|
+
A panel that opens underneath a row, spanning every column, holding whatever
|
|
4
|
+
does not fit in the cells - a summary card, an action strip, a nested table.
|
|
5
|
+
|
|
6
|
+
Setting `renderDetails` turns the lane on. There is no separate flag.
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
const grid = useTMDataGrid({
|
|
10
|
+
data,
|
|
11
|
+
columns,
|
|
12
|
+
renderDetails: ({ row }) => <EmployeeCard employee={row.original} />,
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```demo
|
|
17
|
+
file: rows/DetailsPanel.tsx
|
|
18
|
+
hint: Expand a few rows - the panels differ in height, and each one is measured.
|
|
19
|
+
height: 460
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The panel is as tall as whatever it renders. The grid measures each one, so
|
|
23
|
+
they need not be uniform. `renderDetailsEstHeight` (default `160`) is what the
|
|
24
|
+
virtualizer assumes for a panel it has not measured yet, which keeps the
|
|
25
|
+
scrollbar accurate for rows that open off screen.
|
|
26
|
+
|
|
27
|
+
## The details lane
|
|
28
|
+
|
|
29
|
+
Setting `renderDetails` prepends a generated chevron column,
|
|
30
|
+
`DETAILS_COLUMN_ID` (`"__details__"`), pinned to the left after the checkbox
|
|
31
|
+
and tree columns - `[checkbox, tree, details, …]`.
|
|
32
|
+
|
|
33
|
+
It is a system lane: as wide as the chevron it holds, with no resize handle and
|
|
34
|
+
no column menu, and it cannot be hidden, moved, resized or unpinned. Its header
|
|
35
|
+
is a control rather than a title, expanding and collapsing every panel at once.
|
|
36
|
+
|
|
37
|
+
Group rows get no chevron: they expand into their children from the tree lane.
|
|
38
|
+
|
|
39
|
+
## Opening a row from elsewhere
|
|
40
|
+
|
|
41
|
+
Which rows are open is TanStack's own `expanded` state, so anything can open
|
|
42
|
+
one - a context menu item, a double-click, a button in your own cell:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
<Menu.Item onClick={() => row.toggleExpanded()}>Show details</Menu.Item>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Anything reading `row.getIsExpanded()` inside a cell has to subscribe to the
|
|
49
|
+
store with TanStack Store's
|
|
50
|
+
[`useSelector`](https://tanstack.com/store/latest/docs/framework/react/reference), or the
|
|
51
|
+
React Compiler caches the call along with the `row` identity and the control
|
|
52
|
+
never updates:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
const expanded = useSelector(row.table.store, () => row.getIsExpanded());
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Because it is the standard state, `table.toggleAllRowsExpanded()`,
|
|
59
|
+
`initialState.expanded` and persistence all work with it. It is one of the
|
|
60
|
+
`data` slices, so open panels survive a reload on a
|
|
61
|
+
[persisted](/docs/use-tm-data-grid#persist) grid.
|
|
62
|
+
|
|
63
|
+
> TanStack resets `expanded` whenever the `data` array changes, which would
|
|
64
|
+
> close every panel on each draft commit. The grid therefore sets
|
|
65
|
+
> `autoResetExpanded: false`; pass `autoResetExpanded: true` to have a new
|
|
66
|
+
> `data` array close the panels.
|
|
67
|
+
|
|
68
|
+
## The two kinds of expanding
|
|
69
|
+
|
|
70
|
+
TanStack keeps one `expanded` state, and the grid uses it for two unrelated
|
|
71
|
+
things: opening a group row into its children, and opening a data row into its
|
|
72
|
+
panel. The controls are kept separate. The details header only opens and closes
|
|
73
|
+
panels, and "Expand all groups" in the tree menu only unfolds the tree.
|
|
74
|
+
|
|
75
|
+
`table.toggleAllRowsExpanded()` is the one that does not distinguish them: it
|
|
76
|
+
writes the state's whole-table form, which is every group and every panel at
|
|
77
|
+
once. `resolveExpandAll` and `areAllRowsExpanded` are exported for anything
|
|
78
|
+
building its own control:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
table.setExpanded(
|
|
82
|
+
resolveExpandAll({
|
|
83
|
+
rows: table.getPrePaginatedRowModel().flatRows,
|
|
84
|
+
expanded: table.store.state.expanded,
|
|
85
|
+
target: "details",
|
|
86
|
+
expand: true,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Panel behaviour
|
|
92
|
+
|
|
93
|
+
The panel is a cell spanning the row, inside the row element. It takes the row's
|
|
94
|
+
background, moves with it and is measured with it, but it is **not a row**:
|
|
95
|
+
`aria-rowcount` still counts records, and a click or right-click inside it does
|
|
96
|
+
not select or highlight the row underneath. It carries
|
|
97
|
+
`data-dg-part="details"` with the row's `data-row-id`. See
|
|
98
|
+
[Testing](/docs/testing).
|
|
99
|
+
|
|
100
|
+
Group rows have no panel. Expanding one opens its children.
|
|
101
|
+
|
|
102
|
+
## Reference
|
|
103
|
+
|
|
104
|
+
| Name | Kind | Type | Default | What it does |
|
|
105
|
+
| --- | --- | --- | --- | --- |
|
|
106
|
+
| `renderDetails` | Option | `({ row, table }) => ReactNode` | – | Contents of the panel. Setting it adds the lane. |
|
|
107
|
+
| `TMDataGridDetailsArgs` | Type | `{ row, table }` | – | What `renderDetails` receives. |
|
|
108
|
+
| `renderDetailsEstHeight` | Option | `number` | `160` | Height the virtualizer assumes for an unmeasured panel. |
|
|
109
|
+
| `initialState.expanded` | Table option | `ExpandedState` | `{}` | Rows open at mount. A data slice, so it persists. |
|
|
110
|
+
| `autoResetExpanded` | Table option | `boolean` | `false` | `true` closes the panels when the `data` array changes. Off by default, so a draft commit keeps them open. |
|
|
111
|
+
| `DETAILS_COLUMN_ID` | Export | `"__details__"` | – | Id of the generated chevron column. |
|
|
112
|
+
| `resolveExpandAll` | Export | `(args) => ExpandedState` | – | Expand or collapse every group, or every panel, but not both. |
|
|
113
|
+
| `areAllRowsExpanded` | Export | `(args) => boolean` | – | Whether every row of one target is open. |
|
|
114
|
+
| `TMDataGridExpandAllArgs` · `TMDataGridExpandTarget` | Types | – | – | What `resolveExpandAll` and `areAllRowsExpanded` take, and its `target`: `"groups"` or `"details"`. |
|
|
115
|
+
| `data-dg-part="details"` | Data attribute | – | – | The panel element, carrying the row's `data-row-id`. |
|