@jielga/tmdatagrid 2.0.0-beta.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1281 -768
  3. package/dist/index.js +4607 -3250
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/adding-rows.md +132 -0
  7. package/docs/anatomy.md +119 -0
  8. package/docs/card-view.md +108 -0
  9. package/docs/cell-selection.md +194 -0
  10. package/docs/column-layout.md +182 -0
  11. package/docs/column-menu.md +66 -0
  12. package/docs/columns.md +268 -0
  13. package/docs/components.md +311 -0
  14. package/docs/draft-store.md +242 -0
  15. package/docs/editing.md +303 -0
  16. package/docs/editors.md +250 -0
  17. package/docs/export.md +319 -0
  18. package/docs/filtering.md +362 -0
  19. package/docs/getting-started.md +123 -0
  20. package/docs/grouping.md +165 -0
  21. package/docs/loading-and-empty.md +92 -0
  22. package/docs/localization.md +79 -0
  23. package/docs/menu.md +143 -0
  24. package/docs/migrating-to-2.md +163 -0
  25. package/docs/pagination.md +144 -0
  26. package/docs/persistence.md +114 -0
  27. package/docs/portfolio-rebalancer.md +94 -0
  28. package/docs/query-builder.md +179 -0
  29. package/docs/quick-search.md +84 -0
  30. package/docs/row-details.md +115 -0
  31. package/docs/row-interaction.md +149 -0
  32. package/docs/row-pinning.md +132 -0
  33. package/docs/row-selection.md +136 -0
  34. package/docs/row-styling.md +133 -0
  35. package/docs/scrolling.md +112 -0
  36. package/docs/server-query.md +246 -0
  37. package/docs/server-side.md +206 -0
  38. package/docs/sorting.md +101 -0
  39. package/docs/styling.md +126 -0
  40. package/docs/summary-row.md +76 -0
  41. package/docs/testing.md +744 -0
  42. package/docs/toolbar.md +161 -0
  43. package/docs/use-tm-data-grid.md +361 -0
  44. package/package.json +22 -46
  45. package/skills/appearance/SKILL.md +72 -19
  46. package/skills/cell-selection/SKILL.md +46 -47
  47. package/skills/columns/SKILL.md +90 -34
  48. package/skills/data/SKILL.md +86 -16
  49. package/skills/editing/SKILL.md +67 -40
  50. package/skills/editing/references/common-mistakes.md +77 -69
  51. package/skills/editing/references/editing-api.md +22 -19
  52. package/skills/editing/references/editors-and-validation.md +24 -17
  53. package/skills/filtering/SKILL.md +148 -40
  54. package/skills/getting-started/SKILL.md +17 -15
  55. package/skills/grouping/SKILL.md +31 -16
  56. package/skills/options/SKILL.md +7 -7
  57. package/skills/rows/SKILL.md +22 -18
  58. package/skills/server-side/SKILL.md +170 -17
  59. package/skills/testing/SKILL.md +150 -32
  60. package/skills/testing-components/SKILL.md +230 -0
  61. package/skills/testing-editing/SKILL.md +240 -0
  62. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  63. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
  64. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  65. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  66. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
  67. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  68. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
  69. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
  70. package/src/components/TMDataGridExportPicker.module.css +77 -0
  71. package/src/components/TMDataGridExportPicker.tsx +234 -0
  72. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  73. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  74. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  75. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  76. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  77. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  78. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
  79. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  80. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  81. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  82. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  83. package/src/components/TMDataGridMenu.tsx +357 -0
  84. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
  85. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
  86. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
  87. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  88. package/src/components/TMDataGridToolbar.tsx +181 -0
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  91. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  96. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  97. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  98. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  99. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  100. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  101. package/src/components/filters/controlLayout.ts +32 -0
  102. package/src/components/filters/filterControlFor.ts +65 -0
  103. package/src/components/generatedColumns.tsx +187 -0
  104. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  105. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  106. package/src/components/useHideableColumns.ts +52 -0
  107. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  108. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  109. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  110. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  111. package/src/core/controlledStateSync.ts +108 -0
  112. package/src/core/deletedRows.ts +34 -0
  113. package/src/core/dom.ts +74 -0
  114. package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
  115. package/src/core/export.ts +704 -0
  116. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  117. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  118. package/src/core/filterSurface.ts +99 -0
  119. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  120. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  121. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  122. package/src/core/pageReset.ts +120 -0
  123. package/src/core/pagination.ts +81 -0
  124. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  125. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  126. package/src/{tmdatagrid/index.ts → index.ts} +69 -35
  127. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
  128. package/src/useTMDataGridExport.ts +78 -0
  129. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  130. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  131. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
  134. /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/capabilities.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -10,17 +10,23 @@ description: >
10
10
  onReachEnd is better for loading more, the header and pinned-lane depth
11
11
  shadows, and the four empty states in precedence order with meta.loading,
12
12
  renderEmptyState, hasActiveFilters, TMDataGrid.LoadingIndicator and
13
- TMDataGrid.SummaryCount. Load when adding a pager, tuning scrolling, scrolling
14
- to a row, or deciding what an empty grid should say.
13
+ TMDataGrid.SummaryCount, and export: TMDataGrid.Menu.Export and
14
+ Menu.ExportSelected, useTMDataGridExport, exportGrid, exportOptions, the
15
+ csvExcel / csv / tsv / json formats, meta.exportValue and meta.enableExport.
16
+ Load when adding a pager, tuning scrolling, scrolling to a row, deciding what
17
+ an empty grid should say, or exporting rows to Excel or CSV.
15
18
  metadata:
