@jielga/tmdatagrid 2.0.0-beta.13 → 2.0.0-beta.15

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 (139) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +967 -610
  3. package/dist/index.js +2944 -2327
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +303 -0
  11. package/docs/editing.md +572 -0
  12. package/docs/editors.md +248 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +111 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +31 -15
  41. package/skills/cell-selection/SKILL.md +44 -41
  42. package/skills/columns/SKILL.md +33 -32
  43. package/skills/data/SKILL.md +85 -15
  44. package/skills/editing/SKILL.md +6 -6
  45. package/skills/editing/references/common-mistakes.md +12 -12
  46. package/skills/filtering/SKILL.md +40 -18
  47. package/skills/getting-started/SKILL.md +5 -5
  48. package/skills/grouping/SKILL.md +11 -11
  49. package/skills/options/SKILL.md +5 -5
  50. package/skills/rows/SKILL.md +15 -15
  51. package/skills/server-side/SKILL.md +131 -7
  52. package/skills/testing/SKILL.md +4 -4
  53. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +28 -20
  54. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +5 -4
  55. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  56. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +40 -15
  57. package/src/components/TMDataGridExportPicker.module.css +77 -0
  58. package/src/components/TMDataGridExportPicker.tsx +234 -0
  59. package/src/{tmdatagrid/components → components}/TMDataGridFilterPanel.tsx +22 -6
  60. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  61. package/src/{tmdatagrid/components → components}/TMDataGridFilterSurface.tsx +8 -3
  62. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +20 -7
  63. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +16 -5
  64. package/src/{tmdatagrid/components → components}/TMDataGridHeaderFilterRow.tsx +2 -4
  65. package/src/{tmdatagrid/components → components}/TMDataGridMenu.tsx +115 -16
  66. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +2 -1
  67. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +74 -55
  68. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.tsx +44 -8
  70. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +2 -1
  71. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  72. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  73. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  74. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  75. package/src/core/dom.ts +74 -0
  76. package/src/core/export.ts +840 -0
  77. package/src/{tmdatagrid/core → core}/filterControls.ts +10 -1
  78. package/src/{tmdatagrid/core → core}/labels.ts +40 -2
  79. package/src/{tmdatagrid/core → core}/labelsSv.ts +17 -1
  80. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  81. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  82. package/src/{tmdatagrid/index.ts → index.ts} +43 -4
  83. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +100 -45
  84. package/src/useTMDataGridExport.ts +78 -0
  85. package/src/tmdatagrid/core/cellExport.ts +0 -320
  86. /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
  87. /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
  88. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  89. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +0 -0
  90. /package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +0 -0
  91. /package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +0 -0
  92. /package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +0 -0
  93. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPanel.module.css +0 -0
  94. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  95. /package/src/{tmdatagrid/components → components}/TMDataGridFilterSurface.module.css +0 -0
  96. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  97. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  98. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +0 -0
  99. /package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +0 -0
  100. /package/src/{tmdatagrid/components → components}/TMDataGridHeaderFilterRow.module.css +0 -0
  101. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  102. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  103. /package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +0 -0
  104. /package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +0 -0
  105. /package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +0 -0
  106. /package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +0 -0
  107. /package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +0 -0
  108. /package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +0 -0
  109. /package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +0 -0
  110. /package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +0 -0
  111. /package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +0 -0
  112. /package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +0 -0
  113. /package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +0 -0
  114. /package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +0 -0
  115. /package/src/{tmdatagrid/components → components}/filters/controlLayout.ts +0 -0
  116. /package/src/{tmdatagrid/components → components}/filters/filterControlFor.ts +0 -0
  117. /package/src/{tmdatagrid/components → components}/sticky.module.css +0 -0
  118. /package/src/{tmdatagrid/components → components}/useHideableColumns.ts +0 -0
  119. /package/src/{tmdatagrid/core → core}/capabilities.ts +0 -0
  120. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  121. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  122. /package/src/{tmdatagrid/core → core}/columnOptions.ts +0 -0
  123. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  124. /package/src/{tmdatagrid/core → core}/controlledStateSync.ts +0 -0
  125. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  126. /package/src/{tmdatagrid/core → core}/editEngine.ts +0 -0
  127. /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
  128. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  129. /package/src/{tmdatagrid/core → core}/filterOperators.ts +0 -0
  130. /package/src/{tmdatagrid/core → core}/filterSurface.ts +0 -0
  131. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  132. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  133. /package/src/{tmdatagrid/core → core}/pageReset.ts +0 -0
  134. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  135. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  136. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  137. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  138. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  139. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -0,0 +1,248 @@
