@jielga/tmdatagrid 1.1.0 → 2.0.0-beta.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 (40) hide show
  1. package/dist/index.d.ts +184 -75
  2. package/dist/index.js +1821 -1743
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/skills/appearance/SKILL.md +1 -1
  6. package/skills/cell-selection/SKILL.md +1 -1
  7. package/skills/columns/SKILL.md +42 -17
  8. package/skills/data/SKILL.md +1 -1
  9. package/skills/editing/SKILL.md +56 -56
  10. package/skills/editing/references/editing-api.md +8 -5
  11. package/skills/editing/references/editors-and-validation.md +55 -7
  12. package/skills/filtering/SKILL.md +13 -14
  13. package/skills/getting-started/SKILL.md +1 -1
  14. package/skills/grouping/SKILL.md +1 -1
  15. package/skills/options/SKILL.md +1 -1
  16. package/skills/rows/SKILL.md +10 -5
  17. package/skills/server-side/SKILL.md +1 -1
  18. package/skills/testing/SKILL.md +2 -2
  19. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +49 -16
  20. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +42 -11
  21. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +5 -3
  22. package/src/tmdatagrid/components/TMDataGridTable.tsx +107 -37
  23. package/src/tmdatagrid/components/editors/TMDataGridBooleanEditor.tsx +0 -2
  24. package/src/tmdatagrid/components/editors/TMDataGridDateEditor.tsx +1 -3
  25. package/src/tmdatagrid/components/editors/TMDataGridMultiSelectEditor.tsx +0 -2
  26. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +13 -11
  27. package/src/tmdatagrid/components/editors/TMDataGridSelectEditor.tsx +0 -2
  28. package/src/tmdatagrid/components/editors/TMDataGridStringEditor.tsx +13 -9
  29. package/src/tmdatagrid/components/editors/editorShared.ts +47 -0
  30. package/src/tmdatagrid/components/filters/DgAutocompleteFilter.tsx +1 -1
  31. package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +2 -2
  32. package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +2 -2
  33. package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +1 -1
  34. package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +1 -1
  35. package/src/tmdatagrid/core/columnUtils.ts +44 -3
  36. package/src/tmdatagrid/core/editEngine.ts +112 -37
  37. package/src/tmdatagrid/core/editorFocus.ts +29 -0
  38. package/src/tmdatagrid/core/filterControls.ts +28 -1
  39. package/src/tmdatagrid/index.ts +6 -0
  40. package/src/tmdatagrid/useTMDataGrid.tsx +37 -48
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jielga/tmdatagrid",
3
- "version": "1.1.0",
3
+ "version": "2.0.0-beta.0",
4
4
  "description": "A React data grid built on TanStack Table v9 and Mantine - always virtualized, with resizable, reorderable, sortable, filterable, hideable and pinnable columns.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,7 +16,7 @@ description: >
16
16
  metadata:
17
17
  type: core
18
18
  library: '@jielga/tmdatagrid'
19
- library_version: '1.1.0'
19
+ library_version: '2.0.0-beta.0'
20
20
  sources:
21
21
  - 'Jielga/TMDataGrid:src/docs/styling.md'
22
22
  - 'Jielga/TMDataGrid:src/docs/toolbar.md'
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '1.1.0'
17
+ library_version: '2.0.0-beta.0'
18
18
  sources:
19
19
  - 'Jielga/TMDataGrid:src/docs/cell-selection.md'
20
20
  - 'Jielga/TMDataGrid:src/tmdatagrid/core/cellNavigation.ts'
@@ -3,19 +3,20 @@ name: columns
3
3
  description: >
4
4
  Define and arrange TMDataGrid columns. Covers createTMDataGridColumnHelper,
5
5
  every column meta field (label, type, options, flex, align, autoSize,
6
- enableOrdering, defaultFilterOperator, filterControl, editable, editField,
7
- editor, validate), the six column types, fluid minmax sizing versus fixed
8
- width with minSize / maxSize / size, autosizing and autosizeColumn, hiding
9
- through enableHiding and the columns panel, pinning and why a pinned column
10
- becomes fixed-width, ordering with enableColumnOrdering, meta.enableOrdering,
11
- moveColumn, moveColumnByStep, getStepTargetColumn and the pinned regions,
12
- resetSettings, sorting with multi-sort through isMultiSortEvent and a custom
13
- sortFn, and the generated lanes. Load when adding or changing columns,
14
- controlling widths, hiding, pinning, reordering or sorting them.
6
+ enableOrdering, and the meta.filter and meta.edit namespaces holding
7
+ defaultOperator, control, enabled, field, editor, validate and mapValue), the
8
+ six column types, fluid minmax sizing versus fixed width with minSize /
9
+ maxSize / size, autosizing and autosizeColumn, hiding through enableHiding and
10
+ the columns panel, pinning and why a pinned column becomes fixed-width,
11
+ ordering with enableColumnOrdering, meta.enableOrdering, moveColumn,
12
+ moveColumnByStep, getStepTargetColumn and the pinned regions, resetSettings,
13
+ sorting with multi-sort through isMultiSortEvent and a custom sortFn, and the
14
+ generated lanes. Load when adding or changing columns, controlling widths,
15
+ hiding, pinning, reordering or sorting them.
15
16
  metadata:
