@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -212
- package/dist/index.d.ts +1664 -632
- package/dist/index.js +5226 -3223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/anatomy.md +102 -0
- package/docs/cell-selection.md +154 -0
- package/docs/column-layout.md +204 -0
- package/docs/columns.md +262 -0
- package/docs/components.md +304 -0
- package/docs/editing.md +603 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +326 -0
- package/docs/filtering.md +358 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +111 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +175 -0
- package/docs/quick-search.md +83 -0
- package/docs/row-details.md +113 -0
- package/docs/row-interaction.md +148 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +134 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +111 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +309 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +21 -45
- package/skills/appearance/SKILL.md +70 -17
- package/skills/cell-selection/SKILL.md +70 -76
- package/skills/columns/SKILL.md +131 -32
- package/skills/data/SKILL.md +100 -23
- package/skills/editing/SKILL.md +217 -96
- package/skills/editing/references/common-mistakes.md +111 -24
- package/skills/editing/references/editing-api.md +63 -39
- package/skills/editing/references/editors-and-validation.md +77 -19
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +18 -16
- package/skills/grouping/SKILL.md +32 -15
- package/skills/options/SKILL.md +39 -9
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +10 -7
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
- package/src/components/TMDataGridDraftActions.tsx +307 -0
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +354 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
- package/src/components/TMDataGridToolbar.module.css +21 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
- package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledState.ts +179 -0
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/core/editEngine.ts +2476 -0
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +843 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/labels.ts +66 -8
- package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
- package/src/core/pageReset.ts +120 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/core/resizePreview.ts +141 -0
- package/src/core/summary.ts +59 -0
- package/src/core/useSettledTableState.ts +36 -0
- package/src/{tmdatagrid/index.ts → index.ts} +75 -12
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- package/src/tmdatagrid/core/editEngine.ts +0 -1006
- package/src/tmdatagrid/core/summary.ts +0 -35
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Loading and empty states
|
|
2
|
+
|
|
3
|
+
A grid can have nothing to show for four different reasons, and the body says
|
|
4
|
+
which one it is: still fetching, emptied by a filter, or holding no data.
|
|
5
|
+
|
|
6
|
+
```demo
|
|
7
|
+
file: data/LoadingAndEmpty.tsx
|
|
8
|
+
hint: Switch to loaded, then search for something that cannot match, to see the other branch.
|
|
9
|
+
extraSources: data/employeeColumns.tsx
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Precedence
|
|
13
|
+
|
|
14
|
+
An empty body shows exactly one thing, decided in this order:
|
|
15
|
+
|
|
16
|
+
1. **Loading** - `meta.loading` is true: a centred loader. A grid that is
|
|
17
|
+
fetching never reports itself as empty.
|
|
18
|
+
2. **Entry rows** - an open entry row from `edit.addRow()`: the entry block
|
|
19
|
+
only, with no message beside the form.
|
|
20
|
+
3. **`renderEmptyState`** - your node, centred where the message would be.
|
|
21
|
+
4. **Filtered-empty** - a filter or search is active: a search icon and
|
|
22
|
+
`labels.noResults` ("No rows match your filters").
|
|
23
|
+
5. **Truly-empty** - no data at all: `labels.noRows` ("No rows to show").
|
|
24
|
+
|
|
25
|
+
## Replacing the message
|
|
26
|
+
|
|
27
|
+
`renderEmptyState` replaces states 4 and 5 with one render prop.
|
|
28
|
+
`hasActiveFilters` says which of the two it is replacing, so one prop can render
|
|
29
|
+
two different messages:
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<TMDataGrid.Table<Employee>
|
|
33
|
+
renderEmptyState={({ hasActiveFilters, table }) =>
|
|
34
|
+
hasActiveFilters ? (
|
|
35
|
+
<Stack align="center" gap="xs">
|
|
36
|
+
<Text c="dimmed">Nothing matches your filters</Text>
|
|
37
|
+
<Button variant="light" onClick={() => table.resetColumnFilters()}>
|
|
38
|
+
Clear filters
|
|
39
|
+
</Button>
|
|
40
|
+
</Stack>
|
|
41
|
+
) : (
|
|
42
|
+
<Stack align="center" gap="xs">
|
|
43
|
+
<Text c="dimmed">No employees yet</Text>
|
|
44
|
+
<Button onClick={openCreateModal}>Add the first one</Button>
|
|
45
|
+
</Stack>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Loading with rows on screen
|
|
52
|
+
|
|
53
|
+
The body's loading state only appears while the grid is **empty**. A
|
|
54
|
+
server-driven grid refetching with rows still on screen keeps showing them
|
|
55
|
+
rather than blanking the body on every page change.
|
|
56
|
+
|
|
57
|
+
`TMDataGrid.LoadingIndicator` covers that case: a small spinner while
|
|
58
|
+
`meta.loading` is true, and nothing otherwise. Place it where you want it,
|
|
59
|
+
typically after `Spacer`.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<TMDataGrid.Toolbar>
|
|
63
|
+
<TMDataGrid.SummaryCount />
|
|
64
|
+
<TMDataGrid.Spacer />
|
|
65
|
+
<TMDataGrid.LoadingIndicator />
|
|
66
|
+
</TMDataGrid.Toolbar>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Counting what is there
|
|
70
|
+
|
|
71
|
+
`TMDataGrid.SummaryCount` shows visible rows out of the total. The total is
|
|
72
|
+
`meta.totalRowCount` when you provide it, and the pre-filtered row count
|
|
73
|
+
otherwise. A [server-side](/docs/server-side) grid must provide it, because the
|
|
74
|
+
client only holds one page.
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<TMDataGrid.SummaryCount>
|
|
78
|
+
{visible} of {total} employees
|
|
79
|
+
</TMDataGrid.SummaryCount>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Reference
|
|
83
|
+
|
|
84
|
+
| Name | Kind | Type | Default | What it does |
|
|
85
|
+
| --- | --- | --- | --- | --- |
|
|
86
|
+
| `meta.loading` | Option | `boolean` | `false` | A fetch is in flight. Takes precedence over every empty message. |
|
|
87
|
+
| `meta.noResultsLabel` | Option | `string` | `labels.noResults` | The filtered-empty message, without a render prop. |
|
|
88
|
+
| `meta.totalRowCount` | Option | `number` | Pre-filtered count | The total `SummaryCount` reports. |
|
|
89
|
+
| `renderEmptyState` | Table prop | `({ hasActiveFilters, table }) => ReactNode` | – | Replaces both built-in empty messages. |
|
|
90
|
+
| `TMDataGrid.LoadingIndicator` | Component | – | – | Spinner while `meta.loading`, for when the body has rows. |
|
|
91
|
+
| `TMDataGrid.SummaryCount` | Component | `children` replaces the text | – | Visible rows out of total. |
|
|
92
|
+
| `labels.noRows` · `labels.noResults` | Labels | `string` | English defaults | The two built-in messages. See [Localization](/docs/localization). |
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Localization
|
|
2
|
+
|
|
3
|
+
Every string the grid renders - menu items, panels, tooltips, the pager and
|
|
4
|
+
every `aria-label` - comes from one labels object. English by default.
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
const grid = useTMDataGrid({
|
|
8
|
+
data,
|
|
9
|
+
columns,
|
|
10
|
+
labels: { noResults: "Inga rader matchar dina filter" },
|
|
11
|
+
});
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`labels` takes **any subset** and merges it over the defaults.
|
|
15
|
+
|
|
16
|
+
```demo
|
|
17
|
+
file: customization/Localization.tsx
|
|
18
|
+
extraSources: data/employeeColumns.tsx
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## A complete translation
|
|
22
|
+
|
|
23
|
+
A full Swedish dictionary ships as `TMDATAGRID_LABELS_SV`:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { TMDATAGRID_LABELS_SV } from "@jielga/tmdatagrid";
|
|
27
|
+
|
|
28
|
+
const grid = useTMDataGrid({ data, columns, labels: TMDATAGRID_LABELS_SV });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`TMDATAGRID_LABELS_EN` is the English base, and `TMDataGridLabels` is the full
|
|
32
|
+
dictionary type, so a missing key in a new translation is a compile error.
|
|
33
|
+
|
|
34
|
+
## Labels that carry a value
|
|
35
|
+
|
|
36
|
+
These labels are functions, so each language can place the value where its
|
|
37
|
+
grammar requires:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
labels: {
|
|
41
|
+
groupBy: (column) => `Gruppera på ${column}`,
|
|
42
|
+
pageRange: ({ from, to, total }) => `${from}–${to} av ${total}`,
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Keep the object stable
|
|
47
|
+
|
|
48
|
+
Define it at module scope, or memoize it. The grid re-renders when the labels
|
|
49
|
+
object changes identity.
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
const labels = { noResults: "Inga träffar" } satisfies TMDataGridLabelsOverride;
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Reading them yourself
|
|
56
|
+
|
|
57
|
+
The resolved dictionary comes back from the hook as `grid.labels`, and from
|
|
58
|
+
context as `useTMDataGridContext().labels`, so a
|
|
59
|
+
[toolbar component of your own](/docs/toolbar) uses the same strings as the
|
|
60
|
+
built-in parts, in whatever language is configured.
|
|
61
|
+
|
|
62
|
+
`mergeLabels(base, override)` is the merge itself, exported for composing
|
|
63
|
+
dictionaries before passing one in.
|
|
64
|
+
|
|
65
|
+
`meta.noResultsLabel` remains as a per-instance override of
|
|
66
|
+
`labels.noResults`.
|
|
67
|
+
|
|
68
|
+
## Reference
|
|
69
|
+
|
|
70
|
+
| Name | Kind | Type | Default | What it does |
|
|
71
|
+
| --- | --- | --- | --- | --- |
|
|
72
|
+
| `labels` | Option | `TMDataGridLabelsOverride` | English | Any subset, merged over the defaults. Keep it stable. |
|
|
73
|
+
| `grid.labels` | Hook return | `TMDataGridLabels` | – | The resolved dictionary. |
|
|
74
|
+
| `TMDATAGRID_LABELS_EN` | Export | `TMDataGridLabels` | – | The English base. |
|
|
75
|
+
| `TMDATAGRID_LABELS_SV` | Export | `TMDataGridLabels` | – | A complete Swedish dictionary. |
|
|
76
|
+
| `TMDataGridLabels` | Export | type | – | The full dictionary. What a new translation must cover. |
|
|
77
|
+
| `TMDataGridLabelsOverride` | Export | type | – | A partial dictionary. |
|
|
78
|
+
| `mergeLabels` | Export | `(base, override) => TMDataGridLabels` | – | The merge, for composing dictionaries. |
|
|
79
|
+
| `meta.noResultsLabel` | Option | `string` | `labels.noResults` | Per-instance override of the filtered-empty message. |
|
package/docs/menu.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Grid menu
|
|
2
|
+
|
|
3
|
+
`TMDataGrid.Menu` is the burger button at the end of the toolbar and the Mantine `Menu` it opens.
|
|
4
|
+
Its children are the dropdown: Mantine `Menu.Item`s of your own, and the built-in items under `TMDataGrid.Menu.*`.
|
|
5
|
+
|
|
6
|
+
```tsx
|
|
7
|
+
import { Menu } from "@mantine/core";
|
|
8
|
+
|
|
9
|
+
<TMDataGrid.Toolbar>
|
|
10
|
+
<TMDataGrid.SummaryCount />
|
|
11
|
+
<TMDataGrid.Spacer />
|
|
12
|
+
<TMDataGrid.FilterButton />
|
|
13
|
+
<TMDataGrid.Menu>
|
|
14
|
+
<TMDataGrid.Menu.Export />
|
|
15
|
+
<Menu.Item onClick={saveView}>Save view</Menu.Item>
|
|
16
|
+
<Menu.Divider />
|
|
17
|
+
<Menu.Label>Columns</Menu.Label>
|
|
18
|
+
<TMDataGrid.Menu.Columns />
|
|
19
|
+
</TMDataGrid.Menu>
|
|
20
|
+
</TMDataGrid.Toolbar>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```demo
|
|
24
|
+
file: customization/GridMenu.tsx
|
|
25
|
+
hint: The burger holds the app's own items above the column chooser.
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A custom item reads the grid from context, the same way a [toolbar button](/docs/toolbar#buttons-of-your-own) does.
|
|
29
|
+
`TMDataGrid.Menu.Export` and `TMDataGrid.Menu.ExportSelected` are the built-in export items; their props and formats are on [Export](/docs/export).
|
|
30
|
+
Mantine's `Menu.Divider`, `Menu.Label` and `Menu.Sub` work as they do in any Mantine menu; the grid wraps nothing of Mantine's.
|
|
31
|
+
|
|
32
|
+
`TMDataGrid.Menu` always renders: it cannot see whether its children render anything.
|
|
33
|
+
A menu holding only `TMDataGrid.Menu.Columns` on a grid with `enableHiding: false` opens empty, so hide it with the same check the built-in buttons use:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
const { table, features } = useTMDataGridContext();
|
|
37
|
+
const { canHideAny } = getGridCapabilities(table, features);
|
|
38
|
+
|
|
39
|
+
{canHideAny && (
|
|
40
|
+
<TMDataGrid.Menu>
|
|
41
|
+
<TMDataGrid.Menu.Columns />
|
|
42
|
+
</TMDataGrid.Menu>
|
|
43
|
+
)}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The column chooser as menu items
|
|
47
|
+
|
|
48
|
+
`TMDataGrid.Menu.Columns` is the whole chooser: one checkbox item per column that can be hidden, a search box once there are six of them, **Show/Hide All** and **Reset layout**.
|
|
49
|
+
It renders nothing when no column can be hidden.
|
|
50
|
+
The pieces it is made of are exported for menus that want only some of them.
|
|
51
|
+
|
|
52
|
+
| Component | Renders |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `TMDataGrid.Menu.Columns` | `Menu.Search`, the toggles, a divider, show/hide all and reset layout. By default the search box shows from six columns; `searchable` shows it always and `searchable={false}` never. |
|
|
55
|
+
| `TMDataGrid.Menu.ColumnToggles` | One item per hideable column, with a checkbox that shows both states. `search` narrows the list to the labels containing it. |
|
|
56
|
+
| `TMDataGrid.Menu.ShowHideAll` | One checkbox item over the same list. An indeterminate box marks a partial state, and a click then shows all. |
|
|
57
|
+
| `TMDataGrid.Menu.ResetLayout` | One item calling `resetSettings()`: visibility, order, pinning and widths. |
|
|
58
|
+
|
|
59
|
+
Each of them needs a Mantine `Menu` around it and reads the grid from context, so they work in any Mantine menu rendered inside `TMDataGrid`, not only in the burger:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<Menu width={260} withinPortal>
|
|
63
|
+
<Menu.Target>
|
|
64
|
+
<Button size="compact-xs" variant="subtle">
|
|
65
|
+
View
|
|
66
|
+
</Button>
|
|
67
|
+
</Menu.Target>
|
|
68
|
+
<Menu.Dropdown>
|
|
69
|
+
<Menu.Item onClick={() => setDensity("compact")}>Compact rows</Menu.Item>
|
|
70
|
+
<Menu.Divider />
|
|
71
|
+
<TMDataGrid.Menu.ColumnToggles />
|
|
72
|
+
<Menu.Divider />
|
|
73
|
+
<TMDataGrid.Menu.ShowHideAll />
|
|
74
|
+
<TMDataGrid.Menu.ResetLayout />
|
|
75
|
+
</Menu.Dropdown>
|
|
76
|
+
</Menu>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### As a submenu
|
|
80
|
+
|
|
81
|
+
Every column header's menu has **Manage columns**, a `Menu.Sub` holding `TMDataGrid.Menu.Columns`.
|
|
82
|
+
The same composition works in a menu of your own:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
<Menu.Sub>
|
|
86
|
+
<Menu.Sub.Target>
|
|
87
|
+
<Menu.Sub.Item>Manage columns</Menu.Sub.Item>
|
|
88
|
+
</Menu.Sub.Target>
|
|
89
|
+
<Menu.Sub.Dropdown>
|
|
90
|
+
<TMDataGrid.Menu.Columns searchable={false} />
|
|
91
|
+
</Menu.Sub.Dropdown>
|
|
92
|
+
</Menu.Sub>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`searchable` is for a block at the top level of a dropdown.
|
|
96
|
+
`Menu.Search` registers on the root menu, which switches off type-ahead and the arrow keys of every dropdown of that menu, so a search box inside a submenu breaks the keyboard behaviour of the menu around it.
|
|
97
|
+
ArrowDown from the search box moves to the first listed column, whatever sits above the block.
|
|
98
|
+
|
|
99
|
+
### The panel instead
|
|
100
|
+
|
|
101
|
+
`TMDataGrid.ColumnsPanel` is the same chooser as plain controls, for a host that is not a menu: a Popover, a Drawer, or a settings page.
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
const [opened, setOpened] = useState(false);
|
|
105
|
+
|
|
106
|
+
<Popover opened={opened} onChange={setOpened} withinPortal trapFocus>
|
|
107
|
+
<Popover.Target>
|
|
108
|
+
<ActionIcon aria-label="Columns" onClick={() => setOpened((open) => !open)}>
|
|
109
|
+
<IconColumns3 size={18} />
|
|
110
|
+
</ActionIcon>
|
|
111
|
+
</Popover.Target>
|
|
112
|
+
<Popover.Dropdown p={0}>
|
|
113
|
+
<TMDataGrid.ColumnsPanel />
|
|
114
|
+
</Popover.Dropdown>
|
|
115
|
+
</Popover>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Labels
|
|
119
|
+
|
|
120
|
+
`labels.menuButton` is the burger's tooltip and `aria-label`.
|
|
121
|
+
The chooser uses the panel's strings: `columnsSearchPlaceholder`, `columnsNoMatch`, `columnsShowHideAll` and `columnsReset`.
|
|
122
|
+
See [Localization](/docs/localization).
|
|
123
|
+
|
|
124
|
+
## Testing
|
|
125
|
+
|
|
126
|
+
The burger is `data-dg-part="menu-button"`.
|
|
127
|
+
The items publish the same parts as the panel: `columns-search`, `columns-toggle` with `data-column-id`, `columns-toggle-all` and `columns-reset`, so a test written against the panel reads the same on the menu.
|
|
128
|
+
The export items are `menu-export` and `menu-export-selected`.
|
|
129
|
+
See [Testing](/docs/testing).
|
|
130
|
+
|
|
131
|
+
## Reference
|
|
132
|
+
|
|
133
|
+
| Name | Kind | Type | Default | What it does |
|
|
134
|
+
| --- | --- | --- | --- | --- |
|
|
135
|
+
| `TMDataGrid.Menu` | Component | Mantine `MenuProps` without `children`, plus `children`, `icon`, `label` | `position="bottom-end"`, `shadow="md"`, `width={260}`, `withinPortal` | The burger and its dropdown. `icon` replaces the burger; `label` is the tooltip and `aria-label`, default `labels.menuButton`. |
|
|
136
|
+
| `TMDataGrid.Menu.Columns` | Component | `searchable?: boolean \| "auto"` | `"auto"` | The whole column chooser as menu items. `"auto"` shows the search box from six columns. Renders nothing when no column can be hidden. |
|
|
137
|
+
| `TMDataGrid.Menu.ColumnToggles` | Component | `search?: string` | – | One checkbox item per hideable column. |
|
|
138
|
+
| `TMDataGrid.Menu.ShowHideAll` | Component | – | – | One checkbox item over every hideable column. |
|
|
139
|
+
| `TMDataGrid.Menu.ResetLayout` | Component | – | – | Calls `resetSettings()`. |
|
|
140
|
+
| `TMDataGrid.Menu.Export` · `.ExportSelected` | Components | `TMDataGridExportOptions` | – | The export items. Props on [Export](/docs/export). |
|
|
141
|
+
| `TMDataGrid.ColumnsPanel` | Component | – | – | The chooser as plain controls, for a host that is not a menu. |
|
|
142
|
+
| `labels.menuButton` | Option | `string` | `"Menu"` | The burger's tooltip and `aria-label`. |
|
|
143
|
+
| `TMDataGridMenuProps` · `TMDataGridMenuColumnsProps` · `TMDataGridMenuExportProps` | Export | types | – | The prop types. |
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Pagination
|
|
2
|
+
|
|
3
|
+
**Off by default.** The grid renders every filtered and sorted row and relies
|
|
4
|
+
on [virtualization](/docs/scrolling), which handles any row count. Turn paging
|
|
5
|
+
on when users should move through the data a page at a time, not to keep a
|
|
6
|
+
large grid responsive.
|
|
7
|
+
|
|
8
|
+
There are three modes.
|
|
9
|
+
|
|
10
|
+
**No pagination** - the default. `TMDataGrid.Footer` renders nothing.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
const grid = useTMDataGrid({ data, columns });
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Client pagination** - the table pages the data itself, and the Footer renders
|
|
17
|
+
its pager. Initial page size is 25, configurable through
|
|
18
|
+
`initialState.pagination`.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
const grid = useTMDataGrid({ data, columns, enablePagination: true });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Manual pagination** - the server pages, and the grid stops.
|
|
25
|
+
`manualPagination: true` implies `enablePagination`, so no extra flag is
|
|
26
|
+
needed. See [Server-side data](/docs/server-side).
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
const grid = useTMDataGrid({
|
|
30
|
+
data: page.rows,
|
|
31
|
+
columns,
|
|
32
|
+
manualPagination: true,
|
|
33
|
+
rowCount: page.total,
|
|
34
|
+
state: { pagination },
|
|
35
|
+
onPaginationChange: setPagination,
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```demo
|
|
40
|
+
file: data/Pagination.tsx
|
|
41
|
+
extraSources: data/employeeColumns.tsx
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`enablePagination` is defined by the grid rather than by TanStack, which ships
|
|
45
|
+
the pagination state and APIs but no `enable` option.
|
|
46
|
+
|
|
47
|
+
## Replacing the pager
|
|
48
|
+
|
|
49
|
+
`TMDataGrid.Footer` takes a `renderPagination` slot, handed three things: the
|
|
50
|
+
`state` the pager is showing, the `actions` it can take, and `Controls` - the
|
|
51
|
+
built-in pieces, already wired.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
<TMDataGrid.Footer
|
|
55
|
+
renderPagination={({ state, actions, Controls }) => (
|
|
56
|
+
<Group>
|
|
57
|
+
<Controls.PageSize />
|
|
58
|
+
<Button onClick={actions.previousPage} disabled={!state.canPreviousPage}>
|
|
59
|
+
Back
|
|
60
|
+
</Button>
|
|
61
|
+
<Text>
|
|
62
|
+
{state.pageIndex + 1} / {state.pageCount}
|
|
63
|
+
</Text>
|
|
64
|
+
<Button onClick={actions.nextPage} disabled={!state.canNextPage}>
|
|
65
|
+
Next
|
|
66
|
+
</Button>
|
|
67
|
+
</Group>
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The default footer renders `PageSize`, `Range` and `Pager`, in that order. A
|
|
73
|
+
control kept from `Controls` behaves as it does in the default footer, including
|
|
74
|
+
greying out while a grouping suspends paging.
|
|
75
|
+
|
|
76
|
+
| Member | Renders |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `Controls.PageSize` | "Rows per page" and its select |
|
|
79
|
+
| `Controls.Range` | The "1–25 of 300" label |
|
|
80
|
+
| `Controls.PageNumber` | The "Page 3 of 200" label |
|
|
81
|
+
| `Controls.Pager` | The previous and next buttons |
|
|
82
|
+
|
|
83
|
+
`PageNumber` is not in the default footer.
|
|
84
|
+
It is the label a server-paged grid usually shows in place of a row range, so it is put in through the slot:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<TMDataGrid.Footer
|
|
88
|
+
renderPagination={({ Controls }) => (
|
|
89
|
+
<>
|
|
90
|
+
<Controls.PageSize />
|
|
91
|
+
<Controls.PageNumber />
|
|
92
|
+
<Controls.Pager />
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
/>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
It reads `pageCount`, so a grid declaring `pageCount: -1` shows "Page 3" alone.
|
|
99
|
+
|
|
100
|
+
`state` carries `pageIndex`, `pageSize`, `pageCount`, `rowCount`,
|
|
101
|
+
`canPreviousPage`, `canNextPage`, the `from` / `to` bounds of the current page,
|
|
102
|
+
and `isPagingActive`. `actions` carries `setPageIndex`, `setPageSize`,
|
|
103
|
+
`previousPage`, `nextPage`, `firstPage` and `lastPage`.
|
|
104
|
+
|
|
105
|
+
`getTMDataGridPaginationApi(table)` returns the same `{ state, actions }`
|
|
106
|
+
outside the Footer, for a pager that lives elsewhere on the page. `Controls` is
|
|
107
|
+
not included: those components are bound to the grid's context and work only
|
|
108
|
+
inside the slot.
|
|
109
|
+
|
|
110
|
+
## Grouping
|
|
111
|
+
|
|
112
|
+
While a column is grouped the pager greys itself out and the range is replaced
|
|
113
|
+
with `Grouped · all N rows`. See
|
|
114
|
+
[Grouping](/docs/grouping#grouping-and-pagination).
|
|
115
|
+
|
|
116
|
+
A custom pager can grey itself out the same way:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<TMDataGrid.Footer
|
|
120
|
+
renderPagination={({ state, actions }) => (
|
|
121
|
+
<MyPager {...state} {...actions} disabled={!state.isPagingActive} />
|
|
122
|
+
)}
|
|
123
|
+
/>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`isPagingActive` is live state: whether the pager is currently slicing
|
|
127
|
+
anything. `getGridCapabilities(...).canPaginate` is configuration: whether
|
|
128
|
+
paging is switched on at all. The two differ while a grouping is active.
|
|
129
|
+
|
|
130
|
+
## Reference
|
|
131
|
+
|
|
132
|
+
| Name | Kind | Type | Default | What it does |
|
|
133
|
+
| --- | --- | --- | --- | --- |
|
|
134
|
+
| `enablePagination` | Option | `boolean` | `false` | Client-side paging and the Footer's pager. Grid-defined. |
|
|
135
|
+
| `manualPagination` | Table option | `boolean` | `false` | The server pages. Implies `enablePagination`. |
|
|
136
|
+
| `rowCount` | Table option | `number` | – | The true total, required under `manualPagination`. |
|
|
137
|
+
| `initialState.pagination` | Table option | `{ pageIndex, pageSize }` | `{ 0, 25 }` | Where paging starts. A data slice, so it persists. |
|
|
138
|
+
| `onPaginationChange` | Table option | `OnChangeFn` | – | Controls the pagination state. |
|
|
139
|
+
| `resetPageOnQueryChange` | Option | `boolean` | `true` | Back to page 1 when a filter, the quick search, the sort or the grouping changes. `false` keeps the page. See [Server-side data](/docs/server-side#the-page-index). |
|
|
140
|
+
| `TMDataGrid.Footer` | Component | – | – | The footer bar. Renders nothing when paging is off. |
|
|
141
|
+
| `Footer` `renderPagination` | Slot | `({ state, actions, Controls }) => ReactNode` | Built-in pager | Replaces the pager, and hands over its pieces. |
|
|
142
|
+
| `getTMDataGridPaginationApi` | Export | `(table) => { state, actions }` | – | The pager API, outside the Footer. |
|
|
143
|
+
| `TMDataGridPaginationState` · `TMDataGridPaginationActions` · `TMDataGridPaginationControls` | Exports | types | – | The three parts of the slot's argument. |
|
|
144
|
+
| `isPagingActive` | Export | `(table, features) => boolean` | – | Whether the pager is slicing anything right now. |
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Persistence
|
|
2
|
+
|
|
3
|
+
Restores table state on mount and writes it back on every change, so users
|
|
4
|
+
return to the grid as they left it.
|
|
5
|
+
|
|
6
|
+
State is split across **two keys**: `settingsKey` for the column layout, which
|
|
7
|
+
stays valid indefinitely, and `dataKey` for filters, sorting and pagination,
|
|
8
|
+
which go stale as the data underneath them changes. Either can be cleared
|
|
9
|
+
without touching the other.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
// Module scope: the object is a dependency of the write subscription.
|
|
13
|
+
const persist = {
|
|
14
|
+
dataKey: "employees.data",
|
|
15
|
+
settingsKey: "employees.settings",
|
|
16
|
+
} satisfies TMDataGridPersistence;
|
|
17
|
+
|
|
18
|
+
const grid = useTMDataGrid({ data, columns, persist });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Both keys are optional - pass only `settingsKey` to remember the layout but
|
|
22
|
+
never the filters.
|
|
23
|
+
|
|
24
|
+
```demo
|
|
25
|
+
file: data/Persistence.tsx
|
|
26
|
+
hint: Sort, filter and hide a column, then reload the page. It all comes back; the page index does not.
|
|
27
|
+
extraSources: data/employeeColumns.tsx
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## The two groups
|
|
31
|
+
|
|
32
|
+
| Group | Slices | Lifetime |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `dataKey` | `columnFilters`, `globalFilter`, `sorting`, `pagination`, `expanded` | As long as the data means the same thing |
|
|
35
|
+
| `settingsKey` | `columnVisibility`, `columnSizing`, `columnOrder`, `columnPinning`, `grouping` | Indefinite. This is the user's layout |
|
|
36
|
+
|
|
37
|
+
`DATA_STATE_SLICES` and `SETTINGS_STATE_SLICES` are exported with the same
|
|
38
|
+
values. Slice names are typed per group, so only valid names compile.
|
|
39
|
+
|
|
40
|
+
Settings saved by 1.x with the old `columnPinning` keys `left` and `right` are
|
|
41
|
+
read and migrated to `start` and `end`.
|
|
42
|
+
|
|
43
|
+
## Persisting only some of it
|
|
44
|
+
|
|
45
|
+
A key on its own persists every slice in its group. Pass a tuple to narrow it:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
const persist = {
|
|
49
|
+
// Restore filters and sorting, but always start on the first page.
|
|
50
|
+
dataKey: ["employees.data", ["columnFilters", "sorting"]],
|
|
51
|
+
// Restore column layout but not widths.
|
|
52
|
+
settingsKey: ["employees.settings", ["columnVisibility", "columnOrder"]],
|
|
53
|
+
storageMode: "sessionStorage",
|
|
54
|
+
} satisfies TMDataGridPersistence;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Only the selected slices are read back, so a payload written before you narrowed
|
|
58
|
+
the selection cannot reintroduce slices you have since opted out of.
|
|
59
|
+
|
|
60
|
+
## Behaviour
|
|
61
|
+
|
|
62
|
+
**Restoring happens once**, on mount, through `initialState`. Writing is a
|
|
63
|
+
subscription to the table store, so state changed directly through the table
|
|
64
|
+
API is persisted too.
|
|
65
|
+
|
|
66
|
+
**A payload from another version is dropped whole**, not migrated. Payloads
|
|
67
|
+
carry the exported `PERSIST_PAYLOAD_VERSION`, and anything else, including
|
|
68
|
+
everything written by a 0.x build, which had no stamp, is discarded.
|
|
69
|
+
|
|
70
|
+
**Restored state is realigned against the columns that exist.** Entries naming a
|
|
71
|
+
column removed between deploys are dropped: a stale id in the order, a width for
|
|
72
|
+
a column that no longer exists, or a sort or filter that would be active with no
|
|
73
|
+
column to show it. New columns need no handling, since TanStack appends columns
|
|
74
|
+
missing from `columnOrder` in definition order.
|
|
75
|
+
|
|
76
|
+
**Storage access is guarded.** If storage is unavailable, disabled or full,
|
|
77
|
+
persistence is skipped rather than throwing.
|
|
78
|
+
|
|
79
|
+
**Keys are not namespaced.** Include a tenant or user identifier if several
|
|
80
|
+
people can share a browser profile.
|
|
81
|
+
|
|
82
|
+
## Resetting
|
|
83
|
+
|
|
84
|
+
`resetSettings()` puts the settings state back to what a first visit with clean
|
|
85
|
+
storage would have shown (your `initialState` plus the structural lanes), and,
|
|
86
|
+
with persistence configured, writes through to storage like any other change.
|
|
87
|
+
The columns panel's **Reset layout** button calls it.
|
|
88
|
+
|
|
89
|
+
TanStack's own `resetColumnX()` family cannot do it on a persisted grid: those
|
|
90
|
+
reset to `initialState`, which the mount built *from* the restored payload.
|
|
91
|
+
|
|
92
|
+
## Relation to Mantine
|
|
93
|
+
|
|
94
|
+
Persistence does not use Mantine's `useLocalStorage`; the table owns the state
|
|
95
|
+
and storage only mirrors it. The option names follow Mantine's
|
|
96
|
+
`UseStorageOptions` where they apply, and `storageMode` takes the same values as
|
|
97
|
+
its `StorageType`.
|
|
98
|
+
|
|
99
|
+
## Reference
|
|
100
|
+
|
|
101
|
+
| Name | Kind | Type | Default | What it does |
|
|
102
|
+
| --- | --- | --- | --- | --- |
|
|
103
|
+
| `persist` | Option | `TMDataGridPersistence` | – | The whole configuration. Keep it referentially stable. |
|
|
104
|
+
| `dataKey` | persist field | `string \| [string, DataSlice[]]` | – | Storage key for the data group. |
|
|
105
|
+
| `settingsKey` | persist field | `string \| [string, SettingsSlice[]]` | – | Storage key for the settings group. |
|
|
106
|
+
| `storageMode` | persist field | `"localStorage" \| "sessionStorage"` | `"localStorage"` | Storage area. `"sessionStorage"` is per tab. |
|
|
107
|
+
| `serialize` | persist field | `(value) => string` | `JSON.stringify` | Serializes a payload before storing. |
|
|
108
|
+
| `deserialize` | persist field | `(value: string) => unknown` | `JSON.parse` | Parses a stored payload. |
|
|
109
|
+
| `resetSettings` | Hook return | `() => void` | – | Back to a clean first visit, written through to storage. |
|
|
110
|
+
| `DATA_STATE_SLICES` · `SETTINGS_STATE_SLICES` | Exports | `string[]` | – | The slice names of each group. |
|
|
111
|
+
| `PERSIST_PAYLOAD_VERSION` | Export | `number` | – | The stamp. A payload from another version is dropped. |
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# A portfolio rebalancer
|
|
2
|
+
|
|
3
|
+
A holdings book where one column is a decision and the rest are consequences.
|
|
4
|
+
The user edits **Target**; the grid recomputes drift and the trade to place, totals each sector, and refuses an edit that would allocate more than the whole book.
|
|
5
|
+
|
|
6
|
+
```demo
|
|
7
|
+
file: recipes/PortfolioRebalancer.tsx
|
|
8
|
+
hint: Type 40 into a Target cell - the commit is refused with the total it would have produced.
|
|
9
|
+
height: 620
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Deriving the rows
|
|
13
|
+
|
|
14
|
+
`accessorFn` is handed one row, so a column whose value depends on the other rows - a weight as a share of the portfolio - cannot be written as one.
|
|
15
|
+
Derive the whole collection once and hand the grid the finished shape:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const positions = useMemo(() => {
|
|
19
|
+
const valued = holdings.map((h) => ({ ...h, marketValue: h.price * h.shares }));
|
|
20
|
+
const total = valued.reduce((sum, h) => sum + h.marketValue, 0);
|
|
21
|
+
|
|
22
|
+
return valued.map((h) => ({
|
|
23
|
+
...h,
|
|
24
|
+
currentPct: (h.marketValue / total) * 100,
|
|
25
|
+
drift: h.targetPct - (h.marketValue / total) * 100,
|
|
26
|
+
}));
|
|
27
|
+
}, [holdings]);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`editing.onCommit` writes back to `holdings`, the source array, and the derived rows arrive through `data` on the next render.
|
|
31
|
+
Under `editing.mode: "cell"` with no draft store, every dependent column follows the keystroke that committed.
|
|
32
|
+
|
|
33
|
+
## Gating the one editable column
|
|
34
|
+
|
|
35
|
+
`editing.columns` lists what takes edits. Everything else is market data and stays read-only whatever its own meta says.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
editing: {
|
|
39
|
+
mode: "cell",
|
|
40
|
+
columns: ["targetPct"],
|
|
41
|
+
onCommit: ({ rowId, value }) => save(rowId, value.targetPct),
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## The rule that needs the other rows
|
|
46
|
+
|
|
47
|
+
A target weight is only valid against the rest of the book, so the rule is `editing.tableValidators`, not `meta.edit.validate`.
|
|
48
|
+
`rows` is the collection as it would stand if the commit landed, so the committing row's drafted value is already in it.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
tableValidators: {
|
|
52
|
+
onSubmit: ({ rows }) => {
|
|
53
|
+
const total = rows.reduce((sum, r) => sum + Number(r.value.targetPct ?? 0), 0);
|
|
54
|
+
|
|
55
|
+
return total > 100.005
|
|
56
|
+
? { fields: { targetPct: `Targets would total ${pct(total)}` } }
|
|
57
|
+
: undefined;
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The bound on a single cell - between 0 and 100 - stays on the column as [`meta.edit.validate`](/docs/editors), because it needs nothing but the value.
|
|
63
|
+
|
|
64
|
+
## Totals in two places
|
|
65
|
+
|
|
66
|
+
Sector totals come from `aggregationFn` on each column; the portfolio total comes from a `footer` and [`aggregateColumn`](/docs/summary-row), which follows the filters.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
columnHelper.accessor("marketValue", {
|
|
70
|
+
aggregationFn: "sum",
|
|
71
|
+
footer: ({ table }) =>
|
|
72
|
+
money.format(Number(aggregateColumn({ table, columnId: "marketValue" }))),
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Summing `targetPct` is meaningful only because the values are shares of one whole: a footer reading `100.0%` says the book is fully allocated.
|
|
77
|
+
|
|
78
|
+
## Reading drift
|
|
79
|
+
|
|
80
|
+
Drift is signed, so the cell renders a tint whose strength follows the size of the miss and whose colour follows its direction.
|
|
81
|
+
Rows more than three points out take `--row-bg` as well, which composes under hover and selection instead of replacing them.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<TMDataGrid.Table<Position>
|
|
85
|
+
rowStyle={(row) =>
|
|
86
|
+
!row.getIsGrouped() && Math.abs(row.original.drift) >= 3
|
|
87
|
+
? { "--row-bg": "color-mix(in srgb, var(--mantine-color-yellow-6) 10%, transparent)" }
|
|
88
|
+
: undefined
|
|
89
|
+
}
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A group row is handed to `rowStyle` too, and its `original` is an arbitrary child's record, so the callback guards with `row.getIsGrouped()`.
|
|
94
|
+
[Row styling](/docs/row-styling) covers the rest of the vocabulary.
|