@jielga/tmdatagrid 2.0.0-beta.21 → 2.0.0-beta.23

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/docs/editing.md CHANGED
@@ -2,21 +2,29 @@
2
2
 
3
3
  `@tanstack/react-form` becomes a peer dependency once editing is used.
4
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`.
5
+ Editing is turned on with the `editing` option.
6
+ Two settings inside it work independently:
9
7
 
10
- A row moves through three places, and the words for them are used exactly:
8
+ - `mode` - what counts as a commit: leaving the cell, confirming the cell, or saving the row
9
+ - `draft` - where a commit goes: to your `onCommit` callback at once, or into the grid's draft store until the user presses Save
11
10
 
12
- | Place | What it holds | In | Out |
11
+ The grid never modifies `data`.
12
+ You apply each commit in your callback, and the new values arrive back through `data`.
13
+
14
+ A row is in one of three places:
15
+
16
+ | Place | What it holds | Enters by | Leaves by |
13
17
  | --- | --- | --- | --- |
14
18
  | **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` |
19
+ | **form state** | A row being edited, in its own TanStack Form | `edit.begin`, `edit.addRow` | `edit.commit`, `edit.cancel` |
20
+ | **draft store** | Rows that passed their commit, held as values inside the grid until Save | `edit.commit` under `draft: true` | `edit.saveDrafts`, `edit.cancel` |
21
+
22
+ A row is **open** while it is in form state, and **committed** once it is in the draft store.
23
+ Without `draft: true` a commit goes straight to `onCommit` and the form is dropped, so the draft store stays empty.
24
+
25
+ ## Set up editing
17
26
 
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.
27
+ The smallest setup is cell mode with an `onCommit` that applies each change:
20
28
 
21
29
  ```tsx
22
30
  const grid = useTMDataGrid({
@@ -33,35 +41,34 @@ const grid = useTMDataGrid({
33
41
  });
34
42
  ```
35
43
 
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.
44
+ `onCommit` receives `{ rowId, value, original, changes, source }`:
40
45
 
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
+ - `value` - the whole row as edited
47
+ - `original` - the row as editing began
48
+ - `changes` - the per-field diff, `Array<{ columnId, field, previous, next }>`, one entry in cell mode
49
+ - `source` - the `mode` the commit came from
46
50
 
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.
51
+ `editing` requires `getRowId`: drafts are keyed by row id, and the index fallback would name a different record after a sort.
52
+ `onSaveDrafts` is accepted only under `draft: true`.
53
+ Both are compile errors, not options that silently do nothing.
49
54
 
50
- ## The three modes
55
+ The `editing` object can be written inline.
56
+ Its callbacks are read through a ref on every render, so its identity does not matter.
51
57
 
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.
58
+ Editing turns on cell selection: `cellSelection` defaults to `"single"` while `editing` is set.
59
+
60
+ ## Modes
61
+
62
+ `editing.mode` sets what counts as a commit and which controls trigger it.
63
+ All three modes use the same engine and the same forms.
54
64
 
55
65
  | Mode | Commit | Cancel | Controls |
56
66
  | --------------- | ------------------------------------------ | ----------------- | ------------------------ |
57
67
  | `"cell"` | Enter, Tab or leaving the cell | Escape | none |
58
68
  | `"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.
69
+ | `"row"` | ✓ in the edit lane, or Enter | ✕, or Escape | generated edit lane |
62
70
 
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.
71
+ An entry row from `edit.addRow()` is row-shaped in every mode: every editable cell opens at once, Tab walks them, and Enter, or the lane's ✓, commits it.
65
72
 
66
73
  ```demo
67
74
  file: editing/CellEditing.tsx
@@ -69,28 +76,24 @@ hint: Double-click a cell, or press Enter or F2, or start typing on it.
69
76
  height: 440