16
17
  type: core
17
18
  library: '@jielga/tmdatagrid'
18
- library_version: '1.1.0'
19
+ library_version: '2.0.0-beta.0'
19
20
  sources:
20
21
  - 'Jielga/TMDataGrid:src/docs/columns.md'
21
22
  - 'Jielga/TMDataGrid:src/docs/column-layout.md'
@@ -65,17 +66,41 @@ column model.
65
66
  | `align` | `"left" \| "right" \| "center"` | `"left"` | Applied to both header and cells. |
66
67
  | `autoSize` | `boolean` | `false` | Size to the widest mounted content once, after the first rows render. |
67
68
  | `enableOrdering` | `boolean` | `true` | `false` keeps the column where it is. |
68
- | `defaultFilterOperator` | `TMDataGridFilterOperator` | The type's default | The operator a fresh filter on this column starts with. |
69
- | `filterControl` | `TMDataGridFilterControlComponent` | By `meta.type` | Replaces the value control in this column's filter row. Module scope. |
70
- | `editable` | `boolean \| ((row) => boolean)` | editable where a field maps | Whether this column's cells edit. |
71
- | `editField` | `string` | The `accessorKey` | The data path an edit writes to. The only way an `accessorFn` column edits. |
69
+ | `filter` | `TMDataGridColumnFilterOptions` | – | How this column filters. See below. |
70
+ | `edit` | `TMDataGridColumnEditOptions` | – | How this column is edited. See below. |
71
+
72
+ What the column **is** stays flat; what a stage **does** with it sits in that
73
+ stage's namespace. `type` and `options` are the shared ground both stages read,
74
+ which is why they are not inside either.
75
+
76
+ `meta.filter`:
77
+
78
+ | Field | Type | Default | What it does |
79
+ | --- | --- | --- | --- |
80
+ | `defaultOperator` | `TMDataGridFilterOperator` | The type's default | The operator a fresh filter on this column starts with. |
81
+ | `control` | `TMDataGridFilterControlComponent` | By `meta.type` | Replaces the value control in this column's filter row. Module scope. |
82
+
83
+ `meta.edit`:
84
+
85
+ | Field | Type | Default | What it does |
86
+ | --- | --- | --- | --- |
87
+ | `enabled` | `boolean \| ((row) => boolean)` | editable where a field maps | Whether this column's cells edit. |
88
+ | `field` | `string` | The `accessorKey` | The data path an edit writes to. The only way an `accessorFn` column edits. |
72
89
  | `editor` | `TMDataGridEditorComponent` | By `meta.type` | Replaces the cell editor. Module scope. |
73
90
  | `validate` | `TMDataGridFieldValidate` | – | Field-level validation. A bare schema means `onChange`. |
91
+ | `mapValue` | `TMDataGridEditValueMap` | – | Maps each value an editor writes, on every keystroke. |
92
+
93
+ ```tsx
94
+ meta: {
95
+ type: "number",
96
+ filter: { defaultOperator: "between", control: DgRangeSliderFilter },
97
+ edit: { enabled: (row) => !row.original.locked },
98
+ }
99
+ ```
74
100
 
75
101
  `enableOrdering` lives in `meta` because column ordering is the one feature
76
- TanStack defines no column option for. The four editing fields only act once
77
- `editMode` is set: see the `editing` skill. `filterControl` and
78
- `defaultFilterOperator` belong to the `filtering` skill.
102
+ TanStack defines no column option for. `meta.edit` only acts once `editMode` is
103
+ set: see the `editing` skill. `meta.filter` belongs to the `filtering` skill.
79
104
 
80
105
  ### Column types
81
106
 
@@ -15,7 +15,7 @@ description: >
15
15
  metadata:
16
16
  type: core
17
17
  library: '@jielga/tmdatagrid'
18
- library_version: '1.1.0'
18
+ library_version: '2.0.0-beta.0'
19
19
  sources:
20
20
  - 'Jielga/TMDataGrid:src/docs/pagination.md'
21
21
  - 'Jielga/TMDataGrid:src/docs/scrolling.md'
@@ -4,19 +4,19 @@ description: >
4
4
  Edit cells and rows in TMDataGrid. Covers editMode with its four policies
5
5
  (cell, cellConfirm, row, batch), the required getRowId, onEditCommit and