16
19
  type: core
17
20
  library: '@jielga/tmdatagrid'
18
- library_version: '2.0.0-beta.9'
21
+ library_version: '2.0.0'
19
22
  sources:
20
- - 'Jielga/TMDataGrid:src/docs/pagination.md'
21
- - 'Jielga/TMDataGrid:src/docs/scrolling.md'
22
- - 'Jielga/TMDataGrid:src/docs/loading-and-empty.md'
23
- - 'Jielga/TMDataGrid:src/tmdatagrid/components/TMDataGridFooter.tsx'
23
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/pagination.md'
24
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/scrolling.md'
25
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/loading-and-empty.md'
26
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/export.md'
27
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/components/TMDataGridFooter.tsx'
28
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/core/pagination.ts'
29
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/core/export.ts'
24
30
  ---
25
31
 
26
32
  # TMDataGrid - Pagination, scrolling and empty states
@@ -77,7 +83,10 @@ pieces, already wired.
77
83
  `Controls.PageSize`, `Controls.Range` and `Controls.Pager` are what the default
78
84
  footer renders, in that order, so a custom layout can keep the parts it wants
79
85
  instead of rebuilding them. They behave exactly as before, including greying out
80
- under a suspended pager.
86
+ under a suspended pager. `Controls.PageNumber` - the "Page 3 of 200" label a
87
+ server-paged grid usually shows in place of a row range - is a fourth control,
88
+ not in the default footer; put it in through the slot rather than writing it by
89
+ hand.
81
90
 
82
91
  `state` carries `pageIndex`, `pageSize`, `pageCount`, `rowCount`,
83
92
  `canPreviousPage`, `canNextPage`, `from`, `to` and `isPagingActive`. `actions`
@@ -178,6 +187,67 @@ server-driven grid refetching with rows still on screen keeps showing them.
178
187
  total, where the total is `meta.totalRowCount` when provided and the pre-filtered
179
188
  count otherwise.
180
189
 
190
+ ## Export
191
+
192
+ Every filtered and sorted row across every page, or the selected rows, as a
193
+ file. The built-in entry points are menu items; a button of your own uses the
194
+ hook.
195
+
196
+ ```tsx
197
+ const grid = useTMDataGrid({
198
+ data,
199
+ columns,
200
+ exportOptions: { format: csvFormat(), fileName: "employees" },
201
+ });
202
+
203
+ <TMDataGrid.Menu>
204
+ <TMDataGrid.Menu.Export />
205
+ <TMDataGrid.Menu.ExportSelected />
206
+ </TMDataGrid.Menu>
207
+ ```
208
+
209
+ ```tsx
210
+ function ExportButton() {
211
+ const { exportAll, exportSelected, selectedCount, canExportSelected } =
212
+ useTMDataGridExport();
213
+ return <Button onClick={() => void exportAll()}>Export</Button>;
214
+ }
215
+ ```
216
+
217
+ What is written: the data columns in render order (never the generated lanes,
218
+ never a column with `meta.enableExport: false`), every row after filtering and
219
+ sorting (a grouped grid writes the records under every group, never the group
220
+ rows), and each cell's **value** rather than what it renders -
221
+ `meta.exportValue: ({ value, row, column }) => unknown` substitutes one.
222
+ `columns` on `exportOptions`, the items and the functions is `"visible"` (the
223
+ default), `"all"` (hidden columns too) or a list of ids; `columns="custom"` on
224
+ a menu item opens a picker instead - every exportable column, the visible ones
225
+ ticked, Export and Cancel - driven by `ui.state.exportPicker` and
226
+ `ui.actions.openExportPicker`. `TMDataGrid.Menu.ExportSelected` counts and
227
+ writes the ticked rows of the current view, in grid order; it renders nothing
228
+ when row selection is off.
229
+
230
+ Formats, each a `TMDataGridExportFormat` from a factory:
231
+
232
+ | Factory | Writes |
233
+ | --- | --- |
234
+ | `csvExcelFormat()` (default) | BOM, `sep=;` line, CRLF, `;` fields, `,` decimal - opens straight into columns in Excel. `separator`, `decimalComma` for another locale. |
235
+ | `csvFormat()` | RFC 4180: `,` fields, `.` decimal, BOM, no `sep=` line - for Google Sheets, Numbers and tooling. |
236
+ | `tsvFormat()` | Tab-separated, the clipboard shape as a file. |
237
+ | `jsonFormat()` | One object per row keyed by column label, numbers as numbers, dates as ISO strings. |
238
+ | `xlsxFormat()` | Excel workbook, from the separate `@jielga/tmdatagrid-xlsx` package. |
239
+
240
+ The text formats guard against formula injection by default: a value starting
241
+ with `=`, `+`, `-` or `@` that is not a number is prefixed with `'`.
242
+ `escapeFormulas: false` on the format turns that off.
243
+
244
+ `exportGrid({ table, rows: "all" | "selected" | rows, options })` is the same
245
+ export for code outside a component; `buildExportData` is the step before the
246
+ file. A format of your own is `{ id, extension, mimeType, write(data, { includeHeaders }) }`
247
+ returning a string, a `Blob`, or a promise of either.
248
+
249
+ Source: `packages/tmdatagrid/docs/export.md`.
250
+
181
251
  ## Common mistakes
