@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.
- package/README.md +32 -34
- package/dist/index.d.ts +164 -102
- package/dist/index.js +2060 -1705
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +37 -34
- package/skills/cell-selection/SKILL.md +19 -19
- package/skills/columns/SKILL.md +9 -9
- package/skills/data/SKILL.md +53 -53
- package/skills/editing/SKILL.md +152 -311
- package/skills/editing/references/common-mistakes.md +177 -0
- package/skills/editing/references/editing-api.md +56 -28
- package/skills/editing/references/editors-and-validation.md +26 -19
- package/skills/filtering/SKILL.md +51 -50
- package/skills/getting-started/SKILL.md +10 -10
- package/skills/grouping/SKILL.md +41 -43
- package/skills/options/SKILL.md +4 -4
- package/skills/rows/SKILL.md +59 -59
- package/skills/rows/references/rows-api.md +7 -7
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +11 -10
- package/src/tmdatagrid/components/TMDataGrid.module.css +13 -1
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +20 -8
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +29 -18
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +5 -8
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +2 -2
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +246 -86
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +176 -77
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +11 -6
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +9 -5
- package/src/tmdatagrid/components/TMDataGridTable.module.css +47 -3
- package/src/tmdatagrid/components/TMDataGridTable.tsx +101 -40
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +1 -1
- package/src/tmdatagrid/components/icons.ts +1 -0
- package/src/tmdatagrid/core/autosize.ts +30 -6
- package/src/tmdatagrid/core/capabilities.ts +15 -5
- package/src/tmdatagrid/core/cellExport.ts +6 -7
- package/src/tmdatagrid/core/cellNavigation.ts +2 -2
- package/src/tmdatagrid/core/cellRange.ts +6 -6
- package/src/tmdatagrid/core/columnOrdering.ts +30 -1
- package/src/tmdatagrid/core/columnUtils.ts +14 -0
- package/src/tmdatagrid/core/draftCellContext.ts +68 -0
- package/src/tmdatagrid/core/editEngine.ts +111 -37
- package/src/tmdatagrid/core/filterOperators.ts +6 -6
- package/src/tmdatagrid/core/labels.ts +13 -1
- package/src/tmdatagrid/core/labelsSv.ts +4 -0
- package/src/tmdatagrid/core/matchHighlight.ts +3 -3
- package/src/tmdatagrid/core/persistence.ts +3 -3
- package/src/tmdatagrid/core/rowSelection.ts +3 -3
- package/src/tmdatagrid/index.ts +3 -1
- package/src/tmdatagrid/useTMDataGrid.tsx +141 -99
package/skills/editing/SKILL.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: editing
|
|
3
3
|
description: >
|
|
4
|
-
Edit cells and rows in TMDataGrid. Covers
|
|
5
|
-
(cell, cellConfirm, row,
|
|
6
|
-
|
|
7
|
-
meta.edit.enabled and meta.edit.field, the six built-in
|
|
8
|
-
meta.type, custom editors through meta.edit.editor,
|
|
9
|
-
mapping with meta.edit.mapValue, field validation with
|
|
10
|
-
cross-field rules with rowValidators (Standard
|
|
11
|
-
deleting rows with edit.addRow,
|
|
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.
|
|
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
|
-
`
|
|
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`.** `
|
|
34
|
-
the 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 `
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`
|
|
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
|
-
|
|
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 |
|
|
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
|
-
| `"
|
|
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; `"
|
|
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
|
|
91
|
-
clears the value and commits without opening an editor
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
so a `meta.edit.editor`
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
Under `"
|
|
104
|
-
the draft,
|
|
105
|
-
|
|
106
|
-
`
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
`
|
|
114
|
-
is the whole edited row for
|
|
115
|
-
diff (`columnId`, `field`, `previous`, `next`) for
|
|
116
|
-
|
|
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 `
|
|
119
|
-
the draft on screen with a busy marker
|
|
120
|
-
the error on the row
|
|
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
|
|
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,
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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;
|
|
198
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 })`
|
|
220
|
-
the immediate modes, so
|
|
221
|
-
it toggles a mark instead: the row renders struck through and inert
|
|
222
|
-
(`data-deleted`), the lane
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
254
|
+
## The built-in controls
|
|
227
255
|
|
|
228
|
-
The generated edit lane (`EDIT_COLUMN_ID`, pinned right) appears when
|
|
229
|
-
is `"row"
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
265
|
+
while editing is off, and works under any mode, not only draft.
|
|
236
266
|
|
|
237
|
-
`renderActions` replaces the pair
|
|
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
|
|
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
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
277
|
-
controlled field: `data` from `field.state.value`, and `
|
|
278
|
-
`onRowAdd` / `onRowDelete` composing the next array into
|
|
279
|
-
Use `
|
|
280
|
-
(never index), and
|
|
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
|
|
283
|
-
row belongs to the grid (`meta.edit.validate`,
|
|
284
|
-
other rows or the collection ("has
|
|
285
|
-
form's field validator.** A row's form
|
|
286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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
|
|
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.
|