6
6
  onEditCommitBatch, why the grid never mutates data, per-column gating with
7
- meta.editable and meta.editField, the six built-in editors picked by
8
- meta.type, custom editors through meta.editor, field validation with
9
- meta.validate and cross-field rules with rowValidators (Standard Schema and
10
- Zod), adding and deleting rows with edit.addRow, newRowDefaults, onRowAdd,
11
- onRowDelete, the generated edit lane, TMDataGrid.EditActions with its
12
- renderActions slot, and the public edit engine (begin, commit, cancel,
13
- submitAll, getForm, store). Load when making a grid editable, choosing an edit
14
- mode, wiring a save, writing a cell editor, validating an edit, or when cells
15
- will not open for editing.
7
+ meta.edit.enabled and meta.edit.field, the six built-in editors picked by
8
+ meta.type, custom editors through meta.edit.editor, per-keystroke value
9
+ mapping with meta.edit.mapValue, field validation with meta.edit.validate and
10
+ cross-field rules with rowValidators (Standard Schema and Zod), adding and
11
+ deleting rows with edit.addRow, newRowDefaults, onRowAdd and onRowDelete, the
12
+ generated edit lane, TMDataGrid.EditActions with its renderActions slot, and
13
+ the public edit engine (begin, commit, cancel, submitAll, getForm, store).
14
+ Load when making a grid editable, choosing an edit mode, wiring a save,
15
+ writing a cell editor, validating an edit, or when cells will not open.
16
16
  metadata:
17
17
  type: core
18
18
  library: '@jielga/tmdatagrid'
19
- library_version: '1.1.0'
19
+ library_version: '2.0.0-beta.0'
20
20
  sources:
21
21
  - 'Jielga/TMDataGrid:src/docs/editing.md'
22
22
  - 'Jielga/TMDataGrid:src/docs/query-builder.md'
@@ -90,7 +90,15 @@ An editor opens on double-click, or with the cell cursor on the cell: Enter, F2,
90
90
  or just typing, where the character replaces the value. Delete or Backspace
91
91
  clears the value and commits without opening an editor. Editing brings cell
92
92
  selection with it: `cellSelection` defaults to `"single"` while `editMode` is
93
- set.
93
+ set. Every open gesture puts the caret in the cell it named, and `edit.addRow()`
94
+ in the new row's first editable cell. The grid places it rather than the editor,
95
+ so a `meta.edit.editor` gets it without doing anything to earn it.
96
+
97
+ Under `"row"` the pencil - or a double-click on any cell, which puts the caret
98
+ in the cell clicked - opens every editable cell of the row, and ✓ saves them as
99
+ one commit. Rows accumulate: opening a second row leaves the first open, and
100
+ each row's ✓ and ✕ act on that row alone, so no row is closed or saved to make
101
+ space for the next.
94
102
 
95
103
  Under `"batch"` nothing leaves the grid until `submitAll`. Enter and Tab park
96
104
  the draft, dirty-marked, Escape drops that one draft, and drafts accumulate
@@ -114,14 +122,14 @@ the error on the row, which is how a failed save stays visible.
114
122
  ## Which cells edit
115
123
 
116
124
  A column is editable when it maps to a data path: its `accessorKey`, or
117
- `meta.editField` for a column built on `accessorFn`. Dot paths reach into nested
125
+ `meta.edit.field` for a column built on `accessorFn`. Dot paths reach into nested
118
126
  records, so `accessorKey: "address.city"` edits `values.address.city`.
119
127
 
120
128
  | Gate | Effect |
121
129
  | --- | --- |
122
- | `meta.editable: false` | The column never edits |
123
- | `meta.editable: (row) => boolean` | Per row, per column |
124
- | `meta.editField: "lastName"` | The path an `accessorFn` column writes to |
130
+ | `meta.edit.enabled: false` | The column never edits |
131
+ | `meta.edit.enabled: (row) => boolean` | Per row, per column |
132
+ | `meta.edit.field: "lastName"` | The path an `accessorFn` column writes to |
125
133
  | `isRowEditable: (row) => boolean` | The whole row, in every mode |
126
134
 
127
135
  Group rows and the generated lanes (checkbox, row number, details, edit) never
@@ -132,29 +140,23 @@ edit.
132
140
  columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
133
141
  id: "fullName",
134
142
  header: "Full name",
135
- meta: { label: "Full name", editField: "lastName" },
143
+ meta: { label: "Full name", edit: { field: "lastName" } },
136
144
  });
137
145
  columnHelper.accessor("salary", {
138
146
  header: "Salary",
139
- meta: { editable: (row) => row.original.status !== "Terminated" },
147
+ meta: { edit: { enabled: (row) => row.original.status !== "Terminated" } },
140
148
  });