182
252
 
183
253
  ### CRITICAL Turning pagination on to make a large grid fast
@@ -186,7 +256,7 @@ Virtualization is already unconditional, so paging a 200 000-row grid changes
186
256
  nothing about rendering cost. It only changes how users navigate. Enable it when
187
257
  they should move page by page, not for performance.
188
258
 
189
- Source: `src/docs/pagination.md`, `src/docs/scrolling.md`.
259
+ Source: `packages/tmdatagrid/docs/pagination.md`, `packages/tmdatagrid/docs/scrolling.md`.
190
260
 
191
261
  ### CRITICAL A variable row height
192
262
 
@@ -207,7 +277,7 @@ Correct:
207
277
  useTMDataGrid({ data, columns, meta: { rowHeight: 64 } });
208
278
  ```
209
279
 
210
- Source: `src/docs/scrolling.md` (Row height).
280
+ Source: `packages/tmdatagrid/docs/scrolling.md` (Row height).
211
281
 
212
282
  ### HIGH `scrollIntoView` on a row that is not mounted
213
283
 
@@ -223,7 +293,7 @@ grid.scrollToRow({ rowId: "4000", align: "center" });
223
293
  `scrollToRow` returns `false` when the row is not in the current view (filtered
224
294
  out, on another page, or an id matching no row) and nothing scrolled.
225
295
 
226
- Source: `src/docs/scrolling.md` (Scrolling to a row).
296
+ Source: `packages/tmdatagrid/docs/scrolling.md` (Scrolling to a row).
227
297
 
228
298
  ### HIGH Loading more rows from `onScrollToBottom`
229
299
 
@@ -231,7 +301,7 @@ It fires at the very bottom, so the user waits at the end of the list for the
231
301
  fetch. `onReachEnd` fires a number of rows earlier and latches per row count, so
232
302
  a pending fetch is not requested twice.
233
303
 
234
- Source: `src/docs/scrolling.md` (Edge callbacks).
304
+ Source: `packages/tmdatagrid/docs/scrolling.md` (Edge callbacks).
235
305
 
236
306
  ### HIGH `SummaryCount` reporting the page under manual pagination
237
307
 
@@ -250,7 +320,7 @@ useTMDataGrid({
250
320
  });
251
321
  ```
252
322
 
253
- Source: `src/docs/loading-and-empty.md` (Counting what is there).
323
+ Source: `packages/tmdatagrid/docs/loading-and-empty.md` (Counting what is there).
254
324
 
255
325
  ### MEDIUM Rendering an empty message while data is loading
256
326
 
@@ -264,7 +334,7 @@ Correct:
264
334
  useTMDataGrid({ data, columns, meta: { loading: isFetching } });