70
77
  ```
71
78
 
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.
79
+ ### Opening an editor
77
80
 
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.
81
+ An editor opens on double-click, or, with the cell cursor on the cell, on Enter, F2 or typing, where the first character replaces the value as in a spreadsheet.
82
+ The grid places the caret in the opened cell, so a `meta.edit.editor` receives focus without handling it itself.
83
+ A row added with `edit.addRow()` opens the same way, with the caret in its first editable cell.
84
84
 
85
- ### Row editing
85
+ Delete or Backspace clears the value and commits it without opening an editor.
86
+ The commit is validated: a column rule that rejects the empty value refuses the clear and marks the cell.
87
+ Under `draft: true` the cleared value goes into the draft store with the rest.
88
+
89
+ A commit that fails validation keeps the editor open and marked invalid, with the message in its tooltip, until the value is fixed or Escape drops the edit.
86
90
 
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
+ ### Row editing
91
92
 
92
- Rows **accumulate**: opening a second row leaves the first one open, and each
93
- row's ✓ and ✕ act on that row alone.
93
+ The pencil in the edit lane opens every cell of the row at once, and ✓ commits them as one commit.
94
+ Double-clicking a cell opens the whole row, with the caret in the clicked cell.
95
+ Cross-field rules belong in this mode, because the whole row is validated together.
96
+ Opening a second row leaves the first one open, and each row's ✓ and ✕ act on that row alone.
94
97
 
95
98
  ```demo
96
99
  file: editing/RowEditing.tsx
@@ -100,115 +103,120 @@ height: 440
100
103
 
101
104
  ## The draft store
102
105
 
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.
106
+ `draft: true` changes where a commit goes, and nothing else.
107
+ Instead of reaching `onCommit`, the row is committed into the grid's draft store, where it waits for `edit.saveDrafts()`.
108
+ The mode still decides what counts as a commit: `{ mode: "row", draft: true }` commits a whole row from the lane's ✓, and `{ mode: "cell", draft: true }` commits a row as the caret leaves the cell.
109
+
110
+ `draft: true` requires `getRowId`.
111
+ The usual setup adds `onSaveDrafts`, which receives the whole store in one call when the user presses Save, and `TMDataGrid.DraftActions` in the toolbar, which renders Save and Discard:
105
112
 
106
113
  ```tsx
107
- editing: {
108
- mode: "row",
109
- draft: true,
110
- onSaveDrafts: async ({ updated, created, deleted }) => {
111
- await api.saveBatch({ updated, created, deleted });
114
+ const grid = useTMDataGrid({
115
+ data,
116
+ columns,
117
+ getRowId: (row) => String(row.id),
118
+ editing: {
119
+ mode: "row",
120
+ draft: true,
121
+ onSaveDrafts: async ({ updated, created, deleted }) => {
122
+ await api.saveBatch({ updated, created, deleted });
123
+ },
112
124
  },
113
- }
125
+ });
126
+
127
+ <TMDataGrid {...grid}>
128
+ <TMDataGrid.Toolbar>
129
+ <TMDataGrid.DraftActions />
130
+ </TMDataGrid.Toolbar>
131
+ <TMDataGrid.Table />
132
+ </TMDataGrid>;
114
133
  ```
115
134
 
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.
135
+ In this demo:
119
136
 
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.
137
+ - ✓ commits the row into the draft store, and the Backend panel logs nothing
138
+ - Save sends the whole store as one `onSaveDrafts` call
139
+ - with "Reject Sales rows" on, the backend refuses those rows and they keep their drafts
140
+ - "Go to open row" scrolls to a row left open
125
141
 
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.
142
+ ```demo
143
+ file: editing/DraftEditing.tsx
144
+ hint: Double-click a row, ✓ commits it, Save sends the store.
145
+ height: 440
146
+ ```
134
147
 
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.
148
+ ### Saving the store
140
149
 
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.
150
+ `edit.saveDrafts()` sends the draft store and leaves open rows alone.
151
+ With `onSaveDrafts` set, it makes one call with the whole store, `onSaveDrafts({ updated, created, deleted })`:
144
152
 
145
- The rest of this page is what `draft: true` turns on.
153
+ - `updated` - one entry per committed edit, in the shape `onCommit` receives: `{ rowId, value, original, changes, source }`
154
+ - `created` - one `{ tempId, value }` per committed new row
155
+ - `deleted` - the ids of the rows marked for deletion
146
156
 
