@tanstack/svelte-table 9.0.0-alpha.9 → 9.0.0-beta.10

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 (63) hide show
  1. package/README.md +127 -0
  2. package/dist/AppCell.svelte +13 -0
  3. package/dist/AppCell.svelte.d.ts +9 -0
  4. package/dist/AppHeader.svelte +13 -0
  5. package/dist/AppHeader.svelte.d.ts +9 -0
  6. package/dist/AppTable.svelte +11 -0
  7. package/dist/AppTable.svelte.d.ts +7 -0
  8. package/dist/FlexRender.svelte +103 -0
  9. package/dist/FlexRender.svelte.d.ts +51 -0
  10. package/dist/context-keys.d.ts +3 -0
  11. package/dist/context-keys.js +3 -0
  12. package/dist/createTable.svelte.d.ts +47 -0
  13. package/dist/createTable.svelte.js +78 -0
  14. package/dist/createTableHook.svelte.d.ts +235 -0
  15. package/dist/createTableHook.svelte.js +170 -0
  16. package/dist/createTableState.svelte.d.ts +17 -0
  17. package/dist/createTableState.svelte.js +27 -0
  18. package/dist/flex-render.d.ts +1 -0
  19. package/dist/flex-render.js +2 -0
  20. package/dist/index.d.ts +8 -3
  21. package/dist/index.js +6 -3
  22. package/dist/merge-objects.d.ts +24 -0
  23. package/dist/merge-objects.js +45 -0
  24. package/dist/reactivity.svelte.d.ts +9 -0
  25. package/dist/reactivity.svelte.js +89 -0
  26. package/dist/render-component.d.ts +66 -9
  27. package/dist/render-component.js +62 -4
  28. package/dist/static-functions.d.ts +1 -0
  29. package/dist/static-functions.js +1 -0
  30. package/dist/subscribe.d.ts +24 -0
  31. package/dist/subscribe.js +4 -0
  32. package/package.json +31 -11
  33. package/skills/svelte/client-to-server/SKILL.md +236 -0
  34. package/skills/svelte/compose-with-tanstack-form/SKILL.md +294 -0
  35. package/skills/svelte/compose-with-tanstack-pacer/SKILL.md +176 -0
  36. package/skills/svelte/compose-with-tanstack-query/SKILL.md +296 -0
  37. package/skills/svelte/compose-with-tanstack-store/SKILL.md +270 -0
  38. package/skills/svelte/compose-with-tanstack-virtual/SKILL.md +289 -0
  39. package/skills/svelte/getting-started/SKILL.md +338 -0
  40. package/skills/svelte/migrate-v8-to-v9/SKILL.md +264 -0
  41. package/skills/svelte/production-readiness/SKILL.md +258 -0
  42. package/skills/svelte/table-state/SKILL.md +437 -0
  43. package/src/AppCell.svelte +13 -0
  44. package/src/AppHeader.svelte +13 -0
  45. package/src/AppTable.svelte +11 -0
  46. package/src/FlexRender.svelte +103 -0
  47. package/src/context-keys.ts +3 -0
  48. package/src/createTable.svelte.ts +136 -0
  49. package/src/createTableHook.svelte.ts +636 -0
  50. package/src/createTableState.svelte.ts +30 -0
  51. package/src/flex-render.ts +3 -0
  52. package/src/index.ts +20 -3
  53. package/src/merge-objects.ts +79 -0
  54. package/src/reactivity.svelte.ts +118 -0
  55. package/src/render-component.ts +75 -9
  56. package/src/static-functions.ts +1 -0
  57. package/src/subscribe.ts +46 -0
  58. package/dist/flex-render.svelte +0 -35
  59. package/dist/flex-render.svelte.d.ts +0 -28
  60. package/dist/table.svelte.d.ts +0 -28
  61. package/dist/table.svelte.js +0 -87
  62. package/src/flex-render.svelte +0 -35
  63. package/src/table.svelte.ts +0 -117
