@tanstack/svelte-table 9.0.0-alpha.47 → 9.0.0-alpha.49

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 CHANGED
@@ -49,6 +49,16 @@ A headless table library for building powerful datagrids with full control over
49
49
 
50
50
  ### <a href="https://tanstack.com/table">Read the Docs →</a>
51
51
 
52
+ ## Using an AI Coding Agent?
53
+
54
+ TanStack Table ships [TanStack Intent](https://github.com/TanStack/intent) skills inside each adapter package. After installing the library, run:
55
+
56
+ ```sh
57
+ npx @tanstack/intent@latest install
58
+ ```
59
+
60
+ to add skill-loading guidance for your agent (Claude Code, Cursor, Copilot, etc.). The same CLI also exposes `intent list` to browse available skills and `intent load <skill>` to print one for inspection. Skills version with the library — your agent gets guidance that matches the version of `@tanstack/<framework>-table` you installed. Only available for v9 and above.
61
+
52
62
  ## Get Involved
53
63
 
54
64
  - We welcome issues and pull requests!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/svelte-table",
3
- "version": "9.0.0-alpha.47",
3
+ "version": "9.0.0-alpha.49",
4
4
  "description": "Headless UI for building powerful tables & datagrids for Svelte.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -18,7 +18,8 @@
18
18
  "svelte",
19
19
  "table",
20
20
  "svelte-table",
21
- "datagrid"
21
+ "datagrid",
22
+ "tanstack-intent"
22
23
  ],
23
24
  "type": "module",
24
25
  "types": "dist/index.d.ts",
@@ -47,17 +48,18 @@
47
48
  },
48
49
  "files": [
49
50
  "dist",
50
- "src"
51
+ "src",
52
+ "skills"
51
53
  ],
52
54
  "dependencies": {
53
55
  "@tanstack/svelte-store": "^0.12.0",
54
- "@tanstack/table-core": "9.0.0-alpha.47"
56
+ "@tanstack/table-core": "9.0.0-alpha.49"
55
57
  },
56
58
  "devDependencies": {
57
59
  "@sveltejs/package": "^2.5.7",
58
60
  "@sveltejs/vite-plugin-svelte": "^7.1.2",
59
61
  "eslint-plugin-svelte": "^3.17.1",
60
- "svelte": "^5.55.5",
62
+ "svelte": "^5.55.7",
61
63
  "svelte-check": "^4.4.8"
62
64
  },