147
- ### The lane
157
+ Without `onSaveDrafts`, `saveDrafts` makes one call per row instead: `onCommit` for each edit, `onRowAdd` for each new row and `onRowDelete` for each deletion.
158
+ `draft: true` without `onSaveDrafts` is therefore valid.
148
159
 
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.
160
+ `changes` is a list of descriptors, not a patch object; spreading it into a row compiles and writes nothing.
161
+ To build a patch:
150
162
 
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
163
+ ```tsx
164
+ const patch = Object.fromEntries(entry.changes.map((c) => [c.field, c.next]));
165
+ ```
155
166
 
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.
167
+ Before the rows are sent, `editing.tableValidators` run once more over every committed row.
168
+ Column rules and `rowValidators` ran at commit on the same values, so they do not run again.
169
+ A row the table rules reject is reopened with its errors and left out of the save.
170
+ On the per-row path, a row whose `onCommit` or `onRowAdd` rejects is reopened the same way.
160
171
 
161
- ### Marking the drafts
172
+ `TMDataGrid.DraftActions` renders the whole-grid controls: Save with the count of rows in the store, Discard, and a note counting the rows still open.
173
+ Save is disabled while the store is empty, however much is being typed, and shows a loading state while the save is in flight.
174
+ The toolbar is declarative: include the component yourself when the grid runs a draft store, or call `edit.saveDrafts()` from a control of your own.
175
+ Without `draft: true` there is nothing to save and Save stays disabled.
162
176
 
163
- Rows publish what they are holding, for styling and for tests:
177
+ ### Saving part of the store
164
178
 
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) |
179
+ `onSaveDrafts` decides how much of the store is cleared by what it returns:
171
180
 
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.
181
+ | Returned | Effect |
182
+ | --- | --- |
183
+ | nothing | Everything saved. The store is cleared. |
184
+ | a rejected promise, or a throw | Nothing saved. Every draft is kept. |
185
+ | `{ updated, created, deleted }` | The ids reported `false` are kept; the rest are cleared. |
176
186
 
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:
187
+ Each key takes `false` for the whole bucket, or a map from id to result.
188
+ An id the map does not name counts as saved.
180
189
 
181
190
  ```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
- />
191
+ onSaveDrafts: async ({ updated, created, deleted }) => {
192
+ const failed = await api.saveBatch({ updated, created, deleted });
193
+ return { updated: Object.fromEntries(failed.map((id) => [id, false])) };
194
+ };
189
195
  ```
190
196
 