265
335
  ```
266
336
 
267
- Source: `src/docs/loading-and-empty.md` (What wins).
337
+ Source: `packages/tmdatagrid/docs/loading-and-empty.md` (What wins).
268
338
 
269
339
  ### MEDIUM Trusting the pager while grouped
270
340
 
@@ -272,7 +342,7 @@ Source: `src/docs/loading-and-empty.md` (What wins).
272
342
  grid is rendering the whole tree. A custom pager must read
273
343
  `isPagingActive(table, features)` rather than the page count.
274
344
 
275
- Source: `src/docs/pagination.md` (Grouping suspends it).
345
+ Source: `packages/tmdatagrid/docs/pagination.md` (Grouping suspends it).
276
346
 
277
347
  ## Reference
278
348
 
@@ -283,7 +353,7 @@ Source: `src/docs/pagination.md` (Grouping suspends it).
283
353
  | `rowCount` | Table option | `number` | – | The true total, required under `manualPagination`. |
284
354
  | `initialState.pagination` | Table option | `{ pageIndex, pageSize }` | `{ 0, 25 }` | Where paging starts. A `data` slice, so it persists. |
285
355
  | `onPaginationChange` | Table option | `OnChangeFn` | – | Controls the pagination state. |
286
- | `TMDataGrid.Footer` | Component | `pageSizeOptions`, `pagination` | `[10, 25, 50, 100]` | The footer bar. Renders nothing when paging is off. |
356
+ | `TMDataGrid.Footer` | Component | `pageSizeOptions`, `renderPagination`, Mantine `BoxProps` | `[10, 25, 50, 100]` | The footer bar. Renders nothing when paging is off. Style props set on the bar. |
287
357
  | `Footer` `renderPagination` | Slot | `({ state, actions, Controls }) => ReactNode` | Built-in pager | Replaces the pager, and hands over its pieces. |
288
358
  | `getTMDataGridPaginationApi` | Export | `(table) => { state, actions }` | – | The pager API, outside the Footer. |
289
359
  | `TMDataGridPaginationState` · `TMDataGridPaginationActions` · `TMDataGridPaginationControls` | Exports | types | – | The three parts of the slot argument. |
@@ -18,13 +18,15 @@ description: >
18
18
  metadata:
19
19
  type: core
20
20
  library: '@jielga/tmdatagrid'
21
- library_version: '2.0.0-beta.9'
21
+ library_version: '2.0.0'
22
22
  sources:
23
- - 'Jielga/TMDataGrid:src/docs/editing.md'
24
- - 'Jielga/TMDataGrid:src/docs/query-builder.md'
25
- - 'Jielga/TMDataGrid:src/docs/editors.md'
26
- - 'Jielga/TMDataGrid:src/tmdatagrid/core/editEngine.ts'
27
- - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
23
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/editing.md'
24
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/draft-store.md'
25
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/adding-rows.md'
26
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/query-builder.md'
27
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/docs/editors.md'
28
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/core/editEngine.ts'
29
+ - 'Jielga/TMDataGrid:packages/tmdatagrid/src/useTMDataGrid.tsx'
28
30
  ---
29
31
 
30
32
  # TMDataGrid - Editing
@@ -34,9 +36,10 @@ happen. Three facts decide every wiring question below:
34
36
 
35
37
  - **The grid never mutates `data`.** `editing.onCommit` applies the change
36
38
  wherever the data lives, and the updated rows arrive back through `data`.
37
- - **One row, one form.** Each editing row gets its own TanStack Form, keyed by
38
- row id and living outside the DOM, so a draft survives scrolling, sorting and
39
- filtering.
39
+ - **One row, one form, while it is open.** Each row being edited gets its own
40
+ TanStack Form, keyed by row id and living outside the DOM, so a draft
41
+ survives scrolling, sorting and filtering. A committed row holds values, not
42
+ a form.
40
43
  - **`getRowId` is required** once `editing` is set, and it must be the record's
41
44
  own identity. Drafts are keyed by it.
42
45
 
@@ -85,13 +88,15 @@ They are independent, and every pair is legal.
85
88
  | `"cellConfirm"` | ✓ or Enter; Tab walks input, ✓, ✕ and then leaves, keeping the draft | ✕ or Escape | ✓ / ✕ beside the input |
86
89
  | `"row"` | Save in the edit lane, or Enter | Cancel, or Escape | generated edit lane |
87
90
 
91
+ Leaving a cell commits it only once the value passes: a refused commit keeps the editor open, invalid, with the message in its tooltip, until the value is fixed or Escape drops it.
92
+
88
93
  | `editing.draft` | Where a commit goes |
89
94
  | --- | --- |
90
95
  | `false` (default) | Straight out: `onCommit`, `onRowAdd`, `onRowDelete` |
91
96
  | `true` | Into the grid's draft store, until `edit.saveDrafts()` sends the lot |
92
97
 
93
98
  Which to pick: `"cell"` for spreadsheet feel; `"cellConfirm"` when a stray click must not fire a request; `"row"` when the row is the unit of the save or a rule spans two columns.
94
- Add `draft: true` for many edits sent as one transaction - `{ mode: "row", draft: true }` parks a whole row from the lane's ✓, `{ mode: "cell", draft: true }` parks a row as the caret leaves it.
99
+ Add `draft: true` for many edits sent as one transaction - `{ mode: "row", draft: true }` commits a whole row from the lane's ✓, `{ mode: "cell", draft: true }` commits a row as the caret leaves it.
95
100
 
96
101
  An editor opens on double-click, or with the cell cursor on the cell: Enter, F2,
97
102
  or typing, where the first character replaces the value. Delete or Backspace
@@ -108,11 +113,11 @@ commit. Rows accumulate: opening a second row leaves the first open, and each
108
113
  row's ✓ and ✕ act on that row alone.
109
114
 
110
115
  Under `draft: true` nothing reaches a callback until `saveDrafts`.
111
- The mode's own commit gesture parks the row instead of sending it, Escape drops that one draft, and parked rows accumulate.
112
- `edit.commit(rowId)` parks too, so there is no per-row escape hatch to the consumer.
113
- A parked row is displayed: the cell renders the draft value through the column's own `cell` renderer, with the blue corner marking it dirty and `data-dirty` on the row.
116
+ The mode's own commit gesture puts the row in the draft store instead of sending it, Escape drops that one draft, and committed rows accumulate.
117
+ `edit.commit(rowId)` goes to the draft store too, so there is no per-row escape hatch to the consumer.
118
+ A committed row is displayed: the cell renders the draft value through the column's own `cell` renderer, with the blue corner marking it dirty and `data-dirty` on the row.
114
119
  It is a row like any other to the table: sorting, filtering, quick search, grouping, aggregates, export, selection, the row counts, `edit.getRows()` and `editing.tableValidators` all read its draft values, and the row callbacks receive it with the draft as `row.original`.
115
- A parked row that stops matching a filter leaves the view, and the Save bar still counts it.
120
+ A committed row that stops matching a filter leaves the view, and the Save bar still counts it.
116
121
  `data` itself is never modified, and only top-level rows are overlaid - `getSubRows` children keep their `data` values.
117
122
  An entry row is row-shaped in every mode - every editable cell open at once, the browser's Tab, and the lane's ✓ to enter it.
118
123
 
@@ -131,10 +136,22 @@ a list of row ids. Rows failing validation stay open either way.
131
136
 
132
137
  `onSaveDrafts` decides how much of the store is cleared: returning nothing
133
138
  saves everything, throwing saves nothing, and returning
134
- `{ updated, created, deleted }` saves everything except the ids reported
135
- `false`. Each key takes `false` for the whole bucket or a map of id to result;
136
- an unnamed id saved. A kept row stays committed, so the next `saveDrafts()`
137
- retries it, and `saveDrafts()` resolves `false` when anything was kept.
139
+ `{ updated, created, deleted }` (a `TMDataGridSaveDraftsResponse`) saves
140
+ everything except the ids reported `false`. Each key takes `false` for the
141
+ whole bucket or a map of id to result; an unnamed id saved. A kept row stays
142
+ committed, so the next `saveDrafts()` retries it.
143
+
144
+ `saveDrafts()` resolves a `TMDataGridSaveDraftsResult`, `{ ok, saved, kept, reopened }`.
145
+ Every id the save took from the draft store is in exactly one list - row ids for edits and deletions, temp ids for new rows, all kinds mixed:
146
+
147
+ - `saved` - left the draft store; the consumer accepted it
148
+ - `kept` - still in the draft store, still committed, retried by the next save: an id `onSaveDrafts` returned as failed, every id it was sent when it threw, or, without `onSaveDrafts`, a deletion whose `onRowDelete` threw
149
+ - `reopened` - open again with an error: a table rule rejected it, or on the per-row path its `onCommit` / `onRowAdd` threw
150
+ - `ok` - `true` when `kept` and `reopened` are both empty
151
+
152
+ On the per-row path a deletion always leaves the store and is reported in `saved`.
153
+ An empty store resolves `{ ok: true, saved: [], kept: [], reopened: [] }`.
154
+ Rows still open are in no list.
138
155
 
139
156
  Rows carry `data-dirty` (values typed in), `data-draft` (committed, waiting for
140
157
  Save), `data-deleted` and `data-new` - a committed new row in the body, or an
@@ -216,7 +233,11 @@ rowValidators: {
216
233
  ```
