@jielga/tmdatagrid 2.2.0 → 2.4.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 (50) hide show
  1. package/dist/index.d.ts +302 -11
  2. package/dist/index.js +2338 -2097
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/docs/columns.md +9 -1
  6. package/docs/components.md +1 -0
  7. package/docs/filtering.md +5 -1
  8. package/docs/grouping.md +1 -1
  9. package/docs/localization.md +3 -3
  10. package/docs/persistence.md +189 -2
  11. package/docs/row-details.md +5 -0
  12. package/docs/row-interaction.md +1 -1
  13. package/docs/row-styling.md +17 -0
  14. package/docs/typed-deep-links.md +169 -0
  15. package/docs/use-tm-data-grid.md +6 -2
  16. package/package.json +1 -1
  17. package/skills/appearance/SKILL.md +1 -1
  18. package/skills/appearance/references/labels.md +3 -3
  19. package/skills/cell-selection/SKILL.md +1 -1
  20. package/skills/columns/SKILL.md +6 -3
  21. package/skills/columns/references/columns-api.md +1 -0
  22. package/skills/data/SKILL.md +1 -1
  23. package/skills/editing/SKILL.md +1 -1
  24. package/skills/filtering/SKILL.md +26 -13
  25. package/skills/getting-started/SKILL.md +2 -2
  26. package/skills/grouping/SKILL.md +2 -2
  27. package/skills/migrating-to-2/SKILL.md +1 -1
  28. package/skills/options/SKILL.md +105 -10
  29. package/skills/options/references/typed-links.md +134 -0
  30. package/skills/rows/SKILL.md +18 -9
  31. package/skills/server-side/SKILL.md +1 -1
  32. package/skills/testing/SKILL.md +1 -1
  33. package/skills/testing-components/SKILL.md +1 -1
  34. package/skills/testing-editing/SKILL.md +1 -1
  35. package/src/components/TMDataGridFilterPills.module.css +9 -7
  36. package/src/components/TMDataGridFilterPills.tsx +15 -1
  37. package/src/components/TMDataGridGroupColumn.tsx +19 -4
  38. package/src/components/TMDataGridHeaderFilterRow.tsx +7 -1
  39. package/src/components/TMDataGridTable.module.css +15 -6
  40. package/src/components/TMDataGridTable.tsx +23 -11
  41. package/src/components/sticky.module.css +27 -7
  42. package/src/core/filterControls.ts +8 -0
  43. package/src/core/filterOperators.ts +54 -18
  44. package/src/core/grouping.ts +11 -3
  45. package/src/core/labels.ts +4 -3
  46. package/src/core/persistence.ts +330 -26
  47. package/src/core/urlState.ts +439 -0
  48. package/src/core/valueText.ts +46 -0
  49. package/src/index.ts +14 -0
  50. package/src/useTMDataGrid.tsx +145 -21