191
- `rowClassName` takes a class instead. For CSS alone, target the attribute:
192
- `[data-dg-part="row"][data-draft="true"]`.
197
+ A kept row stays committed rather than reopening, so the next `saveDrafts()` retries it with the values it already holds.
198
+ `saveDrafts()` resolves `false` when anything was kept.
199
+ A kept row carries the same markers as every other draft and nothing more; see [Styling pending rows](#styling-pending-rows).
200
+
201
+ ### Rows left open
193
202
 
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.
203
+ A row left open is neither lost nor sent.
204
+ It keeps everything typed into it, stays open across a save, and joins the next save once it is committed.
205
+ `edit.commitAll()` submits every open row at once; rows that fail validation stay open with their errors.
206
+ "Commit everything, then save" is `commitAll()` followed by `saveDrafts()`.
207
+ `edit.submitAll()` did both in one call and is **deprecated**; replace it with the half you meant.
200
208
 
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.
209
+ The note beside Save counts the open rows.
210
+ On a long grid the open row may be far from the viewport, and because the grid is [virtualized](/docs/scrolling) it may have no element to scroll to.
211
+ `actions.scrollToFirstOpenRow(align?)` scrolls to the first open row in display order and returns whether it could be reached.
206
212
 
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.
213
+ `renderActions` replaces the built-in pair and receives its pieces:
214
+
215
+ - `state` - `draftCount`, `openCount`, `openRowIds`, `isSubmitting` and `isSaving`
216
+ - `actions` - `save`, `commitAll`, `discard`, `scrollToRow` and `scrollToFirstOpenRow`
217
+ - `Controls` - `Save`, `Discard` and `OpenRowsNote`, the built-in pieces
218
+
219
+ The full list is on [Components](/docs/components#tmdatagriddraftactions).
212
220
 
213
221
  ```tsx
214
222
  <TMDataGrid.DraftActions
@@ -231,147 +239,64 @@ whether it could be reached.
231
239
  />
232
240
  ```
233
241
 
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.
242
+ `state.openRowIds` lists the open rows in the order the grid opened them, for a control the grid does not offer, such as a list or a next-open-row cycle.
243
+ `scrollToFirstOpenRow` takes "first" in display order, so the two need not name the same row.
244
+ An entered row appears as its `tempId`; entry rows are always on screen in the entry block, so the scroll returns `true` without moving.
299
245
 
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 |
246
+ ### How a committed row behaves
306
247
 
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.
248
+ A committed row holds no form: its values are data in the draft store.
249
+ To the table it is a row like any other, and everything that reads a row reads the draft:
311
250
 
312
- ```tsx
313
- editing: { mode: "cell", columns: ["targetPct", "note"] }
314
- ```
251
+ - sorting, filtering, quick search and grouping
252
+ - `aggregatedCell`, `footer` and the faceted filter options
253
+ - export, row selection, the row numbers and counts
254
+ - `edit.getRows()` and `editing.tableValidators`
255
+ - the row callbacks `onRowClick`, `renderRowContextMenu`, `renderDetails`, `isRowEditable`, `meta.edit.enabled`, `rowClassName`, `rowStyle` and `enableRowPinning`, which receive a row whose `original` is the committed draft; an entered row is a record under its temp id, with no server id
315
256
 
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.
257
+ `data` itself is never modified, and `getRowCount()` with a `rowCount` you set does not grow.
258
+ Only top-level rows are overlaid; children reached through `getSubRows` keep their `data` values.
259
+ A committed row that stops matching a filter or the quick search leaves the view, and Save still counts it.
318
260
 
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.
261
+ Reopening a committed row, through `begin` or a write with `setCellValue`, `setRowValues` or `clearCell`, builds a fresh form seeded from the committed values and takes the row out of the store until it commits again.
262
+ The row keeps its place in the sort until it commits again or is cancelled.
321
263
 
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
- ```
264
+ A commit moves nothing else: the page stays, and open details panels and groups stay open.
265
+ 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`.
327
266
 
328
- ## Draft lifetime
267
+ A refetch that no longer returns a row drops that row's draft, its open editor and its deletion mark.
268
+ Under `manualPagination` or `manualFiltering` a row missing from `data` is on another page, not gone, so its draft is kept until Save.
329
269
 
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.
270
+ ## Edit lane
333
271
 
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.
272
+ The edit lane is the generated column at the end of every row, with the id `EDIT_COLUMN_ID`.
273
+ The grid adds it under `mode: "row"`, under `draft: true`, or when `onRowDelete` is set.
274
+ What it shows depends on the row's state:
341
275
 
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`:
276
+ | Row state | Icon | Actions |
277
+ | --- | --- | --- |
278
+ | Open | - | The mode's controls: ✓ ("Save row") commits the row, ✕ ("Cancel edit") cancels the edit |
279
+ | Committed edit | Pencil | Revert drops the row's draft |
280
+ | Committed new row | Plus | Pencil reopens it, ✕ removes it |
281
+ | Marked for deletion | Trash | Restore removes the mark |
346
282
 
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
- ```
283
+ A committed row is never offered ✓ again; `TMDataGrid.DraftActions` sends it.
284
+ A committed row also hides the trash: revert first, then delete.
285
+ The trash shows when a deletion has somewhere to go: `onRowDelete` is set, or under `draft: true`, `onSaveDrafts` is.
354
286
 
355
- `useTMDataGridContext()` reaches the engine from inside a cell renderer, and
356
- the selector re-renders the cell as the draft changes.
287
+ If validation blocks a row, its icon turns red and the tooltip shows the message; this covers the open row's ✓ and an entry row's ✓ alike.
288
+ A pathless issue from `rowValidators` has no cell to land on, so that tooltip is where its message shows.
357
289
 
358
- ## Adding and deleting rows
290
+ ## Adding rows
359
291
 
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.
292
+ `edit.addRow()` opens an **entry row** in a sticky block under the header, so the row being typed into stays in view.
293
+ Its cells are ordinary editors over a form seeded from `newRowDefaults`.
294
+ A cell the row does not open, a display column or one with `meta.edit.enabled: false`, renders through the column's own `cell` renderer over the row as shown, following the form as it is typed, as it does on a body row.
367
295
 
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.
296
+ - Enter, or the lane's ✓, commits the row: `onRowAdd` receives it, or under `draft: true` it goes into the draft store and `saveDrafts` reports it in `created`
297
+ - Escape, or ✕, discards it
298
+ - clicking away decides nothing; an entry row is row-shaped in every mode
299
+ - an entry row that was never committed is not part of a save and stays open
375
300
 
376
301
  ```tsx
377
302
  useTMDataGrid({
@@ -388,19 +313,13 @@ useTMDataGrid({
388
313
  <Button onClick={() => grid.edit.addRow()}>Add row</Button>;
389
314
  ```
390
315
 
391
- Annotate `newRowDefaults`' return type: a bare object literal widens a union
392
- field to `string`, and `(): Product => ({ ... })` keeps it checked.
316
+ Annotate the return type of `newRowDefaults`: a bare object literal widens a union field to `string`, and `(): Product => ({ ... })` keeps it checked.
393
317
 
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.
318
+ `addRow(values)` overrides `newRowDefaults` key by key: `addRow()` opens the defaults, `addRow({ department: "Sales" })` opens them with `department` filled in, and passing a whole row duplicates it.
319
+ The seeded values are editable and validate like any other, and nothing reaches `onRowAdd` until the row is committed.
400
320
 
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" })`.
321
+ A grouped column has no cell on the entry row; under the default `groupedColumnMode: "remove"` it is not in the grid at all.
322
+ Seed it instead: `addRow({ region: "EMEA" })`.
404
323
 
405
324
  ```tsx
406
325
  <Button onClick={() => grid.edit.addRow({ department: "Sales", active: true })}>
@@ -410,8 +329,7 @@ cannot type the grouped field. Seed it: `addRow({ region: "EMEA" })`.
410
329
  <Button onClick={() => grid.edit.addRow(selected.original)}>Duplicate</Button>;
411
330
  ```
412
331
 
413
- To limit how many entry rows are open at once, read the entry state off
414
- `edit.store` and gate the button:
332
+ To limit how many entry rows are open at once, read the entry state from `edit.store` and gate the button:
415
333
 
416
334
  ```tsx
417
335
  const hasOpenEntry = useSelector(grid.edit.store, (state) =>
@@ -423,22 +341,18 @@ const hasOpenEntry = useSelector(grid.edit.store, (state) =>
423
341
  </Button>;
424
342
  ```
425
343
 
426
- ### Importing rows
344
+ ### Committed new rows
427
345
 
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.
346
+ Under `draft: true` a committed entry row leaves the entry block and becomes a body row: it carries `data-new` and `data-draft`, is tinted with `--dg-row-new-bg`, and is sorted, filtered and counted with the rest on the values it was entered with.
347
+ Set `newRowsSticky: true` to keep committed rows in the entry block until the save instead, out of the body's sort and out of the row count.
348
+ Double-click, or the lane's pencil, reopens the row into the entry block, which takes it out of the draft store until it is committed again; ✕ removes it.
431
349
 
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.
350
+ ### Importing rows
436
351
 
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.
352
+ `edit.addRows(rows)` opens a batch of entry rows in one write, where a loop over `addRow` is one write per row.
353
+ Each row is seeded over `newRowDefaults` like `addRow`.
354
+ `{ commit: true }` submits the rows too: rows that validate are committed, and rows that fail stay open in the entry block with their errors.
355
+ The result says which went which way, so the bad rows can be reported before anything is saved:
442
356
 
443
357
  ```tsx
444
358
  const { committed, open } = await grid.edit.addRows(parsedRows, {
@@ -448,11 +362,10 @@ if (open.length > 0) notify(`${open.length} rows need attention`);
448
362
  await grid.edit.saveDrafts();
449
363
  ```
450
364
 
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.
365
+ Column rules apply even though the rows never had an editor on screen: the engine runs `meta.edit.validate` at commit.
366
+ Under `draft: true` the whole import is one publish: the rows are validated together and land in the draft store in the same render that shows them, so ten thousand rows take about a second.
367
+ `saveDrafts` sends them the same way.
368
+ Without `draft: true`, `commit: true` adds each valid row through `onRowAdd`, one call per row, in the order given.
456
369
 
457
370
  ```demo
458
371
  file: editing/ImportRows.tsx
@@ -460,42 +373,116 @@ hint: Import parses the pasted rows, commits the valid ones and leaves the rest
460
373
  height: 460
461
374
  ```
462
375
 
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`
376
+ ## Deleting rows
377
+
378
+ `edit.deleteRow(rowId)` calls `onRowDelete({ rowId, row })` at once; put any confirmation in that callback.
379
+ Under `draft: true` it marks the row instead: the row renders struck through and inert with `data-deleted`, the lane shows Restore, and `saveDrafts` reports the id in `deleted`.
380
+ The mark goes straight into the draft store; there is nothing to type.
381
+ `edit.restoreRow(rowId)` removes the mark, which is what the lane's Restore calls.
382
+
383
+ `deleteRow` is idempotent: deleting a marked row leaves it marked.
384
+ On an entry row, committed or not, it discards the entry, and an unknown id is a no-op.
385
+ `edit.deleteRows(rowIds)` does the same over a list in one call, so a selection can be passed as it stands: duplicates, marked rows and stale ids included.
386
+
387
+ A marked row is read-only and not selectable until it is restored:
388
+
389
+ - `begin`, `setCellValue`, `setRowValues` and `clearCell` refuse it, and the keyboard cannot open an editor on it
390
+ - an editor open on the row when it is marked is cancelled
391
+ - its checkbox is disabled, select-all skips it, and the mark drops it from `rowSelection`
392
+ - it still sorts, filters, groups, aggregates and counts, and is left out of an export
393
+ - a committed edit stays under the mark: Restore brings the row back as edited, while Save leaves the edit out of `updated`, reports the row in `deleted` only, and forgets the edit once the deletion is saved
394
+
395
+ A row the engine takes out of the table, an entry row that is discarded or saved or a marked row once its deletion is saved, leaves `rowSelection`, `expanded` and `rowPinning` with it, so a saved deletion does not leave the select-all box indeterminate.
396
+
397
+ The grid never modifies `data`: you apply adds and deletes, and the new rows arrive back through `data`.
398
+ The engine's `tempId` (`__new__1`, …) does not need to become a real id; assign one when you create the record.
399
+
400
+ ## Which cells edit
401
+
402
+ A column is editable when it maps to a data path: its `accessorKey`, or `meta.edit.field` for a column built on `accessorFn`.
403
+ Dot paths reach into nested records: `accessorKey: "address.city"` edits `values.address.city`, and issues from a nested schema map to the right column.
404
+
405
+ | Gate | Effect |
406
+ | --- | --- |
407
+ | `editing.columns: string[]` | Only the named columns edit |
408
+ | `meta.edit.enabled: false` | The column never edits |
409
+ | `meta.edit.enabled: (row) => boolean` | Per row, per column |
410
+ | `editing.isRowEditable: (row) => boolean` | The whole row, in every mode |
411
+
412
+ Group rows and the generated lanes never edit.
413
+
414
+ `editing.columns` lists the column ids that take edits.
415
+ By default, every column that maps to a data path is editable.
416
+
417
+ ```tsx
418
+ editing: { mode: "cell", columns: ["targetPct", "note"] }
419
+ ```
420
+
421
+ 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`.
422
+ The same list decides which cells an entry row opens.
423
+
424
+ `edit.isColumnEditable(column)` answers 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`.
425
+ A per-row `enabled` predicate is the row's half, and `edit.canEditCell(row, column)` asks both.
426
+
427
+ ```demo
428
+ file: editing/EditableGating.tsx
429
+ 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.
430
+ height: 440
431
+ ```
432
+
433
+ ## How a draft renders
434
+
435
+ Forms live outside the DOM, keyed by row id.
436
+ Scrolling an editing row away unmounts its editor; the form keeps its values, dirty state and errors, and the editor remounts over the same form when the row returns.
437
+
438
+ A cell whose row holds a draft renders the draft value through the column's own `cell` renderer, in every mode; a `"cellConfirm"` draft kept when the caret left shows what was typed, not the value in `data`.
439
+ Cell corners show the state: blue for a dirty draft, red for a validation error, and the row carries `data-dirty`.
440
+ An entry row's cells take the red corner, never the blue one.
441
+
442
+ A validation message outlives the editor that found it: the cell keeps its red corner and the lane carries the text until that field's value changes.
443
+ While an editor is open, the field's message shows in a tooltip on it, opened by focus and by hover.
444
+
445
+ Inside a `cell` renderer, `row.original` and `getValue()` are the row as shown, in every data column: the open form's values while the row is edited, the committed draft after ✓, and `data` otherwise.
446
+ A column computed from other fields follows the draft as it is typed, and a button in a cell sends the draft the user sees.
447
+ A handler with no cell context, a toolbar action or a callback that received only an id, reaches the same row with `edit.getRowValues(rowId)`.
448
+
449
+ ```demo
450
+ file: editing/ActionCell.tsx
451
+ hint: Double-click a row, change the salary, press Use: the button gets the draft, and data is untouched until Save.
452
+ height: 320
453
+ ```
454
+
455
+ ## Styling pending rows
456
+
457
+ Rows publish what they hold, for styling and for tests:
458
+
459
+ | Attribute | On | Means |
460
+ | --- | --- | --- |
461
+ | `data-dirty` | Body row, cell | Values typed in, committed or not |
462
+ | `data-draft` | Body row, entry row | Committed into the draft store, waiting for Save |
463
+ | `data-deleted` | Body row | Marked for deletion |
464
+ | `data-new` | Body row, entry row | An entered row, committed (body) or not (entry block) |
465
+
466
+ A row attribute is present, with the value `"true"`, only on the rows it applies to, so `[data-draft]` and `[data-draft="true"]` match the same rows.
467
+ A cell's `data-dirty` is present only while the cell is dirty.
468
+
469
+ The grid styles none of them beyond the corners, the strike-through and the new-row tint.
470
+ To highlight every row pending a save, and to let the user toggle it, use `rowStyle` on the Table:
471
+
472
+ ```tsx
473
+ <TMDataGrid.Table
474
+ rowStyle={(row) =>
475
+ showPending && grid.edit.state.committedRowIds.includes(row.id)
476
+ ? { "--row-bg": "color-mix(in srgb, var(--mantine-color-yellow-6) 15%, transparent)" }
477
+ : undefined
478
+ }
479
+ />
480
+ ```
481
+
482
+ `rowClassName` takes a class instead.
483
+ For CSS alone, target the attribute: `[data-dg-part="row"][data-draft="true"]`.
484
+
485
+ ## The edit API
499
486
 
500
487
  The built-in controls do everything through `edit`, which is public.
501
488
 
@@ -519,18 +506,17 @@ The built-in controls do everything through `edit`, which is public.
519
506
  | `edit.getForm(rowId)` | The open row's live `FormApi`; `undefined` for a committed row |
520
507
  | `edit.getRowValues(rowId)` | The row as shown: its draft where one is held, else the `data` value. `undefined` for an unknown row |
521
508
  | `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 |
509
+ | `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, and `isSaving` |
523
510
 
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.
511
+ `commit`, `commitAll`, `saveDrafts`, `setCellValue`, `setRowValues`, `clearCell` and `addRows` return promises.
512
+ Await each call before starting the next when driving edits in a loop.
527
513
 
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.
514
+ `getForm` returns the open row's own `FormApi`.
515
+ Render it in a drawer or side panel and it shares values, dirty state and errors with the inline cells.
516
+ 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
517
 
532
518
  `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.
519
+ `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
520
  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
521
 
536
522
  ```tsx
@@ -541,12 +527,11 @@ const selected = grid.table
541
527
  const surviving = grid.edit.getRows().filter((row) => !row.deleted);
542
528
  ```
543
529
 
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).
530
+ For the inverse, a `@tanstack/react-form` form _around_ the grid holding the row array, see [A query builder form](/docs/query-builder).
546
531
 
547
532
  ### Bulk actions
548
533
 
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.
534
+ `edit.setCellValue(rowId, columnId, value)` writes one cell and commits its row without an editor ever opening, for a toolbar action or a bulk fill.
550
535
  The row need not be mounted, so a selected row inside a collapsed group takes the write like any other.
551
536
 
552
537
  ```tsx
@@ -555,10 +540,10 @@ for (const row of grid.table.getSelectedRowModel().rows) {
555
540
  }
556
541
  ```
557
542
 
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.
543
+ Under `draft: true` each row is committed into the draft store like a typed edit: the whole batch saves at once through `edit.saveDrafts()`, carries the same markers, and is reverted row by row from the edit lane.
559
544
 
560
545
  `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`.
546
+ 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
547
 
563
548
  ```tsx
564
549
  await grid.edit.setRowValues(row.id, { status: "Closed", closedOn: today() });