@jielga/tmdatagrid 2.0.0-beta.0 → 2.0.0-beta.2

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 (52) hide show
  1. package/README.md +32 -34
  2. package/dist/index.d.ts +164 -102
  3. package/dist/index.js +2060 -1705
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/package.json +1 -1
  7. package/skills/appearance/SKILL.md +37 -34
  8. package/skills/cell-selection/SKILL.md +19 -19
  9. package/skills/columns/SKILL.md +9 -9
  10. package/skills/data/SKILL.md +53 -53
  11. package/skills/editing/SKILL.md +152 -311
  12. package/skills/editing/references/common-mistakes.md +177 -0
  13. package/skills/editing/references/editing-api.md +56 -28
  14. package/skills/editing/references/editors-and-validation.md +26 -19
  15. package/skills/filtering/SKILL.md +51 -50
  16. package/skills/getting-started/SKILL.md +10 -10
  17. package/skills/grouping/SKILL.md +41 -43
  18. package/skills/options/SKILL.md +4 -4
  19. package/skills/rows/SKILL.md +59 -59
  20. package/skills/rows/references/rows-api.md +7 -7
  21. package/skills/server-side/SKILL.md +1 -1
  22. package/skills/testing/SKILL.md +11 -10
  23. package/src/tmdatagrid/components/TMDataGrid.module.css +13 -1
  24. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +20 -8
  25. package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +29 -18
  26. package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +5 -8
  27. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +2 -2
  28. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +246 -86
  29. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +176 -77
  30. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +11 -6
  31. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +9 -5
  32. package/src/tmdatagrid/components/TMDataGridTable.module.css +47 -3
  33. package/src/tmdatagrid/components/TMDataGridTable.tsx +101 -40
  34. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +1 -1
  35. package/src/tmdatagrid/components/icons.ts +1 -0
  36. package/src/tmdatagrid/core/autosize.ts +30 -6
  37. package/src/tmdatagrid/core/capabilities.ts +15 -5
  38. package/src/tmdatagrid/core/cellExport.ts +6 -7
  39. package/src/tmdatagrid/core/cellNavigation.ts +2 -2
  40. package/src/tmdatagrid/core/cellRange.ts +6 -6
  41. package/src/tmdatagrid/core/columnOrdering.ts +30 -1
  42. package/src/tmdatagrid/core/columnUtils.ts +14 -0
  43. package/src/tmdatagrid/core/draftCellContext.ts +68 -0
  44. package/src/tmdatagrid/core/editEngine.ts +111 -37
  45. package/src/tmdatagrid/core/filterOperators.ts +6 -6
  46. package/src/tmdatagrid/core/labels.ts +13 -1
  47. package/src/tmdatagrid/core/labelsSv.ts +4 -0
  48. package/src/tmdatagrid/core/matchHighlight.ts +3 -3
  49. package/src/tmdatagrid/core/persistence.ts +3 -3
  50. package/src/tmdatagrid/core/rowSelection.ts +3 -3
  51. package/src/tmdatagrid/index.ts +3 -1
  52. package/src/tmdatagrid/useTMDataGrid.tsx +141 -99
@@ -1,14 +1,15 @@
1
1
  ---
2
2
  name: editing
3
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.edit.enabled and meta.edit.field, the six built-in editors picked by
8
- meta.type, custom editors through meta.edit.editor, per-keystroke value
9
- mapping with meta.edit.mapValue, field validation with meta.edit.validate and
10
- cross-field rules with rowValidators (Standard Schema and Zod), adding and
11
- deleting rows with edit.addRow, newRowDefaults, onRowAdd and onRowDelete, the
4
+ Edit cells and rows in TMDataGrid. Covers the editing option with its four
5
+ mode policies (cell, cellConfirm, row, draft), the required getRowId,
6
+ editing.onCommit and editing.onCommitDrafts, why the grid never mutates data,
7
+ per-column gating with meta.edit.enabled and meta.edit.field, the six built-in
8
+ editors picked by meta.type, custom editors through meta.edit.editor,
9
+ per-keystroke value mapping with meta.edit.mapValue, field validation with
10
+ meta.edit.validate and cross-field rules with editing.rowValidators (Standard
11
+ Schema and Zod), adding and deleting rows with edit.addRow,
12
+ editing.newRowDefaults, editing.onRowAdd and editing.onRowDelete, the
12
13
  generated edit lane, TMDataGrid.EditActions with its renderActions slot, and