141
149
  ```
142
150
 
143
151
  ## Editors and validation
144
152
 
145
- `meta.type` picks the editor, and `meta.options` feeds the select editors from
146
- the same declaration the filter panel reads.
153
+ `meta.type` picks the editor - `"string"` (the default), `"number"`,
154
+ `"boolean"`, `"date"`, `"select"` and `"multiSelect"` - and `meta.options` feeds
155
+ the two select editors from the same declaration the filter panel reads. Each
156
+ one, with the export that wraps it, is in
157
+ [references/editors-and-validation.md](references/editors-and-validation.md#the-built-in-editors).
147
158
 
148
- | `meta.type` | Editor |
149
- | --- | --- |
150
- | `"string"` (default) | Text input |
151
- | `"number"` | Number input |
152
- | `"boolean"` | Checkbox |
153
- | `"date"` | Native `<input type="date">`, ISO `YYYY-MM-DD` |
154
- | `"select"` | Searchable select from `meta.options`; commits on pick under `"cell"` |
155
- | `"multiSelect"` | Multi-select, same source |
156
-
157
- `meta.editor` replaces one, `meta.validate` guards the field, and
159
+ `meta.edit.editor` replaces one, `meta.edit.validate` guards the field, and
158
160
  `rowValidators` carries cross-field rules. The validators are TanStack Form's
159
161
  own, Standard Schema included, so a Zod schema passes straight through:
160
162
 
@@ -162,7 +164,7 @@ own, Standard Schema included, so a Zod schema passes straight through:
162
164
  import { z } from "zod";
163
165
 
164
166
  // Field level, on the column. A bare schema means { onChange: schema }.
165
- meta: { validate: z.string().min(2, "At least two characters") }
167
+ meta: { edit: { validate: z.string().min(2, "At least two characters") } }
166
168
 
167
169
  // Form level, on the hook. Cross-field rules, under "row" or "batch".
168
170
  rowValidators: {
@@ -174,11 +176,19 @@ rowValidators: {
174
176
  }
175
177
  ```
176
178
 
177
- Pathed issues land on the matching cells, pathless ones on the row. Cell corners
178
- mark state meanwhile: blue for a dirty draft, red for a validation error.
179
+ Pathed issues land on the matching cells, pathless ones on the row, and cell
180
+ corners mark both: blue for a dirty draft, red for a validation error.
181
+
182
+ `meta.edit.mapValue` rewrites a value rather than rejecting it: uppercase a
183
+ code, strip spaces from an IBAN, clamp a number. It runs on every write an
184
+ editor makes, so a text input maps per keystroke, and what it returns is what
185
+ the validators judge and what commits.
179
186
 
180
- The editor contract, wrapping a built-in, and async server-side field errors:
181
- [references/editors-and-validation.md](references/editors-and-validation.md).
187
+ ```tsx
188
+ meta: { edit: { mapValue: ({ value }) => String(value).toUpperCase() } }
189
+ ```
190
+
191
+ Detail for all three: [references/editors-and-validation.md](references/editors-and-validation.md).
182
192
 
183
193
  ## Adding and deleting rows
184
194
 
@@ -242,17 +252,11 @@ while editing is off, and works under any mode, not only batch.
242
252
 
243
253
  ## The engine: `edit`
244
254
 
