@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.
@@ -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.