@jielga/tmdatagrid 0.1.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.
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/index.d.ts +592 -0
- package/dist/index.js +1590 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +2 -0
- package/package.json +111 -0
- package/skills/columns/SKILL.md +195 -0
- package/skills/features/SKILL.md +214 -0
- package/skills/getting-started/SKILL.md +209 -0
- package/skills/options/SKILL.md +208 -0
- package/skills/server-side/SKILL.md +150 -0
- package/src/tmdatagrid/TMDataGrid.module.css +410 -0
- package/src/tmdatagrid/TMDataGrid.tsx +101 -0
- package/src/tmdatagrid/TMDataGridColumnsPanel.tsx +91 -0
- package/src/tmdatagrid/TMDataGridContext.ts +32 -0
- package/src/tmdatagrid/TMDataGridFilterPanel.tsx +182 -0
- package/src/tmdatagrid/TMDataGridFooter.tsx +74 -0
- package/src/tmdatagrid/TMDataGridHeaderCell.tsx +466 -0
- package/src/tmdatagrid/TMDataGridTable.tsx +297 -0
- package/src/tmdatagrid/TMDataGridToolbar.tsx +131 -0
- package/src/tmdatagrid/capabilities.ts +117 -0
- package/src/tmdatagrid/columnOrdering.ts +140 -0
- package/src/tmdatagrid/columnUtils.ts +43 -0
- package/src/tmdatagrid/filterOperators.ts +171 -0
- package/src/tmdatagrid/icons.ts +25 -0
- package/src/tmdatagrid/index.ts +68 -0
- package/src/tmdatagrid/persistence.ts +187 -0
- package/src/tmdatagrid/sizes.ts +29 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +378 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: options
|
|
3
|
+
description: >
|
|
4
|
+
useTMDataGrid options, return value and state persistence. Covers passthrough
|
|
5
|
+
of TanStack TableOptions, the default initialState (pagination, columnPinning,
|
|
6
|
+
globalFilterFn), the meta object (loading, noResultsLabel, rowHeight,
|
|
7
|
+
totalRowCount), the grid's own persist and enableColumnOrdering options, the
|
|
8
|
+
persist option with dataKey/settingsKey slice selection and storageMode, the
|
|
9
|
+
returned table/ui/features triple, ui panel and column drag actions, and
|
|
10
|
+
reading grid state with useSelector. Load when configuring the hook,
|
|
11
|
+
persisting column layout or filters, or reacting to grid state from a parent.
|
|
12
|
+
metadata:
|
|
13
|
+
type: core
|
|
14
|
+
library: '@jielga/tmdatagrid'
|
|
15
|
+
library_version: '0.1.0'
|
|
16
|
+
sources:
|
|
17
|
+
- 'Jielga/TMDataGrid:src/docs/use-tm-data-grid.md'
|
|
18
|
+
- 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
|
|
19
|
+
- 'Jielga/TMDataGrid:src/tmdatagrid/persistence.ts'
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# TMDataGrid — useTMDataGrid
|
|
23
|
+
|
|
24
|
+
Creates the table instance and the state used by the grid interface.
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
const grid = useTMDataGrid<TData>(options);
|
|
28
|
+
// { table, ui, features }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Spread the result onto `TMDataGrid`.
|
|
32
|
+
|
|
33
|
+
## Options
|
|
34
|
+
|
|
35
|
+
All TanStack `TableOptions` are supported and passed through unchanged, including
|
|
36
|
+
`data`, `columns`, `getRowId`, `state`, the `onXChange` callbacks, the `manual*`
|
|
37
|
+
flags and `rowCount`. The `features` option is supplied internally and cannot be
|
|
38
|
+
overridden.
|
|
39
|
+
|
|
40
|
+
`persist` and `enableColumnOrdering` are the grid's own options and are consumed
|
|
41
|
+
by the hook rather than forwarded to TanStack.
|
|
42
|
+
|
|
43
|
+
| Option | Type | Default | Description |
|
|
44
|
+
| --- | --- | --- | --- |
|
|
45
|
+
| `data` | `TData[]` | – | Row data. Keep the reference stable with `useMemo`. |
|
|
46
|
+
| `columns` | `ColumnDef[]` | – | Created with `createTMDataGridColumnHelper`. |
|
|
47
|
+
| `getRowId` | `(row, index) => string` | Row index | Used by row selection and virtualization. |
|
|
48
|
+
| `enableRowSelection` | `boolean \| (row) => boolean` | `true` | `false` removes the checkbox column. |
|
|
49
|
+
| `enableSorting` | `boolean` | `true` | Enables sorting for the table. |
|
|
50
|
+
| `enableColumnFilters` | `boolean` | `true` | Enables filtering for the table. |
|
|
51
|
+
| `enableHiding` | `boolean` | `true` | Enables column visibility for the table. |
|
|
52
|
+
| `enableColumnPinning` | `boolean` | `true` | Enables pinning for the table. |
|
|
53
|
+
| `enableColumnResizing` | `boolean` | `true` | Enables resizing for the table. |
|
|
54
|
+
| `enableColumnOrdering` | `boolean` | `true` | Enables header dragging and the move menu items. Defined by the grid. |
|
|
55
|
+
| `columnResizeMode` | `"onChange" \| "onEnd"` | `"onChange"` | Resize update strategy. |
|
|
56
|
+
| `initialState` | `Partial<TableState>` | See below | Merged over the grid defaults. |
|
|
57
|
+
| `meta` | `TMDataGridTableMeta` | `{}` | Grid configuration, see below. |
|
|
58
|
+
| `persist` | `TMDataGridPersistence` | – | State persistence, see below. |
|
|
59
|
+
|
|
60
|
+
### Default initial state
|
|
61
|
+
|
|
62
|
+
| Slice | Default |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `pagination` | `{ pageIndex: 0, pageSize: 25 }` |
|
|
65
|
+
| `columnPinning.left` | The checkbox column, followed by any columns you provide |
|
|
66
|
+
| `globalFilterFn` | `"includesString"` |
|
|
67
|
+
|
|
68
|
+
## meta
|
|
69
|
+
|
|
70
|
+
| Field | Type | Default | Description |
|
|
71
|
+
| --- | --- | --- | --- |
|
|
72
|
+
| `loading` | `boolean` | – | Displays a loader instead of the empty state. |
|
|
73
|
+
| `noResultsLabel` | `string` | `"No rows match your filters"` | Empty state text. |
|
|
74
|
+
| `rowHeight` | `number` | From `size` | Row height in px. Overrides the size scale. |
|
|
75
|
+
| `totalRowCount` | `number` | – | Unfiltered total used by `SummaryCount`. Required for server-side data. |
|
|
76
|
+
|
|
77
|
+
## persist
|
|
78
|
+
|
|
79
|
+
Restores table state on mount and writes it back on every change. State is split
|
|
80
|
+
across two keys because the groups have different lifetimes: column configuration
|
|
81
|
+
stays valid indefinitely, while filters and pagination go stale as the data
|
|
82
|
+
changes. Separate keys let one group be cleared without touching the other.
|
|
83
|
+
|
|
84
|
+
| Field | Type | Default | Description |
|
|
85
|
+
| --- | --- | --- | --- |
|
|
86
|
+
| `dataKey` | `string \| [string, DataSlice[]]` | – | Storage key for the data group. |
|
|
87
|
+
| `settingsKey` | `string \| [string, SettingsSlice[]]` | – | Storage key for the settings group. |
|
|
88
|
+
| `storageMode` | `"localStorage" \| "sessionStorage"` | `"localStorage"` | Storage area. |
|
|
89
|
+
| `serialize` | `(value) => string` | `JSON.stringify` | Serializes before storing. |
|
|
90
|
+
| `deserialize` | `(value: string) => unknown` | `JSON.parse` | Parses a stored payload. |
|
|
91
|
+
|
|
92
|
+
Both keys are optional.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import type { TMDataGridPersistence } from "@jielga/tmdatagrid";
|
|
96
|
+
|
|
97
|
+
// Module scope: the object is a dependency of the write subscription.
|
|
98
|
+
const persist = {
|
|
99
|
+
dataKey: "employees.data",
|
|
100
|
+
settingsKey: "employees.settings",
|
|
101
|
+
} satisfies TMDataGridPersistence;
|
|
102
|
+
|
|
103
|
+
const grid = useTMDataGrid({ data, columns, persist });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Selecting slices
|
|
107
|
+
|
|
108
|
+
A bare key persists every slice in its group. Pass `[key, slices]` for a subset:
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
const persist = {
|
|
112
|
+
// Restore filters and sorting, but always start on the first page.
|
|
113
|
+
dataKey: ["employees.data", ["columnFilters", "sorting"]],
|
|
114
|
+
// Restore column layout but not widths.
|
|
115
|
+
settingsKey: ["employees.settings", ["columnVisibility", "columnOrder"]],
|
|
116
|
+
storageMode: "sessionStorage",
|
|
117
|
+
} satisfies TMDataGridPersistence;
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| Group | Available slices |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| `dataKey` | `columnFilters`, `globalFilter`, `sorting`, `pagination` |
|
|
123
|
+
| `settingsKey` | `columnVisibility`, `columnSizing`, `columnOrder`, `columnPinning` |
|
|
124
|
+
|
|
125
|
+
`DATA_STATE_SLICES` and `SETTINGS_STATE_SLICES` export the same values. Slice
|
|
126
|
+
names are typed per group, so only valid names are accepted.
|
|
127
|
+
|
|
128
|
+
Restoring happens once on mount through `initialState`. Writing is a subscription
|
|
129
|
+
to the table store, so state changed directly through the table API is persisted
|
|
130
|
+
too. Only selected slices are read back, and unrecognised keys are ignored. All
|
|
131
|
+
storage access is guarded — if storage is unavailable, disabled or full,
|
|
132
|
+
persistence is skipped rather than throwing.
|
|
133
|
+
|
|
134
|
+
## Return value
|
|
135
|
+
|
|
136
|
+
| Field | Type | Description |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| `table` | `Table<TMDataGridFeatures, TData>` | The TanStack table instance. |
|
|
139
|
+
| `ui` | `Store<TMDataGridUiState, TMDataGridUiActions>` | State of the filter and column panels. |
|
|
140
|
+
| `features` | `TMDataGridFeatureFlags` | Table-level feature switches, re-read on each render. |
|
|
141
|
+
|
|
142
|
+
Both stores are subscribable, which is how a parent reacts to grid state without
|
|
143
|
+
owning it:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import { useSelector } from "@tanstack/react-store";
|
|
147
|
+
|
|
148
|
+
const selectedCount = useSelector(
|
|
149
|
+
grid.table.store,
|
|
150
|
+
(state) => Object.keys(state.rowSelection).length,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const filterPanelOpen = useSelector(grid.ui, (state) => state.filterPanelOpen);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### ui actions
|
|
157
|
+
|
|
158
|
+
| Action | Signature |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| `openFilterPanel` | `(columnId?: string \| null) => void` |
|
|
161
|
+
| `closeFilterPanel` | `() => void` |
|
|
162
|
+
| `setColumnsPanelOpen` | `(open: boolean) => void` |
|
|
163
|
+
| `toggleColumnsPanel` | `() => void` |
|
|
164
|
+
| `startColumnDrag` | `(columnId: string) => void` |
|
|
165
|
+
| `endColumnDrag` | `() => void` |
|
|
166
|
+
|
|
167
|
+
The last two are called by the header cells while a column is being dragged, and
|
|
168
|
+
`ui.draggedColumnId` holds the column being moved — browsers keep `dataTransfer`
|
|
169
|
+
unreadable until the drop.
|
|
170
|
+
|
|
171
|
+
`openColumnFilter(grid, columnId)` combines the two steps the column menu uses:
|
|
172
|
+
add an empty filter row for the column if none exists, then open the panel.
|
|
173
|
+
|
|
174
|
+
## Common mistakes
|
|
175
|
+
|
|
176
|
+
### Reading store state during render
|
|
177
|
+
|
|
178
|
+
`grid.table.store.state` is a plain read — it does not subscribe the component,
|
|
179
|
+
so the value is correct on first render and then never updates.
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
// Wrong — never re-renders when the selection changes.
|
|
183
|
+
const count = Object.keys(grid.table.store.state.rowSelection).length;
|
|
184
|
+
|
|
185
|
+
// Right.
|
|
186
|
+
const count = useSelector(
|
|
187
|
+
grid.table.store,
|
|
188
|
+
(state) => Object.keys(state.rowSelection).length,
|
|
189
|
+
);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### persist object built during render
|
|
193
|
+
|
|
194
|
+
The object is a dependency of the write subscription. A fresh literal on every
|
|
195
|
+
render tears down and re-establishes the subscription each time. Define it at
|
|
196
|
+
module scope, or `useMemo` it if the keys depend on props.
|
|
197
|
+
|
|
198
|
+
### Unnamespaced storage keys
|
|
199
|
+
|
|
200
|
+
Storage keys are not namespaced automatically. Where several users can share a
|
|
201
|
+
browser profile, one user's column layout and filters are restored for the next.
|
|
202
|
+
Include a tenant or user identifier in the key.
|
|
203
|
+
|
|
204
|
+
### Unstable data reference
|
|
205
|
+
|
|
206
|
+
A new array identity on each render re-runs the row model. `data={rows ?? []}`
|
|
207
|
+
allocates a fresh `[]` every render when `rows` is undefined — hoist the empty
|
|
208
|
+
array to a module-scope constant or `useMemo` the expression.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: server-side
|
|
3
|
+
description: >
|
|
4
|
+
Drive TMDataGrid from a server with TanStack manual modes — manualPagination,
|
|
5
|
+
manualSorting, manualFiltering, rowCount, controlled state and onXChange
|
|
6
|
+
callbacks. Covers the loading and totalRowCount meta fields, forwarding the
|
|
7
|
+
plain-JSON columnFilters model to an API with isFilterActive, persistence
|
|
8
|
+
interaction, and row selection across pages. Load when the grid is backed by a
|
|
9
|
+
paginated API rather than a local array.
|
|
10
|
+
metadata:
|
|
11
|
+
type: core
|
|
12
|
+
library: '@jielga/tmdatagrid'
|
|
13
|
+
library_version: '0.1.0'
|
|
14
|
+
sources:
|
|
15
|
+
- 'Jielga/TMDataGrid:src/docs/server-side.md'
|
|
16
|
+
- 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# TMDataGrid — Server-side data
|
|
20
|
+
|
|
21
|
+
The grid reads rows through `getPaginatedRowModel()` and totals through
|
|
22
|
+
`getRowCount()` and `getPageCount()`, all of which respect TanStack's manual
|
|
23
|
+
modes. A server-driven grid therefore needs only the standard `manual*`
|
|
24
|
+
configuration — no grid-specific options.
|
|
25
|
+
|
|
26
|
+
## Setup
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|
30
|
+
import { useState } from "react";
|
|
31
|
+
import { TMDataGrid, useTMDataGrid } from "@jielga/tmdatagrid";
|
|
32
|
+
|
|
33
|
+
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
|
|
34
|
+
const [sorting, setSorting] = useState([]);
|
|
35
|
+
const [columnFilters, setColumnFilters] = useState([]);
|
|
36
|
+
|
|
37
|
+
const { data, isFetching } = useQuery({
|
|
38
|
+
queryKey: ["employees", pagination, sorting, columnFilters],
|
|
39
|
+
queryFn: () => fetchEmployees({ pagination, sorting, columnFilters }),
|
|
40
|
+
placeholderData: keepPreviousData,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const grid = useTMDataGrid({
|
|
44
|
+
columns,
|
|
45
|
+
data: data?.rows ?? [],
|
|
46
|
+
getRowId: (row) => String(row.id),
|
|
47
|
+
|
|
48
|
+
manualPagination: true,
|
|
49
|
+
manualSorting: true,
|
|
50
|
+
manualFiltering: true,
|
|
51
|
+
rowCount: data?.total ?? 0,
|
|
52
|
+
|
|
53
|
+
state: { pagination, sorting, columnFilters },
|
|
54
|
+
onPaginationChange: setPagination,
|
|
55
|
+
onSortingChange: setSorting,
|
|
56
|
+
onColumnFiltersChange: setColumnFilters,
|
|
57
|
+
|
|
58
|
+
meta: { loading: isFetching, totalRowCount: data?.totalUnfiltered },
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Differences from client-side data
|
|
63
|
+
|
|
64
|
+
| Concern | Client-side | Server-side |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| Rendered rows | The current page, sliced locally | The rows returned by the server |
|
|
67
|
+
| Footer total | Pre-paginated row count | `options.rowCount` |
|
|
68
|
+
| `SummaryCount` total | Pre-filtered row count | `meta.totalRowCount` |
|
|
69
|
+
| Loading state | Not applicable | `meta.loading` |
|
|
70
|
+
|
|
71
|
+
Column menus, the filter panel and the column manager behave identically in both
|
|
72
|
+
modes.
|
|
73
|
+
|
|
74
|
+
## Sending filters
|
|
75
|
+
|
|
76
|
+
Filter values are plain JSON, so `columnFilters` forwards without transformation:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
[
|
|
80
|
+
{ "id": "lastName", "value": { "operator": "contains", "value": "holm" } },
|
|
81
|
+
{ "id": "age", "value": { "operator": "greaterThan", "value": "30" } }
|
|
82
|
+
]
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Translate at the API boundary, and skip entries whose value is still empty —
|
|
86
|
+
those match all rows:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { isFilterActive } from "@jielga/tmdatagrid";
|
|
90
|
+
|
|
91
|
+
const active = columnFilters.filter((filter) => isFilterActive(filter.value));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Debounce requests. The filter value input updates on every keystroke.
|
|
95
|
+
|
|
96
|
+
## Persistence
|
|
97
|
+
|
|
98
|
+
`persist` works unchanged. `dataKey` restores filters, sorting and pagination
|
|
99
|
+
before the first request, so reloading repeats the query the user last ran.
|
|
100
|
+
|
|
101
|
+
If the same state is also held in your own `useState`, initialise it from the
|
|
102
|
+
same source or let the grid own it. Do not maintain both independently.
|
|
103
|
+
|
|
104
|
+
## Row selection
|
|
105
|
+
|
|
106
|
+
Selection is keyed by `getRowId`, so ids must be stable across pages. Under
|
|
107
|
+
`manualPagination`, rows selected on an earlier page stay in `rowSelection` even
|
|
108
|
+
though they are no longer mounted. Read the state, not the row models:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { useSelector } from "@tanstack/react-store";
|
|
112
|
+
|
|
113
|
+
const selectedIds = useSelector(grid.table.store, (state) =>
|
|
114
|
+
Object.keys(state.rowSelection),
|
|
115
|
+
);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Common mistakes
|
|
119
|
+
|
|
120
|
+
### Omitting rowCount under manualPagination
|
|
121
|
+
|
|
122
|
+
Without `rowCount` the table derives the total from the rows it was handed —
|
|
123
|
+
one page — so `getPageCount()` returns 1. The footer shows "1–25 of 25" and the
|
|
124
|
+
next-page button is disabled, with no error. Pass the server total.
|
|
125
|
+
|
|
126
|
+
### Filters sent without isFilterActive
|
|
127
|
+
|
|
128
|
+
An empty filter value stays in `columnFilters` while the user is still typing.
|
|
129
|
+
Forwarded verbatim it becomes `operator: "contains", value: ""` at the API,
|
|
130
|
+
which most backends translate into a real predicate. Filter with
|
|
131
|
+
`isFilterActive` first.
|
|
132
|
+
|
|
133
|
+
### SummaryCount without meta.totalRowCount
|
|
134
|
+
|
|
135
|
+
`SummaryCount` falls back to the pre-filtered row count, which under manual
|
|
136
|
+
filtering is just the current page. It reads "25 of 25" regardless of how many
|
|
137
|
+
rows the server holds. Pass the unfiltered total as `meta.totalRowCount`.
|
|
138
|
+
|
|
139
|
+
### Unstable getRowId across pages
|
|
140
|
+
|
|
141
|
+
Defaulting to the row index means row 0 of page 2 shares an id with row 0 of
|
|
142
|
+
page 1, so paging silently transfers the selection to different records. Always
|
|
143
|
+
set `getRowId` from a stable primary key when paginating on the server.
|
|
144
|
+
|
|
145
|
+
### Duplicating grid state in local state
|
|
146
|
+
|
|
147
|
+
Holding `pagination` in `useState` *and* letting `persist.dataKey` restore it
|
|
148
|
+
gives two sources of truth: the grid restores the stored page while `useState`
|
|
149
|
+
starts at 0, and the first query runs for the wrong page. Initialise the local
|
|
150
|
+
state from the same store, or drop `pagination` from the persisted slices.
|