13
14
  the public edit engine (begin, commit, cancel, submitAll, getForm, store).
14
15
  Load when making a grid editable, choosing an edit mode, wiring a save,
@@ -16,7 +17,7 @@ description: >
16
17
  metadata:
17
18
  type: core
18
19
  library: '@jielga/tmdatagrid'
19
- library_version: '2.0.0-beta.0'
20
+ library_version: '2.0.0-beta.2'
20
21
  sources:
21
22
  - 'Jielga/TMDataGrid:src/docs/editing.md'
22
23
  - 'Jielga/TMDataGrid:src/docs/query-builder.md'
@@ -27,15 +28,15 @@ sources:
27
28
 
28
29
  # TMDataGrid - Editing
29
30
 
30
- `editMode` turns editing on and picks how commits happen. Three facts decide
31
- every wiring question below:
31
+ The `editing` option turns editing on, and `editing.mode` picks how commits
32
+ happen. Three facts decide every wiring question below:
32
33
 
33
- - **The grid never mutates `data`.** `onEditCommit` applies the change wherever
34
- the data actually lives, and the new rows arrive back through `data`.
34
+ - **The grid never mutates `data`.** `editing.onCommit` applies the change
35
+ wherever the data lives, and the updated rows arrive back through `data`.
35
36
  - **One row, one form.** Each editing row gets its own TanStack Form, keyed by
36
37
  row id and living outside the DOM, so a draft survives scrolling, sorting and
37
38
  filtering.
38
- - **`getRowId` is required** once `editMode` is set, and it must be the record's
39
+ - **`getRowId` is required** once `editing` is set, and it must be the record's
39
40
  own identity. Drafts are keyed by it.
40
41
 
41
42
  `@tanstack/react-form` becomes a peer dependency once editing is used.
@@ -49,15 +50,17 @@ const grid = useTMDataGrid({
49
50
  data: employees,
50
51
  columns,
51
52
  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,
53
+ editing: {
54
+ mode: "cell",
55
+ // The grid writes nothing: apply the change, and the edited row arrives
56
+ // back through `data`.
57
+ onCommit: ({ rowId, value }) =>
58
+ setEmployees((previous) =>
59
+ previous.map((employee) =>
60
+ String(employee.id) === rowId ? value : employee,
61
+ ),
59
62
  ),
60
- ),
63
+ },
61
64
  });
62
65
 
63
66
  <TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }}>