@@ -0,0 +1,169 @@
1
+ # Typed links with TanStack Router
2
+
3
+ Links into a grid, built from the grid's own columns and checked by both the grid's types and the router's.
4
+ The grid's URL param lives in the router's search, and the router encodes it.
5
+
6
+ ```demo
7
+ file: recipes/RouterDeepLinks.tsx
8
+ hint: Go to Links and open one. Change a filter or the search, and watch the address follow.
9
+ extraSources: data/employeeLinks.tsx, data/EmployeeLinksGrid.tsx
10
+ height: 640
11
+ ```
12
+
13
+ ## Define the links from the columns
14
+
15
+ Build the columns with `createTMDataGridColumnHelper`, and pass the same array to `createTMDataGridUrlState`:
16
+
17
+ ```tsx
18
+ export const employeesLink = createTMDataGridUrlState({
19
+ key: "emp",
20
+ columns: employeeLinkColumns,
21
+ slices: ["columnFilters", "globalFilter", "sorting"],
22
+ });
23
+ ```
24
+
25
+ The columns decide what a link may name.
26
+ In this demo, `location` sets `meta.filter.url: false`, so no link can filter it.
27
+ [Typed links](/docs/persistence#typed-links) lists the rules.
28
+
29
+ ## Validate the route's search
30
+
31
+ Pass the definition's `validateSearch` to the route that renders the grid:
32
+
33
+ ```tsx
34
+ const employeesRoute = createRoute({
35
+ getParentRoute: () => rootRoute,
36
+ path: "/employees",
37
+ validateSearch: employeesLink.validateSearch,
38
+ component: EmployeesPage,
39
+ });
40
+ ```
41
+
42
+ A route with search params of its own spreads it into its validator:
43
+
44
+ ```tsx
45
+ validateSearch: (search) => ({
46
+ ...employeesLink.validateSearch(search),
47
+ tab: search.tab === "chart" ? "chart" : "table",
48
+ }),
49
+ ```
50
+
51
+ ## Keep the grid's param in the router
52
+
53
+ Pass the router's search to the definition's `persist`:
54
+
55
+ ```tsx
56
+ const router = useRouter();
57
+ const persist = useMemo(
58
+ () =>
59
+ employeesLink.persist({
60
+ get: (name) => (router.state.location.search as Record<string, unknown>)[name],
61
+ set: (name, value) =>
62
+ void router.navigate({
63
+ to: ".",
64
+ search: (prev: Record<string, unknown>) => ({ ...prev, [name]: value }),
65
+ replace: true,
66
+ }),
67
+ }),
68
+ [router],
69
+ );
70
+ ```
71
+
72
+ The grid writes through `router.navigate`, so the router sees every change.
73
+ `replace: true` keeps a filter change out of the history, as the grid's own URL writes do.
74
+ With `createHashHistory()`, the param is written after the `#`, with the rest of the router's search.
75
+
76
+ ## Link into the grid
77
+
78
+ Pass `search` to the link:
79
+
80
+ ```tsx
81
+ <Link
82
+ to="/employees"
83
+ search={employeesLink.search({
84
+ filters: {
85
+ salary: { operator: "between", value: [40_000, 60_000] },
86
+ status: { operator: "isAnyOf", value: ["Active"] },
87
+ },
88
+ sorting: [{ id: "salary", desc: true }],
89
+ })}
90
+ >
91
+ Active, highest salary first
92
+ </Link>
93
+ ```
94
+
95
+ `search` fails to compile on a column the grid does not have, an operator the column does not offer, or a value of the wrong type.
96
+ The router then checks the result against the route's search.
97
+
98
+ ## Readable URLs
99
+
100
+ By default, TanStack Router writes the param as JSON and escapes it with `URLSearchParams`, so every brace and quote is percent-escaped.
101
+ To write it readable, encode the values with [JSURL2](https://github.com/wmertens/jsurl2) and escape them with `encodeURIComponent`, which leaves JSURL2's `(`, `)`, `~` and `!` as they are:
102
+
103
+ ```tsx
104
+ import { parse, stringify } from "jsurl2";
105
+
106
+ function toSearchValue(value: unknown): string {
107
+ if (typeof value === "object" && value !== null) return stringify(value);
108
+ if (typeof value !== "string") return String(value);
109
+ try {
110
+ parse(value);
111
+ return stringify(value);
112
+ } catch {
113
+ return value;
114
+ }
115
+ }
116
+
117
+ function stringifySearch(search: Record<string, unknown>): string {
118
+ const pairs = Object.entries(search).flatMap(([key, value]) =>
119
+ value === undefined
120
+ ? []
121
+ : [`${encodeURIComponent(key)}=${encodeURIComponent(toSearchValue(value))}`],
122
+ );
123
+ return pairs.length === 0 ? "" : `?${pairs.join("&")}`;
124
+ }
125
+
126
+ const router = createRouter({
127
+ routeTree,
128
+ parseSearch: parseSearchWith(parse),
129
+ stringifySearch,
130
+ });
131
+ ```
132
+
133
+ TanStack Router's own `stringifySearchWith(stringify)` encodes the values as JSURL2 too, but its `URLSearchParams` escapes JSURL2's delimiters again.
134
+
135
+ ```demo
136
+ file: recipes/ReadableSearch.tsx
137
+ hint: The same links as above. Compare the two addresses.
138
+ extraSources: data/employeeLinks.tsx, data/EmployeeLinksGrid.tsx
139
+ height: 600
140
+ ```
141
+
142
+ The serializer applies to every route, and the grid is unaffected.
143
+ JSURL2's `parse` also reads JSON, so a link written before the switch still opens.
144
+
145
+ ## Links without a router
146
+
147
+ Without a router, leave out the storage: the grid reads and writes the browser's URL.
148
+ Build a link with `searchParams`:
149
+
150
+ ```tsx
151
+ const persist = employeesLink.persist();
152
+
153
+ const href = `/employees?${employeesLink.searchParams({
154
+ filters: { department: { operator: "isAnyOf", value: ["Design"] } },
155
+ })}`;
156
+ ```
157
+
158
+ ```demo
159
+ file: recipes/PlainLinks.tsx
160
+ hint: Each link loads this page with the grid's param set.
161
+ extraSources: data/employeeLinks.tsx
162
+ height: 560
163
+ ```
164
+
165
+ ## Links to the page the grid is on
166
+
167
+ The grid reads its param once, when it mounts.
168
+ Note that a link to the route the grid is already on changes the URL but not the grid.
169
+ Link to such a route from another route, or remount the grid on the navigation.
@@ -181,13 +181,16 @@ toolbar components can read the same strings as the built-in parts.
181
181
  ## persist
182
182
 
183
183
  Restores table state on mount and writes it back on every change. State is
184
- split across two keys: `settingsKey` for the column layout, `dataKey` for
185
- filters, sorting and pagination. Either can be cleared without the other.
184
+ split across three keys: `settingsKey` for the column layout, `dataKey` for
185
+ filters, sorting and pagination, and `urlKey` for data slices kept in the URL
186
+ as a deep link. Each can be cleared without the others.
186
187
 
187
188
  | Field | Type | Default | Description |
188
189
  | --- | --- | --- | --- |
189
190
  | `dataKey` | `string \| [string, DataSlice[]]` | – | Storage key for the data group. |
190
191
  | `settingsKey` | `string \| [string, SettingsSlice[]]` | – | Storage key for the settings group. |
192
+ | `urlKey` | `string \| [string, DataSlice[]]` | – | Search param for data slices kept in the URL. See [Deep links](/docs/persistence#deep-links). |
193
+ | `urlStorage` | `TMDataGridUrlStorage` | `location.search` and `history.replaceState` | Reads and writes the URL param. |
191
194
  | `storageMode` | `"localStorage" \| "sessionStorage"` | `"localStorage"` | Storage area. Use `"sessionStorage"` for per-tab state. |
192
195
  | `serialize` | `(value) => string` | `JSON.stringify` | Serializes a payload before storing. |
193
196
  | `deserialize` | `(value: string) => unknown` | `JSON.parse` | Parses a stored payload. |
@@ -222,6 +225,7 @@ const persist = {
222
225
  | Group | Available slices |
223
226
  | --- | --- |
224
227
  | `dataKey` | `columnFilters`, `globalFilter`, `sorting`, `pagination`, `expanded` |
228
+ | `urlKey` | The data slices. A slice listed here is taken out of `dataKey`. |
225
229
  | `settingsKey` | `columnVisibility`, `columnSizing`, `columnOrder`, `columnPinning`, `grouping` |
226
230
 
227
231
  The exported `DATA_STATE_SLICES` and `SETTINGS_STATE_SLICES` arrays contain the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jielga/tmdatagrid",
3
- "version": "2.2.0",
3
+ "version": "2.4.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",
@@ -18,7 +18,7 @@ description: >
18
18
  metadata:
19
19
  type: core
20
20
  library: '@jielga/tmdatagrid'
21
- library_version: '2.2.0'
21
+ library_version: '2.4.0'
22
22
  sources:
23
23
  - 'Jielga/react:packages/tmdatagrid/docs/styling.md'
24
24
  - 'Jielga/react:packages/tmdatagrid/docs/toolbar.md'
@@ -44,8 +44,8 @@ The `labels` option takes a partial override, `TMDataGridLabelsOverride`, merged
44
44
  | `filterFrom` | `From` | The lower bound of a `between` filter: its input, or its slider thumb. |
45
45
  | `filterTo` | `To` | The upper bound of a `between` filter: its input, or its slider thumb. |
46
46
  | `filterAll` | `All` | The no-filter segment of the tri-state filter. |
47
- | `booleanTrue` | `Yes` | The `true` choice of a boolean filter value control and of the tri-state filter. |
48
- | `booleanFalse` | `No` | The `false` choice of a boolean filter value control and of the tri-state filter. |
47
+ | `booleanTrue` | `Yes` | The `true` choice of a boolean filter value control and of the tri-state filter, and a `true` value on a filter pill and in a group row. |
48
+ | `booleanFalse` | `No` | The `false` choice of a boolean filter value control and of the tri-state filter, and a `false` value on a filter pill and in a group row. |
49
49
  | `addFilter` | `Add filter` | Button in `TMDataGrid.FilterPanel`. |
50
50
  | `clearAllFilters` | `Clear all` | Button in `TMDataGrid.FilterPanel` and in `TMDataGrid.FilterPills`. |
51
51
  | `closeFilters` | `Close filters` | `aria-label` of the close button of the filter popup and sidebar. |
@@ -67,7 +67,7 @@ The `labels` option takes a partial override, `TMDataGridLabelsOverride`, merged
67
67
  | `moveRight` | `Move right` | Column menu item. |
68
68
  | `autosizeColumn` | `Autosize column` | Column menu item. |
69
69
  | `hideColumn` | `Hide column` | Column menu item. |
70
- | `filterOn` | `` `Filter on ${column}` `` | `aria-label` of the header filter button, of a header filter cell, and of the value control in it. |
70
+ | `filterOn` | `` `Filter on ${column}` `` | `aria-label` of the header filter button, of the header filter cell of a column that can filter, and of the value control in it. |
71
71
  | `sortColumn` | `` `Sort ${column}` `` | `aria-label` of the header sort button. |
72
72
  | `columnMenu` | `` `${column} column menu` `` | `aria-label` of the column menu trigger. |
73
73
  | `rowsPerPage` | `Rows per page:` | `TMDataGrid.Footer`: the text before the page size select. |
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.2.0'
17
+ library_version: '2.4.0'
18
18
  sources:
19
19
  - 'Jielga/react:packages/tmdatagrid/docs/cell-selection.md'
20
20
  - 'Jielga/react:packages/tmdatagrid/src/core/cellNavigation.ts'
@@ -4,7 +4,7 @@ description: >
4
4
  Define and arrange TMDataGrid columns. Covers createTMDataGridColumnHelper,
5
5
  every column meta field (label, type, options, flex, align, colSpan, autoSize,
6
6
  enableOrdering, and the meta.filter and meta.edit namespaces holding
7
- operators, defaultOperator, control, enabled, field, editor, validate and
7
+ operators, defaultOperator, control, url, enabled, field, editor, validate and
8
8
  mapValue), the six column types, fluid minmax sizing versus fixed width with
9
9
  minSize / maxSize / size, autosizing and autosizeColumn, hiding through
10
10
  enableHiding and the columns panel, pinning and why a pinned column becomes
@@ -16,7 +16,7 @@ description: >
16
16
  metadata:
17
17
  type: core
18
18
  library: '@jielga/tmdatagrid'
19
- library_version: '2.2.0'
19
+ library_version: '2.4.0'
20
20
  sources:
21
21
  - 'Jielga/react:packages/tmdatagrid/docs/columns.md'
22
22
  - 'Jielga/react:packages/tmdatagrid/docs/column-layout.md'
@@ -61,6 +61,9 @@ replaces the dots: `accessorKey: "address.city"` makes column id
61
61
  addresses the column by id - `initialState`, `editing.columns`, `moveColumn`,
62
62
  a test selector - takes the underscore form.
63
63
 
64
+ Helper columns, groups included, carry their id, `meta.type`, value type, `meta.filter.operators` and filter, sort and URL switches in their type (`TMDataGridColumnInfo`).
65
+ `createTMDataGridUrlState` (the `options` skill) reads them to type deep links; only literal values count.
66
+
64
67
  `columnHelper.group` nests columns under a shared header; `columns` takes
65
68
  another `columnHelper.columns` call, not a bare array:
66
69
 
@@ -94,7 +97,7 @@ What the column **is** stays flat; what the filter panel and the edit engine do
94
97
  with it sits in their namespaces. `type` and `options` are read by both stages,
95
98
  which is why they are in neither.
96
99
 
97
- `meta.filter` holds `operators`, `defaultOperator` and `control`; `meta.edit`
100
+ `meta.filter` holds `operators`, `defaultOperator`, `control` and `url`; `meta.edit`
98
101
  holds `enabled`, `field`, `editor`, `validate` and `mapValue`. Their fields are
99
102
  in [references/columns-api.md](references/columns-api.md).
100
103
 
@@ -11,6 +11,7 @@ Reference for the `columns` skill.
11
11
  | `operators` | `readonly TMDataGridFilterOperator[]` | The type's list | The operators this column offers, a subset of its type's. For a backend that answers only some. |
12
12
  | `defaultOperator` | `TMDataGridFilterOperator` | The type's default, else the first offered | The operator a fresh filter on this column starts with. |
13
13
  | `control` | `TMDataGridFilterControlComponent` | By `meta.type` | Replaces the value control in this column's filter row. Module scope. |
14
+ | `url` | `boolean` | `true` | Whether the column's filter is written to the URL under `persist.urlKey`. `false` keeps it out of the link, and out of storage. |
14
15
 
15
16
  `meta.edit` belongs to the `editing` skill, and only acts once `editing` is set:
16
17
 
@@ -18,7 +18,7 @@ description: >
18
18
  metadata:
19
19
  type: core
20
20
  library: '@jielga/tmdatagrid'
21
- library_version: '2.2.0'
21
+ library_version: '2.4.0'
22
22
  sources:
23
23
  - 'Jielga/react:packages/tmdatagrid/docs/pagination.md'
24
24
  - 'Jielga/react:packages/tmdatagrid/docs/scrolling.md'
@@ -18,7 +18,7 @@ description: >
18
18
  metadata:
19
19
  type: core
20
20
  library: '@jielga/tmdatagrid'
21
- library_version: '2.2.0'
21
+ library_version: '2.4.0'
22
22
  sources:
23
23
  - 'Jielga/react:packages/tmdatagrid/docs/editing.md'
24
24
  - 'Jielga/react:packages/tmdatagrid/docs/draft-store.md'
@@ -4,21 +4,21 @@ 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
6
  meta.type offers each, meta.filter.operators to offer a column only a subset
7
- of them, meta.filter.defaultOperator, isFilterActive and the half-typed
8
- filter, the filters option and its surfaces (popup, sidebar, none, plus
9
- inHeader for header filters), TMDataGrid.FilterPanel and its layout prop,
10
- TMDataGrid.FilterButton, TMDataGrid.FilterPills with its api prop,
11
- openColumnFilter, replacing a value control with DgRangeSliderFilter /
12
- DgDateRangeFilter / DgAutocompleteFilter / DgTriStateFilter or a
13
- meta.filter.control component, per-column filterFn, and the quick search:
14
- TMDataGrid.Search, quickSearchMode fuzzy or contains, fuzzyGlobalFilterFn,
15
- enableMatchHighlighting and enableGlobalFilter. Load when adding filters,
16
- choosing operators, building a filter control, showing active filters outside
17
- the grid, or wiring a search box.
7
+ of them, meta.filter.defaultOperator, meta.filter.url for deep links,
8
+ isFilterActive and the half-typed filter, the filters option and its surfaces
9
+ (popup, sidebar, none, plus inHeader for header filters),
10
+ TMDataGrid.FilterPanel and its layout prop, TMDataGrid.FilterButton,
11
+ TMDataGrid.FilterPills with its api prop, openColumnFilter, replacing a value
12
+ control with DgRangeSliderFilter / DgDateRangeFilter / DgAutocompleteFilter /
13
+ DgTriStateFilter or a meta.filter.control component, per-column filterFn, and
14
+ the quick search: TMDataGrid.Search, quickSearchMode fuzzy or contains,
15
+ fuzzyGlobalFilterFn, enableMatchHighlighting and enableGlobalFilter. Load when
16
+ adding filters, choosing operators, building a filter control, showing active
17
+ filters outside the grid, or wiring a search box.
18
18
  metadata:
19
19
  type: core
20
20
  library: '@jielga/tmdatagrid'
21
- library_version: '2.2.0'
21
+ library_version: '2.4.0'
22
22
  sources:
23
23
  - 'Jielga/react:packages/tmdatagrid/docs/filtering.md'
24
24
  - 'Jielga/react:packages/tmdatagrid/docs/quick-search.md'
@@ -105,6 +105,18 @@ columnHelper.accessor("customer", {
105
105
  `getColumnDefaultOperator(column)` the operator a fresh filter opens on. The
106
106
  `server-side` skill shows the list typed together with the API mapping table.
107
107
 
108
+ `meta.filter.url` determines whether the column's filter goes into the URL
109
+ search param when `persist.urlKey` carries `columnFilters`. By default it does.
110
+ Set it to `false` for a filter that should not travel in a shared link; the
111
+ filter is then not persisted anywhere. The `options` skill covers `urlKey`.
112
+
113
+ ```tsx
114
+ columnHelper.accessor("owner", {
115
+ header: "Owner",
116
+ meta: { filter: { url: false } },
117
+ });
118
+ ```
119
+
108
120
  ## The filters option
109
121
 
110
122
  `filters` on `useTMDataGrid` picks the surface. It is read field by field, so an
@@ -185,6 +197,7 @@ No props.
185
197
 
186
198
  One pill per **active** filter, `First name: Sofia ✕`, where the ✕ clears it and a click on the label calls `openColumnFilter`.
187
199
  The label spells the operator out unless it is the type's default: `Age is greater than 30`, but `First name: Sofia`.
200
+ A value with an option reads as the option's label, and a boolean value as `labels.booleanTrue` or `labels.booleanFalse`.
188
201
  It takes the grid as an `api` prop instead of reading context, so it renders anywhere on the page.
189
202
 
190
203
  ```tsx
@@ -469,7 +482,7 @@ Source: `packages/tmdatagrid/docs/quick-search.md` (Fuzzy by default).
469
482
  | `getOperatorsForType` | Export | `(type) => operators` | – | The operator list a type offers. |
470
483
  | `getColumnOperators` · `getColumnDefaultOperator` | Exports | `(column) => operators` · `(column) => operator` | – | One column's list after `meta.filter.operators`, and the operator a fresh filter on it opens on. |
471
484
  | `FILTER_OPERATOR_LABELS` | Export | record | – | The label shown for each operator. |
472
- | `formatFilterLabel` | Export | `({ label, type, filter }) => string` | – | The one-line description used on the pills. |
485
+ | `formatFilterLabel` | Export | `({ label, type, filter, operatorLabels?, options?, labels? }) => string` | – | The one-line description used on the pills. `operatorLabels` names the operators, `options` gives a value its option's label, and `labels` gives a boolean its `booleanTrue` / `booleanFalse`; each defaults to English or to the value itself. |
473
486
  | `emptyValueForOperator` · `operatorNeedsValue` · `operatorTakesArrayValue` · `operatorTakesRangeValue` | Exports | – | – | What shape of value an operator expects. |
474
487
  | `TMDataGridFilterValueInput` | Export | component | – | The default value control, for falling back to. |
475
488
  | `DgRangeSliderFilter` · `DgDateRangeFilter` · `DgAutocompleteFilter` · `DgTriStateFilter` | Exports | components | – | The four ready-made controls. |
@@ -13,7 +13,7 @@ description: >
13
13
  metadata:
14
14
  type: core
15
15
  library: '@jielga/tmdatagrid'
16
- library_version: '2.2.0'
16
+ library_version: '2.4.0'
17
17
  sources:
18
18
  - 'Jielga/react:packages/tmdatagrid/docs/getting-started.md'
19
19
  - 'Jielga/react:packages/tmdatagrid/docs/anatomy.md'
@@ -355,7 +355,7 @@ Source: `packages/tmdatagrid/docs/card-view.md`,
355
355
  | `openColumnFilter(api, columnId)` | Adds an empty filter for a column and opens the panel. |
356
356
  | `getColumnLabel(column)` | `meta.label`, a string header, or the column id. |
357
357
  | `getColumnType(column)` | `meta.type`, defaulting to `"string"`. |
358
- | `formatFilterLabel({ label, type, filter })` | The one-line filter description used on the pills. |
358
+ | `formatFilterLabel({ label, type, filter, operatorLabels?, options?, labels? })` | The one-line filter description used on the pills. |
359
359
  | `moveColumn({ table, columnId, targetId, side })` | Moves a column next to another. See the `columns` skill. |
360
360
  | `moveColumnByStep({ table, columnId, direction })` | Moves a column one position within its region. |
361
361
  | `SELECT_COLUMN_ID` | Id of the generated checkbox column. |
@@ -14,7 +14,7 @@ description: >
14
14
  metadata:
15
15
  type: core
16
16
  library: '@jielga/tmdatagrid'
17
- library_version: '2.2.0'
17
+ library_version: '2.4.0'
18
18
  sources:
19
19
  - 'Jielga/react:packages/tmdatagrid/docs/grouping.md'
20
20
  - 'Jielga/react:packages/tmdatagrid/docs/summary-row.md'
@@ -275,7 +275,7 @@ Source: `packages/tmdatagrid/docs/grouping.md` (Server-side grids).
275
275
  | `aggregateColumn` | Export | `({ table, columnId, fn }) => unknown` | `fn: "sum"` | Aggregates over every filtered row, all pages. |
276
276
  | `TMDataGridAggregationName` | Export | type | – | The registered function names. |
277
277
  | `GROUP_COLUMN_ID` | Export | `"__group__"` | – | Id of the generated tree column. |
278
- | `formatGroupValue` | Export | `(value) => string` | – | How the tree lane renders a group's value. |
278
+ | `formatGroupValue` | Export | `(value, blankLabel?, { type?, options?, labels? }?) => string` | – | How the tree lane writes a group's value: `blankLabel` for an empty value, an option's label for a value with an option, `labels.booleanTrue` / `booleanFalse` for a boolean, and `toLocaleDateString()` for a `Date`. |
279
279
  | `getGroupDataRows` | Export | `(row) => Row[]` | – | Every record under a group row, at any depth. |
280
280
  | `isPagingActive` | Export | `(table, features) => boolean` | – | Whether the pager is slicing anything. `false` while grouped. |
281
281
  | `--dg-row-group-bg` | CSS variable | colour | Themed | Group row background. |
@@ -15,7 +15,7 @@ description: >
15
15
  metadata:
16
16
  type: lifecycle
17
17
  library: '@jielga/tmdatagrid'
18
- library_version: '2.2.0'
18
+ library_version: '2.4.0'
19
19
  sources:
20
20
  - 'Jielga/react:packages/tmdatagrid/docs/migrating-to-2.md'
21
21
  - 'Jielga/react:packages/tmdatagrid/src/index.ts'
@@ -9,21 +9,23 @@ description: >
9
9
  labels, editing, filters, exportOptions, enableColumnOrdering,
10
10
  enablePagination, resetPageOnQueryChange, enableRowNumbers, selectionMode,
11
11
  showSelectedBackground, cellSelection, renderDetails, overscan,
12
- quickSearchMode), persist with dataKey/settingsKey slices and storageMode, the
13
- returned api (table, ui, edit, features, labels, resetSettings, scrollToRow),
14
- ui actions, and reading grid state with useSelector over table.store, since
15
- table.getState() does not exist in v9. Load when configuring the hook, looking
16
- up an option's default, persisting layout or filters, or reacting to grid
17
- state from a parent.
12
+ quickSearchMode), persist with dataKey/settingsKey slices, storageMode and
13
+ urlKey and createTMDataGridUrlState deep links, the returned api (table, ui,
14
+ edit, features, labels, resetSettings, scrollToRow), ui actions, and reading
15
+ grid state with useSelector over table.store, since table.getState() does not
16
+ exist in v9. Load when configuring the hook, looking up an option's default,
17
+ persisting layout or filters, or reacting to grid state from a parent.
18
18
  metadata:
19
19
  type: core
20
20
  library: '@jielga/tmdatagrid'
21
- library_version: '2.2.0'
21
+ library_version: '2.4.0'
22
22
  sources:
23
23
  - 'Jielga/react:packages/tmdatagrid/docs/use-tm-data-grid.md'
24
24
  - 'Jielga/react:packages/tmdatagrid/docs/persistence.md'
25
+ - 'Jielga/react:packages/tmdatagrid/docs/typed-deep-links.md'
25
26
  - 'Jielga/react:packages/tmdatagrid/src/useTMDataGrid.tsx'
26
27
  - 'Jielga/react:packages/tmdatagrid/src/core/persistence.ts'
28
+ - 'Jielga/react:packages/tmdatagrid/src/core/urlState.ts'
27
29
  ---
28
30
 
29
31
  # TMDataGrid - useTMDataGrid
@@ -160,14 +162,17 @@ const grid = useTMDataGrid({
160
162
  ## persist
161
163
 
162
164
  Restores table state on mount and writes it back on every change. State is split
163
- across two keys because the groups have different lifetimes: column configuration
164
- stays valid indefinitely, while filters and pagination go stale as the data
165
- changes. Separate keys let one group be cleared without touching the other.
165
+ across three keys because the groups have different lifetimes: column configuration
166
+ stays valid indefinitely, filters and pagination go stale as the data
167
+ changes, and a deep link has to travel with the page URL. Separate keys let one
168
+ group be cleared without touching the others.
166
169
 
167
170
  | Field | Type | Default | Description |
168
171
  | --- | --- | --- | --- |
169
172
  | `dataKey` | `TMDataGridPersistKey<TMDataGridDataSlice>`: `string \| [string, DataSlice[]]` | – | Storage key for the data group. |
170
173
  | `settingsKey` | `TMDataGridPersistKey<TMDataGridSettingsSlice>`: `string \| [string, SettingsSlice[]]` | – | Storage key for the settings group. |
174
+ | `urlKey` | `TMDataGridPersistKey<TMDataGridDataSlice>`: `string \| [string, DataSlice[]]` | – | Search param for data slices kept in the URL. See [Deep links](#deep-links). |
175
+ | `urlStorage` | `TMDataGridUrlStorage`: `{ get(name): string \| null; set(name, value: string \| null): void }` | `location.search` and `history.replaceState` | Reads and writes the URL param. |
171
176
  | `storageMode` | `TMDataGridStorageMode`: `"localStorage" \| "sessionStorage"` | `"localStorage"` | Storage area. `"sessionStorage"` is per tab. |
172
177
  | `serialize` | `(value) => string` | `JSON.stringify` | Serializes before storing. |
173
178
  | `deserialize` | `(value: string) => unknown` | `JSON.parse` | Parses a stored payload. |
@@ -203,6 +208,7 @@ const persist = {
203
208
  | Group | Available slices |
204
209
  | --- | --- |
205
210
  | `dataKey` | `columnFilters`, `globalFilter`, `sorting`, `pagination`, `expanded` |
211
+ | `urlKey` | The data slices. A slice listed here is taken out of `dataKey`. |
206
212
  | `settingsKey` | `columnVisibility`, `columnSizing`, `columnOrder`, `columnPinning`, `grouping` |
207
213
 
208
214
  `grouping` is a settings slice because it names columns rather than values, so it
@@ -227,6 +233,95 @@ dropped.
227
233
 
228
234
  Source: `packages/tmdatagrid/docs/persistence.md` (Behaviour).
229
235
 
236
+ ### Deep links
237
+
238
+ `urlKey` keeps data slices in one URL search param, so a link to the page opens
239
+ the grid on the same filters. The string is the param name; a bare key takes
240
+ every data slice, a tuple a subset, as with `dataKey`.
241
+
242
+ ```tsx
243
+ const persist = {
244
+ settingsKey: "employees.settings",
245
+ // Sorting is remembered per browser; the filters travel in the link.
246
+ dataKey: ["employees.data", ["sorting"]],
247
+ urlKey: ["employees", ["columnFilters", "globalFilter"]],
248
+ } satisfies TMDataGridPersistence;
249
+ ```
250
+
251
+ - A slice lives in one group. A slice listed under `urlKey` is taken out of
252
+ `dataKey`: a bare `dataKey` hands it over silently, while a `dataKey` tuple
253
+ that names the slice explicitly is reported with `console.error` on mount.
254
+ - The param carries only the slices whose value differs from the initial state
255
+ (`initialState` merged over the grid defaults), and only the column filters
256
+ that are active. When nothing differs, the param is removed. A link without the param, or without a slice, opens on the
257
+ initial state; a URL slice never falls back to storage.
258
+ - Writes use `history.replaceState`, never `pushState`. Only the grid's own
259
+ param changes; the path, the hash, other params and `history.state` are kept.
260
+ Two grids on one page use two keys.
261
+ - The URL payload has no version stamp, so a bookmarked link survives a
262
+ release. The shape guards and the realignment against existing columns apply
263
+ as for storage, and `serialize` / `deserialize` apply to the URL payload too.
264
+ - `meta.filter.url: false` on a column keeps its filter out of the param, on
265
+ read and on write. The column's filter is then not persisted anywhere, and a
266
+ filter on it in `initialState` still applies when a link is opened. The
267
+ flag has no effect unless `urlKey` carries `columnFilters`.
268
+ - `urlStorage` replaces the default read and write of the param, for a router
269
+ that owns the URL. `get` returns the param's string or `null`; `set` receives
270
+ the string, or `null` to remove the param. `set` is only called when the
271
+ value changed. Keep `persist` referentially stable here too: react-router
272
+ rebuilds `setSearchParams` on every URL change, so read it through a ref.
273
+
274
+ ```tsx
275
+ // react-router: let the router do the replace.
276
+ const [, setSearchParams] = useSearchParams();
277
+ const setSearchParamsRef = useRef(setSearchParams);
278
+ setSearchParamsRef.current = setSearchParams;
279
+
280
+ const persist = useMemo(
281
+ () =>
282
+ ({
283
+ urlKey: ["employees", ["columnFilters"]],
284
+ urlStorage: {
285
+ get: (name) => new URLSearchParams(window.location.search).get(name),
286
+ set: (name, value) =>
287
+ setSearchParamsRef.current(
288
+ (prev) => {
289
+ if (value === null) prev.delete(name);
290
+ else prev.set(name, value);
291
+ return prev;
292
+ },
293
+ { replace: true },
294
+ ),
295
+ },
296
+ }) satisfies TMDataGridPersistence,
297
+ [],
298
+ );
299
+ ```
300
+
301
+ Source: `packages/tmdatagrid/docs/persistence.md` (Deep links).
302
+
303
+ ### Typed links
304
+
305
+ `createTMDataGridUrlState({ key, columns, slices? })` defines a grid's deep
306
+ links from the columns the grid renders, built with
307
+ `createTMDataGridColumnHelper`. A link can name only the filters, operators,
308
+ values and sorts those columns offer; anything else is a type error. The
309
+ definition gives `urlKey` for `persist`, the link as `searchParams`, `search`
310
+ or `payload`, a route `validateSearch`, and `persist(storage?)`, which keeps
311
+ the param in a router's search such as TanStack Router's.
312
+
313
+ ```tsx
314
+ export const employeesUrl = createTMDataGridUrlState({ key: "employees", columns });
315
+
316
+ const persist = { urlKey: employeesUrl.urlKey } satisfies TMDataGridPersistence;
317
+ const href = `/employees?${employeesUrl.searchParams({
318
+ filters: { department: { operator: "isAnyOf", value: ["Design"] } },
319
+ })}`;
320
+ ```
321
+
322
+ Every rule, every member and the TanStack Router setup are in
323
+ [references/typed-links.md](references/typed-links.md).
324
+
230
325
  ## Return value
231
326
 
232
327
  | Field | Type | Description |