63
65
  "peerDependencies": {
@@ -0,0 +1,238 @@
1
+ ---
2
+ name: svelte/client-to-server
3
+ description: >
4
+ Convert a client-side Svelte table to server-side (manual) modes. Toggle `manualPagination`,
5
+ `manualSorting`, `manualFiltering`, `manualGrouping`, `manualExpanding` for whatever the server
6
+ owns, drop the matching `_rowModels` factories and `_features` you no longer need, supply
7
+ `rowCount` for the pager, then drive the request from `table.atoms.pagination` /
8
+ `table.atoms.sorting` / etc. (or external atoms you own) — using rune-aware getters
9
+ (`get data()`, `get rowCount()`) so the table re-syncs in `$effect.pre`. Svelte 5+ only.
10
+ type: lifecycle
11
+ library: tanstack-table
12
+ framework: svelte
13
+ library_version: '9.0.0-alpha.48'
14
+ requires:
15
+ - state-management
16
+ - pagination
17
+ - filtering
18
+ - sorting
19
+ - svelte/table-state
20
+ sources:
21
+ - TanStack/table:examples/svelte/basic-external-atoms/
22
+ - TanStack/table:examples/svelte/basic-external-state/
23
+ - TanStack/table:examples/svelte/with-tanstack-query/
24
+ - TanStack/table:docs/framework/svelte/guide/table-state.md
25
+ ---
26
+
27
+ # Client → Server (Svelte)
28
+
29
+ You have a working client-side table. The dataset is too big to ship to the browser, or it
30
+ lives behind an API. You want sorting / filtering / pagination to run on the server while the
31
+ table still feels the same in the UI.
32
+
33
+ ## Mental model
34
+
35
+ Each "manual mode" flag tells the table: **don't run this stage of the pipeline; trust the data
36
+ you receive.** You can mix modes freely — manual pagination + client-side sorting on the
37
+ already-paged window is perfectly valid for medium datasets.
38
+
39
+ | Flag | Meaning | What you must provide |
40
+ | ------------------ | -------------------------------------------- | ----------------------------------------------------------- |
41
+ | `manualPagination` | Server owns slicing; do not paginate locally | `rowCount` (or `pageCount`) |
42
+ | `manualSorting` | Server owns ordering | Sort the server query by `sorting` state |
43
+ | `manualFiltering` | Server owns row filtering | Filter the server query by `columnFilters` / `globalFilter` |
44
+ | `manualGrouping` | Server returns already-grouped rows | Pre-shaped data |
45
+ | `manualExpanding` | Server resolves sub-rows | Server-provided sub-row tree |
46
+
47
+ When a stage is manual, you can drop its row-model factory. `manualPagination: true` does not
48
+ need `paginatedRowModel: createPaginatedRowModel()`.
49
+
50
+ ## Step 1 — Identify what's moving server-side
51
+
52
+ For a typical "search and paginate against a database" screen:
53
+
54
+ - Pagination → server
55
+ - Filtering (column filter inputs + a global search box) → server
56
+ - Sorting → server (usually, since a partial page can't be sorted client-side meaningfully)
57
+ - Selection / visibility / column ordering → still client
58
+
59
+ So the table keeps `rowSelectionFeature` etc., drops `columnFilteringFeature` /
60
+ `rowPaginationFeature` / `rowSortingFeature` _row models_ but keeps the _features_ so the
61
+ state slices and UI APIs still exist.
62
+
63
+ > Subtle point: keep the **feature** even if you drop the row model. The feature is what gives
64
+ > you `column.getCanSort()`, `table.setPageIndex()`, `column.setFilterValue()` — all the
65
+ > control-surface APIs. Dropping it kills the UI.
66
+
67
+ ## Step 2 — Own the relevant state with external atoms
68
+
69
+ External atoms make state portable: the data layer (a fetch / query / store) can read the
70
+ same atoms the table writes to. Use `@tanstack/svelte-store`:
71
+
72
+ ```ts
73
+ import { createAtom, useSelector } from '@tanstack/svelte-store'
74
+ import type {
75
+ ColumnFiltersState,
76
+ PaginationState,
77
+ SortingState,
78
+ } from '@tanstack/svelte-table'
79
+
80
+ const paginationAtom = createAtom<PaginationState>({
81
+ pageIndex: 0,
82
+ pageSize: 10,
83
+ })
84
+ const sortingAtom = createAtom<SortingState>([])
85
+ const filtersAtom = createAtom<ColumnFiltersState>([])
86
+
87
+ // For Svelte markup that should react to changes:
88
+ const pagination = useSelector(paginationAtom)
89
+ const sorting = useSelector(sortingAtom)
90
+ const filters = useSelector(filtersAtom)
91
+ ```
92
+
93
+ ## Step 3 — Configure the table
94
+
95
+ ```svelte
96
+ <script lang="ts">
97
+ import {
98
+ columnFilteringFeature,
99
+ createTable,
100
+ rowPaginationFeature,
101
+ rowSortingFeature,
102
+ tableFeatures,
103
+ } from '@tanstack/svelte-table'
104
+
105
+ const _features = tableFeatures({
106
+ columnFilteringFeature,
107
+ rowPaginationFeature,
108
+ rowSortingFeature,
109
+ })
110
+
111
+ // No row-model factories for these — server owns them.
112
+ const table = createTable({
113
+ _features,
114
+ _rowModels: {},
115
+ columns,
116
+ get data() {
117
+ return query.data?.rows ?? []
118
+ },
119
+ get rowCount() {
120
+ return query.data?.rowCount
121
+ },
122
+ atoms: {
123
+ pagination: paginationAtom,
124
+ sorting: sortingAtom,
125
+ columnFilters: filtersAtom,
126
+ },
127
+ manualPagination: true,
128
+ manualSorting: true,
129
+ manualFiltering: true,
130
+ })
131
+ </script>
132
+ ```
133
+
134
+ `rowCount` is what makes `table.getPageCount()` / `table.getCanNextPage()` correct under
135
+ manual pagination. Without it the pager has no idea how many pages exist.
136
+
137
+ ## Step 4 — Drive the fetch from those atoms
138
+
139
+ Wire whatever data layer you use (TanStack Query, a raw `fetch`, SvelteKit `load`, etc.) to
140
+ read the atoms. With TanStack Query:
141
+
142
+ ```ts
143
+ import { createQuery, keepPreviousData } from '@tanstack/svelte-query'
144
+
145
+ const dataQuery = createQuery<{ rows: Array<Person>; rowCount: number }>(
146
+ () => ({
147
+ queryKey: ['people', pagination.current, sorting.current, filters.current],
148
+ queryFn: () =>
149
+ fetch('/api/people', {
150
+ method: 'POST',
151
+ body: JSON.stringify({
152
+ pageIndex: pagination.current.pageIndex,
153
+ pageSize: pagination.current.pageSize,
154
+ sorting: sorting.current,
155
+ filters: filters.current,
156
+ }),
157
+ }).then((r) => r.json()),
158
+ placeholderData: keepPreviousData,
159
+ }),
160
+ )
161
+ ```
162
+
163
+ `placeholderData: keepPreviousData` is what kills the "rows blank for one tick on every
164
+ page change" flash.
165
+
166
+ ## Step 5 — Reset behavior
167
+
168
+ When the user changes a filter, you usually want to jump back to page 0. The table does this
169
+ automatically when client-side filtering owns the data, but with manual mode the data layer
170
+ controls it. Simplest fix: explicitly reset.
171
+
172
+ ```ts
173
+ $effect(() => {
174
+ // re-runs whenever filters.current identity changes
175
+ filters.current
176
+ table.setPageIndex(0)
177
+ })
178
+ ```
179
+
180
+ Or wrap your filter `onChange` handlers to also call `table.setPageIndex(0)`.
181
+
182
+ ## Step 6 — A note on global filtering
183
+
184
+ If you also support `globalFilterFeature`, debounce the input. `column.setFilterValue` and
185
+ `table.setGlobalFilter` fire per keystroke; without debouncing you fire one request per typed
186
+ character. See the `compose-with-tanstack-pacer` skill for the pattern.
187
+
188
+ ## Hybrid example — manual pagination only
189
+
190
+ Sometimes you only paginate server-side and let the page-sized window sort/filter on the
191
+ client.
192
+
193
+ ```ts
194
+ const table = createTable({
195
+ _features: tableFeatures({
196
+ columnFilteringFeature,
197
+ rowPaginationFeature,
198
+ rowSortingFeature,
199
+ }),
200
+ _rowModels: {
201
+ filteredRowModel: createFilteredRowModel(filterFns), // client filters the page
202
+ sortedRowModel: createSortedRowModel(sortFns), // client sorts the page
203
+ },
204
+ columns,
205
+ get data() {
206
+ return query.data?.rows ?? []
207
+ },
208
+ get rowCount() {
209
+ return query.data?.rowCount
210
+ },
211
+ atoms: { pagination: paginationAtom },
212
+ manualPagination: true,
213
+ })
214
+ ```
215
+
216
+ Only the manual flag for the stage you're moving server-side.
217
+
218
+ ## Common failure modes
219
+
220
+ - **Forgot `rowCount`.** `table.getPageCount()` returns `-1`, the pager looks broken.
221
+ - **Dropped the feature, not just the row model.** Lost `column.getCanSort()` and friends.
222
+ Keep the feature when you still need its UI APIs; only drop the row-model factory.
223
+ - **Both `state.pagination` and `atoms.pagination`.** Atoms silently win; the `on*Change`
224
+ callback never fires.
225
+ - **Re-creating atoms inside reactive blocks.** Atoms must be stable across renders. Declare
226
+ them at module / component-init scope, not inside `$derived` or `$effect`.
227
+ - **Forgetting to reset page on filter change.** Stay on page 12 of a now-2-page result set.
228
+ - **Plain `data: query.data?.rows`.** No getter, no reactivity. Use `get data()`.
229
+ - **Reimplementing pagination math.** `table.setPageIndex / nextPage / previousPage /
230
+ firstPage / lastPage / setPageSize / getCanNextPage / getCanPreviousPage / getPageCount`
231
+ already exist and respect manual mode.
232
+
233
+ ## Related skills
234
+
235
+ - `tanstack-table/svelte/compose-with-tanstack-query` — the same flow with a Query data layer.
236
+ - `tanstack-table/svelte/compose-with-tanstack-pacer` — debouncing filter inputs.
237
+ - `tanstack-table/svelte/compose-with-tanstack-store` — atom interop and per-slice subscription.
238
+ - `tanstack-table/core/pagination` / `filtering` / `sorting` — feature deep dives.
@@ -0,0 +1,295 @@
1
+ ---
2
+ name: svelte/compose-with-tanstack-form
3
+ description: >
4
+ Editable cells in `@tanstack/svelte-table` powered by `@tanstack/svelte-form`. The table is the
5
+ layout primitive; the form owns the state. Use `createFormHook` to register reusable field
6
+ components (`TextField`, `NumberField`, `SelectField`), then in each column's `cell` renderer
7
+ return `renderComponent(MyFieldCell, { form, rowIndex, fieldName })` and inside that cell call
8
+ `form.Field` (or an `AppField`) with `name="data[${rowIndex}].${fieldName}"`. Drive the table's
9
+ `data` from `form.state.values.data`. Svelte 5+ only.
10
+ type: composition
11
+ library: tanstack-table
12
+ framework: svelte
13
+ library_version: '9.0.0-alpha.48'
14
+ requires:
15
+ - row-selection
16
+ - column-definitions
17
+ sources:
18
+ - TanStack/table:examples/svelte/with-tanstack-form/
19
+ - TanStack/table:docs/framework/svelte/svelte-table.md
20
+ ---
21
+
22
+ # Compose with TanStack Form (Svelte)
23
+
24
+ Editable tables are a classic source of state-management chaos. With v9 + TanStack Form, the
25
+ division of labor is crisp:
26
+
27
+ - **Form** owns the editable values (per-row, per-field).
28
+ - **Table** owns the layout (columns, filtering, pagination of the same form data).
29
+ - **Cells** are just field renderers — they read and write through Form's field APIs.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pnpm add @tanstack/svelte-form @tanstack/svelte-table
35
+ ```
36
+
37
+ ## Set up a field-component-rich Form hook
38
+
39
+ Define a `createAppForm` once with the reusable field components. This is the form-side
40
+ equivalent of `createTableHook`.
41
+
42
+ ```ts
43
+ // hooks/form.ts
44
+ import { createFormHook, createFormHookContexts } from '@tanstack/svelte-form'
45
+ import TextField from '../components/TextField.svelte'
46
+ import NumberField from '../components/NumberField.svelte'
47
+ import SelectField from '../components/SelectField.svelte'
48
+ import SubmitButton from '../components/SubmitButton.svelte'
49
+ import FormStateIndicator from '../components/FormStateIndicator.svelte'
50
+
51
+ export const { fieldContext, formContext } = createFormHookContexts()
52
+
53
+ export const { useAppForm: createAppForm } = createFormHook({
54
+ fieldComponents: { TextField, NumberField, SelectField },
55
+ formComponents: { SubmitButton, FormStateIndicator },
56
+ fieldContext,
57
+ formContext,
58
+ })
59
+ ```
60
+
61
+ ## Reusable field cell components
62
+
63
+ Each cell type is a small Svelte component that knows which row + field it edits and uses
64
+ `form.Field`. The shape is the same across types — `TextFieldCell`, `NumberFieldCell`,
65
+ `SelectFieldCell`.
66
+
67
+ ```svelte
68
+ <!-- TextFieldCell.svelte -->
69
+ <script lang="ts">
70
+ type Props = {
71
+ form: ReturnType<typeof createAppForm>
72
+ rowIndex: number
73
+ fieldName: string
74
+ }
75
+ let { form, rowIndex, fieldName }: Props = $props()
76
+ </script>
77
+
78
+ <form.Field name={`data[${rowIndex}].${fieldName}`}>
79
+ {#snippet children(field)}
80
+ <input
81
+ type="text"
82
+ value={field.state.value}
83
+ oninput={(e) => field.handleChange((e.target as HTMLInputElement).value)}
84
+ onblur={field.handleBlur}
85
+ />
86
+ {#if field.state.meta.errors?.length}
87
+ <small class="error">{field.state.meta.errors.join(', ')}</small>
88
+ {/if}
89
+ {/snippet}
90
+ </form.Field>
91
+ ```
92
+
93
+ ## Wire the table to form state
94
+
95
+ ```svelte
96
+ <script lang="ts">
97
+ import {
98
+ columnFilteringFeature,
99
+ createColumnHelper,
100
+ createFilteredRowModel,
101
+ createPaginatedRowModel,
102
+ createTable,
103
+ filterFns,
104
+ FlexRender,
105
+ renderComponent,
106
+ rowPaginationFeature,
107
+ tableFeatures,
108
+ } from '@tanstack/svelte-table'
109
+ import { z } from 'zod'
110
+ import { createAppForm } from './hooks/form'
111
+ import TextFieldCell from './TextFieldCell.svelte'
112
+ import NumberFieldCell from './NumberFieldCell.svelte'
113
+ import SelectFieldCell from './SelectFieldCell.svelte'
114
+ import { makeData, type Person } from './makeData'
115
+
116
+ const _features = tableFeatures({
117
+ rowPaginationFeature,
118
+ columnFilteringFeature,
119
+ })
120
+
121
+ const columnHelper = createColumnHelper<typeof _features, Person>()
122
+
123
+ const personSchema = z.object({
124
+ firstName: z.string().min(1),
125
+ lastName: z.string().min(1),
126
+ age: z.number().min(0).max(150),
127
+ visits: z.number().min(0),
128
+ progress: z.number().min(0).max(100),
129
+ status: z.enum(['relationship', 'complicated', 'single']),
130
+ })
131
+
132
+ const formSchema = z.object({ data: z.array(personSchema) })
133
+ type FormData = z.infer<typeof formSchema>
134
+
135
+ const form = createAppForm(() => ({
136
+ defaultValues: { data: makeData(1_000) } as FormData,
137
+ validators: { onChange: formSchema },
138
+ onSubmit: ({ value }) => alert(`Saved ${value.data.length} rows`),
139
+ }))
140
+
141
+ const columns = columnHelper.columns([
142
+ columnHelper.accessor('firstName', {
143
+ header: 'First Name',
144
+ cell: ({ row }) =>
145
+ renderComponent(TextFieldCell, {
146
+ form,
147
+ rowIndex: row.index,
148
+ fieldName: 'firstName',
149
+ }),
150
+ }),
151
+ columnHelper.accessor('age', {
152
+ header: 'Age',
153
+ cell: ({ row }) =>
154
+ renderComponent(NumberFieldCell, {
155
+ form,
156
+ rowIndex: row.index,
157
+ fieldName: 'age',
158
+ }),
159
+ }),
160
+ columnHelper.accessor('status', {
161
+ header: 'Status',
162
+ cell: ({ row }) =>
163
+ renderComponent(SelectFieldCell, {
164
+ form,
165
+ rowIndex: row.index,
166
+ }),
167
+ }),
168
+ // ...
169
+ ])
170
+
171
+ const table = createTable({
172
+ _features,
173
+ _rowModels: {
174
+ filteredRowModel: createFilteredRowModel(filterFns),
175
+ paginatedRowModel: createPaginatedRowModel(),
176
+ },
177
+ columns,
178
+ get data() {
179
+ // The form is the source of truth.
180
+ return form.state.values.data
181
+ },
182
+ })
183
+ </script>
184
+
185
+ <form
186
+ onsubmit={(e) => {
187
+ e.preventDefault()
188
+ void form.handleSubmit()
189
+ }}
190
+ >
191
+ <form.AppForm>
192
+ {#snippet children()}
193
+ <form.FormStateIndicator />
194
+ <form.SubmitButton label="Save" />
195
+ {/snippet}
196
+ </form.AppForm>
197
+
198
+ <button
199
+ type="button"
200
+ onclick={() =>
201
+ form.pushFieldValue('data', {
202
+ firstName: '',
203
+ lastName: '',
204
+ age: 0,
205
+ visits: 0,
206
+ progress: 0,
207
+ status: 'single',
208
+ })}>Add Row</button
209
+ >
210
+
211
+ <table>
212
+ <thead>
213
+ {#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
214
+ <tr>
215
+ {#each headerGroup.headers as header (header.id)}
216
+ <th><FlexRender {header} /></th>
217
+ {/each}
218
+ </tr>
219
+ {/each}
220
+ </thead>
221
+ <tbody>
222
+ {#each table.getRowModel().rows as row (row.id)}
223
+ <tr>
224
+ {#each row.getAllCells() as cell (cell.id)}
225
+ <td><FlexRender {cell} /></td>
226
+ {/each}
227
+ </tr>
228
+ {/each}
229
+ </tbody>
230
+ </table>
231
+ </form>
232
+ ```
233
+
234
+ ## Why `row.index` and not `row.id`?
235
+
236
+ Form indexes its arrays positionally. `row.index` is the position inside the **current** row
237
+ model (after filter + sort + paging). If you want a positional address into `form.state.values.data`,
238
+ you usually want the **original** index — pass `row.original` somewhere that exposes it, or
239
+ store an `id` field and look it up.
240
+
241
+ For the common case where the table renders the array in its natural order (no sort, no
242
+ filter that reorders), `row.index` matches the form-array position.
243
+
244
+ ## Add Row / Remove Row
245
+
246
+ Use the form's array helpers; the table re-renders because its `data` getter points at
247
+ `form.state.values.data`.
248
+
249
+ ```ts
250
+ form.pushFieldValue('data', newPerson)
251
+ form.removeFieldValue('data', rowIndex)
252
+ form.replaceFieldValue('data', rowIndex, updatedPerson)
253
+ ```
254
+
255
+ ## Pairing with selection
256
+
257
+ Add `rowSelectionFeature` to enable row checkboxes, then a "delete selected" button can use
258
+ `table.getSelectedRowModel()` to collect rows and `form.removeFieldValue` to remove them.
259
+
260
+ ```ts
261
+ const selectedRows = table.getSelectedRowModel().rows
262
+ const indexesDesc = selectedRows.map((r) => r.index).sort((a, b) => b - a)
263
+ for (const i of indexesDesc) {
264
+ form.removeFieldValue('data', i)
265
+ }
266
+ table.resetRowSelection()
267
+ ```
268
+
269
+ Remove in descending order so earlier removals don't shift later indexes.
270
+
271
+ ## Pairing with virtualization
272
+
273
+ You can virtualize the rows even with editable cells — but be aware that **virtualized rows
274
+ unmount when scrolled out of view**, taking their inline form fields with them. If a cell has
275
+ unsaved local-only state, you'll lose it. Use Form fields (which live on the form's state)
276
+ and you're fine — the field state survives the unmount.
277
+
278
+ ## Common failure modes
279
+
280
+ - **`renderComponent` from React docs.** Use the Svelte adapter's `renderComponent` from
281
+ `@tanstack/svelte-table`. The signature is the same shape but the runtime is different.
282
+ - **`form` not reactive in cells.** Pass `form` as a prop; don't reach for it via context
283
+ unless you set up `formContext`.
284
+ - **Wrong field name.** `data[${row.index}].firstName` — string template, not a plain join.
285
+ - **Reordering / filtering breaks `row.index`.** As above. Either keep a stable id and resolve
286
+ back to the form-array index, or accept that `row.index` only addresses the visible window.
287
+ - **Editing inside virtualized rows without form state.** Field values lost on scroll.
288
+ - **Reimplementing form state with `$state` per cell.** Defeats the whole point — Form already
289
+ owns this state and runs validation.
290
+
291
+ ## Related skills
292
+
293
+ - `tanstack-table/core/row-selection` — checkbox column patterns.
294
+ - `tanstack-table/core/column-definitions` — accessor / display columns.
295
+ - `tanstack-table/svelte/table-state` — `getRowModel()` and reactivity.