1
+ # Editors and validation
2
+
3
+ Which control a cell opens when editing starts, what the value becomes on the
4
+ way in, and what prevents a bad one being committed. All three follow from the
5
+ column: `meta.type` picks the editor, and `meta.edit` holds the rest.
6
+
7
+ ```demo
8
+ file: editing/EditorsAndValidation.tsx
9
+ hint: Type a single letter into String, or 5 into Number, to see validation reject it.
10
+ height: 440
11
+ ```
12
+
13
+ ## The built-in editors
14
+
15
+ `meta.type` picks one, and `meta.options` feeds the select editors from the same
16
+ source the filter panel reads. Neither lives under `meta.edit`.
17
+
18
+ | `meta.type` | Editor | Writes |
19
+ | --- | --- | --- |
20
+ | `string` (default) | Text input | `string` |
21
+ | `number` | Number input | `number`, or `null` while the cell is empty or the text is not yet a number |
22
+ | `boolean` | Checkbox | `boolean` |
23
+ | `date` | Native `<input type="date">` | A `Date`, or the `"YYYY-MM-DD"` string; `null` when cleared |
24
+ | `select` | Searchable select from `meta.options`. Commits on pick under `"cell"` | `string \| null` |
25
+ | `multiSelect` | Multi-select, same source | `string[]` |
26
+
27
+ **Writes** is the value the editor puts into the draft: what `meta.edit.mapValue` is handed, what `meta.edit.validate` checks, and what a commit carries in `value` and in `changes[].next`.
28
+
29
+ The number editor writes `null` rather than `NaN` while the text does not parse, so a half-typed number leaves the field empty instead of committing a number no rule can describe.
30
+ The date editor picks between its two types once, when it opens, from what the cell held: a `Date` cell keeps receiving `Date`s and a string cell keeps receiving `"YYYY-MM-DD"` strings, so clearing and retyping cannot flip the type.
31
+
32
+ Each is a named export (`TMDataGridStringEditor`, `TMDataGridNumberEditor`,
33
+ `TMDataGridBooleanEditor`, `TMDataGridDateEditor`, `TMDataGridSelectEditor`,
34
+ `TMDataGridMultiSelectEditor`), so a custom editor can wrap one instead of
35
+ starting from scratch.
36
+
37
+ ## Writing your own
38
+
39
+ `meta.edit.editor` fills the same slot as the built-ins. It is a **component**,
40
+ rendered as JSX, so hooks may be used inside, and it receives the live TanStack
41
+ Form `field` API. Bind any control to it as you would inside a form:
42
+
43
+ ```tsx
44
+ const SalaryEditor: TMDataGridEditorComponent = ({ field, commit }) => (
45
+ <Slider
46
+ value={field.state.value}
47
+ onChange={field.handleChange}
48
+ onChangeEnd={() => void commit()}
49
+ />
50
+ );
51
+
52
+ meta: { edit: { editor: SalaryEditor } }
53
+ ```
54
+
55
+ **Define editors at module scope.** An inline arrow function gets a new identity
56
+ on every render, which remounts the editor mid-edit and discards what was typed.
57
+
58
+ The field's validation message is shown by the host, in a tooltip on the
59
+ editor: open while the input is focused, and on hover. A custom editor gets the
60
+ same tooltip, so it renders no message of its own.
61
+
62
+ The built-in editors mark the input invalid and add no inline text, since the
63
+ message would be clipped by the width of the cell. A custom editor does the
64
+ same by binding a boolean to the control's `error` prop:
65
+
66
+ ```tsx
67
+ const hasError = field.state.meta.errors.length > 0;
68
+
69
+ <Slider value={field.state.value} onChange={field.handleChange} error={hasError} />;
70
+ ```
71
+
72
+ ## Mapping the value as it is typed
73
+
74
+ A column can rewrite every value on its way into the draft: uppercase a code,
75
+ strip spaces out of an IBAN, clamp a number into range. `meta.edit.mapValue`
76
+ runs on each write an editor makes, which is each keystroke for a text input and
77
+ each pick for a select.
78
+
79
+ ```demo
80
+ file: editing/MappedInput.tsx
81
+ hint: Type into either column. Code uppercases, Reference keeps digits only.
82
+ height: 420
83
+ ```
84
+
85
+ ```tsx
86
+ meta: {
87
+ edit: {
88
+ mapValue: ({ value }) =>
89
+ typeof value === "string" ? value.toUpperCase() : value,
90
+ },
91
+ }
92
+ ```
93
+
94
+ What it returns is what the cell shows, what the validators check and what is
95
+ committed.
96
+
97
+ The map is applied inside the editor host, around the field every editor writes
98
+ through, so one declaration covers all six built-in editors, your own
99
+ `meta.edit.editor`, and the character that opened the editor when typing started
100
+ the edit.
101
+
102
+ Some writes are not mapped: the value the editor opens with, and the writes that go through the form rather than through an editor - `edit.clearCell()`, the Delete key, `edit.setCellValue()` and `edit.setRowValues()`.
103
+ An editor calling `field.setValue` instead of `field.handleChange` also bypasses the map.
104
+ `handleChange` is the mapped path.
105
+
106
+ The map receives the row and column as well, so it can depend on the record
107
+ being edited:
108
+
109
+ ```tsx
110
+ meta: {
111
+ edit: {
112
+ mapValue: ({ value, row }) =>
113
+ row.original.country === "SE" && typeof value === "string"
114
+ ? value.replace(/ /g, "")
115
+ : value,
116
+ },
117
+ }
118
+ ```
119
+
120
+ `previous` is the value the field held before this write, which a mask needs in
121
+ order to tell an insertion from a deletion.
122
+
123
+ ### Keeping the caret
124
+
125
+ A mapped value differs from the one the input holds, so React writes the new
126
+ value into the DOM node and the browser collapses the selection to the end of
127
+ the field.
128
+
129
+ The built-in string and number editors restore the caret to where it was typed,
130
+ shifted by however much the map changed the length, so a mask that inserts or
131
+ strips characters keeps the caret beside the same text.
132
+
133
+ A custom editor rendering its own input has the same problem, and solves it the
134
+ same way: record `selectionStart` as the value is handed over, and restore it in
135
+ a layout effect once the mapped value has rendered.
136
+
137
+ ## Validation
138
+
139
+ The validators are TanStack Form's own: a Standard Schema (Zod, Valibot,
140
+ ArkType) or a plain function returning an error message or nothing.
141
+
142
+ ```tsx
143
+ // Per column: field-level validators. A bare schema or function means { onChange: it }.
144
+ meta: { edit: { validate: z.string().min(2, "Too short") } }
145
+
146
+ // The same rule without a schema library:
147
+ meta: {
148
+ edit: {
149
+ validate: ({ value }: { value: unknown }) =>
150
+ typeof value === "string" && value.length < 2 ? "Too short" : undefined,
151
+ },
152
+ }
153
+
154
+ // Per row: form-level validators - cross-field rules live here.
155
+ useTMDataGrid({
156
+ editing: {
157
+ mode: "cell",
158
+ rowValidators: {
159
+ onSubmit: z.object({ salary: z.number().positive() })
160
+ .refine((r) => r.status !== "Terminated" || r.salary === 0, {
161
+ message: "A terminated employee has no salary",
162
+ }),
163
+ },
164
+ },
165
+ });
166
+ ```
167
+
168
+ A plain function is typed `TMDataGridValidator`, whose `value` is `never`, so annotate the parameter - `{ value: unknown }`, or the type the column's editor writes - rather than leaving it to be inferred.
169
+
170
+ Pathed issues land on the matching cells; pathless ones on the row, where the
171
+ message shows in the edit lane's tooltip - on the open row's ✓, and on the
172
+ parked row's marker. To show a pathless message somewhere of your own, read it
173
+ from `edit.getForm(rowId)?.state.errors`; `edit.store` carries the flag
174
+ (`hasRowError`) and the field messages (`errorMessages`), not the row text.
175
+
176
+ A commit blocked by validation keeps the editor open, invalid, with the message
177
+ in its tooltip. A rejected `editing.onCommit` keeps the draft too, with the
178
+ error on the row. Server-side field errors can be returned natively through
179
+ `editing.rowValidators.onSubmitAsync`'s `{ form, fields }` shape.
180
+
181
+ Cross-field rules need a mode that commits the whole row at once. Under
182
+ `"cell"` each cell commits alone, so a rule spanning two columns cannot be
183
+ satisfied by either one. Use `editing.rowValidators.onSubmit` with `"row"`. See
184
+ [Editing](/docs/editing#row-editing).
185
+
186
+ A rule about the whole collection, such as "no duplicates" or "shares sum to
187
+ 100", is neither a field rule nor a row rule. It takes
188
+ `editing.tableValidators` - see [Cross-row rules](#cross-row-rules).
189
+
190
+ ## Cross-row rules
191
+
192
+ `editing.tableValidators` holds the rules that need the other rows: no
193
+ duplicate keys, no overlapping ranges, allocations summing to a total. Its
194
+ validators receive the committing row and `rows`, the collection as it would
195
+ stand if the commit landed - every draft overlaid, entry rows appended,
196
+ deletion-marked rows removed:
197
+
198
+ ```tsx
199
+ editing: {
200
+ mode: "cell",
201
+ draft: true,
202
+ tableValidators: {
203
+ onSubmit: ({ value, rowId, rows }) =>
204
+ rows.some((r) => r.rowId !== rowId && r.value.code === value.code)
205
+ ? { fields: { code: "Codes must be unique" } }
206
+ : undefined,
207
+ },
208
+ }
209
+ ```
210
+
211
+ The result is the `rowValidators` vocabulary: nothing passes, a string is a
212
+ row-level message, and `{ form, fields }` lands pathed issues on the
213
+ committing row's cells. `onSubmit` runs first, and its failure stands without
214
+ `onSubmitAsync` running.
215
+
216
+ The rules run at every commit, after the row's own validators, and again for
217
+ every parked row during `saveDrafts` - a draft that a later edit has
218
+ invalidated fails there, keeps its markers, and the save resolves `false`.
219
+ Errors land on the committing row only; the row it clashes with is not
220
+ marked.
221
+
222
+ `rows` is unfiltered, so a rule sees the whole collection whatever the view
223
+ shows, and it never contains group rows.
224
+
225
+ ```demo
226
+ file: editing/TableValidation.tsx
227
+ hint: Give two teams the same code, or push the shares past 100, and the commit is refused. Drafts count - a clash with a pending edit is caught too.
228
+ height: 380
229
+ ```
230
+
231
+ A grid inside an outer form can put collection rules in the form's own field
232
+ validator instead. See [A query builder form](/docs/query-builder).
233
+
234
+ ## Reference
235
+
236
+ | Name | Kind | Type | Default | What it does |
237
+ | --- | --- | --- | --- | --- |
238
+ | `meta.edit.editor` | Column meta | `TMDataGridEditorComponent` | By `meta.type` | Replaces the cell editor. |
239
+ | `meta.edit.validate` | Column meta | `TMDataGridFieldValidate` | – | Field-level validation. A bare schema or function means `onChange`. |
240
+ | `meta.edit.mapValue` | Column meta | `TMDataGridEditValueMap` | – | Maps each value an editor writes, before it reaches the draft. |
241
+ | `editing.rowValidators` | Option | `TMDataGridRowValidators` | – | Form-level validation, for cross-field rules. |
242
+ | `editing.tableValidators` | Option | `TMDataGridTableValidators` | – | Cross-row rules, handed the collection with every draft overlaid. See [Cross-row rules](#cross-row-rules). |
243
+ | `TMDataGridTableValidateArgs` | Export | type | – | What a table validator receives: `value`, `rowId`, `isNew`, `rows`. |
244
+ | `TMDataGridEditorArgs` | Export | type | – | What an editor component receives: `field`, `commit`, `cancel`, `row`, `column`. |
245
+ | `TMDataGridEditValueMapArgs` | Export | type | – | What `mapValue` receives: `value`, `previous`, `row`, `column`, `table`. |
246
+ | `TMDataGridStringEditor` · `NumberEditor` · `BooleanEditor` · `DateEditor` · `SelectEditor` · `MultiSelectEditor` | Exports | components | – | The six built-ins, for wrapping. |
247
+ | `normalizeFieldValidate` | Export | `(validate) => validators` | – | Turns a bare schema into TanStack Form's validator shape. |
248
+ | `getEditFieldName` | Export | `(column) => string` | – | The data path a column's edits write to. |
package/docs/export.md ADDED
@@ -0,0 +1,326 @@
1
+ # Export
2
+
3
+ Downloads the grid as a file: every filtered and sorted row across every page, or the selected rows, in the format the grid is configured with.
4
+ `TMDataGrid.Menu.Export` and `TMDataGrid.Menu.ExportSelected` are the built-in entry points; `useTMDataGridExport` is the same export as click handlers for a control of your own.
5
+
6
+ ```tsx
7
+ <TMDataGrid.Toolbar>
8
+ <TMDataGrid.SummaryCount />
9
+ <TMDataGrid.Spacer />
10
+ <TMDataGrid.Menu>
11
+ <TMDataGrid.Menu.Export />
12
+ <TMDataGrid.Menu.ExportSelected />
13
+ <Menu.Divider />
14
+ <TMDataGrid.Menu.Columns />
15
+ </TMDataGrid.Menu>
16
+ </TMDataGrid.Toolbar>
17
+ ```
18
+
19
+ ```demo
20
+ file: data/Export.tsx
21
+ hint: Tick a few rows, then open the menu. The Excel CSV opens straight into columns.
22
+ ```
23
+
24
+ ## What is written
25
+
26
+ By default the columns are the visible data columns in render order, so a hidden column is not exported and a pinned one keeps its place.
27
+ `columns` on `exportOptions`, on the menu items and on the functions picks another set:
28
+
29
+ - `"visible"` - the data columns on screen. The default.
30
+ - `"all"` - every exportable column, hidden ones in the place they would take if shown.
31
+ - a list of column ids - those columns, in render order.
32
+
33
+ The generated lanes (checkbox, details, edit, row numbers) are never exported, whichever is asked for.
34
+ Set `meta.enableExport: false` to leave a column of your own out the same way:
35
+
36
+ ```tsx
37
+ columnHelper.display({
38
+ id: "actions",
39
+ cell: ActionsCell,
40
+ meta: { enableExport: false },
41
+ });
42
+ ```
43
+
44
+ The rows are every row after filtering and sorting, across every page.
45
+ On a grouped grid the records under every group are written, whether the group is open or not, and the group rows are not.
46
+
47
+ Each cell is written as its **value**, `row.getValue(column.id)`, not as what the cell renders.
48
+ A cell showing `32 000 kr` exports `32000`; a select column exports the option's `value`.
49
+ Set `meta.exportValue` to write something else:
50
+
51
+ ```tsx
52
+ columnHelper.accessor("status", {
53
+ meta: {
54
+ type: "select",
55
+ options: STATUS_OPTIONS,
56
+ exportValue: ({ value }) => STATUS_LABELS[value as EmployeeStatus],
57
+ },
58
+ });
59
+ ```
60
+
61
+ `exportValue` receives `{ value, row, column }` and returns the value the format writes.
62
+ Both meta fields also apply to Ctrl+C under [cell selection](/docs/cell-selection#copy-and-export).
63
+
64
+ ## The exportOptions option
65
+
66
+ `exportOptions` on `useTMDataGrid` sets the format, the file name and whether the column labels go in as the first row:
67
+
68
+ ```tsx
69
+ const grid = useTMDataGrid({
70
+ data,
71
+ columns,
72
+ exportOptions: { format: csvFormat(), fileName: "employees" },
73
+ });
74
+ ```
75
+
76
+ | Field | Type | Default | Description |
77
+ | --- | --- | --- | --- |
78
+ | `format` | `TMDataGridExportFormat` | `csvExcelFormat()` | The file format. See [Formats](#formats). |
79
+ | `fileName` | `string` | `"export"` | The file name without extension; the format adds its own. |
80
+ | `includeHeaders` | `boolean` | `true` | Determines whether the column labels are written as the first row. |
81
+ | `columns` | `"visible" \| "all" \| Array<string>` | `"visible"` | Which columns are written. See [What is written](#what-is-written). |
82
+
83
+ The option is read field by field, so a literal is fine.
84
+ Every export of the grid reads it: the menu items, the hook, and the export item of the cell-range menu.
85
+
86
+ ## Formats
87
+
88
+ A format is a `TMDataGridExportFormat`, and the package ships four factories:
89
+
90
+ | Factory | File | Writes |
91
+ | --- | --- | --- |
92
+ | `csvExcelFormat()` | `.csv` | A CSV Excel opens straight into columns: UTF-8 BOM, a `sep=;` first line, CRLF endings, `;` between fields, `,` as the decimal mark. |
93
+ | `csvFormat()` | `.csv` | CSV as RFC 4180 has it: `,` between fields, `.` as the decimal mark, CRLF, a UTF-8 BOM, no `sep=` line. |
94
+ | `tsvFormat()` | `.tsv` | Tab-separated text, the clipboard shape as a file. |
95
+ | `jsonFormat()` | `.json` | An array with one object per row, keyed by column label, values as the data holds them. |
96
+
97
+ Excel xlsx is a separate package, since it needs a spreadsheet library the grid should not carry: see [Excel export (xlsx)](/docs/xlsx).
98
+
99
+ By default the grid writes `csvExcelFormat()` with the Nordic conventions, because an Excel running a Swedish, Norwegian, Danish or Finnish locale reads `;` as its list separator and `,` as its decimal mark, and a file written the other way opens as one column of text.
100
+ To write for an English-locale Excel, set the separator and the decimal mark on the format:
101
+
102
+ ```tsx
103
+ exportOptions: { format: csvExcelFormat({ separator: ",", decimalComma: false }) }
104
+ ```
105
+
106
+ The `sep=` line is Excel's alone; Google Sheets and Numbers show it as a first row.
107
+ Use `csvFormat()` for a file that goes there.
108
+
109
+ `csvExcelFormat`, `csvFormat` and `tsvFormat` write values through `formatExportValue`: numbers with the format's decimal mark, dates in the `sv-SE` form (`2026-07-31 14:05:00`, which Excel reads as a date), booleans as `true` / `false`, arrays joined with `, `, and any other object as JSON.
110
+ `jsonFormat` writes numbers as numbers, dates as ISO strings, and `undefined` as `null`.
111
+
112
+ | Option | Formats | Type | Default | Description |
113
+ | --- | --- | --- | --- | --- |
114
+ | `separator` | csvExcel, csv | `string` | `";"` for csvExcel, `","` for csv | The character between fields. |
115
+ | `decimalComma` | csvExcel, csv, tsv | `boolean` | `true` for csvExcel and tsv, `false` for csv | Determines whether numbers are written as `1,5` rather than `1.5`. |
116
+ | `escapeFormulas` | csvExcel, csv, tsv | `boolean` | `true` | Determines whether text a spreadsheet would run as a formula is prefixed. See [Formula guard](#formula-guard). |
117
+ | `space` | json | `number` | `2` | Indentation passed to `JSON.stringify`. |
118
+
119
+ ### Formula guard
120
+
121
+ Excel and Google Sheets run a cell that starts with `=`, `+`, `-` or `@` as a formula, so text one user typed into the grid would run in another user's spreadsheet when the file is opened.
122
+ By default the text formats prefix such a value with an apostrophe, which every spreadsheet reads as "text follows" and shows.
123
+ Text that parses as a number (`-5`, `+4670123456`) is not prefixed; a phone number written with spaces (`+46 70 123 45 67`) is.
124
+ To write every value as it is, set `escapeFormulas: false` on the format:
125
+
126
+ ```tsx
127
+ exportOptions: { format: csvExcelFormat({ escapeFormulas: false }) }
128
+ ```
129
+
130
+ `guardFormula(text)` is the rule as a function, for a format of your own.
131
+
132
+ ## TMDataGrid.Menu.Export
133
+
134
+ A menu item that downloads every filtered and sorted row, all pages, in the grid's format.
135
+ It needs a Mantine `Menu` around it and reads the grid from context, so it works in `TMDataGrid.Menu` and in any Mantine menu rendered inside `TMDataGrid`.
136
+ Its props override `exportOptions` for this item alone, which is how one menu offers two formats:
137
+
138
+ ```tsx
139
+ <TMDataGrid.Menu>
140
+ <TMDataGrid.Menu.Export />
141
+ <TMDataGrid.Menu.Export
142
+ format={csvFormat()}
143
+ fileName="employees-plain"
144
+ label="Export all rows as plain CSV"
145
+ />
146
+ </TMDataGrid.Menu>
147
+ ```
148
+
149
+ | Prop | Type | Default | Description |
150
+ | --- | --- | --- | --- |
151
+ | `format` | `TMDataGridExportFormat` | `exportOptions.format` | The file format for this item. |
152
+ | `fileName` | `string` | `exportOptions.fileName` | The file name for this item. |
153
+ | `includeHeaders` | `boolean` | `exportOptions.includeHeaders` | Determines whether this item writes the header row. |
154
+ | `columns` | `"visible" \| "all" \| Array<string> \| "custom"` | `exportOptions.columns` | Which columns the item writes. `"custom"` opens the column picker. |
155
+ | `label` | `ReactNode` | `labels.exportAll` | The item's text. |
156
+
157
+ ### The column picker
158
+
159
+ `columns="custom"` opens a dialog instead of downloading, titled with the format ("Export as CSV"): every exportable column as a checkbox, the visible ones ticked and the hidden ones marked **Hidden**, and an Export button that downloads the ticked columns in render order.
160
+ The line under the title names the selected rows when the item is `ExportSelected`.
161
+ A select-all row heads the list with a count of the ticked columns, a search box appears once there are six columns, and the list scrolls inside the dialog past half the viewport.
162
+ Under a search, select all ticks the matches and leaves the rest as they are.
163
+ Cancel closes it with no download.
164
+ The dialog follows the grid's `size`: its width, title and controls step with the toolbar's.
165
+
166
+ ```tsx
167
+ <TMDataGrid.Menu.Export columns="custom" label="Export all rows, choose columns" />
168
+ ```
169
+
170
+ The picker is rendered by the `TMDataGrid` root and driven by `ui.state.exportPicker`, which holds `{ rows, options }` while it is open and `null` otherwise.
171
+ `ui.actions.openExportPicker({ rows: "all", options })` opens it from a control of your own; `getExportableColumns(table)` is the list it shows.
172
+
173
+ ## TMDataGrid.Menu.ExportSelected
174
+
175
+ A menu item that downloads the selected rows of the current view, in grid order, with the same props as `TMDataGrid.Menu.Export`.
176
+ By default its text carries the count (`Export 3 selected rows`); `label` replaces it.
177
+ It is disabled while nothing is selected.
178
+ It renders nothing when row selection is off (`selectionMode: "highlight"` or `enableRowSelection: false`).
179
+
180
+ A selected row the filters have since hidden is not written and not counted.
181
+
182
+ ## useTMDataGridExport
183
+
184
+ The export as click handlers, for a button or an item of your own anywhere inside `TMDataGrid`:
185
+
186
+ ```tsx
187
+ import { useTMDataGridExport } from "@jielga/tmdatagrid";
188
+
189
+ function ExportButtons() {
190
+ const { exportAll, exportSelected, selectedCount, canExportSelected } =
191
+ useTMDataGridExport();
192
+
193
+ return (
194
+ <>
195
+ <Button size="xs" onClick={() => void exportAll()}>
196
+ Export
197
+ </Button>
198
+ {canExportSelected && (
199
+ <Button size="xs" disabled={selectedCount === 0} onClick={() => void exportSelected()}>
200
+ Export {selectedCount} selected
201
+ </Button>
202
+ )}
203
+ </>
204
+ );
205
+ }
206
+ ```
207
+
208
+ It takes a `TMDataGridExportOptions` argument, folded over the grid's `exportOptions` for this caller.
209
+ `exportAll` and `exportSelected` take one more, folded over both for that call, which is how a control passes a chosen set of columns.
210
+
211
+ | Field | Type | Description |
212
+ | --- | --- | --- |
213
+ | `exportAll` | `(overrides?) => Promise<void>` | Downloads every filtered and sorted row, all pages. |
214
+ | `exportSelected` | `(overrides?) => Promise<void>` | Downloads the selected rows of the current view. Resolves without a download when none is selected. |
215
+ | `selectedCount` | `number` | How many rows `exportSelected` would write. Subscribes to the selection. |
216
+ | `canExportSelected` | `boolean` | `true` while row selection is on. |
217
+
218
+ ## Exporting from outside the grid
219
+
220
+ `exportGrid` is the same export for code that holds the table and nothing else:
221
+
222
+ ```tsx
223
+ import { exportGrid, jsonFormat } from "@jielga/tmdatagrid";
224
+
225
+ await exportGrid({ table: grid.table, rows: "selected", options: { format: jsonFormat() } });
226
+ ```
227
+
228
+ `rows` is `"all"` (the default), `"selected"`, or an array of TanStack rows of your own; `options.columns` picks the columns.
229
+ `buildExportData({ table, rows, columns })` is the step before the file: the columns, their labels and the raw values as a `TMDataGridExportData`, for a format or a post-processing step of your own.
230
+ `writeExportFile(data, settings)` writes and downloads it.
231
+
232
+ ## Writing your own format
233
+
234
+ A format is an object with an id, a file extension, a MIME type and a `write` function.
235
+ `write` receives the `TMDataGridExportData` and `{ includeHeaders }`, and returns the file as a string or a `Blob`, or a promise of either:
236
+
237
+ ```tsx
238
+ import type { TMDataGridExportFormat } from "@jielga/tmdatagrid";
239
+
240
+ export function markdownFormat(): TMDataGridExportFormat {
241
+ return {
242
+ id: "markdown",
243
+ extension: "md",
244
+ mimeType: "text/markdown",
245
+ write: ({ headers, rows }, { includeHeaders }) => {
246
+ const line = (cells: Array<unknown>) => `| ${cells.map(String).join(" | ")} |`;
247
+ const head = includeHeaders
248
+ ? [line(headers), line(headers.map(() => "---"))]
249
+ : [];
250
+ return [...head, ...rows.map(line)].join("\n");
251
+ },
252
+ };
253
+ }
254
+ ```
255
+
256
+ Set `decimalComma` on the format when it writes text with a decimal mark, so Ctrl+C writes the same mark.
257
+ `formatExportValue(value, { decimalComma })` is the grid's own text rule, for a format that wants it.
258
+
259
+ ## Copy and the cell-range menu
260
+
261
+ Under `cellSelection: "range"`, Ctrl+C and the **Export cells** item of the right-click menu write the selected rectangle through the same `exportOptions`, column meta and format.
262
+ See [Copy and export](/docs/cell-selection#copy-and-export).
263
+
264
+ ## Labels
265
+
266
+ | Label | Default | Used by |
267
+ | --- | --- | --- |
268
+ | `exportAll` | `"Export all rows"` | `TMDataGrid.Menu.Export` |
269
+ | `exportSelected` | `(count) => "Export 3 selected rows"` | `TMDataGrid.Menu.ExportSelected` |
270
+ | `exportCells` | `"Export cells"` | The cell-range menu |
271
+ | `exportPickerTitle` | `` (format) => `Export as ${format}` `` | The column picker; `format` is the extension in upper case |
272
+ | `exportPickerHint` | `(selected) => "Select the columns to export"`, or `… for the ${selected} selected rows` | The line under the picker's title |
273
+ | `exportPickerConfirm` | `"Export"` | The column picker |
274
+ | `exportPickerCancel` | `"Cancel"` | The column picker |
275
+ | `exportPickerSelectAll` | `"Select all"` | The column picker |
276
+ | `exportPickerCount` | `` (checked, total) => `${checked} of ${total}` `` | The column picker |
277
+ | `exportPickerHidden` | `"Hidden"` | The column picker, on a column the grid hides |
278
+
279
+ See [Localization](/docs/localization).
280
+
281
+ ## Testing
282
+
283
+ `TMDataGrid.Menu.Export` is `data-dg-part="menu-export"` and `TMDataGrid.Menu.ExportSelected` is `data-dg-part="menu-export-selected"`.
284
+ The column picker is `export-picker`, its checkboxes `export-column` with `data-column-id`, its select-all row `export-column-all` with the count `export-picker-count`, its search box `export-picker-search`, the line under the title `export-picker-hint`, and its buttons `export-picker-confirm` and `export-picker-cancel`.
285
+ The search box and the picker share `columnsSearchPlaceholder` and `columnsNoMatch` with the column chooser.
286
+ A download is an anchor with an object URL, clicked; a jsdom test stubs `URL.createObjectURL` and `HTMLAnchorElement.prototype.click` to read the file back.
287
+ See [Testing](/docs/testing).
288
+
289
+ ## Deprecated names
290
+
291
+ The names below still work and go in the next beta.
292
+
293
+ | Deprecated | Use instead |
294
+ | --- | --- |
295
+ | `cellExport` on `TMDataGrid.Table` | `exportOptions` on `useTMDataGrid`; `separator` and `decimalComma` become `csvExcelFormat({ separator, decimalComma })` |
296
+ | `exportGridToCsv({ table, options })` | `exportGrid({ table, options })` |
297
+ | `TMDataGridCellExportOptions` · `DEFAULT_CELL_EXPORT_OPTIONS` | `TMDataGridExportOptions` · `DEFAULT_EXPORT_OPTIONS` |
298
+ | `buildCellMatrix` · `buildGridCellMatrix` · `TMDataGridCellMatrix` | `buildExportData` · `TMDataGridExportData` |
299
+ | `toExcelCsv(matrix, { separator })` | `csvExcelFormat({ separator }).write(data, options)` |
300
+ | `downloadTextFile` | `downloadFile` |
301
+ | `labels.exportCsv` | `labels.exportCells` |
302
+
303
+ ## Reference
304
+
305
+ | Name | Kind | Type | Default | What it does |
306
+ | --- | --- | --- | --- | --- |
307
+ | `exportOptions` | Option | `TMDataGridExportOptions` | `DEFAULT_EXPORT_OPTIONS` | Format, file name and header row for every export of the grid. |
308
+ | `meta.enableExport` | Column meta | `boolean` | `true` | `false` leaves the column out of every export and of Ctrl+C. |
309
+ | `meta.exportValue` | Column meta | `({ value, row, column }) => unknown` | – | The value written in place of `row.getValue`. |
310
+ | `TMDataGrid.Menu.Export` | Component | `TMDataGridMenuExportProps` | – | Menu item: every filtered row. `columns="custom"` opens the picker. |
311
+ | `TMDataGrid.Menu.ExportSelected` | Component | `TMDataGridMenuExportProps` | – | Menu item: the selected rows. Renders nothing when row selection is off. |
312
+ | `ui.state.exportPicker` | UI state | `{ rows, options } \| null` | `null` | The column picker while open. |
313
+ | `ui.actions.openExportPicker` · `closeExportPicker` | UI actions | – | – | Open the picker from your own code. |
314
+ | `getExportableColumns` | Function | `(table) => Array<Column>` | – | Every column an export could take, hidden ones included. |
315
+ | `useTMDataGridExport` | Hook | `(overrides?) => TMDataGridExportApi` | – | `exportAll`, `exportSelected`, `selectedCount`, `canExportSelected`. |
316
+ | `exportGrid` | Function | `({ table, rows?, options? }) => Promise<void>` | – | Downloads the grid from outside a component. |
317
+ | `buildExportData` | Function | `({ table, rows?, columns?, bounds? }) => TMDataGridExportData` | – | The columns, labels and values a format writes. |
318
+ | `writeExportFile` | Function | `(data, settings) => Promise<void>` | – | Writes export data in a format and downloads it. |
319
+ | `csvExcelFormat` · `csvFormat` · `tsvFormat` · `jsonFormat` | Functions | `(options?) => TMDataGridExportFormat` | – | The built-in formats. |
320
+ | `guardFormula` · `formatExportValue` | Functions | – | – | The formula guard and the text rule, for a format of your own. |
321
+ | `resolveExportOptions` | Function | `(...overrides) => TMDataGridExportSettings` | – | The defaults with overrides folded over, `undefined` fields skipped. |
322
+ | `countSelectedExportRows` | Function | `(table) => number` | – | How many rows `"selected"` would write. |
323
+ | `downloadFile` | Function | `({ fileName, content, mimeType }) => void` | – | Downloads a string or a `Blob`. |
324
+ | `DEFAULT_EXPORT_OPTIONS` | Constant | `TMDataGridExportSettings` | – | `csvExcelFormat()`, `"export"`, `true`. |
325
+ | `TMDataGridExportFormat` · `TMDataGridExportData` · `TMDataGridExportOptions` · `TMDataGridExportSettings` · `TMDataGridExportRows` · `TMDataGridExportApi` | Types | – | – | The export types. |
326
+ | `data-dg-part` | Data attribute | `menu-export`, `menu-export-selected` | – | On the menu items. |