@jielga/tmdatagrid 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,499 @@
1
+ ---
2
+ name: editing
3
+ description: >
4
+ Edit cells and rows in TMDataGrid. Covers editMode with its four policies
5
+ (cell, cellConfirm, row, batch), the required getRowId, onEditCommit and
6
+ onEditCommitBatch, why the grid never mutates data, per-column gating with
7
+ meta.editable and meta.editField, the six built-in editors picked by
8
+ meta.type, custom editors through meta.editor, field validation with
9
+ meta.validate and cross-field rules with rowValidators (Standard Schema and
10
+ Zod), adding and deleting rows with edit.addRow, newRowDefaults, onRowAdd,
11
+ onRowDelete, the generated edit lane, TMDataGrid.EditActions with its
12
+ renderActions slot, and the public edit engine (begin, commit, cancel,
13
+ submitAll, getForm, store). Load when making a grid editable, choosing an edit
14
+ mode, wiring a save, writing a cell editor, validating an edit, or when cells
15
+ will not open for editing.
16
+ metadata:
17
+ type: core
18
+ library: '@jielga/tmdatagrid'
19
+ library_version: '1.1.0'
20
+ sources:
21
+ - 'Jielga/TMDataGrid:src/docs/editing.md'
22
+ - 'Jielga/TMDataGrid:src/docs/query-builder.md'
23
+ - 'Jielga/TMDataGrid:src/docs/editors.md'
24
+ - 'Jielga/TMDataGrid:src/tmdatagrid/core/editEngine.ts'
25
+ - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
26
+ ---
27
+
28
+ # TMDataGrid - Editing
29
+
30
+ `editMode` turns editing on and picks how commits happen. Three facts decide
31
+ every wiring question below:
32
+
33
+ - **The grid never mutates `data`.** `onEditCommit` applies the change wherever
34
+ the data actually lives, and the new rows arrive back through `data`.
35
+ - **One row, one form.** Each editing row gets its own TanStack Form, keyed by
36
+ row id and living outside the DOM, so a draft survives scrolling, sorting and
37
+ filtering.
38
+ - **`getRowId` is required** once `editMode` is set, and it must be the record's
39
+ own identity. Drafts are keyed by it.
40
+
41
+ `@tanstack/react-form` becomes a peer dependency once editing is used.
42
+
43
+ ## Setup
44
+
45
+ ```tsx
46
+ const [employees, setEmployees] = useState(initial);
47
+
48
+ const grid = useTMDataGrid({
49
+ data: employees,
50
+ columns,
51
+ getRowId: (row) => String(row.id),
52
+ editMode: "cell",
53
+ // The grid writes nothing: apply the change, and the edited row arrives
54
+ // back through `data`.
55
+ onEditCommit: ({ rowId, value }) =>
56
+ setEmployees((previous) =>
57
+ previous.map((employee) =>
58
+ String(employee.id) === rowId ? value : employee,
59
+ ),
60
+ ),
61
+ });
62
+
63
+ <TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }}>
64
+ <TMDataGrid.Table<Employee> />
65
+ </TMDataGrid>;
66
+ ```
67
+
68
+ The editing options travel together, and the types say so: passing any of them
69
+ without `editMode` is a compile error, `getRowId` stops being optional the
70
+ moment `editMode` is set, and `onEditCommitBatch` exists only under
71
+ `editMode: "batch"`.
72
+
73
+ ## The four modes
74
+
75
+ One axis, four policies over the same engine.
76
+
77
+ | Mode | Commits on | Cancels on | Chrome |
78
+ | --- | --- | --- | --- |
79
+ | `"cell"` | Enter, Tab, blur | Escape | none |
80
+ | `"cellConfirm"` | ✓ or Enter; blur keeps the draft | ✕ or Escape | ✓ / ✕ beside the input |
81
+ | `"row"` | Save in the edit lane, or Ctrl+Enter | Cancel, or Escape | generated edit lane |
82
+ | `"batch"` | `edit.submitAll()` | `edit.cancelAll()` | `TMDataGrid.EditActions` |
83
+
84
+ Which to pick: `"cell"` for saved-as-you-go spreadsheet feel; `"cellConfirm"`
85
+ when a stray click must not fire a request; `"row"` when the row is the unit of
86
+ the save or a rule spans two columns; `"batch"` for many edits sent as one
87
+ transaction.
88
+
89
+ An editor opens on double-click, or with the cell cursor on the cell: Enter, F2,
90
+ or just typing, where the character replaces the value. Delete or Backspace
91
+ clears the value and commits without opening an editor. Editing brings cell
92
+ selection with it: `cellSelection` defaults to `"single"` while `editMode` is
93
+ set.
94
+
95
+ Under `"batch"` nothing leaves the grid until `submitAll`. Enter and Tab park
96
+ the draft, dirty-marked, Escape drops that one draft, and drafts accumulate
97
+ across rows. `submitAll` then commits every open row: through the per-row
98
+ `onEditCommit` loop by default, or through one
99
+ `onEditCommitBatch({ rows, added, deleted })` call when that is set. Rows
100
+ failing validation stay open either way, and a rejected batch keeps every
101
+ draft.
102
+
103
+ ## What a commit receives
104
+
105
+ `onEditCommit` is handed `{ rowId, value, original, changes, source }`: `value`
106
+ is the whole edited row for consumers who save records, `changes` the per-field
107
+ diff (`columnId`, `field`, `previous`, `next`) for consumers who PATCH, one
108
+ entry in cell mode.
109
+
110
+ The engine drops the draft only when `onEditCommit` resolves. A slow save keeps
111
+ the draft on screen with a busy marker; a **rejection keeps the form open** with
112
+ the error on the row, which is how a failed save stays visible.
113
+
114
+ ## Which cells edit
115
+
116
+ A column is editable when it maps to a data path: its `accessorKey`, or
117
+ `meta.editField` for a column built on `accessorFn`. Dot paths reach into nested
118
+ records, so `accessorKey: "address.city"` edits `values.address.city`.
119
+
120
+ | Gate | Effect |
121
+ | --- | --- |
122
+ | `meta.editable: false` | The column never edits |
123
+ | `meta.editable: (row) => boolean` | Per row, per column |
124
+ | `meta.editField: "lastName"` | The path an `accessorFn` column writes to |
125
+ | `isRowEditable: (row) => boolean` | The whole row, in every mode |
126
+
127
+ Group rows and the generated lanes (checkbox, row number, details, edit) never
128
+ edit.
129
+
130
+ ```tsx
131
+ // Computed column writing back to a real field, and per-row gating.
132
+ columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
133
+ id: "fullName",
134
+ header: "Full name",
135
+ meta: { label: "Full name", editField: "lastName" },
136
+ });
137
+ columnHelper.accessor("salary", {
138
+ header: "Salary",
139
+ meta: { editable: (row) => row.original.status !== "Terminated" },
140
+ });
141
+ ```
142
+
143
+ ## Editors and validation
144
+
145
+ `meta.type` picks the editor, and `meta.options` feeds the select editors from
146
+ the same declaration the filter panel reads.
147
+
148
+ | `meta.type` | Editor |
149
+ | --- | --- |
150
+ | `"string"` (default) | Text input |
151
+ | `"number"` | Number input |
152
+ | `"boolean"` | Checkbox |
153
+ | `"date"` | Native `<input type="date">`, ISO `YYYY-MM-DD` |
154
+ | `"select"` | Searchable select from `meta.options`; commits on pick under `"cell"` |
155
+ | `"multiSelect"` | Multi-select, same source |
156
+
157
+ `meta.editor` replaces one, `meta.validate` guards the field, and
158
+ `rowValidators` carries cross-field rules. The validators are TanStack Form's
159
+ own, Standard Schema included, so a Zod schema passes straight through:
160
+
161
+ ```tsx
162
+ import { z } from "zod";
163
+
164
+ // Field level, on the column. A bare schema means { onChange: schema }.
165
+ meta: { validate: z.string().min(2, "At least two characters") }
166
+
167
+ // Form level, on the hook. Cross-field rules, under "row" or "batch".
168
+ rowValidators: {
169
+ onSubmit: z
170
+ .object({ salary: z.number().positive(), status: z.string() })
171
+ .refine((row) => row.status !== "Terminated" || row.salary === 0, {
172
+ message: "A terminated employee has no salary",
173
+ }),
174
+ }
175
+ ```
176
+
177
+ Pathed issues land on the matching cells, pathless ones on the row. Cell corners
178
+ mark state meanwhile: blue for a dirty draft, red for a validation error.
179
+
180
+ The editor contract, wrapping a built-in, and async server-side field errors:
181
+ [references/editors-and-validation.md](references/editors-and-validation.md).
182
+
183
+ ## Adding and deleting rows
184
+
185
+ `edit.addRow()` opens an entry row in a sticky block under the header, seeded
186
+ from `newRowDefaults`. Enter, or the lane's ✓, commits the add through
187
+ `onRowAdd` under the immediate modes; batch parks it for `submitAll`. Escape, or
188
+ ✕, discards it.
189
+
190
+ ```tsx
191
+ const grid = useTMDataGrid({
192
+ data: employees,
193
+ columns,
194
+ getRowId: (row) => String(row.id),
195
+ editMode: "batch",
196
+ newRowDefaults: () => ({ id: 0, firstName: "", salary: 30_000 }),
197
+ onEditCommitBatch: async ({ rows, added, deleted }) => {
198
+ await api.saveBatch({ rows, added, deleted });
199
+ },
200
+ });
201
+
202
+ <TMDataGrid.Toolbar>
203
+ <Button onClick={() => grid.edit.addRow()}>Add row</Button>
204
+ <TMDataGrid.Spacer />
205
+ <TMDataGrid.EditActions />
206
+ </TMDataGrid.Toolbar>;
207
+ ```
208
+
209
+ `edit.deleteRow(rowId)` calls `onRowDelete({ rowId, row })` straight away under
210
+ the immediate modes, so a confirmation belongs inside that callback. Under batch
211
+ it toggles a mark instead: the row renders struck through and inert
212
+ (`data-deleted`), the lane's trash becomes a restore, and `submitAll` reports
213
+ the ids in `deleted`. Adds and deletes are applied by you, the same as edits -
214
+ the engine's `tempId` (`__new__1`, …) never needs to become a real id.
215
+
216
+ ## The chrome
217
+
218
+ The generated edit lane (`EDIT_COLUMN_ID`, pinned right) appears when `editMode`
219
+ is `"row"`, or `onRowDelete` is set, or `editMode` is `"batch"` with
220
+ `onEditCommitBatch` set. Nothing else conjures it, and `"cell"` mode has no
221
+ chrome at all by design.
222
+
223
+ `TMDataGrid.EditActions` is Save with the pending count plus Discard. It greys
224
+ out while nothing is pending, spins while a submit is in flight, renders nothing
225
+ while editing is off, and works under any mode, not only batch.
226
+
227
+ `renderActions` replaces the pair while handing over its pieces:
228
+
229
+ ```tsx
230
+ <TMDataGrid.EditActions
231
+ renderActions={({ state, Controls }) => (
232
+ <Group>
233
+ {state.pendingCount > 0 && <Badge>{state.pendingCount}</Badge>}
234
+ <Controls.Save />
235
+ <Controls.Discard />
236
+ </Group>
237
+ )}
238
+ />
239
+ ```
240
+
241
+ `state` is `{ pendingCount, isSubmitting }`, `actions` is `{ save, discard }`.
242
+
243
+ ## The engine: `edit`
244
+
245
+ `grid.edit` is public, and everything the chrome does goes through it.
246
+
247
+ | Member | Does |
248
+ | --- | --- |
249
+ | `edit.begin({ rowId, columnId })` | Opens an editor. `columnId: null` opens the whole row |
250
+ | `edit.commit(rowId)` | Commits one row. `Promise<boolean>`, `false` when blocked |
251
+ | `edit.cancel(rowId)` / `edit.cancelAll()` | Drops one draft, or every draft |
252
+ | `edit.submitAll()` | Commits every open row. `Promise<boolean>` |
253
+ | `edit.addRow()` / `edit.deleteRow(rowId)` | Rows in and out |
254
+ | `edit.getForm(rowId)` | The row's live TanStack Form `FormApi` |
255
+ | `edit.store` | The projection store, for `useSelector` |
256
+
257
+ ```tsx
258
+ import { useSelector } from "@tanstack/react-store";
259
+
260
+ const pendingCount = useSelector(
261
+ grid.edit.store,
262
+ (state) => state.openRowIds.length + state.deletedRowIds.length,
263
+ );
264
+ ```
265
+
266
+ `edit.store` holds `active`, `openRowIds`, `rows` (per row `dirtyFields`,
267
+ `errorFields`, `isSubmitting`), `newRows` and `deletedRowIds`. The full member
268
+ list, `clearCell`, `deactivate` and the gates included, is in
269
+ [references/editing-api.md](references/editing-api.md).
270
+
271
+ `getForm` is the *one row, one form* promise made public: render that same form
272
+ in a drawer and it shares values, dirty state and errors with the inline cells,
273
+ because it is the same `FormApi`.
274
+
275
+ ## Inside an outer form
276
+
277
+ A `@tanstack/react-form` form can own the row array, with the grid as a
278
+ controlled field: `data` from `field.state.value`, and `onEditCommit` /
279
+ `onRowAdd` / `onRowDelete` composing the next array into `field.handleChange`.
280
+ Use `editMode: "row"` so a row reaches the form on approval, map by row id
281
+ (never index), and mint negative ids for new rows.
282
+
283
+ The validation split is structural, not stylistic: **a rule decidable from one
284
+ row belongs to the grid (`meta.validate`, `rowValidators`); a rule needing the
285
+ other rows or the collection ("has rows", "no duplicates") belongs to the
286
+ form's field validator.** A row's form cannot see the array, and `edit.store`
287
+ publishes field names, never values, so the form cannot see a draft.
288
+
289
+ ## Common mistakes
290
+
291
+ ### CRITICAL Expecting the grid to write into `data`
292
+
293
+ The grid holds no copy of the rows. Without `onEditCommit` an edit commits, the
294
+ draft clears, and the cell snaps back to the value in `data`, which looks like
295
+ the edit was silently discarded.
296
+
297
+ Wrong:
298
+
299
+ ```tsx
300
+ useTMDataGrid({ data: employees, columns, getRowId, editMode: "cell" });
301
+ ```
302
+
303
+ Correct:
304
+
305
+ ```tsx
306
+ useTMDataGrid({
307
+ data: employees,
308
+ columns,
309
+ getRowId,
310
+ editMode: "cell",
311
+ onEditCommit: ({ rowId, value }) =>
312
+ setEmployees((previous) =>
313
+ previous.map((employee) =>
314
+ String(employee.id) === rowId ? value : employee,
315
+ ),
316
+ ),
317
+ });
318
+ ```
319
+
320
+ Source: `src/docs/editing.md`, `src/tmdatagrid/core/editEngine.ts`.
321
+
322
+ ### CRITICAL `getRowId` built from the row index
323
+
324
+ `getRowId` satisfies the compiler with any string, so an index-based id passes.
325
+ Drafts are keyed by it, so after a sort or a filter the draft is applied to
326
+ whichever record now sits at that index.
327
+
328
+ Wrong:
329
+
330
+ ```tsx
331
+ getRowId: (row, index) => String(index),
332
+ ```
333
+
334
+ Correct:
335
+
336
+ ```tsx
337
+ getRowId: (row) => String(row.id),
338
+ ```
339
+
340
+ Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
341
+
342
+ ### HIGH A cell editor defined inside the component
343
+
344
+ `meta.editor` is rendered as JSX, so its identity is its component type. An
345
+ inline arrow is a new type on every render, which unmounts the open editor and
346
+ loses what was being typed.
347
+
348
+ Wrong:
349
+
350
+ ```tsx
351
+ meta: { editor: ({ field }) => <Slider value={field.state.value} /> },
352
+ ```
353
+
354
+ Correct:
355
+
356
+ ```tsx
357
+ // Module scope, referenced by name.
358
+ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
359
+ <Slider
360
+ value={Number(field.state.value ?? 0)}
361
+ onChange={(next) => field.handleChange(next)}
362
+ onChangeEnd={() => void commit()}
363
+ />
364
+ );
365
+
366
+ meta: { editor: SalaryEditor },
367
+ ```
368
+
369
+ Source: `src/docs/editors.md`, `src/tmdatagrid/core/editEngine.ts`.
370
+
371
+ ### HIGH A cross-field rule under `editMode: "cell"`
372
+
373
+ Under `"cell"` each cell commits on its own, so a rule spanning two columns can
374
+ never be satisfied by either one: the first cell edited is rejected against the
375
+ other column's old value, and the row cannot be saved at all.
376
+
377
+ Wrong:
378
+
379
+ ```tsx
380
+ useTMDataGrid({ editMode: "cell", rowValidators: { onSubmit: endAfterStart } });
381
+ ```
382
+
383
+ Correct:
384
+
385
+ ```tsx
386
+ // "row" (or "batch") validates the whole row in one commit.
387
+ useTMDataGrid({ editMode: "row", rowValidators: { onSubmit: endAfterStart } });
388
+ ```
389
+
390
+ Source: `src/docs/editors.md` (cross-field rules want a mode that commits the
391
+ whole row at once).
392
+
393
+ ### HIGH An `accessorFn` column that never opens an editor
394
+
395
+ Editability follows the data path, not the column. A column built on an accessor
396
+ function has no `accessorKey`, so it maps to nothing and stays read-only while
397
+ every other column edits. No warning is raised.
398
+
399
+ Wrong:
400
+
401
+ ```tsx
402
+ columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
403
+ id: "fullName",
404
+ header: "Full name",
405
+ });
406
+ ```
407
+
408
+ Correct:
409
+
410
+ ```tsx
411
+ columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
412
+ id: "fullName",
413
+ header: "Full name",
414
+ meta: { label: "Full name", editField: "lastName" },
415
+ });
416
+ ```
417
+
418
+ Source: `src/docs/editing.md` (Which cells edit).
419
+
420
+ ### HIGH Swallowing the error in `onEditCommit`
421
+
422
+ The draft is dropped when `onEditCommit` resolves. A `try/catch` that logs the
423
+ failure resolves it, so a save that failed on the server clears the editor and
424
+ the grid shows the old value as though nothing happened.
425
+
426
+ Wrong:
427
+
428
+ ```tsx
429
+ onEditCommit: async ({ rowId, changes }) => {
430
+ try {
431
+ await api.patch(rowId, changes);
432
+ } catch (error) {
433
+ console.error(error);
434
+ }
435
+ },
436
+ ```
437
+
438
+ Correct:
439
+
440
+ ```tsx
441
+ // Reject: the form stays open with the error on the row.
442
+ onEditCommit: async ({ rowId, changes }) => {
443
+ await api.patch(rowId, changes);
444
+ },
445
+ ```
446
+
447
+ Source: `src/tmdatagrid/useTMDataGrid.tsx` (`onEditCommit`).
448
+
449
+ ### HIGH Submitting an outer form while the grid holds a draft
450
+
451
+ The outer form's array contains only committed rows. Under any mode a mid-edit
452
+ row is invisible to it, and under `"batch"` *every* edit is until
453
+ `submitAll()` - so a form submit saves stale rows and collection rules pass
454
+ over pending values.
455
+
456
+ Correct:
457
+
458
+ ```tsx
459
+ const hasOpenDraft = useSelector(grid.edit.store, (s) => s.openRowIds.length > 0);
460
+
461
+ <Button type="submit" disabled={!canSubmit || hasOpenDraft}>Save</Button>
462
+ // or flush instead of blocking:
463
+ const flushed = await grid.edit.submitAll();
464
+ if (flushed) await form.handleSubmit();
465
+ ```
466
+
467
+ Source: `src/docs/query-builder.md` (Which mode, Submitting).
468
+
469
+ ### MEDIUM Reading a commit's result as the saved value
470
+
471
+ `edit.commit(rowId)` and `edit.submitAll()` resolve to a `boolean` saying
472
+ whether the form closed, and resolve `false` when validation or a rejected save
473
+ kept it open. Ignoring the result reports a save that did not happen.
474
+
475
+ ```tsx
476
+ const saved = await grid.edit.submitAll();
477
+ notifications.show({ message: saved ? "Saved" : "Some rows need attention" });
478
+ ```
479
+
480
+ Source: `src/tmdatagrid/core/editEngine.ts` (`TMDataGridEditApi`).
481
+
482
+ ### MEDIUM Expecting `onRowDelete` to fire under batch
483
+
484
+ Under the immediate modes `edit.deleteRow` calls `onRowDelete` at once. Under
485
+ `"batch"` it only toggles a deletion mark, and the ids arrive later in
486
+ `submitAll`'s `deleted`, so a batch grid that deletes through `onRowDelete`
487
+ alone never removes anything. Handle `deleted` in `onEditCommitBatch`.
488
+
489
+ Source: `src/docs/editing.md` (Adding and deleting rows).
490
+
491
+ ## References
492
+
493
+ - [Editors and validation](references/editors-and-validation.md) - the editor
494
+ contract, wrapping a built-in, field and row validators, server-side errors.
495
+ - [Editing API](references/editing-api.md) - every option, callback, column meta
496
+ field, export, CSS variable and data attribute editing owns.
497
+
498
+ See also: the `columns` skill for `meta.type` and the shared `meta.options`
499
+ source, and the `testing` skill for driving editors from a test.
@@ -0,0 +1,124 @@
1
+ # Editing API
2
+
3
+ Reference for the `editing` skill. Everything editing owns, in one table per
4
+ kind.
5
+
6
+ ## Hook options
7
+
8
+ | Name | Type | Default | What it does |
9
+ | --- | --- | --- | --- |
10
+ | `editMode` | `"cell" \| "cellConfirm" \| "row" \| "batch"` | off | Turns editing on and picks the commit policy. |
11
+ | `getRowId` | `(row) => string` | – | A TanStack table option, required once `editMode` is set. Drafts are keyed by it. |
12
+ | `isRowEditable` | `(row) => boolean` | – | Closes a whole row to editing, in every mode. |
13
+ | `rowValidators` | `TMDataGridRowValidators` | – | Form-level validation. Cross-field rules live here. |
14
+ | `newRowDefaults` | `TData \| (() => TData)` | – | Seeds the entry row's form. A function is called per added row. |
15
+ | `cellSelection` | `"none" \| "single" \| "range"` | `"single"` while editing | Editing turns the cell cursor on; set it explicitly to override. |
16
+
17
+ Passing any editing option without `editMode` is a compile error, and
18
+ `onEditCommitBatch` exists only in the `"batch"` branch of the type.
19
+
20
+ ## Callbacks
21
+
22
+ | Name | Argument | What it does |
23
+ | --- | --- | --- |
24
+ | `onEditCommit` | `{ rowId, value, original, changes, source }` | Applies one row's change. Reject to keep the draft and show the error. |
25
+ | `onEditCommitBatch` | `{ rows, added, deleted }` | Batch only. One call for the whole save. Without it, `submitAll` loops `onEditCommit`. |
26
+ | `onRowAdd` | `{ tempId, value }` | Commits an entry row. Mint the real id here. |
27
+ | `onRowDelete` | `{ rowId, row }` | Deletes a row under the immediate modes, and puts the trash can in the edit lane. |
28
+
29
+ `changes` entries are `{ columnId, field, previous, next }`; `field` is the data
30
+ path, which may be dotted.
31
+
32
+ ## Column meta
33
+
34
+ | Name | Type | Default | What it does |
35
+ | --- | --- | --- | --- |
36
+ | `meta.type` | `TMDataGridColumnType` | `"string"` | Picks the built-in editor, and the filter operators. |
37
+ | `meta.options` | `TMDataGridOptionsSource` | – | Choices for `select` / `multiSelect`, shared with the filter panel. Array, `"faceted"`, or a function. |
38
+ | `meta.editable` | `boolean \| ((row) => boolean)` | editable where a field maps | Whether this column's cells edit. |
39
+ | `meta.editField` | `string` | The `accessorKey` | The data path an edit writes to. The only way an `accessorFn` column edits. |
40
+ | `meta.editor` | `TMDataGridEditorComponent` | By `meta.type` | Replaces the cell editor. Define at module scope. |
41
+ | `meta.validate` | `TMDataGridFieldValidate` | – | Field-level validation. A bare schema means `onChange`. |
42
+
43
+ ## The `edit` engine
44
+
45
+ `grid.edit`, type `TMDataGridEditApi`.
46
+
47
+ | Member | Signature | Notes |
48
+ | --- | --- | --- |
49
+ | `begin` | `({ rowId, columnId }) => void` | `columnId: null` opens the whole row (row mode). |
50
+ | `commit` | `(rowId) => Promise<boolean>` | `false` keeps the form open with its errors. |
51
+ | `cancel` | `(rowId) => void` | Drops one draft. |
52
+ | `cancelAll` | `() => void` | Drops every draft. |
53
+ | `deactivate` | `() => void` | Closes the editor without touching the draft, as blur does under `"cellConfirm"`. |
54
+ | `submitAll` | `() => Promise<boolean>` | Batch's save. `true` when every row landed. |
55
+ | `clearCell` | `(rowId, columnId) => Promise<boolean>` | What Delete does: writes the type's empty value and commits. |
56
+ | `addRow` | `() => string` | Opens an entry row, returns its `tempId`. |
57
+ | `deleteRow` | `(rowId) => void` | `onRowDelete` under the immediate modes, a deletion mark under batch. |
58
+ | `canEditCell` | `(row, column) => boolean` | The gate the chrome uses. |
59
+ | `canEditRow` | `(row) => boolean` | The pencil's gate. |
60
+ | `canDeleteRows` | `() => boolean` | Whether delete chrome makes sense. |
61
+ | `getForm` | `(rowId) => TMDataGridRowEditForm \| undefined` | The row's live `FormApi`. |
62
+ | `state` | `TMDataGridEditState` | Snapshot, for reads outside React. |
63
+ | `store` | `Store<TMDataGridEditState>` | For `useSelector`. |
64
+
65
+ `TMDataGridEditState`:
66
+
67
+ ```ts
68
+ type TMDataGridEditState = {
69
+ active: { rowId: string; columnId: string | null } | null;
70
+ openRowIds: ReadonlyArray<string>;
71
+ rows: Record<
72
+ string,
73
+ {
74
+ dirtyFields: ReadonlyArray<string>;
75
+ errorFields: ReadonlyArray<string>;
76
+ isSubmitting: boolean;
77
+ }
78
+ >;
79
+ newRows: ReadonlyArray<{ tempId: string }>;
80
+ deletedRowIds: ReadonlyArray<string>;
81
+ };
82
+ ```
83
+
84
+ ## Components and exports
85
+
86
+ | Name | Kind | What it is |
87
+ | --- | --- | --- |
88
+ | `TMDataGrid.EditActions` | Component | Save with the pending count, and Discard. Any mode; renders nothing while editing is off. Takes `renderActions` over `{ state, actions, Controls }`. |
89
+ | `TMDataGridEditActions` | Export | The same component, for use outside the namespace. |
90
+ | `EDIT_COLUMN_ID` | Export | `"__edit__"`, the generated edit lane's id. |
91
+ | `clearedValueForType` | Export | `(type) => unknown` - what Delete writes per column type. |
92
+ | `getEditFieldName` | Export | `(column) => string` - the data path a column's edits write to. |
93
+ | `normalizeFieldValidate` | Export | `(validate) => validators` - a bare schema into Form's shape. |
94
+ | `TMDataGridStringEditor` … `TMDataGridMultiSelectEditor` | Exports | The six built-in editors, for wrapping. |
95
+
96
+ Types: `TMDataGridEditMode`, `TMDataGridEditApi`, `TMDataGridEditState`,
97
+ `TMDataGridEditCommitArgs`, `TMDataGridEditCommitBatchArgs`,
98
+ `TMDataGridEditChange`, `TMDataGridEditorArgs`, `TMDataGridEditorComponent`,
99
+ `TMDataGridEditField`, `TMDataGridEditRowProjection`, `TMDataGridFieldValidate`,
100
+ `TMDataGridRowValidators`, `TMDataGridRowEditForm`, `TMDataGridRowAddArgs`,
101
+ `TMDataGridRowDeleteArgs`, `TMDataGridEditingOptions`.
102
+
103
+ ## The edit lane
104
+
105
+ Generated, pinned right, id `EDIT_COLUMN_ID`. It appears when any of these
106
+ holds:
107
+
108
+ - `editMode: "row"` - the lane is Save and Cancel's home
109
+ - `onRowDelete` is set - the trash can has somewhere to report to
110
+ - `editMode: "batch"` **and** `onEditCommitBatch` is set
111
+
112
+ `"cell"` and `"cellConfirm"` have no lane unless `onRowDelete` asks for one.
113
+
114
+ ## Styling and test hooks
115
+
116
+ | Name | Kind | What it is |
117
+ | --- | --- | --- |
118
+ | `--dg-entry-height` | CSS variable | Height of the sticky entry block. From `size`. |
119
+ | `data-deleted` | Row attribute | On a row marked for deletion under batch. |
120
+ | `data-dg-part="editor-input"` | Part | The control inside an editing cell. |
121
+ | `data-dg-part="save-row"` / `"cancel-row"` | Parts | The edit lane's buttons, with `data-row-id`. |
122
+ | `data-dg-part="save-all"` / `"discard-all"` | Parts | `EditActions`. |
123
+
124
+ See the `testing` skill for how to compose these into selectors.