245
- `grid.edit` is public, and everything the chrome does goes through it.
246
-
247
- | Member | Does |
248
- | --- | --- |
249
- | `edit.begin({ rowId, columnId })` | Opens an editor. `columnId: null` opens the whole row |
250
- | `edit.commit(rowId)` | Commits one row. `Promise<boolean>`, `false` when blocked |
251
- | `edit.cancel(rowId)` / `edit.cancelAll()` | Drops one draft, or every draft |
252
- | `edit.submitAll()` | Commits every open row. `Promise<boolean>` |
253
- | `edit.addRow()` / `edit.deleteRow(rowId)` | Rows in and out |
254
- | `edit.getForm(rowId)` | The row's live TanStack Form `FormApi` |
255
- | `edit.store` | The projection store, for `useSelector` |
255
+ `grid.edit` is public, and everything the chrome does goes through it:
256
+ `begin` and `commit`, `cancel` / `cancelAll`, `submitAll`, `addRow` /
257
+ `deleteRow`, `getForm`, and `store` for `useSelector`. Every member with its
258
+ signature, the gates, `clearCell`, `deactivate`, and the `edit.store` shape are
259
+ in [references/editing-api.md](references/editing-api.md#the-edit-engine).
256
260
 
257
261
  ```tsx
258
262
  import { useSelector } from "@tanstack/react-store";
@@ -263,11 +267,6 @@ const pendingCount = useSelector(
263
267
  );
264
268
  ```
265
269
 
266
- `edit.store` holds `active`, `openRowIds`, `rows` (per row `dirtyFields`,
267
- `errorFields`, `isSubmitting`), `newRows` and `deletedRowIds`. The full member
268
- list, `clearCell`, `deactivate` and the gates included, is in
269
- [references/editing-api.md](references/editing-api.md).
270
-
271
270
  `getForm` is the *one row, one form* promise made public: render that same form
272
271
  in a drawer and it shares values, dirty state and errors with the inline cells,
273
272
  because it is the same `FormApi`.
@@ -281,7 +280,7 @@ Use `editMode: "row"` so a row reaches the form on approval, map by row id
281
280
  (never index), and mint negative ids for new rows.
282
281
 
283
282
  The validation split is structural, not stylistic: **a rule decidable from one
284
- row belongs to the grid (`meta.validate`, `rowValidators`); a rule needing the
283
+ row belongs to the grid (`meta.edit.validate`, `rowValidators`); a rule needing the
285
284
  other rows or the collection ("has rows", "no duplicates") belongs to the
286
285
  form's field validator.** A row's form cannot see the array, and `edit.store`
287
286
  publishes field names, never values, so the form cannot see a draft.
@@ -341,14 +340,14 @@ Source: `src/tmdatagrid/useTMDataGrid.tsx` (`TMDataGridEditingCallbacks`).
341
340
 
342
341
  ### HIGH A cell editor defined inside the component
343
342
 
344
- `meta.editor` is rendered as JSX, so its identity is its component type. An
343
+ `meta.edit.editor` is rendered as JSX, so its identity is its component type. An
345
344
  inline arrow is a new type on every render, which unmounts the open editor and
346
345
  loses what was being typed.
347
346
 
348
347
  Wrong:
349
348
 
350
349
  ```tsx
351
- meta: { editor: ({ field }) => <Slider value={field.state.value} /> },
350
+ meta: { edit: { editor: ({ field }) => <Slider value={field.state.value} /> } },
352
351
  ```
353
352
 
354
353
  Correct:
@@ -363,7 +362,7 @@ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
363
362
  />
364
363
  );
365
364
 
366
- meta: { editor: SalaryEditor },
365
+ meta: { edit: { editor: SalaryEditor } },
367
366
  ```
368
367
 
369
368
  Source: `src/docs/editors.md`, `src/tmdatagrid/core/editEngine.ts`.
@@ -411,7 +410,7 @@ Correct:
411
410
  columnHelper.accessor((row) => `${row.firstName} ${row.lastName}`, {
412
411
  id: "fullName",
413
412
  header: "Full name",
414
- meta: { label: "Full name", editField: "lastName" },
413
+ meta: { label: "Full name", edit: { field: "lastName" } },
415
414
  });
416
415
  ```
417
416
 
@@ -491,7 +490,8 @@ Source: `src/docs/editing.md` (Adding and deleting rows).
491
490
  ## References
492
491
 
493
492
  - [Editors and validation](references/editors-and-validation.md) - the editor
494
- contract, wrapping a built-in, field and row validators, server-side errors.
493
+ contract, wrapping a built-in, what `mapValue` leaves alone, field and row
494
+ validators, server-side errors.
495
495
  - [Editing API](references/editing-api.md) - every option, callback, column meta
496
496
  field, export, CSS variable and data attribute editing owns.
497
497
 
@@ -35,10 +35,10 @@ path, which may be dotted.
35
35
  | --- | --- | --- | --- |
36
36
  | `meta.type` | `TMDataGridColumnType` | `"string"` | Picks the built-in editor, and the filter operators. |
37
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`. |
38
+ | `meta.edit.enabled` | `boolean \| ((row) => boolean)` | editable where a field maps | Whether this column's cells edit. |
39
+ | `meta.edit.field` | `string` | The `accessorKey` | The data path an edit writes to. The only way an `accessorFn` column edits. |
40
+ | `meta.edit.editor` | `TMDataGridEditorComponent` | By `meta.type` | Replaces the cell editor. Define at module scope. |
41
+ | `meta.edit.validate` | `TMDataGridFieldValidate` | – | Field-level validation. A bare schema means `onChange`. |
42
42
 
43
43
  ## The `edit` engine
44
44
 
@@ -46,7 +46,7 @@ path, which may be dotted.
46
46
 
47
47
  | Member | Signature | Notes |
48
48
  | --- | --- | --- |
49
- | `begin` | `({ rowId, columnId }) => void` | `columnId: null` opens the whole row (row mode). |
49
+ | `begin` | `({ rowId, columnId }) => void` | Row mode opens the whole row either way; `columnId` says which cell takes the caret, `null` (the pencil) its first editable one. |
50
50
  | `commit` | `(rowId) => Promise<boolean>` | `false` keeps the form open with its errors. |
51
51
  | `cancel` | `(rowId) => void` | Drops one draft. |
52
52
  | `cancelAll` | `() => void` | Drops every draft. |
@@ -66,7 +66,10 @@ path, which may be dotted.
66
66
 
67
67
  ```ts