@@ -0,0 +1,294 @@
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
+ filteredRowModel: createFilteredRowModel(),
120
+ paginatedRowModel: createPaginatedRowModel(),
121
+ filterFns,
122
+ })
123
+
124
+ const columnHelper = createColumnHelper<typeof features, Person>()
125
+
126
+ const personSchema = z.object({
127
+ firstName: z.string().min(1),
128
+ lastName: z.string().min(1),
129
+ age: z.number().min(0).max(150),
130
+ visits: z.number().min(0),
131
+ progress: z.number().min(0).max(100),
132
+ status: z.enum(['relationship', 'complicated', 'single']),
133
+ })
134
+
135
+ const formSchema = z.object({ data: z.array(personSchema) })
136
+ type FormData = z.infer<typeof formSchema>
137
+
138
+ const form = createAppForm(() => ({
139
+ defaultValues: { data: makeData(1_000) } as FormData,
140
+ validators: { onChange: formSchema },
141
+ onSubmit: ({ value }) => alert(`Saved ${value.data.length} rows`),
142
+ }))
143
+
144
+ const columns = columnHelper.columns([
145
+ columnHelper.accessor('firstName', {
146
+ header: 'First Name',
147
+ cell: ({ row }) =>
148
+ renderComponent(TextFieldCell, {
149
+ form,
150
+ rowIndex: row.index,
151
+ fieldName: 'firstName',
152
+ }),
153
+ }),
154
+ columnHelper.accessor('age', {
155
+ header: 'Age',
156
+ cell: ({ row }) =>
157
+ renderComponent(NumberFieldCell, {
158
+ form,
159
+ rowIndex: row.index,
160
+ fieldName: 'age',
161
+ }),
162
+ }),
163
+ columnHelper.accessor('status', {
164
+ header: 'Status',
165
+ cell: ({ row }) =>
166
+ renderComponent(SelectFieldCell, {
167
+ form,
168
+ rowIndex: row.index,
169
+ }),
170
+ }),
171
+ // ...
172
+ ])
173
+
174
+ const table = createTable({
175
+ features,
176
+ columns,
177
+ get data() {
178
+ // The form is the source of truth.
179
+ return form.state.values.data
180
+ },
181
+ })
182
+ </script>
183
+
184
+ <form
185
+ onsubmit={(e) => {
186
+ e.preventDefault()
187
+ void form.handleSubmit()
188
+ }}
189
+ >
190
+ <form.AppForm>
191
+ {#snippet children()}
192
+ <form.FormStateIndicator />
193
+ <form.SubmitButton label="Save" />
194
+ {/snippet}
195
+ </form.AppForm>
196
+
197
+ <button
198
+ type="button"
199
+ onclick={() =>
200
+ form.pushFieldValue('data', {
201
+ firstName: '',
202
+ lastName: '',
203
+ age: 0,
204
+ visits: 0,
205
+ progress: 0,
206
+ status: 'single',
207
+ })}>Add Row</button
208
+ >
209
+
210
+ <table>
211
+ <thead>
212
+ {#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
213
+ <tr>
214
+ {#each headerGroup.headers as header (header.id)}
215
+ <th><FlexRender {header} /></th>
216
+ {/each}
217
+ </tr>
218
+ {/each}
219
+ </thead>
220
+ <tbody>
221
+ {#each table.getRowModel().rows as row (row.id)}
222
+ <tr>
223
+ {#each row.getAllCells() as cell (cell.id)}
224
+ <td><FlexRender {cell} /></td>
225
+ {/each}
226
+ </tr>
227
+ {/each}
228
+ </tbody>
229
+ </table>
230
+ </form>
231
+ ```
232
+
233
+ ## Why `row.index` and not `row.id`?
234
+
235
+ Form indexes its arrays positionally. `row.index` is the position inside the **current** row
236
+ model (after filter + sort + paging). If you want a positional address into `form.state.values.data`,
237
+ you usually want the **original** index — pass `row.original` somewhere that exposes it, or
238
+ store an `id` field and look it up.
239
+
240
+ For the common case where the table renders the array in its natural order (no sort, no
241
+ filter that reorders), `row.index` matches the form-array position.
242
+
243
+ ## Add Row / Remove Row
244
+
245
+ Use the form's array helpers; the table re-renders because its `data` getter points at
246
+ `form.state.values.data`.
247
+
248
+ ```ts
249
+ form.pushFieldValue('data', newPerson)
250
+ form.removeFieldValue('data', rowIndex)
251
+ form.replaceFieldValue('data', rowIndex, updatedPerson)
252
+ ```
253
+
254
+ ## Pairing with selection
255
+
256
+ Add `rowSelectionFeature` to enable row checkboxes, then a "delete selected" button can use
257
+ `table.getSelectedRowModel()` to collect rows and `form.removeFieldValue` to remove them.
258
+
259
+ ```ts
260
+ const selectedRows = table.getSelectedRowModel().rows
261
+ const indexesDesc = selectedRows.map((r) => r.index).sort((a, b) => b - a)
262
+ for (const i of indexesDesc) {
263
+ form.removeFieldValue('data', i)
264
+ }
265
+ table.resetRowSelection()
266
+ ```
267
+
268
+ Remove in descending order so earlier removals don't shift later indexes.
269
+
270
+ ## Pairing with virtualization
271
+
272
+ You can virtualize the rows even with editable cells — but be aware that **virtualized rows
273
+ unmount when scrolled out of view**, taking their inline form fields with them. If a cell has
274
+ unsaved local-only state, you'll lose it. Use Form fields (which live on the form's state)
275
+ and you're fine — the field state survives the unmount.
276
+
277
+ ## Common failure modes
278
+
279
+ - **`renderComponent` from React docs.** Use the Svelte adapter's `renderComponent` from
280
+ `@tanstack/svelte-table`. The signature is the same shape but the runtime is different.
281
+ - **`form` not reactive in cells.** Pass `form` as a prop; don't reach for it via context
282
+ unless you set up `formContext`.
283
+ - **Wrong field name.** `data[${row.index}].firstName` — string template, not a plain join.
284
+ - **Reordering / filtering breaks `row.index`.** As above. Either keep a stable id and resolve
285
+ back to the form-array index, or accept that `row.index` only addresses the visible window.
286
+ - **Editing inside virtualized rows without form state.** Field values lost on scroll.
287
+ - **Reimplementing form state with `$state` per cell.** Defeats the whole point — Form already
288
+ owns this state and runs validation.
289
+
290
+ ## Related skills
291
+
292
+ - `tanstack-table/core/row-selection` — checkbox column patterns.
293
+ - `tanstack-table/core/column-definitions` — accessor / display columns.
294
+ - `tanstack-table/svelte/table-state` — `getRowModel()` and reactivity.
@@ -0,0 +1,176 @@
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` (which requires `columnSizingFeature` to also be registered in `tableFeatures`) 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.