@@ -65,59 +68,73 @@ const grid = useTMDataGrid({
65
68
  </TMDataGrid>;
66
69
  ```
67
70
 
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"`.
71
+ Two rules are compile errors, not options that silently do nothing: `editing`
72
+ requires `getRowId`, and `editing.onCommitDrafts` exists only under
73
+ `editing.mode: "draft"`. Draft _without_ `onCommitDrafts` is fine: `submitAll`
74
+ falls back to the per-row `editing.onCommit` loop.
72
75
 
73
76
  ## The four modes
74
77
 
75
- One axis, four policies over the same engine.
78
+ All four use the same engine and the same forms. `editing.mode` sets what counts
79
+ as a commit, and which controls trigger it.
76
80
 
77
- | Mode | Commits on | Cancels on | Chrome |
81
+ | Mode | Commits on | Cancels on | Controls |
78
82
  | --- | --- | --- | --- |
79
83
  | `"cell"` | Enter, Tab, blur | Escape | none |
80
84
  | `"cellConfirm"` | ✓ or Enter; blur keeps the draft | ✕ or Escape | ✓ / ✕ beside the input |
81
85
  | `"row"` | Save in the edit lane, or Ctrl+Enter | Cancel, or Escape | generated edit lane |
82
- | `"batch"` | `edit.submitAll()` | `edit.cancelAll()` | `TMDataGrid.EditActions` |
86
+ | `"draft"` | `edit.submitAll()` | `edit.cancelAll()`, or per row in the lane | `TMDataGrid.EditActions` + the edit lane |
83
87
 
84
88
  Which to pick: `"cell"` for saved-as-you-go spreadsheet feel; `"cellConfirm"`
85
89
  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
90
+ the save or a rule spans two columns; `"draft"` for many edits sent as one
87
91
  transaction.
88
92
 
89
93
  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. Every open gesture puts the caret in the cell it named, and `edit.addRow()`
94
- in the new row's first editable cell. The grid places it rather than the editor,
95
- so a `meta.edit.editor` gets it without doing anything to earn it.
96
-
97
- Under `"row"` the pencil - or a double-click on any cell, which puts the caret
98
- in the cell clicked - opens every editable cell of the row, and ✓ saves them as
99
- one commit. Rows accumulate: opening a second row leaves the first open, and
100
- each row's ✓ and ✕ act on that row alone, so no row is closed or saved to make
101
- space for the next.
102
-
103
- Under `"batch"` nothing leaves the grid until `submitAll`. Enter and Tab park
104
- the draft, dirty-marked, Escape drops that one draft, and drafts accumulate
105
- across rows. `submitAll` then commits every open row: through the per-row
106
- `onEditCommit` loop by default, or through one
107
- `onEditCommitBatch({ rows, added, deleted })` call when that is set. Rows
108
- failing validation stay open either way, and a rejected batch keeps every
109
- draft.
94
+ or typing, where the first character replaces the value. Delete or Backspace
95
+ clears the value and commits without opening an editor; under `"draft"` the
96
+ cleared value is held with the other drafts instead. Editing implies cell
97
+ selection: `cellSelection` defaults to `"single"` while `editing` is set. The
98
+ grid places the caret in the cell that was opened, and `edit.addRow()` places it
99
+ in the new row's first editable cell, so a `meta.edit.editor` receives focus
100
+ without handling it itself.
101
+
102
+ Under `"row"` the pencil, or a double-click on any cell, which puts the caret in
103
+ the cell clicked, opens every editable cell of the row, and ✓ saves them as one
104
+ commit. Rows accumulate: opening a second row leaves the first open, and each
105
+ row's ✓ and ✕ act on that row alone.
106
+
107
+ Under `"draft"` nothing reaches a callback until `submitAll`. Enter and Tab hold
108
+ the draft, Tab moving on to the next editable cell, Escape drops that one draft,
109
+ and drafts accumulate across rows, surviving filters, sorts and scrolling.
110
+ `edit.commit(rowId)` validates and holds the draft too, so there is no per-row
111
+ escape hatch to the consumer. A held draft is displayed: the cell renders the
112
+ draft value through the column's own `cell` renderer, with the blue corner
113
+ marking it dirty and `data-dirty` on the row.
114
+
115
+ The edit lane is the change indicator and the per-row undo: an edited row shows
116
+ a pencil icon and Revert, which drops that row's draft; a new row a plus icon, a
117
+ pencil that reopens it, and ✕; a row marked for deletion a trash icon and
118
+ Restore. A row holding a dirty draft hides the trash - revert first, then
119
+ delete. A row blocked by validation turns its icon red with the message in the
120
+ tooltip.
121
+
122
+ `submitAll` then commits every pending change: through the per-row
123
+ `editing.onCommit` / `editing.onRowAdd` / `editing.onRowDelete` loop by default,
124
+ or through one `editing.onCommitDrafts({ rows, added, deleted })` call when that
125
+ is set. Rows failing validation stay open either way, and a rejected save keeps
126
+ every draft.
110
127
 
111
128
  ## What a commit receives
112
129
 
113
- `onEditCommit` is handed `{ rowId, value, original, changes, source }`: `value`
114
- is the whole edited row for consumers who save records, `changes` the per-field
115
- diff (`columnId`, `field`, `previous`, `next`) for consumers who PATCH, one
116
- entry in cell mode.
130
+ `editing.onCommit` is handed `{ rowId, value, original, changes, source }`.
131
+ `value` is the whole edited row, for saving a record; `changes` is the per-field
132
+ diff (`columnId`, `field`, `previous`, `next`), for a PATCH, and holds one entry
133
+ in cell mode.
117
134
 
118
- The engine drops the draft only when `onEditCommit` resolves. A slow save keeps
119
- the draft on screen with a busy marker; a **rejection keeps the form open** with
120
- the error on the row, which is how a failed save stays visible.
135
+ The engine drops the draft only when `editing.onCommit` resolves. A slow save
136
+ keeps the draft on screen with a busy marker, and a **rejection keeps the form
137
+ open** with the error on the row.
121
138
 
122
139
  ## Which cells edit
123
140
 
@@ -130,7 +147,7 @@ records, so `accessorKey: "address.city"` edits `values.address.city`.
130
147
  | `meta.edit.enabled: false` | The column never edits |
131
148
  | `meta.edit.enabled: (row) => boolean` | Per row, per column |
132
149
  | `meta.edit.field: "lastName"` | The path an `accessorFn` column writes to |
133
- | `isRowEditable: (row) => boolean` | The whole row, in every mode |
150
+ | `editing.isRowEditable: (row) => boolean` | The whole row, in every mode |
134
151
 
135
152
  Group rows and the generated lanes (checkbox, row number, details, edit) never
136
153
  edit.
@@ -157,8 +174,8 @@ one, with the export that wraps it, is in
157
174
  [references/editors-and-validation.md](references/editors-and-validation.md#the-built-in-editors).
158
175
 
159
176
  `meta.edit.editor` replaces one, `meta.edit.validate` guards the field, and
160
- `rowValidators` carries cross-field rules. The validators are TanStack Form's
161
- own, Standard Schema included, so a Zod schema passes straight through:
177
+ `editing.rowValidators` carries cross-field rules. The validators are TanStack
178
+ Form's own, Standard Schema included, so a Zod schema passes straight through:
162
179
 
163
180
  ```tsx
164
181
  import { z } from "zod";
@@ -166,7 +183,7 @@ import { z } from "zod";
166
183
  // Field level, on the column. A bare schema means { onChange: schema }.
167
184
  meta: { edit: { validate: z.string().min(2, "At least two characters") } }
168
185
 
169
- // Form level, on the hook. Cross-field rules, under "row" or "batch".
186
+ // Form level, inside `editing`. Cross-field rules, under "row" or "draft".
170
187
  rowValidators: {
171
188
  onSubmit: z
172
189
  .object({ salary: z.number().positive(), status: z.string() })
@@ -179,10 +196,10 @@ rowValidators: {
179
196
  Pathed issues land on the matching cells, pathless ones on the row, and cell
180
197
  corners mark both: blue for a dirty draft, red for a validation error.
181
198
 
182
- `meta.edit.mapValue` rewrites a value rather than rejecting it: uppercase a
183
- code, strip spaces from an IBAN, clamp a number. It runs on every write an
184
- editor makes, so a text input maps per keystroke, and what it returns is what
185
- the validators judge and what commits.
199
+ `meta.edit.mapValue` rewrites a value instead of rejecting it: uppercase a code,
200
+ strip spaces from an IBAN, clamp a number. It runs on every write an editor
201
+ makes, so a text input maps per keystroke, and what it returns is what the
202
+ validators check and what is committed.
186
203
 
187
204
  ```tsx
188
205
  meta: { edit: { mapValue: ({ value }) => String(value).toUpperCase() } }
@@ -193,19 +210,30 @@ Detail for all three: [references/editors-and-validation.md](references/editors-
193
210
  ## Adding and deleting rows
194
211
 
195
212
  `edit.addRow()` opens an entry row in a sticky block under the header, seeded
196
- from `newRowDefaults`. Enter, or the lane's ✓, commits the add through
197
- `onRowAdd` under the immediate modes; batch parks it for `submitAll`. Escape, or
198
- ✕, discards it.
213
+ from `editing.newRowDefaults`. Enter, or the lane's ✓, commits the add through
214
+ `editing.onRowAdd` under the immediate modes; under draft mode it enters the
215
+ row, which is validated, held with the other drafts, and reported in
216
+ `submitAll`'s `added`. Escape, or ✕, discards the entry.
217
+
218
+ Under `"draft"` an entered row renders as a value row with no inputs, marked
219
+ `data-new` and `data-confirmed` and tinted with `--dg-row-new-bg`. By default it
220
+ joins the scrolling flow above the body rows; `editing.newRowsSticky: true`
221
+ keeps entered rows pinned in the entry block until Save all. Double-click, or
222
+ the lane's pencil, reopens it; ✕ removes it. To limit how many entry rows are
223
+ open at once, gate the Add button on
224
+ `useSelector(grid.edit.store, (s) => s.newRows.some((n) => !n.confirmed))`.
199
225
 
200
226
  ```tsx
201
227
  const grid = useTMDataGrid({
202
228
  data: employees,
203
229
  columns,
204
230
  getRowId: (row) => String(row.id),
205
- editMode: "batch",
206
- newRowDefaults: () => ({ id: 0, firstName: "", salary: 30_000 }),
207
- onEditCommitBatch: async ({ rows, added, deleted }) => {
208
- await api.saveBatch({ rows, added, deleted });
231
+ editing: {
232
+ mode: "draft",
233
+ newRowDefaults: () => ({ id: 0, firstName: "", salary: 30_000 }),
234
+ onCommitDrafts: async ({ rows, added, deleted }) => {
235
+ await api.saveBatch({ rows, added, deleted });
236
+ },
209
237
  },
210
238
  });
211
239
 
@@ -216,25 +244,27 @@ const grid = useTMDataGrid({
216
244
  </TMDataGrid.Toolbar>;
217
245
  ```
218
246
 
219
- `edit.deleteRow(rowId)` calls `onRowDelete({ rowId, row })` straight away under
220
- the immediate modes, so a confirmation belongs inside that callback. Under batch
221
- it toggles a mark instead: the row renders struck through and inert
222
- (`data-deleted`), the lane's trash becomes a restore, and `submitAll` reports
223
- the ids in `deleted`. Adds and deletes are applied by you, the same as edits -
224
- the engine's `tempId` (`__new__1`, …) never needs to become a real id.
247
+ `edit.deleteRow(rowId)` calls `editing.onRowDelete({ rowId, row })` immediately
248
+ under the immediate modes, so put any confirmation inside that callback. Under
249
+ draft mode it toggles a mark instead: the row renders struck through and inert
250
+ (`data-deleted`), the lane shows Restore, and `submitAll` reports the ids in
251
+ `deleted`. You apply adds and deletes, the same as edits. The engine's `tempId`
252
+ (`__new__1`, …) does not need to become a real id.
225
253
 
226
- ## The chrome
254
+ ## The built-in controls
227
255
 
228
- The generated edit lane (`EDIT_COLUMN_ID`, pinned right) appears when `editMode`
229
- is `"row"`, or `onRowDelete` is set, or `editMode` is `"batch"` with
230
- `onEditCommitBatch` set. Nothing else conjures it, and `"cell"` mode has no
231
- chrome at all by design.
256
+ The generated edit lane (`EDIT_COLUMN_ID`, pinned right) appears when
257
+ `editing.mode` is `"row"` or `"draft"`, or when `editing.onRowDelete` is set.
258
+ Nothing else adds it, and `"cell"` mode renders no controls of its own. Under
259
+ `"row"` it holds Save and Cancel while a row is open; under `"draft"` it holds
260
+ the row-state marker with Revert and Restore, and Save and Cancel never appear.
261
+ Every control carries a tooltip from the labels.
232
262
 
233
263
  `TMDataGrid.EditActions` is Save with the pending count plus Discard. It greys
234
264
  out while nothing is pending, spins while a submit is in flight, renders nothing
235
- while editing is off, and works under any mode, not only batch.
265
+ while editing is off, and works under any mode, not only draft.
236
266
 
237
- `renderActions` replaces the pair while handing over its pieces:
267
+ `renderActions` replaces the pair and hands over its pieces:
238
268
 
239
269
  ```tsx
240
270
  <TMDataGrid.EditActions
@@ -252,248 +282,59 @@ while editing is off, and works under any mode, not only batch.
252
282
 
253
283
  ## The engine: `edit`
254
284
 
255
- `grid.edit` is public, and everything the chrome does goes through it:
285
+ `grid.edit` is public, and everything the built-in controls do goes through it:
256
286
  `begin` and `commit`, `cancel` / `cancelAll`, `submitAll`, `addRow` /
257
- `deleteRow`, `getForm`, and `store` for `useSelector`. Every member with its
258
- signature, the gates, `clearCell`, `deactivate`, and the `edit.store` shape are
259
- in [references/editing-api.md](references/editing-api.md#the-edit-engine).
260
-
261
- ```tsx
262
- import { useSelector } from "@tanstack/react-store";
287
+ `deleteRow`, `getForm`, and `store` for `useSelector` (an example is under
288
+ [Submitting an outer form](#high-submitting-an-outer-form-while-the-grid-holds-a-draft)).
289
+ Every member with its signature, the gates, `clearCell`, `deactivate`, and the
290
+ `edit.store` shape are in
291
+ [references/editing-api.md](references/editing-api.md#the-edit-engine).
263
292
 
264
- const pendingCount = useSelector(
265
- grid.edit.store,
266
- (state) => state.openRowIds.length + state.deletedRowIds.length,
267
- );
268
- ```
269
-
270
- `getForm` is the *one row, one form* promise made public: render that same form
271
- in a drawer and it shares values, dirty state and errors with the inline cells,
272
- because it is the same `FormApi`.
293
+ `getForm` exposes the row's form: render it in a drawer and it shares values,
294
+ dirty state and errors with the inline cells, because it is the same
295
+ `FormApi`.
273
296
 
274
297
  ## Inside an outer form
275
298
 
276
- A `@tanstack/react-form` form can own the row array, with the grid as a
277
- controlled field: `data` from `field.state.value`, and `onEditCommit` /
278
- `onRowAdd` / `onRowDelete` composing the next array into `field.handleChange`.
279
- Use `editMode: "row"` so a row reaches the form on approval, map by row id
280
- (never index), and mint negative ids for new rows.
299
+ A `@tanstack/react-form` form can hold the row array, with the grid as a
300
+ controlled field: `data` from `field.state.value`, and `editing.onCommit` /
301
+ `editing.onRowAdd` / `editing.onRowDelete` composing the next array into
302
+ `field.handleChange`. Use `editing.mode: "row"` so a row reaches the form on
303
+ approval, map by row id (never index), and assign negative ids to new rows.
281
304
 
282
- The validation split is structural, not stylistic: **a rule decidable from one
283
- row belongs to the grid (`meta.edit.validate`, `rowValidators`); a rule needing the
284
- other rows or the collection ("has rows", "no duplicates") belongs to the
285
- form's field validator.** A row's form cannot see the array, and `edit.store`
286
- publishes field names, never values, so the form cannot see a draft.
305
+ The validation split follows from what each side can see: **a rule decidable
306
+ from one row belongs to the grid (`meta.edit.validate`,
307
+ `editing.rowValidators`); a rule needing the other rows or the collection ("has
308
+ rows", "no duplicates") belongs to the form's field validator.** A row's form
309
+ cannot see the array, and `edit.store` publishes field names but not values, so
310
+ the form cannot see a draft.
287
311
 
288
312
  ## Common mistakes
289
313
 
290
- ### CRITICAL Expecting the grid to write into `data`
291
-
292
- The grid holds no copy of the rows. Without `onEditCommit` an edit commits, the
293
- draft clears, and the cell snaps back to the value in `data`, which looks like
294
- the edit was silently discarded.
295
-
296
- Wrong:
297
-
298
- ```tsx
299
- useTMDataGrid({ data: employees, columns, getRowId, editMode: "cell" });
300
- ```
301
-
302
- Correct:
303
-
304
- ```tsx
305
- useTMDataGrid({
306
- data: employees,
307
- columns,
308
- getRowId,
309
- editMode: "cell",
310
- onEditCommit: ({ rowId, value }) =>
311
- setEmployees((previous) =>
312
- previous.map((employee) =>
313
- String(employee.id) === rowId ? value : employee,
314
- ),
315
- ),
316
- });
317
- ```
318
-
319
- Source: `src/docs/editing.md`, `src/tmdatagrid/core/editEngine.ts`.
320
-
321
- ### CRITICAL `getRowId` built from the row index
322
-
323
- `getRowId` satisfies the compiler with any string, so an index-based id passes.
324
- Drafts are keyed by it, so after a sort or a filter the draft is applied to
325
- whichever record now sits at that index.
326
-
327
- Wrong:
328
-
329
- ```tsx
330
- getRowId: (row, index) => String(index),
331
- ```
332
-
333
- Correct:
334
-
335
- ```tsx
336
- getRowId: (row) => String(row.id),
337
- ```
338
-
339
- Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
340
-
341
- ### HIGH A cell editor defined inside the component
342
-
343
- `meta.edit.editor` is rendered as JSX, so its identity is its component type. An
344
- inline arrow is a new type on every render, which unmounts the open editor and
345
- loses what was being typed.
346
-
347
- Wrong:
348
-
349
- ```tsx
350
- meta: { edit: { editor: ({ field }) => <Slider value={field.state.value} /> } },
351
- ```
352
-
353
- Correct:
354
-
355
- ```tsx
356
- // Module scope, referenced by name.
357
- const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
358
- <Slider
359
- value={Number(field.state.value ?? 0)}
360
- onChange={(next) => field.handleChange(next)}
361
- onChangeEnd={() => void commit()}
362
- />
363
- );
364
-
365
- meta: { edit: { editor: SalaryEditor } },
366
- ```
367
-
368
- Source: `src/docs/editors.md`, `src/tmdatagrid/core/editEngine.ts`.
369
-
370
- ### HIGH A cross-field rule under `editMode: "cell"`
371
-
372
- Under `"cell"` each cell commits on its own, so a rule spanning two columns can
373
- never be satisfied by either one: the first cell edited is rejected against the
374
- other column's old value, and the row cannot be saved at all.
375
-
376
- Wrong:
377
-
378
- ```tsx
379
- useTMDataGrid({ editMode: "cell", rowValidators: { onSubmit: endAfterStart } });
380
- ```
381
-
382
- Correct:
383
-
384
- ```tsx
385
- // "row" (or "batch") validates the whole row in one commit.
386
- useTMDataGrid({ editMode: "row", rowValidators: { onSubmit: endAfterStart } });
387
- ```
388
-
389
- Source: `src/docs/editors.md` (cross-field rules want a mode that commits the
390
- whole row at once).
391
-
392
- ### HIGH An `accessorFn` column that never opens an editor
393
-
394
- Editability follows the data path, not the column. A column built on an accessor
395
- function has no `accessorKey`, so it maps to nothing and stays read-only while
396
- every other column edits. No warning is raised.
397
-
398
- Wrong:
399
-
400
- ```tsx
401
- columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
402
- id: "fullName",
403
- header: "Full name",
404
- });
405
- ```
406
-
407
- Correct:
408
-
409
- ```tsx
410
- columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
411
- id: "fullName",
412
- header: "Full name",
413
- meta: { label: "Full name", edit: { field: "lastName" } },
414
- });
415
- ```
416
-
417
- Source: `src/docs/editing.md` (Which cells edit).
418
-
419
- ### HIGH Swallowing the error in `onEditCommit`
420
-
421
- The draft is dropped when `onEditCommit` resolves. A `try/catch` that logs the
422
- failure resolves it, so a save that failed on the server clears the editor and
423
- the grid shows the old value as though nothing happened.
424
-
425
- Wrong:
314
+ Each one compiles and raises no warning. The reason and the fix for every entry
315
+ are in [references/common-mistakes.md](references/common-mistakes.md).
426
316
 
427
- ```tsx
428
- onEditCommit: async ({ rowId, changes }) => {
429
- try {
430
- await api.patch(rowId, changes);
431
- } catch (error) {
432
- console.error(error);
433
- }
434
- },
435
- ```
436
-
437
- Correct:
438
-
439
- ```tsx
440
- // Reject: the form stays open with the error on the row.
441
- onEditCommit: async ({ rowId, changes }) => {
442
- await api.patch(rowId, changes);
443
- },
444
- ```
445
-
446
- Source: `src/tmdatagrid/useTMDataGrid.tsx` (`onEditCommit`).
447
-
448
- ### HIGH Submitting an outer form while the grid holds a draft
449
-
450
- The outer form's array contains only committed rows. Under any mode a mid-edit
451
- row is invisible to it, and under `"batch"` *every* edit is until
452
- `submitAll()` - so a form submit saves stale rows and collection rules pass
453
- over pending values.
454
-
455
- Correct:
456
-
457
- ```tsx
458
- const hasOpenDraft = useSelector(grid.edit.store, (s) => s.openRowIds.length > 0);
459
-
460
- <Button type="submit" disabled={!canSubmit || hasOpenDraft}>Save</Button>
461
- // or flush instead of blocking:
462
- const flushed = await grid.edit.submitAll();
463
- if (flushed) await form.handleSubmit();
464
- ```
465
-
466
- Source: `src/docs/query-builder.md` (Which mode, Submitting).
467
-
468
- ### MEDIUM Reading a commit's result as the saved value
469
-
470
- `edit.commit(rowId)` and `edit.submitAll()` resolve to a `boolean` saying
471
- whether the form closed, and resolve `false` when validation or a rejected save
472
- kept it open. Ignoring the result reports a save that did not happen.
473
-
474
- ```tsx
475
- const saved = await grid.edit.submitAll();
476
- notifications.show({ message: saved ? "Saved" : "Some rows need attention" });
477
- ```
478
-
479
- Source: `src/tmdatagrid/core/editEngine.ts` (`TMDataGridEditApi`).
480
-
481
- ### MEDIUM Expecting `onRowDelete` to fire under batch
482
-
483
- Under the immediate modes `edit.deleteRow` calls `onRowDelete` at once. Under
484
- `"batch"` it only toggles a deletion mark, and the ids arrive later in
485
- `submitAll`'s `deleted`, so a batch grid that deletes through `onRowDelete`
486
- alone never removes anything. Handle `deleted` in `onEditCommitBatch`.
487
-
488
- Source: `src/docs/editing.md` (Adding and deleting rows).
317
+ | Severity | Mistake |
318
+ | --- | --- |
319
+ | CRITICAL | Expecting the grid to write into `data` - without `editing.onCommit` the cell reverts |
320
+ | CRITICAL | `getRowId` built from the row index - drafts follow the index, not the record |
321
+ | HIGH | A cell editor defined inside the component - a new type per render unmounts the editor |
322
+ | HIGH | A cross-field rule under `mode: "cell"` - `rowValidators` needs `"row"` or `"draft"` |
323
+ | HIGH | An `accessorFn` column with no `meta.edit.field` - it maps to nothing and stays read-only |
324
+ | HIGH | Swallowing the error in `editing.onCommit` - a resolved catch drops the draft |
325
+ | HIGH | Submitting an outer form while the grid holds a draft - it saves stale rows |
326
+ | MEDIUM | Reading a commit's result as the saved value - it is a `boolean` about the form |
327
+ | MEDIUM | Expecting `editing.onRowDelete` to fire under draft - the mark waits for `submitAll` |
489
328
 
490
329
  ## References
491
330
 
492
331
  - [Editors and validation](references/editors-and-validation.md) - the editor
493
- contract, wrapping a built-in, what `mapValue` leaves alone, field and row
332
+ API, wrapping a built-in, what `mapValue` leaves alone, field and row
494
333
  validators, server-side errors.
495
334
  - [Editing API](references/editing-api.md) - every option, callback, column meta
496
- field, export, CSS variable and data attribute editing owns.
335
+ field, export, CSS variable and data attribute belonging to editing.
336
+ - [Common mistakes](references/common-mistakes.md) - the failure modes above,
337
+ each with its wrong and correct form.
497
338
 
498
339
  See also: the `columns` skill for `meta.type` and the shared `meta.options`
499
340
  source, and the `testing` skill for driving editors from a test.