@jielga/tmdatagrid 2.0.0-beta.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1281 -768
  3. package/dist/index.js +4607 -3250
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/adding-rows.md +132 -0
  7. package/docs/anatomy.md +119 -0
  8. package/docs/card-view.md +108 -0
  9. package/docs/cell-selection.md +194 -0
  10. package/docs/column-layout.md +182 -0
  11. package/docs/column-menu.md +66 -0
  12. package/docs/columns.md +268 -0
  13. package/docs/components.md +311 -0
  14. package/docs/draft-store.md +242 -0
  15. package/docs/editing.md +303 -0
  16. package/docs/editors.md +250 -0
  17. package/docs/export.md +319 -0
  18. package/docs/filtering.md +362 -0
  19. package/docs/getting-started.md +123 -0
  20. package/docs/grouping.md +165 -0
  21. package/docs/loading-and-empty.md +92 -0
  22. package/docs/localization.md +79 -0
  23. package/docs/menu.md +143 -0
  24. package/docs/migrating-to-2.md +163 -0
  25. package/docs/pagination.md +144 -0
  26. package/docs/persistence.md +114 -0
  27. package/docs/portfolio-rebalancer.md +94 -0
  28. package/docs/query-builder.md +179 -0
  29. package/docs/quick-search.md +84 -0
  30. package/docs/row-details.md +115 -0
  31. package/docs/row-interaction.md +149 -0
  32. package/docs/row-pinning.md +132 -0
  33. package/docs/row-selection.md +136 -0
  34. package/docs/row-styling.md +133 -0
  35. package/docs/scrolling.md +112 -0
  36. package/docs/server-query.md +246 -0
  37. package/docs/server-side.md +206 -0
  38. package/docs/sorting.md +101 -0
  39. package/docs/styling.md +126 -0
  40. package/docs/summary-row.md +76 -0
  41. package/docs/testing.md +744 -0
  42. package/docs/toolbar.md +161 -0
  43. package/docs/use-tm-data-grid.md +361 -0
  44. package/package.json +22 -46
  45. package/skills/appearance/SKILL.md +72 -19
  46. package/skills/cell-selection/SKILL.md +46 -47
  47. package/skills/columns/SKILL.md +90 -34
  48. package/skills/data/SKILL.md +86 -16
  49. package/skills/editing/SKILL.md +67 -40
  50. package/skills/editing/references/common-mistakes.md +77 -69
  51. package/skills/editing/references/editing-api.md +22 -19
  52. package/skills/editing/references/editors-and-validation.md +24 -17
  53. package/skills/filtering/SKILL.md +148 -40
  54. package/skills/getting-started/SKILL.md +17 -15
  55. package/skills/grouping/SKILL.md +31 -16
  56. package/skills/options/SKILL.md +7 -7
  57. package/skills/rows/SKILL.md +22 -18
  58. package/skills/server-side/SKILL.md +170 -17
  59. package/skills/testing/SKILL.md +150 -32
  60. package/skills/testing-components/SKILL.md +230 -0
  61. package/skills/testing-editing/SKILL.md +240 -0
  62. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  63. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
  64. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  65. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  66. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
  67. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  68. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
  69. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
  70. package/src/components/TMDataGridExportPicker.module.css +77 -0
  71. package/src/components/TMDataGridExportPicker.tsx +234 -0
  72. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  73. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  74. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  75. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  76. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  77. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  78. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
  79. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  80. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  81. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  82. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  83. package/src/components/TMDataGridMenu.tsx +357 -0
  84. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
  85. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
  86. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
  87. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  88. package/src/components/TMDataGridToolbar.tsx +181 -0
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  91. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  96. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  97. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  98. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  99. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  100. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  101. package/src/components/filters/controlLayout.ts +32 -0
  102. package/src/components/filters/filterControlFor.ts +65 -0
  103. package/src/components/generatedColumns.tsx +187 -0
  104. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  105. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  106. package/src/components/useHideableColumns.ts +52 -0
  107. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  108. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  109. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  110. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  111. package/src/core/controlledStateSync.ts +108 -0
  112. package/src/core/deletedRows.ts +34 -0
  113. package/src/core/dom.ts +74 -0
  114. package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
  115. package/src/core/export.ts +704 -0
  116. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  117. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  118. package/src/core/filterSurface.ts +99 -0
  119. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  120. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  121. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  122. package/src/core/pageReset.ts +120 -0
  123. package/src/core/pagination.ts +81 -0
  124. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  125. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  126. package/src/{tmdatagrid/index.ts → index.ts} +69 -35
  127. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
  128. package/src/useTMDataGridExport.ts +78 -0
  129. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  130. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  131. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
  134. /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/capabilities.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -0,0 +1,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`. |