68
68
  type TMDataGridEditState = {
69
+ // The cell the last open gesture named - where the caret goes.
69
70
  active: { rowId: string; columnId: string | null } | null;
71
+ // Rows with a live form. One under `"cell"`; as many as the user opened
72
+ // under `"row"`, `"cellConfirm"` and `"batch"` - which rows are editing.
70
73
  openRowIds: ReadonlyArray<string>;
71
74
  rows: Record<
72
75
  string,
@@ -29,7 +29,7 @@ the data), or a function of the table, column and row.
29
29
 
30
30
  ## The editor contract
31
31
 
32
- `meta.editor` fills the same slot the built-ins fill. It is a **component**,
32
+ `meta.edit.editor` fills the same slot the built-ins fill. It is a **component**,
33
33
  rendered as JSX, so hooks are legal inside it.
34
34
 
35
35
  ```ts
@@ -43,11 +43,16 @@ type TMDataGridEditorArgs = {
43
43
  commit: () => Promise<boolean>; // what Enter would do
44
44
  cancel: () => void; // what Escape would do
45
45
  size: TMDataGridSize;
46
- autoFocus: boolean; // take focus on mount
47
46
  seedText?: string; // set when typing opened the editor
48
47
  };
49
48
  ```
50
49
 
50
+ The grid places the caret itself, once per open gesture: it focuses
51
+ `data-dg-part="editor-input"` when the editor publishes it, and the first
52
+ focusable element inside the editor otherwise. So a custom editor gets the caret
53
+ without asking for focus - set `editor-input` only to name which of several
54
+ inputs should take it.
55
+
51
56
  Bind any control to `field` exactly as inside any TanStack Form:
52
57
  `field.state.value`, `field.state.meta.errors`, `field.handleChange`,
53
58
  `field.handleBlur`.
