@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.
Files changed (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  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}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
@@ -0,0 +1,603 @@
1
+ # Editing
2
+
3
+ `@tanstack/react-form` becomes a peer dependency once editing is used.
4
+
5
+ Editing is configured through one option, `editing`, and it has two axes.
6
+ `mode` decides what counts as a commit; `draft` decides where that commit goes.
7
+ The grid never mutates `data`, so you apply the commit and the new values
8
+ arrive back through `data`.
9
+
10
+ A row moves through three places, and the words for them are used exactly:
11
+
12
+ | Place | What it holds | In | Out |
13
+ | --- | --- | --- | --- |
14
+ | **data** | Your rows, the only source of truth | - | - |
15
+ | **form state** | A row being edited: its own TanStack Form, undecided | `edit.begin`, `edit.addRow` | `edit.cancel` |
16
+ | **draft store** | Rows that passed their commit, held as values in the grid | `edit.commit` | `edit.saveDrafts` |
17
+
18
+ A row is **open** while it is form state, and **committed** once it is in the draft store.
19
+ The store only has dwell under `draft: true`; without it a commit goes straight to `onCommit` and the form is dropped, which is the same pipeline with the middle step lasting no time at all.
20
+
21
+ ```tsx
22
+ const grid = useTMDataGrid({
23
+ data,
24
+ columns,
25
+ getRowId: (row) => String(row.id),
26
+ editing: {
27
+ mode: "cell",
28
+ onCommit: async ({ rowId, value, changes }) => {
29
+ // changes is a list of descriptors, not a patch object
30
+ await api.patch(rowId, Object.fromEntries(changes.map((c) => [c.field, c.next])));
31
+ },
32
+ },
33
+ });
34
+ ```
35
+
36
+ `onCommit` receives `{ rowId, value, original, changes, source }`.
37
+ `value` is the whole row as edited, `original` the row as editing began, and
38
+ `changes` the per-field diff: `Array<{ columnId, field, previous, next }>`, one
39
+ entry in cell mode.
40
+
41
+ `editing` requires `getRowId`: drafts are keyed by row id, and the index
42
+ fallback would name a different record after any sort. `onSaveDrafts` is
43
+ accepted only under `draft: true`. Both are compile errors rather than options
44
+ that silently do nothing. `draft: true` without `onSaveDrafts` is fine:
45
+ `saveDrafts` falls back to the per-row `onCommit` loop.
46
+
47
+ The `editing` object may be written inline. Its callbacks are read through a ref
48
+ on every render, so its identity does not matter.
49
+
50
+ ## The three modes
51
+
52
+ All three use the same engine and the same forms. `editing.mode` sets what
53
+ counts as a commit and which controls trigger it.
54
+
55
+ | Mode | Commit | Cancel | Controls |
56
+ | --------------- | ------------------------------------------ | ----------------- | ------------------------ |
57
+ | `"cell"` | Enter, Tab or leaving the cell | Escape | none |
58
+ | `"cellConfirm"` | ✓ or Enter; Tab walks input, ✓, ✕ and then leaves, keeping the draft | ✕ or Escape | ✓ / ✕ beside the input |
59
+ | `"row"` | Save in the edit lane, or Enter | Cancel, or Escape | generated edit lane |
60
+
61
+ An entry row from `edit.addRow()` is row-shaped in every mode: every editable cell opens at once, Tab walks them, and the lane's ✓ is what enters it.
62
+
63
+ Leaving a cell commits it only once the value passes.
64
+ A refused commit keeps the editor open, invalid, with the message in its tooltip, until the value is fixed or Escape drops it.
65
+
66
+ ```demo
67
+ file: editing/CellEditing.tsx
68
+ hint: Double-click a cell, or press Enter or F2, or start typing on it.
69
+ height: 440
70
+ ```
71
+
72
+ **Opening an editor**: double-click, or, with the cell cursor on the cell,
73
+ Enter, F2, or typing, where the first character replaces the value as it would
74
+ in a spreadsheet. The grid places the caret in the cell that was opened, so a
75
+ `meta.edit.editor` receives focus without handling it itself. A row added with
76
+ `edit.addRow()` opens the same way, with the caret in its first editable cell.
77
+
78
+ Delete or Backspace clears the value and commits without opening an editor;
79
+ under `draft: true` the cleared value goes into the draft store with the rest. The
80
+ commit validates either way - a column rule that rejects the empty value
81
+ refuses the clear and marks the cell, rather than writing past the rule.
82
+ Editing implies cell selection: `cellSelection` defaults to `"single"` while
83
+ `editing` is set.
84
+
85
+ ### Row editing
86
+
87
+ The pencil opens every cell of the row at once, and ✓ saves them as **one
88
+ commit**. Cross-field rules belong here, since the whole row is validated
89
+ together. Double-clicking a cell opens the whole row, with the caret in the cell
90
+ that was clicked.
91
+
92
+ Rows **accumulate**: opening a second row leaves the first one open, and each
93
+ row's ✓ and ✕ act on that row alone.
94
+
95
+ ```demo
96
+ file: editing/RowEditing.tsx
97
+ hint: Put a Sales row over 60 000 kr and Save reports why it is rejected.
98
+ height: 440
99
+ ```
100
+
101
+ ## The draft store
102
+
103
+ `draft: true` changes where a commit goes, and nothing else: instead of reaching `onCommit`, the row is committed into the grid's draft store and waits for `edit.saveDrafts()`.
104
+ The mode still decides what counts as a commit, so the two combine freely - `{ mode: "row", draft: true }` commits a whole row from the lane's ✓, `{ mode: "cell", draft: true }` commits a row as the caret leaves it.
105
+
106
+ ```tsx
107
+ editing: {
108
+ mode: "row",
109
+ draft: true,
110
+ onSaveDrafts: async ({ updated, created, deleted }) => {
111
+ await api.saveBatch({ updated, created, deleted });
112
+ },
113
+ }
114
+ ```
115
+
116
+ A committed row is displayed: the cell renders the draft value through the column's own `cell` renderer, with the blue corner marking it dirty.
117
+ It is also a row like any other to the table: it sorts, filters, groups, aggregates and counts on its draft values.
118
+ A committed row that stops matching a filter or the quick search leaves the view, and the Save bar still counts it.
119
+
120
+ Everything that reads a row reads the draft: sorting, filtering, quick search, grouping, `aggregatedCell` and `footer`, the faceted filter options, export, row selection, the row numbers and counts, `edit.getRows()` and `editing.tableValidators`.
121
+ The row callbacks are handed the same rows: `onRowClick`, `renderRowContextMenu`, `renderDetails`, `isRowEditable`, `meta.edit.enabled`, `rowClassName`, `rowStyle` and `enableRowPinning` receive a row whose `original` is the committed draft, and for an entered row a record under its temp id, carrying no server id.
122
+ `data` itself is never modified, and `getRowCount()` with a `rowCount` you set does not grow.
123
+ Only top-level rows are overlaid: children reached through `getSubRows` keep their `data` values.
124
+ A row reopened for a further edit keeps its place until it commits again or is cancelled.
125
+
126
+ A commit moves nothing else: the page stays, and open details panels and groups stay open.
127
+ TanStack's `autoResetPageIndex` and `autoResetExpanded` fire on any change to the `data` array, which under `draft: true` is every commit, so the grid switches both off and resets the page on a query change itself - see `resetPageOnQueryChange`.
128
+ A refetch that no longer returns a row drops that row's draft, its open editor and its deletion mark: the server has nothing for Save to act on.
129
+ Under `manualPagination` or `manualFiltering` a row missing from `data` is on another page, not gone, so its draft is kept until Save.
130
+
131
+ A committed row holds no form: its values are data in the draft store.
132
+ `begin` on it, or a write through `setCellValue`, `setRowValues` or `clearCell`, builds a fresh form seeded with those values and takes the row back out of the store until it commits again.
133
+ At `saveDrafts` only `editing.tableValidators` run again, over every committed row; a row they reject is reopened with its errors, and so is a row whose `onCommit` or `onRowAdd` rejects on the per-row path.
134
+
135
+ A row left open is not lost and not sent. It keeps everything typed into it,
136
+ stays open across a save, and joins the next save once it is committed. This
137
+ is what `edit.commitAll()` is for: it submits every open row at once, so
138
+ "commit everything, then save" is two calls, and the rows that fail validation
139
+ stay open with their errors instead of travelling half-checked.
140
+
141
+ A row that fails validation on the way out is the other way a row stays open.
142
+ Its message outlives the editor that found it: the cell keeps its invalid marker and the lane carries the text, until the value that failed is changed.
143
+ While an editor is open, a field's own message shows in a tooltip on it, opened by focus and by hover.
144
+
145
+ The rest of this page is what `draft: true` turns on.
146
+
147
+ ### The lane
148
+
149
+ The edit lane holds two things at once, one per axis: the mode's own controls while a row is open, and the draft store's marker once it is committed.
150
+
151
+ - a committed edit - a pencil icon, and Revert, which drops the row's draft
152
+ - a committed new row - a plus icon, a pencil that reopens it, and ✕, which removes it
153
+ - a row marked for deletion - a trash icon, and Restore
154
+ - an open row - whatever the mode offers: Save and Cancel
155
+
156
+ A committed row has had its submit, so the lane never offers to save it again - `TMDataGrid.DraftActions` is what sends it.
157
+ A committed row also hides the trash: revert first, then delete.
158
+ If validation blocks a row, its icon turns red with the message in the tooltip: the open row's ✓, an entry row's included.
159
+ A pathless issue from `rowValidators` has no cell to land on, so that tooltip is where its message shows.
160
+
161
+ ### Marking the drafts
162
+
163
+ Rows publish what they are holding, for styling and for tests:
164
+
165
+ | Attribute | On | Means |
166
+ | --- | --- | --- |
167
+ | `data-dirty` | Body row, cell | Values typed in, decided or not |
168
+ | `data-draft` | Body row, entry row | Committed into the draft store, waiting for Save |
169
+ | `data-deleted` | Body row | Marked for deletion |
170
+ | `data-new` | Body row, entry row | An entered row, committed (body) or not (entry block) |
171
+
172
+ A row attribute is published on every body row, `"true"` or `"false"`, so match
173
+ the value - `[data-draft="true"]` - rather than the bare attribute, which
174
+ matches every row. A cell's `data-dirty` is present only while the cell is
175
+ dirty.
176
+
177
+ The grid paints none of them beyond the markers already described. To
178
+ highlight everything pending a save, and to let the user toggle it, use
179
+ `rowStyle` on the Table:
180
+
181
+ ```tsx
182
+ <TMDataGrid.Table
183
+ rowStyle={(row) =>
184
+ showPending && grid.edit.state.committedRowIds.includes(row.id)
185
+ ? { "--row-bg": "color-mix(in srgb, var(--mantine-color-yellow-6) 15%, transparent)" }
186
+ : undefined
187
+ }
188
+ />
189
+ ```
190
+
191
+ `rowClassName` takes a class instead. For CSS alone, target the attribute:
192
+ `[data-dg-part="row"][data-draft="true"]`.
193
+
194
+ `TMDataGrid.DraftActions` in the toolbar provides the whole-grid controls: Save
195
+ with the draft-store count, Discard, and a note counting the rows still open.
196
+ Save sends the store and leaves open rows alone, so it greys out while nothing
197
+ is committed however much is being typed - the note is what keeps those rows
198
+ visible rather than silently left behind.
199
+ The toolbar is declarative: the grid does not add or remove this component for you, so include it when the grid runs a draft store - without `draft: true` there is nothing to save and Save stays disabled.
200
+
201
+ `renderActions` replaces the set and hands over its pieces: `state.draftCount`,
202
+ `state.openCount`, `state.openRowIds`, `state.isSubmitting`, `state.isSaving`,
203
+ the `save`, `commitAll`, `discard`, `scrollToRow` and `scrollToFirstOpenRow`
204
+ actions, and `Controls.Save` / `Controls.Discard` / `Controls.OpenRowsNote` as
205
+ the built-in pieces.
206
+
207
+ Counting the open rows is only half the job on a long grid: the row that still
208
+ needs a decision may be nowhere near the viewport, and the grid is always
209
+ [virtualized](/docs/scrolling), so it may have no element to scroll to.
210
+ `actions.scrollToFirstOpenRow(align?)` goes to the topmost one and answers
211
+ whether it could be reached.
212
+
213
+ ```tsx
214
+ <TMDataGrid.DraftActions
215
+ renderActions={({ state, actions, Controls }) => (
216
+ <Group>
217
+ {state.draftCount > 0 && <Badge>{state.draftCount} ready</Badge>}
218
+ <Button
219
+ disabled={state.openCount === 0}
220
+ onClick={() => {
221
+ actions.scrollToFirstOpenRow("center");
222
+ }}
223
+ >
224
+ Go to open row
225
+ </Button>
226
+ <Controls.OpenRowsNote />
227
+ <Controls.Save />
228
+ <Controls.Discard />
229
+ </Group>
230
+ )}
231
+ />
232
+ ```
233
+
234
+ `state.openRowIds` is the ids behind `openCount`, for a control the grid does
235
+ not offer - a list, or a next-open-row cycle. It is in the order the grid
236
+ opened the rows, while `scrollToFirstOpenRow` takes "first" in display order,
237
+ so the two need not name the same row. An entered row appears as its `tempId`;
238
+ those are always on screen in the entry block, so the scroll answers `true`
239
+ without moving.
240
+
241
+ ```demo
242
+ file: editing/DraftEditing.tsx
243
+ hint: Double-click a row, ✓ commits it into the draft store and the Backend panel stays quiet. Save sends the whole store in one call; with "Reject Sales rows" on, the backend refuses those and they keep their drafts. "Go to open row" returns to a row left undecided.
244
+ height: 440
245
+ ```
246
+
247
+ `edit.saveDrafts()` sends the draft store, through the per-row `onCommit` /
248
+ `onRowAdd` / `onRowDelete` loop by default, or through one
249
+ `onSaveDrafts({ updated, created, deleted })` call when that is set - the whole
250
+ store in one payload, for a server that applies it as a transaction.
251
+ `updated` entries are the shape `onCommit` receives -
252
+ `{ rowId, value, original, changes, source }`. `created` entries are
253
+ `{ tempId, value }`, and `deleted` is a list of row ids.
254
+
255
+ `changes` is a list of descriptors, not a patch object; spreading it into a row
256
+ compiles and writes nothing. To build a patch:
257
+
258
+ ```tsx
259
+ const patch = Object.fromEntries(entry.changes.map((c) => [c.field, c.next]));
260
+ ```
261
+
262
+ ### Saving part of the store
263
+
264
+ `onSaveDrafts` decides how much of the store is cleared:
265
+
266
+ | Returned | Effect |
267
+ | --- | --- |
268
+ | nothing | Everything saved. The store is cleared. |
269
+ | a rejected promise, or a throw | Nothing saved. Every draft is kept. |
270
+ | `{ updated, created, deleted }` | The ids reported `false` are kept; the rest are cleared. |
271
+
272
+ Each key takes `false` for the whole bucket, or a map of id to result. An id
273
+ the map does not name saved.
274
+
275
+ ```tsx
276
+ onSaveDrafts: async ({ updated, created, deleted }) => {
277
+ const failed = await api.saveBatch({ updated, created, deleted });
278
+ return { updated: Object.fromEntries(failed.map((id) => [id, false])) };
279
+ };
280
+ ```
281
+
282
+ A kept row stays committed rather than reopening, so the next `saveDrafts()`
283
+ retries it with the values it already holds. `saveDrafts()` resolves `false`
284
+ when anything was kept.
285
+
286
+ Nothing about a kept row is styled by the grid. It carries the same markers
287
+ every draft carries - see [Marking the drafts](#marking-the-drafts).
288
+
289
+ `edit.submitAll()` is the old single verb and is **deprecated**: it now does
290
+ `commitAll()` followed by `saveDrafts()`, which is what it always did in
291
+ effect. Replace it with whichever half you meant.
292
+
293
+ ## Which cells edit
294
+
295
+ A column is editable when it maps to a data path: its `accessorKey`, or
296
+ `meta.edit.field` for a column built on `accessorFn`. Dot paths reach into
297
+ nested records: `accessorKey: "address.city"` edits `values.address.city`, and
298
+ issues from a nested schema map to the right column.
299
+
300
+ | Gate | Effect |
301
+ | --- | --- |
302
+ | `editing.columns: string[]` | Only the named columns edit |
303
+ | `meta.edit.enabled: false` | The column never edits |
304
+ | `meta.edit.enabled: (row) => boolean` | Per row, per column |
305
+ | `editing.isRowEditable: (row) => boolean` | The whole row, in every mode |
306
+
307
+ Group rows and the generated lanes never edit.
308
+
309
+ `editing.columns` lists the column ids that take edits.
310
+ Unset, the default, every column mapping to a data path is editable.
311
+
312
+ ```tsx
313
+ editing: { mode: "cell", columns: ["targetPct", "note"] }
314
+ ```
315
+
316
+ It gates before `meta.edit`, never past it: a column left out takes no edits whatever its own meta says, and a listed column still answers to its `meta.edit.enabled`.
317
+ The same list decides which cells an entry row opens.
318
+
319
+ `edit.isColumnEditable(column)` asks the column's half of the question on its own, for a toolbar or a menu with no row in hand: the column maps to a field, `editing.columns` lists it when that is set, and `meta.edit.enabled` is not `false`.
320
+ A per-row `enabled` predicate is the row's half, and `edit.canEditCell(row, column)` asks both.
321
+
322
+ ```demo
323
+ file: editing/EditableGating.tsx
324
+ hint: ID never edits · Salary is closed on Terminated rows · rows under 25 are closed entirely · Full name is computed but writes to Last name.
325
+ height: 440
326
+ ```
327
+
328
+ ## Draft lifetime
329
+
330
+ Forms live outside the DOM, keyed by row id. Scrolling an editing row away
331
+ unmounts the editor; the form keeps its values, dirty state and errors, and the
332
+ editor remounts over the same form when the row returns.
333
+
334
+ A cell whose row holds a draft renders the draft value through the column's
335
+ own `cell` renderer, in every mode - a `"cellConfirm"` draft kept on the way
336
+ out displays what was typed, not the value in `data`. Cell corners show the
337
+ state: blue for a dirty draft, red for a validation error, and the row carries
338
+ `data-dirty`. A red corner outlives the editor that found the error: it stands
339
+ until that field's value changes. An entry row's cells take the red corner,
340
+ and never the blue one.
341
+
342
+ The draft is displayed by the column that owns the field. A column computed
343
+ from other fields - `accessorFn` or `display` - reads `row.original`, which is
344
+ `data`, so it shows the saved record while the row is edited. To make a
345
+ computed cell follow the draft, read the drafted row from `edit.store`:
346
+
347
+ ```tsx
348
+ function useDraftedRow(rowId: string, original: Product): Product {
349
+ const { edit } = useTMDataGridContext();
350
+ const values = useSelector(edit.store, (state) => state.rows[rowId]?.values);
351
+ return (values as Product | undefined) ?? original;
352
+ }
353
+ ```
354
+
355
+ `useTMDataGridContext()` reaches the engine from inside a cell renderer, and
356
+ the selector re-renders the cell as the draft changes.
357
+
358
+ ## Adding and deleting rows
359
+
360
+ `edit.addRow()` opens an **entry row** in a sticky block under the header, so a
361
+ row being typed into stays in view. Entry cells are ordinary editors over a form
362
+ seeded from `newRowDefaults`.
363
+ Enter, or the lane's ✓, commits the row: `onRowAdd` receives it, or under `draft: true` it is committed into the draft store and `saveDrafts` reports it in `created`.
364
+ Escape, or ✕, discards the entry.
365
+ Clicking away decides nothing - an entry row is row-shaped in every mode.
366
+ An entry row never OK'd is not part of a save; it stays open.
367
+
368
+ Under `draft: true` a committed entry row leaves the entry block and becomes a
369
+ body row: marked `data-new` and `data-draft`, tinted with `--dg-row-new-bg`, and
370
+ sorted, filtered and counted with the rest on the values it was entered with.
371
+ Set `newRowsSticky: true` to keep committed rows in the entry block until the
372
+ save instead, out of the body's sort and out of the row count. Double-click, or
373
+ the lane's pencil, reopens the row back into the entry block - which takes it
374
+ out of the draft store until it is committed again; ✕ removes it.
375
+
376
+ ```tsx
377
+ useTMDataGrid({
378
+ editing: {
379
+ mode: "row",
380
+ draft: true,
381
+ newRowDefaults: () => ({ id: 0, name: "", hired: today() }),
382
+ onSaveDrafts: async ({ updated, created, deleted }) => {
383
+ await api.saveBatch({ updated, created, deleted });
384
+ },
385
+ },
386
+ });
387
+
388
+ <Button onClick={() => grid.edit.addRow()}>Add row</Button>;
389
+ ```
390
+
391
+ Annotate `newRowDefaults`' return type: a bare object literal widens a union
392
+ field to `string`, and `(): Product => ({ ... })` keeps it checked.
393
+
394
+ `addRow` takes the values the row starts from. They override `newRowDefaults`
395
+ key by key, so `addRow()` opens the `newRowDefaults` row and
396
+ `addRow({ department: "Sales" })` opens that row with `department` filled in.
397
+ Passing a whole row duplicates it. The entry row is an ordinary form either way:
398
+ the seeded values are editable, validate like any other, and nothing reaches
399
+ `onRowAdd` until the row is committed.
400
+
401
+ A grouped column has no cell on the entry row - under the default
402
+ `groupedColumnMode: "remove"` it is not in the grid at all - so an entry row
403
+ cannot type the grouped field. Seed it: `addRow({ region: "EMEA" })`.
404
+
405
+ ```tsx
406
+ <Button onClick={() => grid.edit.addRow({ department: "Sales", active: true })}>
407
+ Add to Sales
408
+ </Button>;
409
+
410
+ <Button onClick={() => grid.edit.addRow(selected.original)}>Duplicate</Button>;
411
+ ```
412
+
413
+ To limit how many entry rows are open at once, read the entry state off
414
+ `edit.store` and gate the button:
415
+
416
+ ```tsx
417
+ const hasOpenEntry = useSelector(grid.edit.store, (state) =>
418
+ state.newRows.some((newRow) => !newRow.committed),
419
+ );
420
+
421
+ <Button disabled={hasOpenEntry} onClick={() => grid.edit.addRow()}>
422
+ Add row
423
+ </Button>;
424
+ ```
425
+
426
+ ### Importing rows
427
+
428
+ `edit.addRows(rows)` opens a batch of entry rows in one write, where a loop
429
+ over `addRow` is one write per row. Each row is seeded over `newRowDefaults`
430
+ exactly as `addRow` is.
431
+
432
+ `{ commit: true }` submits the rows too, which is what an import wants: rows
433
+ that validate are committed, and rows that fail stay open in the entry block
434
+ carrying their errors, for the user to fix. The result says which went which
435
+ way, so the file's bad rows can be reported before anything is saved.
436
+
437
+ Under `draft: true` the whole import is one publish: the rows are validated
438
+ together and land in the draft store in the same render that shows them, so
439
+ ten thousand rows take about a second, and the grid renders once rather than
440
+ once per row. `saveDrafts` sends them the same way. A committed row is held
441
+ as plain values, not as a form.
442
+
443
+ ```tsx
444
+ const { committed, open } = await grid.edit.addRows(parsedRows, {
445
+ commit: true,
446
+ });
447
+ if (open.length > 0) notify(`${open.length} rows need attention`);
448
+ await grid.edit.saveDrafts();
449
+ ```
450
+
451
+ Column rules are enforced here even though the rows never had an editor on
452
+ screen: the engine runs `meta.edit.validate` itself at commit, so an imported
453
+ row is held to the same rules as a typed one. Without `draft: true` there is
454
+ no store to commit into, so `commit: true` adds each valid row through `onRowAdd`
455
+ - one call per row, in the order given.
456
+
457
+ ```demo
458
+ file: editing/ImportRows.tsx
459
+ hint: Import parses the pasted rows, commits the valid ones and leaves the rest open with their errors. The second button imports ten thousand generated rows, twenty of them invalid.
460
+ height: 460
461
+ ```
462
+
463
+ `edit.deleteRow(rowId)` calls `onRowDelete({ rowId, row })` immediately; put
464
+ any confirmation in that callback. Under `draft: true` it marks the row
465
+ instead: the row renders struck through and inert
466
+ (`data-deleted`), the lane shows Restore, and `saveDrafts` reports the ids in
467
+ `deleted`. A deletion mark is a decision the moment it is made, so it goes
468
+ straight into the draft store - there is nothing to type. The mark is
469
+ idempotent - deleting a marked row again leaves it marked - and
470
+ `edit.restoreRow(rowId)` is the undo, which is what the lane's Restore calls.
471
+ On an entry row, committed or not, `deleteRow` just discards the entry, and an
472
+ id the grid does not know is a no-op. `edit.deleteRows(rowIds)` is the same
473
+ over a list in one call, for a bulk action: because each id marks
474
+ idempotently, discards an entry row or does nothing, the list may be passed
475
+ exactly as a selection stands - duplicates, already-marked rows and stale ids
476
+ included. The trash can shows when the deletion has somewhere to report to:
477
+ `onRowDelete` is set, or under `draft: true`, `onSaveDrafts` is.
478
+
479
+ A marked row is read-only and not selectable until it is restored: `begin`,
480
+ `setCellValue`, `setRowValues` and `clearCell` refuse it, the keyboard cannot
481
+ open an editor on it, its checkbox is disabled, select-all skips it, and the
482
+ mark drops it from `rowSelection`. An editor open on the row when it is marked
483
+ is cancelled. A committed edit stays under the mark, so Restore brings the row
484
+ back as edited; Save leaves that edit out of `updated` - the row is in
485
+ `deleted` only - and forgets it once the deletion is saved. A marked row still
486
+ sorts, filters, groups, aggregates and counts, and is left out of an export.
487
+
488
+ A row the engine takes out of the table - an entry row that is discarded or
489
+ saved, a marked row once its deletion is saved - leaves `rowSelection`,
490
+ `expanded` and `rowPinning` with it. TanStack itself never drops an id from
491
+ those maps, so without this a deleted row would keep the select-all box
492
+ indeterminate and count as selected for good.
493
+
494
+ The grid still never mutates `data`: you apply adds and deletes, and the new
495
+ rows arrive back through `data`. The engine's `tempId` (`__new__1`, …) does not
496
+ need to become a real id; assign one when you create the record.
497
+
498
+ ## The engine: `edit`
499
+
500
+ The built-in controls do everything through `edit`, which is public.
501
+
502
+ | Member | Does |
503
+ | --- | --- |
504
+ | `edit.begin({ rowId, columnId })` | Opens a row into form state. On a committed row, takes it back out of the draft store |
505
+ | `edit.commit(rowId)` | Submits one row: into the draft store under `draft: true`, to `onCommit` otherwise. Resolves `false` if validation blocked it |
506
+ | `edit.commitAll()` | Submits every open row. Resolves `false` when one stayed open |
507
+ | `edit.saveDrafts()` | Sends the draft store. Open rows are left alone |
508
+ | `edit.submitAll()` | **Deprecated** - `commitAll()` then `saveDrafts()` |
509
+ | `edit.cancel(rowId)` / `edit.cancelAll()` | Drops drafts - form state and the draft store alike |
510
+ | `edit.setCellValue(rowId, columnId, value)` | Writes one cell and commits the row, with no editor. Resolves `false` if the cell takes no edit, or validation refused the value |
511
+ | `edit.setRowValues(rowId, values)` | The same for several cells of one row, in one commit. All or nothing |
512
+ | `edit.clearCell(rowId, columnId)` | Writes the type's empty value and commits it - what Delete does |
513
+ | `edit.addRow(values?)` | Opens one entry row, seeded over `newRowDefaults` |
514
+ | `edit.addRows(rows, options?)` | Opens a batch; `{ commit: true }` submits the rows too - one publish for the lot under `draft: true` |
515
+ | `edit.deleteRow(rowId)` | Deletes a row, or marks it deleted under `draft: true`. Idempotent; discards an entry row; ignores an unknown id |
516
+ | `edit.deleteRows(rowIds)` | `deleteRow` over a list in one call - safe to feed a selection as it stands |
517
+ | `edit.restoreRow(rowId)` | Removes a row's deletion mark - what the lane's Restore calls |
518
+ | `edit.isColumnEditable(column)` | Whether a column takes edits at all, with no row in hand |
519
+ | `edit.getForm(rowId)` | The open row's live `FormApi`; `undefined` for a committed row |
520
+ | `edit.getRowValues(rowId)` | The row as shown: its draft where one is held, else the `data` value. `undefined` for an unknown row |
521
+ | `edit.getRows()` | Every row as shown - drafts overlaid, entry rows appended, deletion-marked rows included and flagged `deleted` |
522
+ | `edit.store` | Open rows, committed rows, active cell, dirty and error projections, draft values, the committed values the table shows (`committedValues`), entry rows, deletion marks |
523
+
524
+ `commit`, `commitAll`, `saveDrafts`, `setCellValue`, `setRowValues`,
525
+ `clearCell` and `addRows` return promises. Await each call before starting the
526
+ next when driving edits in a loop.
527
+
528
+ `getForm` returns the open row's own `FormApi`. Render it in a drawer or side
529
+ panel and it shares values, dirty state and errors with the inline cells.
530
+ A committed row has no form, so `getForm` returns `undefined` for it: call `begin` first, which reopens the row with a form seeded from the committed values.
531
+
532
+ `getRowValues` and `getRows` read what the grid shows rather than what `data` holds: an open form's values, a committed draft, or the `data` value when neither exists.
533
+ `getRows` walks the core row model, so it is unfiltered and never contains group rows, and it filters nothing out - a row marked deleted comes back flagged `deleted`, an entry row flagged `isNew` under its temp id.
534
+ The order is the core row model's, committed new rows ahead of the `data` rows, and then the entry rows the table does not hold: the ones still being typed into, and the committed ones under `newRowsSticky`.
535
+
536
+ ```tsx
537
+ const selected = grid.table
538
+ .getSelectedRowModel()
539
+ .rows.flatMap((row) => grid.edit.getRowValues(row.id) ?? []);
540
+
541
+ const surviving = grid.edit.getRows().filter((row) => !row.deleted);
542
+ ```
543
+
544
+ For the inverse, a `@tanstack/react-form` form _around_ the grid holding the row
545
+ array, see [A query builder form](/docs/query-builder).
546
+
547
+ ### Bulk actions
548
+
549
+ `edit.setCellValue(rowId, columnId, value)` writes one cell and commits its row without an editor ever opening: a typed edit without the typing, for a toolbar action or a bulk fill.
550
+ The row need not be mounted, so a selected row inside a collapsed group takes the write like any other.
551
+
552
+ ```tsx
553
+ for (const row of grid.table.getSelectedRowModel().rows) {
554
+ await grid.edit.setCellValue(row.id, "targetPct", equalWeight(row.original));
555
+ }
556
+ ```
557
+
558
+ Under `draft: true` each row is committed into the draft store like any hand-made edit, so the whole basket saves at once through `edit.saveDrafts()`, carries the same change markers, and is reverted row by row from the edit lane.
559
+
560
+ `edit.setRowValues(rowId, values)` does the same for several cells of one row in a single commit: one `onCommit` call and one draft entry rather than one per column.
561
+ Keys are column ids, and it is all or nothing - if any named cell takes no edit, nothing is written and it resolves `false`.
562
+
563
+ ```tsx
564
+ await grid.edit.setRowValues(row.id, { status: "Closed", closedOn: today() });
565
+ ```
566
+
567
+ Both resolve `false` when the cell takes no edit - no such row or column, `editing.columns` excludes it, `meta.edit.enabled` is off, or the row is not editable - and when validation refuses the value, which leaves the row open carrying its errors.
568
+ `value` is the stored value: no editor runs, so `meta.edit.mapValue` does not run either, while `meta.edit.validate` does.
569
+
570
+ ## Reference
571
+
572
+ | Name | Kind | Type | Default | What it does |
573
+ | ----------------------------- | -------------- | ------------------------------------------------ | ----------------- | ------------------------------------------------------------------------------------------------ |
574
+ | `editing` | Option | `TMDataGridEditingOptions` | – | Turns editing on. One object holding both axes and every editing callback. |
575
+ | `editing.mode` | Member | `"cell" \| "cellConfirm" \| "row"` | – | Picks what counts as a commit and which controls trigger it. |
576
+ | `editing.draft` | Member | `boolean` | `false` | Holds commits in the draft store for `edit.saveDrafts()` instead of sending them out. |
577
+ | `getRowId` | Table option | `(row) => string` | – | Required once `editing` is set. Drafts are keyed by it. |
578
+ | `editing.columns` | Member | `ReadonlyArray<string>` | Every mapped column | The column ids that take edits. Gates before `meta.edit`, never past it. |
579
+ | `editing.isRowEditable` | Member | `(row) => boolean` | – | Closes a whole row to editing. |
580
+ | `editing.rowValidators` | Member | TanStack Form validators | – | Form-level rules for the whole editing row. See [Editors](/docs/editors). |
581
+ | `editing.tableValidators` | Member | `TMDataGridTableValidators` | – | Cross-row rules, handed the collection with every draft overlaid. See [Editors](/docs/editors#cross-row-rules). |
582
+ | `editing.onCommit` | Callback | `({ rowId, value, original, changes, source }) => void \| Promise` | – | Applies one row's change. Reject to keep the draft. |
583
+ | `editing.onSaveDrafts` | Callback | `({ updated, created, deleted }) => void \| Result \| Promise` | – | `draft: true` only. One call for the whole draft store. See [Saving part of the store](#saving-part-of-the-store). |
584
+ | `editing.onCommitDrafts` | Callback | `({ updated, created, deleted }) => void \| Result \| Promise` | – | **Deprecated** - renamed to `onSaveDrafts`. Still honoured. |
585
+ | `editing.newRowsSticky` | Member | `boolean` | `false` | `draft: true` only. Keeps committed entry rows in the sticky entry block, out of the body's sort, until the save. |
586
+ | `editing.newRowDefaults` | Member | `TData \| () => TData` | – | Seeds the entry row's form. |
587
+ | `editing.onRowAdd` | Callback | `({ tempId, value }) => void \| Promise` | – | Commits an added row. |
588
+ | `editing.onRowDelete` | Callback | `({ rowId, row }) => void \| Promise` | – | Deletes a row. Shows the trash; under `draft: true`, `onSaveDrafts` shows it too. |
589
+ | `meta.edit.enabled` | Column meta | `boolean \| (row) => boolean` | `true` | Whether a column's cells edit. |
590
+ | `meta.edit.field` | Column meta | `string` | The `accessorKey` | The data path an edit writes to. |
591
+ | `meta.edit.mapValue` | Column meta | `({ value, previous, row, column }) => unknown` | – | Maps each value an editor writes. See [Editors](/docs/editors#mapping-the-value-as-it-is-typed). |
592
+ | `EDIT_COLUMN_ID` | Export | `"__edit__"` | – | Id of the generated edit lane. |
593
+ | `TMDataGrid.DraftActions` | Component | – | – | Save and Discard for pending edits. |
594
+ | `DraftActions` `renderActions` | Slot | `({ state, actions, Controls }) => ReactNode` | Built-in pair | Replaces the buttons, and hands over their pieces. See [Components](/docs/components#tmdatagriddraftactions). |
595
+ | `actions.scrollToFirstOpenRow` | Slot action | `(align?) => boolean` | `align: "auto"` | Scrolls to the first open row in display order. `false` when none could be reached. |
596
+ | `clearedValueForType` | Export | `(type) => unknown` | – | What Delete writes for each column type. |
597
+ | `--dg-entry-height` | CSS variable | length | From `size` | Height of the sticky entry block. |
598
+ | `--dg-row-new-bg` | CSS variable | color | Green tint | Background of a committed new row, in the body or the entry block. |
599
+ | `data-deleted` | Data attribute | – | – | On a row marked for deletion under `draft: true`. |
600
+ | `data-dirty` | Data attribute | – | – | On a body row holding a dirty draft. |
601
+ | `data-draft` | Data attribute | – | – | On a body row or entry row committed into the draft store, waiting for a save. |
602
+ | `data-new` | Data attribute | – | – | On a body row that is a committed new row, and on an entry row. |
603
+ | `data-committed` | Data attribute | – | – | On an entry row once it is committed, awaiting the save. Seen only under `newRowsSticky`. |