217
234
 
218
235
  Pathed issues land on the matching cells, pathless ones on the row, and cell
219
- corners mark both: blue for a dirty draft, red for a validation error.
236
+ corners mark both: blue for a dirty draft, red for a validation error. A
237
+ field's message shows in a tooltip on the open editor, which the host renders
238
+ for a custom editor as much as a built-in one; the plain-function form of a
239
+ validator types `value` as `never`, so annotate the parameter -
240
+ `({ value }: { value: unknown })`.
220
241
 
221
242
  `editing.tableValidators` carries the rules that need the other rows - no
222
243
  duplicate keys, no overlapping ranges, shares summing to a total. Its
@@ -225,8 +246,9 @@ duplicate keys, no overlapping ranges, shares summing to a total. Its
225
246
  overlaid, committed new rows among them, the entry rows the table does not hold
226
247
  appended, deletion-marked rows removed. Each row appears once. Same result
227
248
  vocabulary as `rowValidators`; errors land on the committing row. The rules
228
- re-run per parked row during `saveDrafts`, so a draft a later edit
229
- invalidated blocks the save.
249
+ re-run per committed row during `saveDrafts`, the only validation that runs
250
+ there: a committed row a later edit invalidated is reopened with its errors
251
+ and the save reports it in `reopened`.
230
252
 
231
253
  ```tsx
232
254
  tableValidators: {
@@ -255,8 +277,8 @@ from `editing.newRowDefaults`. `edit.addRow(values)` overrides that seed key by
255
277
  key, so `addRow()` opens the `newRowDefaults` row and `addRow(values)` opens it
256
278
  with those fields filled in - pass a whole row to duplicate it. Enter, or the
257
279
  lane's ✓, commits the add through `editing.onRowAdd`; under `draft: true` it
258
- parks the row in the draft store, validated, and
259
- `saveDrafts` reports it in `added`. Escape, or ✕, discards the entry. An entry
280
+ commits the row into the draft store, validated, and
281
+ `saveDrafts` reports it in `created`. Escape, or ✕, discards the entry. An entry
260
282
  row never OK'd is not part of a save - it stays open.
261
283
 
262
284
  Under `draft: true` a committed entry row leaves the entry block and becomes a
@@ -297,13 +319,14 @@ const grid = useTMDataGrid({
297
319
  `edit.addRows(rows, options?)` opens a batch in one write. `{ commit: true }`
298
320
  submits each row as it lands - the import case: valid rows are committed,
299
321
  invalid ones stay open in the entry block with their errors, and the result
300
- (`{ committed, open }`) says which went which way. Column rules are enforced
301
- even though the rows never had an editor on screen, because the engine runs
302
- `meta.edit.validate` itself at commit.
322
+ (`{ ok, committed, open }`) says which went which way; `ok` is `true` when
323
+ `open` is empty. Column rules are enforced even though the rows never had an
324
+ editor on screen, because the engine runs `meta.edit.validate` itself at
325
+ commit.
303
326
 
304
327
  ```tsx
305
- const { committed, open } = await grid.edit.addRows(parsed, { commit: true });
306
- if (open.length > 0) notify(`${open.length} rows need attention`);
328
+ const { ok, open } = await grid.edit.addRows(parsed, { commit: true });
329
+ if (!ok) notify(`${open.length} rows need attention`);
307
330
  await grid.edit.saveDrafts();
308
331
  ```
309
332
 
@@ -318,8 +341,8 @@ it toggles a mark instead: the row renders struck through and inert
318
341
 
319
342
  The generated edit lane (`EDIT_COLUMN_ID`, pinned right) appears when `editing.mode` is `"row"`, when `editing.draft` is on, or when `editing.onRowDelete` is set.
320
343
  Nothing else adds it.
321
- It holds one thing per axis: the mode's own controls while a row is open - Save and Cancel under `"row"` - and, once a row is parked, the row-state marker with Revert or Restore.
322
- A parked row never offers a save.
344
+ It holds one thing per axis: the mode's own controls while a row is open - Save and Cancel under `"row"` - and, once a row is committed, the row-state marker with Revert or Restore.
345
+ A committed row never offers a save.
323
346
  The trash shows when the deletion has somewhere to report to: `onRowDelete` is set, or under `draft: true`, `onSaveDrafts` is.
324
347
  If validation blocks a row, its marker - or the open row's ✓ - turns red with the message in the tooltip, which is where a pathless `rowValidators` message shows.
325
348
  Every control carries a tooltip from the labels.
@@ -353,10 +376,10 @@ while editing is off, and works under any mode, not only draft.
353
376
  ```