@@ -73,7 +78,7 @@ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit, cancel }) => (
73
78
 
74
79
  columnHelper.accessor("salary", {
75
80
  header: "Salary",
76
- meta: { type: "number", editor: SalaryEditor },
81
+ meta: { type: "number", edit: { editor: SalaryEditor } },
77
82
  });
78
83
  ```
79
84
 
@@ -114,9 +119,52 @@ Pass `args` through whole. `field` is a live TanStack Form `FieldApi`, not a
114
119
  plain object, so spreading or rebuilding it loses the binding its methods rely
115
120
  on.
116
121
 
122
+ ## Mapping the value
123
+
124
+ `meta.edit.mapValue` maps every value an editor writes, before it reaches the
125
+ draft. It runs per write, which for a text input is per keystroke and for a
126
+ select is per pick.
127
+
128
+ ```tsx
129
+ meta: {
130
+ edit: {
131
+ // Uppercase as it is typed.
132
+ mapValue: ({ value }) =>
133
+ typeof value === "string" ? value.toUpperCase() : value,
134
+ },
135
+ }
136
+
137
+ meta: {
138
+ edit: {
139
+ // Depends on the record being edited; `previous` is the value the field
140
+ // held before this write.
141
+ mapValue: ({ value, previous, row }) =>
142
+ row.original.country === "SE" ? normalise(value) : previous,
143
+ },
144
+ }
145
+ ```
146
+
147
+ The grid applies it in the editor host, around the field every editor writes
148
+ through, so it covers the six built-ins, a custom `meta.edit.editor`, and the
149
+ type-to-edit seed character.
150
+
151
+ Left unmapped on purpose:
152
+
153
+ - The value an editor opens with. Mapping it would rewrite stored data nobody
154
+ edited, mark a pristine row dirty and swallow the select-all.
155
+ - `edit.clearCell()`, the Delete key: it writes the type's empty value through
156
+ the form, so there is no input to map.
157
+ - An editor calling `field.setValue`. `handleChange` is the mapped path.
158
+
159
+ The built-in string and number editors restore the caret after a mapped write,
160
+ shifted by the length the map changed, because React reassigning `input.value`
161
+ otherwise drops the caret at the end of the field. A custom editor rendering its
162
+ own input has to do the same: record `selectionStart` on change, restore it in a
163
+ layout effect.
164
+
117
165
  ## Field validation
118
166
 
119
- `meta.validate` takes TanStack Form's field-validator vocabulary. A bare schema
167
+ `meta.edit.validate` takes TanStack Form's field-validator vocabulary. A bare schema
120
168
  (Zod, or any Standard Schema, or a plain function) means `{ onChange: it }`; the
121
169
  object form takes every trigger Form defines.
122
170
 
@@ -124,13 +172,13 @@ object form takes every trigger Form defines.
124
172
  import { z } from "zod";
125
173
 
126
174
  // Bare schema: validates on change.
127
- meta: { validate: z.string().min(2, "At least two characters") }
175
+ meta: { edit: { validate: z.string().min(2, "At least two characters") } }
128
176
 
129
177
  // Object form: pick the trigger.
130
- meta: { validate: { onBlur: z.string().email("Not an email address") } }
178
+ meta: { edit: { validate: { onBlur: z.string().email("Not an email address") } } }
131
179
 
132
180
  // A plain function works too.
133
- meta: { validate: ({ value }) => (value > 0 ? undefined : "Must be positive") }
181
+ meta: { edit: { validate: ({ value }) => (value > 0 ? undefined : "Must be positive") } }
134
182
  ```
135
183
 
136
184
  `normalizeFieldValidate(validate)` is exported for consumers building their own
@@ -3,11 +3,11 @@ name: filtering
3
3
  description: >
4
4
  Narrow the rows a TMDataGrid shows. Covers the shared tmDataGrid filter
5
5
  function and its {operator, value} model, the eighteen operators and which
6
- meta.type offers each, meta.defaultFilterOperator, isFilterActive and the
6
+ meta.type offers each, meta.filter.defaultOperator, isFilterActive and the
7
7
  half-typed filter, the filter panel, TMDataGrid.FilterButton,
8
8
  TMDataGrid.FilterPills with its api prop, openColumnFilter, replacing a value
9
9
  control with DgRangeSliderFilter / DgDateRangeFilter / DgAutocompleteFilter /
10
- DgTriStateFilter or a meta.filterControl component, per-column filterFn, and
10
+ DgTriStateFilter or a meta.filter.control component, per-column filterFn, and
11
11
  the quick search: TMDataGrid.Search, quickSearchMode fuzzy or contains,
12
12
  fuzzyGlobalFilterFn, enableMatchHighlighting and enableGlobalFilter. Load when
13
13
  adding filters, choosing operators, building a filter control, showing active
@@ -15,7 +15,7 @@ description: >
15
15
  metadata:
16
16
  type: core
17
17
  library: '@jielga/tmdatagrid'
18
- library_version: '1.1.0'
18
+ library_version: '2.0.0-beta.0'
19
19
  sources:
20
20
  - 'Jielga/TMDataGrid:src/docs/filtering.md'
21
21
  - 'Jielga/TMDataGrid:src/docs/quick-search.md'
@@ -70,13 +70,13 @@ and `equals` on a `date` column plays the role of "is". On a `multiSelect`
70
70
  column, whose cells hold arrays, `isAnyOf` is an intersection test and `isNoneOf`
71
71
  its complement. `between` is inclusive at both ends.
72
72
 
73
- `meta.defaultFilterOperator` sets which one a fresh filter opens on, and must be
73
+ `meta.filter.defaultOperator` sets which one a fresh filter opens on, and must be
74
74
  one of the type's own:
75
75
 
76
76
  ```tsx
77
77
  columnHelper.accessor("salary", {
78
78
  header: "Salary",
79
- meta: { type: "number", defaultFilterOperator: "between" },
79
+ meta: { type: "number", filter: { defaultOperator: "between" } },
80
80
  });
81
81
  ```
82
82
 
@@ -118,17 +118,16 @@ named exports:
118
118
  ```tsx
119
119
  meta: {
120
120
  type: "number",
121
- filterControl: DgRangeSliderFilter,
122
- defaultFilterOperator: "between",
121
+ filter: { control: DgRangeSliderFilter, defaultOperator: "between" },
123
122
  }
124
123
  ```
125
124
 
126
- Pair the range-shaped ones with `defaultFilterOperator: "between"` so the filter
125
+ Pair the range-shaped ones with `defaultOperator: "between"` so the filter
127
126
  opens on them. For operators outside their shape every built-in falls back to
128
127
  `TMDataGridFilterValueInput`, which is exported so a custom control can take the
129
128
  same escape.
130
129
 
131
- `meta.filterControl` is a **component**, rendered as JSX, so hooks are legal
130
+ `meta.filter.control` is a **component**, rendered as JSX, so hooks are legal
132
131
  inside. It is a **value-only contract**: it reads `operator` to shape itself and
133
132
  writes the bare value through `onChange`, and the grid composes the stored
134
133
  `{ operator, value }` around it. The column and operator dropdowns stay the
@@ -157,7 +156,7 @@ const StatusFilter: TMDataGridFilterControlComponent = ({
157
156
  </Chip.Group>
158
157
  );
159
158
 
160
- meta: { type: "select", filterControl: StatusFilter }
159
+ meta: { type: "select", filter: { control: StatusFilter } }
161
160
  ```
162
161
 
163
162
  `args.options` arrives pre-resolved for a column that declares `meta.options` or
@@ -259,7 +258,7 @@ Source: `src/docs/filtering.md` (Operators).
259
258
 
260
259
  ### HIGH A filter control defined inside the component
261
260
 
262
- `meta.filterControl` is rendered as JSX, so its identity is its component type.
261
+ `meta.filter.control` is rendered as JSX, so its identity is its component type.
263
262
  An inline arrow is a new type every render, which remounts the control and loses
264
263
  what was being typed into it.
265
264
 
@@ -269,7 +268,7 @@ Correct:
269
268
  // Module scope, referenced by name.
270
269
  const StatusFilter: TMDataGridFilterControlComponent = (args) => { /* … */ };
271
270
 
272
- meta: { filterControl: StatusFilter }
271
+ meta: { filter: { control: StatusFilter } }
273
272
  ```
274
273
 
275
274
  Source: `src/docs/filtering.md` (Writing your own).
@@ -318,8 +317,8 @@ Source: `src/docs/quick-search.md` (Fuzzy by default).
318
317
  | `enableColumnFilters` | Table option | `boolean` | `true` | `false` removes the panel, the button and the menu item. |
319
318
  | `enableColumnFilter` | Column option | `boolean` | `true` | `false` takes one column out of filtering. |
320
319
  | `meta.type` | Column meta | `TMDataGridColumnType` | `"string"` | Selects the operators and the value control. |
321
- | `meta.defaultFilterOperator` | Column meta | `TMDataGridFilterOperator` | The type's default | The operator a fresh filter opens on. |
322
- | `meta.filterControl` | Column meta | `TMDataGridFilterControlComponent` | By type and operator | Replaces the value control. Module scope. |
320
+ | `meta.filter.defaultOperator` | Column meta | `TMDataGridFilterOperator` | The type's default | The operator a fresh filter opens on. |
321
+ | `meta.filter.control` | Column meta | `TMDataGridFilterControlComponent` | By type and operator | Replaces the value control. Module scope. |
323
322
  | `filterFn` | Column option | name or fn | `"tmDataGrid"` | Custom matching for one column. |
324
323
  | `quickSearchMode` | Option | `"fuzzy" \| "contains"` | `"fuzzy"` | How the quick search matches. |
325
324
  | `enableMatchHighlighting` | Option | `boolean` | `false` | Mark matched text in default-rendered cells. |
@@ -11,7 +11,7 @@ description: >
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '1.1.0'
14
+ library_version: '2.0.0-beta.0'
15
15
  sources:
16
16
  - 'Jielga/TMDataGrid:src/docs/getting-started.md'
17
17
  - 'Jielga/TMDataGrid:src/docs/anatomy.md'
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '1.1.0'
17
+ library_version: '2.0.0-beta.0'
18
18
  sources:
19
19
  - 'Jielga/TMDataGrid:src/docs/grouping.md'
20
20
  - 'Jielga/TMDataGrid:src/docs/summary-row.md'
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '1.1.0'
17
+ library_version: '2.0.0-beta.0'
18
18
  sources:
19
19
  - 'Jielga/TMDataGrid:src/docs/use-tm-data-grid.md'
20
20
  - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
@@ -17,7 +17,7 @@ description: >
17
17
  metadata:
18
18
  type: core
19
19
  library: '@jielga/tmdatagrid'
20
- library_version: '1.1.0'
20
+ library_version: '2.0.0-beta.0'
21
21
  sources:
22
22
  - 'Jielga/TMDataGrid:src/docs/row-selection.md'
23
23
  - 'Jielga/TMDataGrid:src/docs/row-interaction.md'
@@ -249,10 +249,15 @@ const grid = useTMDataGrid({
249
249
  });
250
250
  ```
251
251
 
252
- There is no built-in pin gesture - pin from wherever suits, most naturally the
253
- row context menu, through TanStack's own `row.pin("top" | "bottom" | false)`,
254
- `row.getIsPinned()` and `row.getCanPin()`. Pinned rows leave the scrolling order
255
- and render in sticky blocks: top under the header, bottom above the summary row.
252
+ There is no built-in pin gesture and no pin icon - build one from TanStack's own
253
+ `row.pin("top" | "bottom" | false)`, `row.getIsPinned()` and `row.getCanPin()`,
254
+ either as a display column whose cell is a pin button or in the row context
255
+ menu. Read the pinned state through a subscription
256
+ (`useSelector(row.table.store, () => row.getIsPinned())`) rather than calling it
257
+ in a component body: the `row` identity survives a pin, so the React Compiler
258
+ would cache the call and the icon would never change. Whichever gesture you
259
+ build, put unpin in it too. Pinned rows leave the scrolling order and render in
260
+ sticky blocks: top under the header, bottom above the summary row.
256
261
 
257
262
  Pinned rows are still body rows - selection, editing, details, the context menu
258
263
  and per-row styling all behave normally. What they sit out are the statements