@tanstack/svelte-table 9.0.0-beta.7 → 9.0.0-beta.71
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 -1
- package/dist/FlexRender.svelte +15 -1
- package/dist/createTable.svelte.d.ts +26 -36
- package/dist/createTable.svelte.js +27 -32
- package/dist/createTableHook.svelte.d.ts +51 -18
- package/dist/createTableHook.svelte.js +18 -10
- package/dist/experimental-worker-plugin.d.ts +1 -0
- package/dist/experimental-worker-plugin.js +1 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/dist/reactivity.svelte.d.ts +3 -1
- package/dist/reactivity.svelte.js +6 -1
- package/dist/render-component.js +4 -0
- package/package.json +16 -9
- package/skills/create-table-hook/SKILL.md +168 -0
- package/skills/getting-started/SKILL.md +172 -0
- package/skills/migrate-v8-to-v9/SKILL.md +201 -0
- package/skills/table-state/SKILL.md +262 -0
- package/skills/with-tanstack-query/SKILL.md +149 -0
- package/skills/with-tanstack-virtual/SKILL.md +150 -0
- package/dist/subscribe.d.ts +0 -24
- package/dist/subscribe.js +0 -4
- package/skills/svelte/client-to-server/SKILL.md +0 -238
- package/skills/svelte/compose-with-tanstack-form/SKILL.md +0 -295
- package/skills/svelte/compose-with-tanstack-pacer/SKILL.md +0 -176
- package/skills/svelte/compose-with-tanstack-query/SKILL.md +0 -299
- package/skills/svelte/compose-with-tanstack-store/SKILL.md +0 -277
- package/skills/svelte/compose-with-tanstack-virtual/SKILL.md +0 -286
- package/skills/svelte/getting-started/SKILL.md +0 -340
- package/skills/svelte/migrate-v8-to-v9/SKILL.md +0 -256
- package/skills/svelte/production-readiness/SKILL.md +0 -256
- package/skills/svelte/table-state/SKILL.md +0 -441
- package/src/AppCell.svelte +0 -13
- package/src/AppHeader.svelte +0 -13
- package/src/AppTable.svelte +0 -11
- package/src/FlexRender.svelte +0 -103
- package/src/context-keys.ts +0 -3
- package/src/createTable.svelte.ts +0 -137
- package/src/createTableHook.svelte.ts +0 -639
- package/src/createTableState.svelte.ts +0 -30
- package/src/flex-render.ts +0 -3
- package/src/global.d.ts +0 -1
- package/src/index.ts +0 -21
- package/src/merge-objects.ts +0 -79
- package/src/reactivity.svelte.ts +0 -118
- package/src/render-component.ts +0 -107
- package/src/static-functions.ts +0 -1
- package/src/subscribe.ts +0 -46
|
@@ -1,238 +0,0 @@
|
|
|
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.
|
|
@@ -1,295 +0,0 @@
|
|
|
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.
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: svelte/compose-with-tanstack-pacer
|
|
3
|
-
description: >
|
|
4
|
-
Use `@tanstack/svelte-pacer` to debounce / throttle high-frequency writes that drive a
|
|
5
|
-
`@tanstack/svelte-table` v9 instance — column filter inputs and column resize state are the
|
|
6
|
-
two hot paths. Import `createDebouncer` (or `createThrottler`) from
|
|
7
|
-
`@tanstack/svelte-pacer/debouncer`, wrap the call site that hits `column.setFilterValue`,
|
|
8
|
-
`table.setGlobalFilter`, or commits a resize, and call `.maybeExecute(value)` on each event.
|
|
9
|
-
Svelte 5+ only — pacer instances live at component-init scope.
|
|
10
|
-
type: composition
|
|
11
|
-
library: tanstack-table
|
|
12
|
-
framework: svelte
|
|
13
|
-
library_version: '9.0.0-alpha.48'
|
|
14
|
-
requires:
|
|
15
|
-
- filtering
|
|
16
|
-
- column-layout
|
|
17
|
-
sources:
|
|
18
|
-
- TanStack/table:examples/svelte/with-tanstack-form/
|
|
19
|
-
- TanStack/table:docs/framework/svelte/guide/table-state.md
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
# Compose with TanStack Pacer (Svelte)
|
|
23
|
-
|
|
24
|
-
Two places in a v9 table take state writes at event-loop rate: **filter inputs** (one
|
|
25
|
-
keystroke = one `setFilterValue`) and **column resizing** (one pointermove = one
|
|
26
|
-
`columnSizing` write). Without rate-limiting they either flood the network (server-side
|
|
27
|
-
filtering) or burn CPU on tens of thousands of re-renders per drag.
|
|
28
|
-
|
|
29
|
-
`@tanstack/svelte-pacer` gives you `createDebouncer`, `createThrottler`, and friends. Wrap
|
|
30
|
-
the call site and you're done.
|
|
31
|
-
|
|
32
|
-
## Install
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
pnpm add @tanstack/svelte-pacer
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Debounce a column filter input
|
|
39
|
-
|
|
40
|
-
Client-side debouncing reduces re-render churn. Server-side debouncing also kills request
|
|
41
|
-
storms.
|
|
42
|
-
|
|
43
|
-
```svelte
|
|
44
|
-
<script lang="ts">
|
|
45
|
-
import { createDebouncer } from '@tanstack/svelte-pacer/debouncer'
|
|
46
|
-
import type { Column } from '@tanstack/svelte-table'
|
|
47
|
-
|
|
48
|
-
type Props = { column: Column<typeof features, Person> }
|
|
49
|
-
let { column }: Props = $props()
|
|
50
|
-
|
|
51
|
-
let localValue = $state((column.getFilterValue() as string) ?? '')
|
|
52
|
-
|
|
53
|
-
const debouncedSetFilter = createDebouncer(
|
|
54
|
-
(value: string) => column.setFilterValue(value),
|
|
55
|
-
{ wait: 200 },
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
function onInput(e: Event) {
|
|
59
|
-
const value = (e.target as HTMLInputElement).value
|
|
60
|
-
localValue = value
|
|
61
|
-
debouncedSetFilter.maybeExecute(value)
|
|
62
|
-
}
|
|
63
|
-
</script>
|
|
64
|
-
|
|
65
|
-
<input type="text" value={localValue} oninput={onInput} placeholder="Search…" />
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Why the `localValue`? So the input stays snappy (controlled by `$state`) while the table only
|
|
69
|
-
sees the debounced commit.
|
|
70
|
-
|
|
71
|
-
## Debounce a global filter
|
|
72
|
-
|
|
73
|
-
Same shape, calling `table.setGlobalFilter`:
|
|
74
|
-
|
|
75
|
-
```svelte
|
|
76
|
-
<script lang="ts">
|
|
77
|
-
import { createDebouncer } from '@tanstack/svelte-pacer/debouncer'
|
|
78
|
-
|
|
79
|
-
let search = $state('')
|
|
80
|
-
const debouncedSetGlobalFilter = createDebouncer(
|
|
81
|
-
(value: string) => table.setGlobalFilter(value),
|
|
82
|
-
{ wait: 250 },
|
|
83
|
-
)
|
|
84
|
-
</script>
|
|
85
|
-
|
|
86
|
-
<input
|
|
87
|
-
type="text"
|
|
88
|
-
value={search}
|
|
89
|
-
oninput={(e) => {
|
|
90
|
-
search = (e.target as HTMLInputElement).value
|
|
91
|
-
debouncedSetGlobalFilter.maybeExecute(search)
|
|
92
|
-
}}
|
|
93
|
-
/>
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## Throttle column resizing
|
|
97
|
-
|
|
98
|
-
`columnResizingFeature` writes to `columnSizing` continuously. v9 supports `columnResizeMode:
|
|
99
|
-
'onChange' | 'onEnd'` — the default is `'onChange'` (commit per-frame). For very heavy tables,
|
|
100
|
-
either:
|
|
101
|
-
|
|
102
|
-
1. Set `columnResizeMode: 'onEnd'` so the commit only happens at pointerup.
|
|
103
|
-
2. Or, keep `'onChange'` for the visual handle but throttle the side effects you fire off it.
|
|
104
|
-
|
|
105
|
-
Throttle a side effect:
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
import { createThrottler } from '@tanstack/svelte-pacer/throttler'
|
|
109
|
-
|
|
110
|
-
const throttledSaveSizing = createThrottler(
|
|
111
|
-
(sizing: ColumnSizingState) => persistColumnSizingToStorage(sizing),
|
|
112
|
-
{ wait: 100 },
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
$effect(() => {
|
|
116
|
-
const sizing = table.atoms.columnSizing.get()
|
|
117
|
-
throttledSaveSizing.maybeExecute(sizing)
|
|
118
|
-
})
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## Patterns to avoid
|
|
122
|
-
|
|
123
|
-
### Don't debounce inside a `$effect`
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
// WRONG — new debouncer instance every effect re-run
|
|
127
|
-
$effect(() => {
|
|
128
|
-
const d = createDebouncer((v) => column.setFilterValue(v), { wait: 200 })
|
|
129
|
-
d.maybeExecute(value)
|
|
130
|
-
})
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Pacer instances must be stable. Declare them once at component init scope.
|
|
134
|
-
|
|
135
|
-
### Don't debounce things that should be immediate
|
|
136
|
-
|
|
137
|
-
Selection toggles, page changes, sort clicks — these are user-driven discrete events. Don't
|
|
138
|
-
debounce them. The user expects instant feedback.
|
|
139
|
-
|
|
140
|
-
### Don't double-commit
|
|
141
|
-
|
|
142
|
-
```ts
|
|
143
|
-
// WRONG — local state syncs immediately AND the debounced commit fires
|
|
144
|
-
oninput={(e) => {
|
|
145
|
-
column.setFilterValue(e.currentTarget.value)
|
|
146
|
-
debouncedSetFilter.maybeExecute(e.currentTarget.value)
|
|
147
|
-
}}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Pick one. If you want a snappy input, store the input value in local `$state` and only call
|
|
151
|
-
the debounced commit.
|
|
152
|
-
|
|
153
|
-
## Coordinating with TanStack Query
|
|
154
|
-
|
|
155
|
-
If your filter triggers a server query, debouncing the filter handler is necessary but not
|
|
156
|
-
sufficient — `placeholderData: keepPreviousData` is what keeps the UI from flashing. See the
|
|
157
|
-
`compose-with-tanstack-query` skill.
|
|
158
|
-
|
|
159
|
-
## Common failure modes
|
|
160
|
-
|
|
161
|
-
- **Recreating pacer instances inside `$effect` / `$derived`.** Each re-run produces a fresh
|
|
162
|
-
debouncer; nothing is ever delayed.
|
|
163
|
-
- **Hand-rolled `setTimeout` debounce.** Loses leading-edge / trailing-edge guarantees; harder
|
|
164
|
-
to cancel on unmount. Use pacer.
|
|
165
|
-
- **Debouncing a value that drives layout.** Causes user-visible lag where there shouldn't
|
|
166
|
-
be. Keep the input controlled by local state and only debounce the table-write call.
|
|
167
|
-
- **Forgetting to call `.maybeExecute`.** Constructing a debouncer doesn't enqueue anything;
|
|
168
|
-
you have to call `.maybeExecute(value)` per event.
|
|
169
|
-
- **Reimplementing pacer with `$effect` timers.** Don't.
|
|
170
|
-
|
|
171
|
-
## Related skills
|
|
172
|
-
|
|
173
|
-
- `tanstack-table/core/filtering` — column / global filter mechanics.
|
|
174
|
-
- `tanstack-table/core/column-layout` — column resizing modes (`onChange` vs `onEnd`).
|
|
175
|
-
- `tanstack-table/svelte/compose-with-tanstack-query` — pairing pacer with server queries.
|
|
176
|
-
- `tanstack-table/svelte/production-readiness` — where pacer fits in the perf checklist.
|