@jielga/tmdatagrid 1.0.0 → 1.0.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 +8 -8
- package/dist/index.d.ts +225 -225
- package/dist/index.js +58 -50
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -2
- package/skills/appearance/SKILL.md +322 -0
- package/skills/cell-selection/SKILL.md +240 -0
- package/skills/columns/SKILL.md +261 -86
- package/skills/data/SKILL.md +289 -0
- package/skills/editing/SKILL.md +492 -0
- package/skills/editing/references/editing-api.md +124 -0
- package/skills/editing/references/editors-and-validation.md +198 -0
- package/skills/filtering/SKILL.md +344 -0
- package/skills/getting-started/SKILL.md +48 -27
- package/skills/grouping/SKILL.md +264 -0
- package/skills/options/SKILL.md +31 -20
- package/skills/rows/SKILL.md +369 -0
- package/skills/rows/references/rows-api.md +117 -0
- package/skills/server-side/SKILL.md +7 -7
- package/skills/testing/SKILL.md +12 -12
- package/src/tmdatagrid/TMDataGridContext.ts +2 -2
- package/src/tmdatagrid/components/TMDataGrid.module.css +2 -2
- package/src/tmdatagrid/components/TMDataGrid.tsx +5 -5
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +4 -4
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +2 -2
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +6 -6
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +2 -2
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +4 -4
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +6 -6
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +6 -6
- package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +2 -2
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridFooter.module.css +1 -1
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +12 -6
- package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +1 -1
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +5 -5
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +8 -8
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +12 -12
- package/src/tmdatagrid/components/TMDataGridRowNumberColumn.tsx +4 -4
- package/src/tmdatagrid/components/TMDataGridSearch.tsx +5 -5
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +8 -8
- package/src/tmdatagrid/components/TMDataGridTable.module.css +18 -18
- package/src/tmdatagrid/components/TMDataGridTable.tsx +116 -116
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +5 -5
- package/src/tmdatagrid/components/editors/TMDataGridBooleanEditor.tsx +1 -1
- package/src/tmdatagrid/components/editors/TMDataGridDateEditor.tsx +2 -2
- package/src/tmdatagrid/components/editors/TMDataGridMultiSelectEditor.tsx +1 -1
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +1 -1
- package/src/tmdatagrid/components/editors/TMDataGridSelectEditor.tsx +2 -2
- package/src/tmdatagrid/components/editors/editorShared.ts +3 -3
- package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +1 -1
- package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +1 -1
- package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +1 -1
- package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +2 -2
- package/src/tmdatagrid/components/sticky.module.css +8 -8
- package/src/tmdatagrid/core/autosize.ts +4 -4
- package/src/tmdatagrid/core/capabilities.ts +17 -17
- package/src/tmdatagrid/core/cellExport.ts +13 -13
- package/src/tmdatagrid/core/cellNavigation.ts +6 -6
- package/src/tmdatagrid/core/cellRange.ts +4 -4
- package/src/tmdatagrid/core/columnOptions.ts +2 -2
- package/src/tmdatagrid/core/columnOrdering.ts +2 -2
- package/src/tmdatagrid/core/columnUtils.ts +3 -3
- package/src/tmdatagrid/core/editEngine.ts +49 -49
- package/src/tmdatagrid/core/expanding.ts +5 -5
- package/src/tmdatagrid/core/filterControls.ts +5 -5
- package/src/tmdatagrid/core/filterOperators.ts +14 -14
- package/src/tmdatagrid/core/labels.ts +8 -8
- package/src/tmdatagrid/core/matchHighlight.ts +4 -4
- package/src/tmdatagrid/core/persistence.ts +8 -8
- package/src/tmdatagrid/core/quickSearch.ts +8 -8
- package/src/tmdatagrid/core/rowPinning.ts +3 -3
- package/src/tmdatagrid/core/rowSelection.ts +12 -12
- package/src/tmdatagrid/core/sizes.ts +1 -1
- package/src/tmdatagrid/core/summary.ts +3 -3
- package/src/tmdatagrid/useTMDataGrid.tsx +79 -79
- package/skills/features/SKILL.md +0 -352
|
@@ -0,0 +1,492 @@
|
|
|
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, and the public
|
|
12
|
+
edit engine (begin, commit, cancel, submitAll, getForm, store). Load when
|
|
13
|
+
making a grid editable, choosing an edit mode, wiring a save, writing a cell
|
|
14
|
+
editor, validating an edit, or when cells will not open for editing.
|
|
15
|
+
metadata:
|
|
16
|
+
type: core
|
|
17
|
+
library: '@jielga/tmdatagrid'
|
|
18
|
+
library_version: '1.0.2'
|
|
19
|
+
sources:
|
|
20
|
+
- 'Jielga/TMDataGrid:src/docs/editing.md'
|
|
21
|
+
- 'Jielga/TMDataGrid:src/docs/editors.md'
|
|
22
|
+
- 'Jielga/TMDataGrid:src/tmdatagrid/core/editEngine.ts'
|
|
23
|
+
- 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# TMDataGrid - Editing
|
|
27
|
+
|
|
28
|
+
`editMode` turns editing on and picks how commits happen. Three facts decide
|
|
29
|
+
every wiring question below:
|
|
30
|
+
|
|
31
|
+
- **The grid never mutates `data`.** `onEditCommit` applies the change wherever
|
|
32
|
+
the data actually lives, and the new rows arrive back through `data`.
|
|
33
|
+
- **One row, one form.** Each editing row gets its own TanStack Form, keyed by
|
|
34
|
+
row id and living outside the DOM, so a draft survives scrolling, sorting and
|
|
35
|
+
filtering.
|
|
36
|
+
- **`getRowId` is required** once `editMode` is set, and it must be the record's
|
|
37
|
+
own identity. Drafts are keyed by it.
|
|
38
|
+
|
|
39
|
+
`@tanstack/react-form` becomes a peer dependency once editing is used.
|
|
40
|
+
|
|
41
|
+
## Setup
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { useState } from "react";
|
|
45
|
+
import {
|
|
46
|
+
createTMDataGridColumnHelper,
|
|
47
|
+
TMDataGrid,
|
|
48
|
+
useTMDataGrid,
|
|
49
|
+
} from "@jielga/tmdatagrid";
|
|
50
|
+
|
|
51
|
+
type Employee = { id: number; firstName: string; salary: number };
|
|
52
|
+
|
|
53
|
+
const columnHelper = createTMDataGridColumnHelper<Employee>();
|
|
54
|
+
|
|
55
|
+
const columns = columnHelper.columns([
|
|
56
|
+
columnHelper.accessor("firstName", { header: "First name", minSize: 140 }),
|
|
57
|
+
columnHelper.accessor("salary", {
|
|
58
|
+
header: "Salary",
|
|
59
|
+
minSize: 130,
|
|
60
|
+
meta: { type: "number", align: "right" },
|
|
61
|
+
}),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
export function Employees({ initial }: { initial: Array<Employee> }) {
|
|
65
|
+
const [employees, setEmployees] = useState(initial);
|
|
66
|
+
|
|
67
|
+
const grid = useTMDataGrid({
|
|
68
|
+
data: employees,
|
|
69
|
+
columns,
|
|
70
|
+
getRowId: (row) => String(row.id),
|
|
71
|
+
editMode: "cell",
|
|
72
|
+
// The grid writes nothing: apply the change, and the edited row arrives
|
|
73
|
+
// back through `data`.
|
|
74
|
+
onEditCommit: ({ rowId, value }) =>
|
|
75
|
+
setEmployees((previous) =>
|
|
76
|
+
previous.map((employee) =>
|
|
77
|
+
String(employee.id) === rowId ? value : employee,
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }}>
|
|
84
|
+
<TMDataGrid.Table<Employee> />
|
|
85
|
+
</TMDataGrid>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The editing options travel together, and the types say so: passing any of them
|
|
91
|
+
without `editMode` is a compile error, `getRowId` stops being optional the
|
|
92
|
+
moment `editMode` is set, and `onEditCommitBatch` exists only under
|
|
93
|
+
`editMode: "batch"`.
|
|
94
|
+
|
|
95
|
+
## The four modes
|
|
96
|
+
|
|
97
|
+
One axis, four policies over the same engine.
|
|
98
|
+
|
|
99
|
+
| Mode | Commits on | Cancels on | Chrome |
|
|
100
|
+
| --- | --- | --- | --- |
|
|
101
|
+
| `"cell"` | Enter, Tab, blur | Escape | none |
|
|
102
|
+
| `"cellConfirm"` | ✓ or Enter; blur keeps the draft | ✕ or Escape | ✓ / ✕ beside the input |
|
|
103
|
+
| `"row"` | Save in the edit lane, or Ctrl+Enter | Cancel, or Escape | generated edit lane |
|
|
104
|
+
| `"batch"` | `edit.submitAll()` | `edit.cancelAll()` | `TMDataGrid.EditActions` |
|
|
105
|
+
|
|
106
|
+
Which to pick:
|
|
107
|
+
|
|
108
|
+
- One field at a time, saved as you go, spreadsheet feel: `"cell"`.
|
|
109
|
+
- The same, but every commit costs a request you do not want fired by a stray
|
|
110
|
+
click: `"cellConfirm"`.
|
|
111
|
+
- The row is the unit of the save, or a rule spans two columns: `"row"`.
|
|
112
|
+
- Many edits reviewed and sent as one transaction: `"batch"`.
|
|
113
|
+
|
|
114
|
+
An editor opens on double-click, or with the cell cursor on the cell: Enter, F2,
|
|
115
|
+
or just typing, where the character replaces the value. Delete or Backspace
|
|
116
|
+
clears the value and commits without opening an editor. Editing brings cell
|
|
117
|
+
selection with it: `cellSelection` defaults to `"single"` while `editMode` is
|
|
118
|
+
set.
|
|
119
|
+
|
|
120
|
+
Under `"batch"` nothing leaves the grid until `submitAll`. Enter and Tab park
|
|
121
|
+
the draft, dirty-marked, Escape drops that one draft, and drafts accumulate
|
|
122
|
+
across rows. `submitAll` then commits every open row: through the per-row
|
|
123
|
+
`onEditCommit` loop by default, or through one
|
|
124
|
+
`onEditCommitBatch({ rows, added, deleted })` call when that is set. Rows
|
|
125
|
+
failing validation stay open either way, and a rejected batch keeps every
|
|
126
|
+
draft.
|
|
127
|
+
|
|
128
|
+
## What a commit receives
|
|
129
|
+
|
|
130
|
+
`onEditCommit` is handed `{ rowId, value, original, changes, source }`: `value`
|
|
131
|
+
is the whole edited row for consumers who save records, `changes` the per-field
|
|
132
|
+
diff (`columnId`, `field`, `previous`, `next`) for consumers who PATCH, one
|
|
133
|
+
entry in cell mode.
|
|
134
|
+
|
|
135
|
+
The engine drops the draft only when `onEditCommit` resolves. A slow save keeps
|
|
136
|
+
the draft on screen with a busy marker; a **rejection keeps the form open** with
|
|
137
|
+
the error on the row, which is how a failed save stays visible.
|
|
138
|
+
|
|
139
|
+
## Which cells edit
|
|
140
|
+
|
|
141
|
+
A column is editable when it maps to a data path: its `accessorKey`, or
|
|
142
|
+
`meta.editField` for a column built on `accessorFn`. Dot paths reach into nested
|
|
143
|
+
records, so `accessorKey: "address.city"` edits `values.address.city`.
|
|
144
|
+
|
|
145
|
+
| Gate | Effect |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `meta.editable: false` | The column never edits |
|
|
148
|
+
| `meta.editable: (row) => boolean` | Per row, per column |
|
|
149
|
+
| `meta.editField: "lastName"` | The path an `accessorFn` column writes to |
|
|
150
|
+
| `isRowEditable: (row) => boolean` | The whole row, in every mode |
|
|
151
|
+
|
|
152
|
+
Group rows and the generated lanes (checkbox, row number, details, edit) never
|
|
153
|
+
edit.
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// Computed column: writes back to a real field.
|
|
157
|
+
columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
|
|
158
|
+
id: "fullName",
|
|
159
|
+
header: "Full name",
|
|
160
|
+
meta: { label: "Full name", editField: "lastName" },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Editable only while the employee is active.
|
|
164
|
+
columnHelper.accessor("salary", {
|
|
165
|
+
header: "Salary",
|
|
166
|
+
meta: {
|
|
167
|
+
type: "number",
|
|
168
|
+
align: "right",
|
|
169
|
+
editable: (row) => row.original.status !== "Terminated",
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Editors and validation
|
|
175
|
+
|
|
176
|
+
`meta.type` picks the editor, and `meta.options` feeds the select editors from
|
|
177
|
+
the same declaration the filter panel reads.
|
|
178
|
+
|
|
179
|
+
| `meta.type` | Editor |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `"string"` (default) | Text input |
|
|
182
|
+
| `"number"` | Number input |
|
|
183
|
+
| `"boolean"` | Checkbox |
|
|
184
|
+
| `"date"` | Native `<input type="date">`, ISO `YYYY-MM-DD` |
|
|
185
|
+
| `"select"` | Searchable select from `meta.options`; commits on pick under `"cell"` |
|
|
186
|
+
| `"multiSelect"` | Multi-select, same source |
|
|
187
|
+
|
|
188
|
+
`meta.editor` replaces one, `meta.validate` guards the field, and
|
|
189
|
+
`rowValidators` carries cross-field rules. The validators are TanStack Form's
|
|
190
|
+
own, Standard Schema included, so a Zod schema passes straight through:
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
import { z } from "zod";
|
|
194
|
+
|
|
195
|
+
// Field level, on the column. A bare schema means { onChange: schema }.
|
|
196
|
+
meta: { validate: z.string().min(2, "At least two characters") }
|
|
197
|
+
|
|
198
|
+
// Form level, on the hook. Cross-field rules, under "row" or "batch".
|
|
199
|
+
rowValidators: {
|
|
200
|
+
onSubmit: z
|
|
201
|
+
.object({ salary: z.number().positive(), status: z.string() })
|
|
202
|
+
.refine((row) => row.status !== "Terminated" || row.salary === 0, {
|
|
203
|
+
message: "A terminated employee has no salary",
|
|
204
|
+
}),
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Pathed issues land on the matching cells, pathless ones on the row. Cell corners
|
|
209
|
+
mark state meanwhile: blue for a dirty draft, red for a validation error.
|
|
210
|
+
|
|
211
|
+
The editor contract, wrapping a built-in, and async server-side field errors:
|
|
212
|
+
[references/editors-and-validation.md](references/editors-and-validation.md).
|
|
213
|
+
|
|
214
|
+
## Adding and deleting rows
|
|
215
|
+
|
|
216
|
+
`edit.addRow()` opens an entry row in a sticky block under the header, seeded
|
|
217
|
+
from `newRowDefaults`. Enter, or the lane's ✓, commits the add through
|
|
218
|
+
`onRowAdd` under the immediate modes; batch parks it for `submitAll`. Escape, or
|
|
219
|
+
✕, discards it.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
const grid = useTMDataGrid({
|
|
223
|
+
data: employees,
|
|
224
|
+
columns,
|
|
225
|
+
getRowId: (row) => String(row.id),
|
|
226
|
+
editMode: "batch",
|
|
227
|
+
newRowDefaults: () => ({ id: 0, firstName: "", salary: 30_000 }),
|
|
228
|
+
onEditCommitBatch: async ({ rows, added, deleted }) => {
|
|
229
|
+
await api.saveBatch({ rows, added, deleted });
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
<TMDataGrid.Toolbar>
|
|
234
|
+
<Button onClick={() => grid.edit.addRow()}>Add row</Button>
|
|
235
|
+
<TMDataGrid.Spacer />
|
|
236
|
+
<TMDataGrid.EditActions />
|
|
237
|
+
</TMDataGrid.Toolbar>;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`edit.deleteRow(rowId)` calls `onRowDelete({ rowId, row })` straight away under
|
|
241
|
+
the immediate modes, so a confirmation belongs inside that callback. Under batch
|
|
242
|
+
it toggles a mark instead: the row renders struck through and inert
|
|
243
|
+
(`data-deleted`), the lane's trash becomes a restore, and `submitAll` reports
|
|
244
|
+
the ids in `deleted`.
|
|
245
|
+
|
|
246
|
+
Adds and deletes are applied by you, the same as edits. The engine's `tempId`
|
|
247
|
+
(`__new__1`, `__new__2`, …) never needs to become a real id: mint one when you
|
|
248
|
+
create the record.
|
|
249
|
+
|
|
250
|
+
## The chrome
|
|
251
|
+
|
|
252
|
+
The generated edit lane (`EDIT_COLUMN_ID`, pinned right) appears when `editMode`
|
|
253
|
+
is `"row"`, or `onRowDelete` is set, or `editMode` is `"batch"` with
|
|
254
|
+
`onEditCommitBatch` set. Nothing else conjures it, and `"cell"` mode has no
|
|
255
|
+
chrome at all by design.
|
|
256
|
+
|
|
257
|
+
`TMDataGrid.EditActions` is Save with the pending count plus Discard. It greys
|
|
258
|
+
out while nothing is pending, spins while a submit is in flight, renders nothing
|
|
259
|
+
while editing is off, and works under any mode, not only batch.
|
|
260
|
+
|
|
261
|
+
## The engine: `edit`
|
|
262
|
+
|
|
263
|
+
`grid.edit` is public, and everything the chrome does goes through it.
|
|
264
|
+
|
|
265
|
+
| Member | Does |
|
|
266
|
+
| --- | --- |
|
|
267
|
+
| `edit.begin({ rowId, columnId })` | Opens an editor. `columnId: null` opens the whole row |
|
|
268
|
+
| `edit.commit(rowId)` | Commits one row. `Promise<boolean>`, `false` when blocked |
|
|
269
|
+
| `edit.cancel(rowId)` / `edit.cancelAll()` | Drops one draft, or every draft |
|
|
270
|
+
| `edit.submitAll()` | Commits every open row. `Promise<boolean>` |
|
|
271
|
+
| `edit.addRow()` / `edit.deleteRow(rowId)` | Rows in and out |
|
|
272
|
+
| `edit.getForm(rowId)` | The row's live TanStack Form `FormApi` |
|
|
273
|
+
| `edit.store` | The projection store, for `useSelector` |
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
import { useSelector } from "@tanstack/react-store";
|
|
277
|
+
|
|
278
|
+
const pendingCount = useSelector(
|
|
279
|
+
grid.edit.store,
|
|
280
|
+
(state) => state.openRowIds.length + state.deletedRowIds.length,
|
|
281
|
+
);
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`edit.store` holds `active`, `openRowIds`, `rows` (per row `dirtyFields`,
|
|
285
|
+
`errorFields`, `isSubmitting`), `newRows` and `deletedRowIds`. The full member
|
|
286
|
+
list, `clearCell`, `deactivate` and the gates included, is in
|
|
287
|
+
[references/editing-api.md](references/editing-api.md).
|
|
288
|
+
|
|
289
|
+
`getForm` is the *one row, one form* promise made public: render that same form
|
|
290
|
+
in a drawer and it shares values, dirty state and errors with the inline cells,
|
|
291
|
+
because it is the same `FormApi`.
|
|
292
|
+
|
|
293
|
+
## Common mistakes
|
|
294
|
+
|
|
295
|
+
### CRITICAL Expecting the grid to write into `data`
|
|
296
|
+
|
|
297
|
+
The grid holds no copy of the rows. Without `onEditCommit` an edit commits, the
|
|
298
|
+
draft clears, and the cell snaps back to the value in `data`, which looks like
|
|
299
|
+
the edit was silently discarded.
|
|
300
|
+
|
|
301
|
+
Wrong:
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
useTMDataGrid({ data: employees, columns, getRowId, editMode: "cell" });
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Correct:
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
useTMDataGrid({
|
|
311
|
+
data: employees,
|
|
312
|
+
columns,
|
|
313
|
+
getRowId,
|
|
314
|
+
editMode: "cell",
|
|
315
|
+
onEditCommit: ({ rowId, value }) =>
|
|
316
|
+
setEmployees((previous) =>
|
|
317
|
+
previous.map((employee) =>
|
|
318
|
+
String(employee.id) === rowId ? value : employee,
|
|
319
|
+
),
|
|
320
|
+
),
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Source: `src/docs/editing.md`, `src/tmdatagrid/core/editEngine.ts`.
|
|
325
|
+
|
|
326
|
+
### CRITICAL `getRowId` built from the row index
|
|
327
|
+
|
|
328
|
+
`getRowId` satisfies the compiler with any string, so an index-based id passes.
|
|
329
|
+
Drafts are keyed by it, so after a sort or a filter the draft is applied to
|
|
330
|
+
whichever record now sits at that index.
|
|
331
|
+
|
|
332
|
+
Wrong:
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
getRowId: (row, index) => String(index),
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Correct:
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
getRowId: (row) => String(row.id),
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
|
|
345
|
+
|
|
346
|
+
### HIGH A cell editor defined inside the component
|
|
347
|
+
|
|
348
|
+
`meta.editor` is rendered as JSX, so its identity is its component type. An
|
|
349
|
+
inline arrow is a new type on every render, which unmounts the open editor and
|
|
350
|
+
loses what was being typed.
|
|
351
|
+
|
|
352
|
+
Wrong:
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
meta: { editor: ({ field }) => <Slider value={field.state.value} /> },
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Correct:
|
|
359
|
+
|
|
360
|
+
```tsx
|
|
361
|
+
// Module scope, referenced by name.
|
|
362
|
+
const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
|
|
363
|
+
<Slider
|
|
364
|
+
value={Number(field.state.value ?? 0)}
|
|
365
|
+
onChange={(next) => field.handleChange(next)}
|
|
366
|
+
onChangeEnd={() => void commit()}
|
|
367
|
+
/>
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
meta: { editor: SalaryEditor },
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Source: `src/docs/editors.md`, `src/tmdatagrid/core/editEngine.ts`.
|
|
374
|
+
|
|
375
|
+
### HIGH A cross-field rule under `editMode: "cell"`
|
|
376
|
+
|
|
377
|
+
Under `"cell"` each cell commits on its own, so a rule spanning two columns can
|
|
378
|
+
never be satisfied by either one: the first cell edited is rejected against the
|
|
379
|
+
other column's old value, and the row cannot be saved at all.
|
|
380
|
+
|
|
381
|
+
Wrong:
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
useTMDataGrid({ editMode: "cell", rowValidators: { onSubmit: endAfterStart } });
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Correct:
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
// "row" (or "batch") validates the whole row in one commit.
|
|
391
|
+
useTMDataGrid({ editMode: "row", rowValidators: { onSubmit: endAfterStart } });
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Source: `src/docs/editors.md` (cross-field rules want a mode that commits the
|
|
395
|
+
whole row at once).
|
|
396
|
+
|
|
397
|
+
### HIGH An `accessorFn` column that never opens an editor
|
|
398
|
+
|
|
399
|
+
Editability follows the data path, not the column. A column built on an accessor
|
|
400
|
+
function has no `accessorKey`, so it maps to nothing and stays read-only while
|
|
401
|
+
every other column edits. No warning is raised.
|
|
402
|
+
|
|
403
|
+
Wrong:
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
|
|
407
|
+
id: "fullName",
|
|
408
|
+
header: "Full name",
|
|
409
|
+
});
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Correct:
|
|
413
|
+
|
|
414
|
+
```tsx
|
|
415
|
+
columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
|
|
416
|
+
id: "fullName",
|
|
417
|
+
header: "Full name",
|
|
418
|
+
meta: { label: "Full name", editField: "lastName" },
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Source: `src/docs/editing.md` (Which cells edit).
|
|
423
|
+
|
|
424
|
+
### HIGH Swallowing the error in `onEditCommit`
|
|
425
|
+
|
|
426
|
+
The draft is dropped when `onEditCommit` resolves. A `try/catch` that logs the
|
|
427
|
+
failure resolves it, so a save that failed on the server clears the editor and
|
|
428
|
+
the grid shows the old value as though nothing happened.
|
|
429
|
+
|
|
430
|
+
Wrong:
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
onEditCommit: async ({ rowId, changes }) => {
|
|
434
|
+
try {
|
|
435
|
+
await api.patch(rowId, changes);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.error(error);
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Correct:
|
|
443
|
+
|
|
444
|
+
```tsx
|
|
445
|
+
// Reject: the form stays open with the error on the row.
|
|
446
|
+
onEditCommit: async ({ rowId, changes }) => {
|
|
447
|
+
await api.patch(rowId, changes);
|
|
448
|
+
},
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
Source: `src/tmdatagrid/useTMDataGrid.tsx` (`onEditCommit`).
|
|
452
|
+
|
|
453
|
+
### MEDIUM Reading a commit's result as the saved value
|
|
454
|
+
|
|
455
|
+
`edit.commit(rowId)` and `edit.submitAll()` resolve to a `boolean` saying
|
|
456
|
+
whether the form closed, and resolve `false` when validation or a rejected save
|
|
457
|
+
kept it open. Ignoring the result reports a save that did not happen.
|
|
458
|
+
|
|
459
|
+
Wrong:
|
|
460
|
+
|
|
461
|
+
```tsx
|
|
462
|
+
await grid.edit.submitAll();
|
|
463
|
+
notifications.show({ message: "Saved" });
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Correct:
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
const saved = await grid.edit.submitAll();
|
|
470
|
+
notifications.show({ message: saved ? "Saved" : "Some rows need attention" });
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Source: `src/tmdatagrid/core/editEngine.ts` (`TMDataGridEditApi`).
|
|
474
|
+
|
|
475
|
+
### MEDIUM Expecting `onRowDelete` to fire under batch
|
|
476
|
+
|
|
477
|
+
Under the immediate modes `edit.deleteRow` calls `onRowDelete` at once. Under
|
|
478
|
+
`"batch"` it only toggles a deletion mark, and the ids arrive later in
|
|
479
|
+
`submitAll`'s `deleted`, so a batch grid that deletes through `onRowDelete`
|
|
480
|
+
alone never removes anything. Handle `deleted` in `onEditCommitBatch`.
|
|
481
|
+
|
|
482
|
+
Source: `src/docs/editing.md` (Adding and deleting rows).
|
|
483
|
+
|
|
484
|
+
## References
|
|
485
|
+
|
|
486
|
+
- [Editors and validation](references/editors-and-validation.md) - the editor
|
|
487
|
+
contract, wrapping a built-in, field and row validators, server-side errors.
|
|
488
|
+
- [Editing API](references/editing-api.md) - every option, callback, column meta
|
|
489
|
+
field, export, CSS variable and data attribute editing owns.
|
|
490
|
+
|
|
491
|
+
See also: the `columns` skill for `meta.type` and the shared `meta.options`
|
|
492
|
+
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. |
|
|
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.
|