354
377
 
355
378
  `state` is
356
- `{ draftCount, openCount, openRowIds, pendingCount, isSubmitting }` -
357
- `pendingCount` deprecated, reading as `draftCount + openCount`. `actions` is
379
+ `{ draftCount, openCount, openRowIds, isSubmitting, isSaving }`. `actions` is
358
380
  `{ save, commitAll, discard, scrollToRow, scrollToFirstOpenRow }`, and
359
381
  `Controls` is `{ Save, Discard, OpenRowsNote }`.
382
+ `actions.save` and `actions.commitAll` resolve what `edit.saveDrafts()` and `edit.commitAll()` resolve.
360
383
 
361
384
  The grid is always virtualized, so an open row far down the list has no element
362
385
  to scroll to. `actions.scrollToFirstOpenRow(align?)` moves the virtualizer to
@@ -376,16 +399,20 @@ row it reaches.
376
399
  `setCellValue` / `setRowValues` / `clearCell`, `addRow` / `addRows` /
377
400
  `deleteRow`, `getForm`, and `store` for `useSelector` (an example is under
378
401
  [Submitting an outer form](#high-submitting-an-outer-form-while-the-grid-holds-a-draft)).
379
- `edit.store` publishes each open or parked row's drafted values as
380
- `rows[rowId].values`, which is what a computed cell or a cross-row check reads
381
- - `useTMDataGridContext()` reaches the engine from inside a cell renderer.
402
+ `commitAll()` resolves a `TMDataGridCommitAllResult`, `{ ok, committed, open }`: every row open at the call is in exactly one list, and `ok` is `true` when `open` is empty.
403
+ `commit(rowId)` alone resolves a plain boolean.
404
+ `edit.store` publishes each open or committed row's drafted values as
405
+ `rows[rowId].values`, which is what a cross-row check reads. A `cell`
406
+ renderer needs no lookup: its `row.original` is already the row as shown, and
407
+ `getRowValues(rowId)` is the same row for a handler with no cell context.
382
408
  Every member with its signature, the gates, `isColumnEditable`, `deactivate`,
383
409
  and the `edit.store` shape are in
384
410
  [references/editing-api.md](references/editing-api.md#the-edit-engine).
385
411
 
386
- `getForm` exposes the row's form: render it in a drawer and it shares values,
387
- dirty state and errors with the inline cells, because it is the same
388
- `FormApi`.
412
+ `getForm` exposes an open row's form: render it in a drawer and it shares
413
+ values, dirty state and errors with the inline cells, because it is the same
414
+ `FormApi`. It is `undefined` for a committed row, which holds values and no
415
+ form; `begin` reopens the row with a form seeded from them.
389
416
 
390
417
  `edit.setCellValue(rowId, columnId, value)` writes one cell and commits its row with no editor open, which is what a toolbar action or a bulk fill wants.
391
418
  The row need not be mounted, so a selected row inside a collapsed group takes the write like any other.
@@ -397,7 +424,7 @@ for (const row of grid.table.getSelectedRowModel().rows) {
397
424
  }
398
425
  ```
399
426
 
400
- Under `draft: true` each row parks in the draft store like any hand-made edit, with the same change markers and the same per-row revert, and the basket leaves through `saveDrafts`.
427
+ Under `draft: true` each row is committed into the draft store like any hand-made edit, with the same change markers and the same per-row revert, and the basket leaves through `saveDrafts`.
401
428
  `value` is the stored value: no editor runs, so `meta.edit.mapValue` does not run either, while `meta.edit.validate` does.
402
429
  Both resolve `false` when the cell takes no edit - no such row or column, `editing.columns` excludes it, `meta.edit.enabled` is off, or the row is not editable - and when validation refuses the value, which leaves the row open carrying its errors.
403
430
 
@@ -428,7 +455,6 @@ are in [references/common-mistakes.md](references/common-mistakes.md).
428
455
  | CRITICAL | Expecting the grid to write into `data` - without `editing.onCommit` the cell reverts |
429
456
  | CRITICAL | `getRowId` built from the row index - drafts follow the index, not the record |
430
457
  | HIGH | A cell editor defined inside the component - a new type per render unmounts the editor |
431
- | HIGH | A custom editor that binds no error text - a refused commit shows no message; bind `field.state.meta.errors` |
432
458
  | HIGH | A cross-field rule under `mode: "cell"` - `rowValidators` needs `"row"` |
433
459
  | HIGH | An `accessorFn` column with no `meta.edit.field` - it maps to nothing and stays read-only |
434
460
  | HIGH | Swallowing the error in `editing.onCommit` - a resolved catch drops the draft |
@@ -437,6 +463,7 @@ are in [references/common-mistakes.md](references/common-mistakes.md).
437
463
  | MEDIUM | A computed column frozen while a row is edited - `accessorFn` reads `data`; read the draft from `edit.store`'s `rows[rowId].values` |
438
464
  | MEDIUM | Reading a commit's result as the saved value - it is a `boolean` about the form |
439
465
  | MEDIUM | Expecting `editing.onRowDelete` to fire under draft - the mark waits for `saveDrafts` |
466
+ | MEDIUM | A custom editor that binds no invalid state - the host shows the message in a tooltip, but the control keeps its normal border; bind a boolean to `error` |
440
467
 
441
468
  ## References
442
469
 
@@ -22,7 +22,7 @@ useTMDataGrid({
22
22
  Correct: wire `editing.onCommit` to apply the change where the data lives, as
23
23
  in the skill's Setup section.
24
24
 
25
- Source: `src/docs/editing.md`, `src/tmdatagrid/core/editEngine.ts`.
25
+ Source: `packages/tmdatagrid/docs/editing.md`, `packages/tmdatagrid/src/core/editEngine.ts`.
26
26
 
27
27
  ## CRITICAL `getRowId` built from the row index
28
28
 
@@ -42,7 +42,7 @@ Correct:
42
42
  getRowId: (row) => String(row.id),
43
43
  ```
44
44
 
45
- Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
45
+ Source: `packages/tmdatagrid/src/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
46
46
 
47
47
  ## HIGH A cell editor defined inside the component
48
48
 
@@ -71,45 +71,7 @@ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
71
71
  meta: { edit: { editor: SalaryEditor } },
72
72
  ```
73
73
 
74
- Source: `src/docs/editors.md`, `src/tmdatagrid/core/editEngine.ts`.
75
-
76
- ## HIGH A custom editor that binds no error text
77
-
78
- The built-in editors bind the field's first error to the input's `error` prop.
79
- A custom editor that binds nothing still blocks the commit, but all the user
80
- sees is `data-invalid` on the cell - the row stays open with no message
81
- anywhere on screen, which reads as a broken grid rather than a rejected value.
82
-
83
- Wrong:
84
-
85
- ```tsx
86
- const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
87
- <Slider value={field.state.value} onChange={field.handleChange} onChangeEnd={() => void commit()} />
88
- );
89
- ```
90
-
91
- Correct:
92
-
93
- ```tsx
94
- const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => {
95
- const error = field.state.meta.errors
96
- .map((e) => (typeof e === "string" ? e : e?.message))
97
- .find(Boolean);
98
- return (
99
- <Slider
100
- value={field.state.value}
101
- onChange={field.handleChange}
102
- onChangeEnd={() => void commit()}
103
- error={error}
104
- />
105
- );
106
- };
107
- ```
108
-
109
- An entry of `field.state.meta.errors` is a string from a function validator,
110
- or an issue carrying a `message` from a schema.
111
-
112
- Source: `src/docs/editors.md` (Writing your own).
74
+ Source: `packages/tmdatagrid/docs/editors.md`, `packages/tmdatagrid/src/core/editEngine.ts`.
113
75
 
114
76
  ## HIGH A cross-field rule under `editing.mode: "cell"`
115
77
 
@@ -120,7 +82,7 @@ other column's old value, and the row cannot be saved.
120
82
  Correct: `editing.rowValidators` needs `mode: "row"`, which validates the
121
83
  whole row in one commit.
122
84
 
123
- Source: `src/docs/editors.md` (Validation).
85
+ Source: `packages/tmdatagrid/docs/editors.md` (Validation).
124
86
 
125
87
  ## HIGH An `accessorFn` column that never opens an editor
126
88
 
@@ -147,7 +109,7 @@ columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
147
109
  });
148
110
  ```
149
111
 
150
- Source: `src/docs/editing.md` (Which cells edit).
112
+ Source: `packages/tmdatagrid/docs/editing.md` (Which cells edit).
151
113
 
152
114
  ## HIGH Swallowing the error in `editing.onCommit`
153
115
 
@@ -170,7 +132,7 @@ onCommit: async ({ rowId, value }) => {
170
132
  Correct: no `catch` - let the rejection propagate, and the form stays open
171
133
  with the error on the row.
172
134
 
173
- Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
135
+ Source: `packages/tmdatagrid/src/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
174
136
 
175
137
  ## HIGH Submitting an outer form while the grid holds a draft
176
138
 
@@ -186,12 +148,14 @@ const hasOpenDraft = useSelector(grid.edit.store, (s) => s.openRowIds.length > 0
186
148
 
187
149
  <Button type="submit" disabled={!canSubmit || hasOpenDraft}>Save</Button>
188
150
  // or commit and flush instead of blocking:
189
- await grid.edit.commitAll();
190
- const flushed = await grid.edit.saveDrafts();
191
- if (flushed) await form.handleSubmit();
151
+ const committed = await grid.edit.commitAll();
152
+ const flushed = committed.ok ? await grid.edit.saveDrafts() : undefined;
153
+ if (flushed?.ok) await form.handleSubmit();
192
154
  ```
193
155
 
194
- Source: `src/docs/query-builder.md` (Which mode, Submitting).
156
+ Both results are objects, so test `.ok`: `if (await grid.edit.saveDrafts())` is always true.
157
+
158
+ Source: `packages/tmdatagrid/docs/query-builder.md` (Which mode, Submitting).
195
159
 
196
160
  ## HIGH A bulk write built from `begin`, `getForm` and `commit`
197
161
 
@@ -223,40 +187,51 @@ await grid.edit.setRowValues(rowId, { status: "Closed", closedOn: today() });
223
187
  Both write the stored value through the row's form, so `meta.edit.mapValue` does not run - no editor is involved - while `meta.edit.validate` does, at the commit.
224
188
  A refused value leaves the row open carrying its errors and the call resolves `false`, so read the result rather than assuming the fill landed.
225
189
 
226
- Source: `src/tmdatagrid/core/editEngine.ts` (`begin`, `writeFields`).
190
+ Source: `packages/tmdatagrid/src/core/editEngine.ts` (`begin`, `writeFields`).
227
191
 
228
- ## MEDIUM A computed column frozen while a row is edited
192
+ ## MEDIUM Reading an open row's values off `row.original` outside a cell
229
193
 
230
- A held draft is displayed by the column that owns the field. A column computed
231
- from other fields - `accessorFn` or `display` - reads `row.original`, which is
232
- `data`, so it keeps showing the saved record while the values it derives from
233
- are being typed.
194
+ Inside a `cell` renderer, `row.original` and `getValue()` are the row as
195
+ shown: the open form's values while the row is edited, the committed draft
196
+ after ✓, and `data` otherwise - a computed column follows the draft as it is
197
+ typed, and a button in a cell sends the draft the user sees. Outside it, the
198
+ row callbacks (`onRowClick`, `renderRowContextMenu`, `rowClassName`) and any
199
+ code holding a row id see `data`, or the committed draft, and never an open
200
+ form's values.
234
201
 
235
- Correct: read the drafted row from `edit.store` inside the cell renderer:
202
+ Correct: reach the row as shown by id:
236
203
 
237
204
  ```tsx
238
- function useDraftedRow(rowId: string, original: Product): Product {
239
- const { edit } = useTMDataGridContext();
240
- const values = useSelector(edit.store, (state) => state.rows[rowId]?.values);
241
- return (values as Product | undefined) ?? original;
242
- }
205
+ const shown = grid.edit.getRowValues(row.id) ?? row.original;
243
206
  ```
244
207
 
245
- Source: `src/docs/editing.md` (Draft lifetime).
208
+ Source: `packages/tmdatagrid/docs/editing.md` (How a draft renders).
246
209
 
247
210
  ## MEDIUM Reading a commit's result as the saved value
248
211
 
249
- `edit.commit(rowId)`, `edit.commitAll()` and `edit.saveDrafts()` resolve to a
250
- `boolean` saying whether everything landed, and resolve `false` when validation
251
- or a rejected save kept a row open. Ignoring the result reports a save that did
252
- not happen.
212
+ `edit.commit(rowId)` resolves a `boolean`: `false` when validation or a rejected commit kept the row open.
213
+ `edit.commitAll()` resolves `{ ok, committed, open }`, and `edit.saveDrafts()` resolves `{ ok, saved, kept, reopened }`.
214
+ Each id is in exactly one list, and `ok` is `false` when any row stayed open, was kept in the draft store, or was reopened with an error.
215
+ Ignoring the result reports a save that did not happen, and testing the object itself is always true.
216
+
217
+ Wrong:
253
218
 
254
219
  ```tsx
255
- const saved = await grid.edit.saveDrafts();
256
- notifications.show({ message: saved ? "Saved" : "Some rows need attention" });
220
+ if (await grid.edit.saveDrafts()) notifications.show({ message: "Saved" });
257
221
  ```
258
222
 
259
- Source: `src/tmdatagrid/core/editEngine.ts` (`TMDataGridEditApi`).
223
+ Correct:
224
+
225
+ ```tsx
226
+ const { ok, kept, reopened } = await grid.edit.saveDrafts();
227
+ notifications.show({
228
+ message: ok
229
+ ? "Saved"
230
+ : `${kept.length} rows not saved, ${reopened.length} rows need attention`,
231
+ });
232
+ ```
233
+
234
+ Source: `packages/tmdatagrid/src/core/editEngine.ts` (`TMDataGridEditApi`).
260
235
 
261
236
  ## MEDIUM Expecting `editing.onRowDelete` to fire under a draft store
262
237
 
@@ -266,4 +241,37 @@ Under `draft: true` it only toggles a deletion mark, so nothing is removed until
266
241
  no such callback, in the per-row `editing.onRowDelete` loop. A confirmation
267
242
  placed inside `editing.onRowDelete` therefore guards the save, not the trash.
268
243
 
269
- Source: `src/docs/editing.md` (Adding and deleting rows).
244
+ Source: `packages/tmdatagrid/docs/adding-rows.md`.
245
+
246
+ ## MEDIUM A custom editor that binds no invalid state
247
+
248
+ The message is the host's: it shows in a tooltip on the editor whatever the
249
+ editor is. The invalid styling is the editor's own, and one that binds nothing
250
+ keeps its normal border while the commit is refused, so the only marks on
251
+ screen are the tooltip and `data-invalid` on the cell.
252
+
253
+ Wrong:
254
+
255
+ ```tsx
256
+ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
257
+ <Slider value={field.state.value} onChange={field.handleChange} onChangeEnd={() => void commit()} />
258
+ );
259
+ ```
260
+
261
+ Correct:
262
+
263
+ ```tsx
264
+ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => {
265
+ const hasError = field.state.meta.errors.length > 0;
266
+ return (
267
+ <Slider
268
+ value={field.state.value}
269
+ onChange={field.handleChange}
270
+ onChangeEnd={() => void commit()}
271
+ error={hasError}
272
+ />
273
+ );
274
+ };
275
+ ```
276
+
277
+ Source: `packages/tmdatagrid/docs/editors.md` (Writing your own).