@tanstack/svelte-table 9.0.0-beta.5 → 9.0.0-beta.51
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 +2 -0
- package/dist/createTable.svelte.d.ts +0 -1
- package/dist/createTable.svelte.js +1 -2
- package/dist/createTableHook.svelte.d.ts +47 -13
- package/dist/createTableHook.svelte.js +16 -8
- package/dist/experimental-worker-plugin.d.ts +1 -0
- package/dist/experimental-worker-plugin.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/render-component.js +4 -0
- package/package.json +10 -5
- 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 +195 -0
- package/skills/table-state/SKILL.md +205 -0
- package/skills/with-tanstack-query/SKILL.md +147 -0
- package/skills/with-tanstack-virtual/SKILL.md +150 -0
- package/src/createTable.svelte.ts +1 -2
- package/src/createTableHook.svelte.ts +127 -21
- package/src/experimental-worker-plugin.ts +1 -0
- package/src/index.ts +1 -0
- 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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: with-tanstack-virtual
|
|
3
|
+
description: >
|
|
4
|
+
Virtualize Svelte Table final row or column models with reactive counts and scroll targets, stable keys, dynamic measurement, absolute transforms, sticky regions, grid/flex sizing, and infinite data.
|
|
5
|
+
metadata:
|
|
6
|
+
type: composition
|
|
7
|
+
library: '@tanstack/svelte-table'
|
|
8
|
+
framework: svelte
|
|
9
|
+
library_version: '9.0.0-beta.51'
|
|
10
|
+
requires:
|
|
11
|
+
- '@tanstack/table-core#core'
|
|
12
|
+
- getting-started
|
|
13
|
+
- table-state
|
|
14
|
+
sources:
|
|
15
|
+
- 'TanStack/table:docs/framework/svelte/guide/virtualization.md'
|
|
16
|
+
- 'TanStack/table:examples/svelte/virtualized-rows'
|
|
17
|
+
- 'TanStack/table:examples/svelte/virtualized-columns'
|
|
18
|
+
- 'TanStack/table:examples/svelte/virtualized-infinite-scrolling'
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
This skill builds on `@tanstack/table-core#core`, `getting-started`, and `table-state`. Virtual is a rendering layer over Table’s final model, never a `tableFeatures` plugin.
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
```svelte
|
|
26
|
+
<script lang="ts">
|
|
27
|
+
import { get } from 'svelte/store'
|
|
28
|
+
import { createVirtualizer } from '@tanstack/svelte-virtual'
|
|
29
|
+
|
|
30
|
+
let scrollElement = $state<HTMLDivElement>()
|
|
31
|
+
const rows = $derived(table.getRowModel().rows)
|
|
32
|
+
const rowVirtualizer = createVirtualizer({
|
|
33
|
+
count: rows.length,
|
|
34
|
+
getScrollElement: () => scrollElement ?? null,
|
|
35
|
+
estimateSize: () => 34,
|
|
36
|
+
getItemKey: (index) => rows[index]!.id,
|
|
37
|
+
overscan: 5,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// The store adapter does not track getter options. Push reactive inputs.
|
|
41
|
+
$effect(() => {
|
|
42
|
+
get(rowVirtualizer).setOptions({
|
|
43
|
+
count: rows.length,
|
|
44
|
+
getScrollElement: () => scrollElement ?? null,
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div
|
|
50
|
+
bind:this={scrollElement}
|
|
51
|
+
style="height: 500px; overflow: auto; position: relative"
|
|
52
|
+
>
|
|
53
|
+
<div style:height={`${$rowVirtualizer.getTotalSize()}px`}>
|
|
54
|
+
{#each $rowVirtualizer.getVirtualItems() as item (item.key)}
|
|
55
|
+
<div style={`position:absolute;transform:translateY(${item.start}px)`}>
|
|
56
|
+
{rows[item.index].id}
|
|
57
|
+
</div>
|
|
58
|
+
{/each}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Core Patterns
|
|
64
|
+
|
|
65
|
+
### Virtualize visible models
|
|
66
|
+
|
|
67
|
+
Use `table.getRowModel().rows` for rows and `table.getVisibleLeafColumns()` for columns. Recompute counts when filtering, sorting, expansion, or visibility changes.
|
|
68
|
+
|
|
69
|
+
### Make CSS geometry agree with measurement
|
|
70
|
+
|
|
71
|
+
Use one scroll container, a total-size spacer, positioned items, and either fixed estimates or `measureElement`. For semantic tables with dynamic rows, follow the maintained grid/flex examples rather than assuming native table layout will honor transforms.
|
|
72
|
+
|
|
73
|
+
### Fetch before the virtual end
|
|
74
|
+
|
|
75
|
+
In infinite scrolling, compare the last virtual item with fetched row count, then request the next Query page only when more server rows exist and no fetch is active.
|
|
76
|
+
|
|
77
|
+
## Common Mistakes
|
|
78
|
+
|
|
79
|
+
### HIGH Virtualizing raw data
|
|
80
|
+
|
|
81
|
+
Wrong:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const rows = data
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Correct:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const rows = $derived(table.getRowModel().rows)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Raw data ignores Table filtering, sorting, grouping, expansion, and pagination decisions.
|
|
94
|
+
|
|
95
|
+
Source: `examples/svelte/virtualized-rows/src/App.svelte`
|
|
96
|
+
|
|
97
|
+
### HIGH Expecting getter options to stay reactive
|
|
98
|
+
|
|
99
|
+
Wrong:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const virtualizer = createVirtualizer({
|
|
103
|
+
get count() {
|
|
104
|
+
return rows.length
|
|
105
|
+
},
|
|
106
|
+
getScrollElement,
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Correct:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const virtualizer = createVirtualizer({ count: rows.length, getScrollElement })
|
|
114
|
+
$effect(() => {
|
|
115
|
+
get(virtualizer).setOptions({
|
|
116
|
+
count: rows.length,
|
|
117
|
+
getScrollElement,
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`createVirtualizer` returns a Svelte store, and its adapter does not track getter options. Push rune-derived counts and the bound scroll element with `$effect` and `get(store).setOptions(...)`.
|
|
123
|
+
|
|
124
|
+
Source: `examples/svelte/virtualized-rows/src/App.svelte`
|
|
125
|
+
|
|
126
|
+
### HIGH Omitting the geometry contract
|
|
127
|
+
|
|
128
|
+
Wrong:
|
|
129
|
+
|
|
130
|
+
```svelte
|
|
131
|
+
{#each rowVirtualizer.getVirtualItems() as item}<div>
|
|
132
|
+
{rows[item.index].id}
|
|
133
|
+
</div>{/each}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Correct:
|
|
137
|
+
|
|
138
|
+
```svelte
|
|
139
|
+
<div style:height={`${$rowVirtualizer.getTotalSize()}px`}>
|
|
140
|
+
<div style="position:absolute"></div>
|
|
141
|
+
</div>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Virtual supplies ranges and measurements, not spacer height, transforms, sticky regions, or column widths. In markup, call virtualizer methods through the store auto-subscription (`$rowVirtualizer`); use `get(rowVirtualizer)` in script code.
|
|
145
|
+
|
|
146
|
+
Source: `docs/framework/svelte/guide/virtualization.md`
|
|
147
|
+
|
|
148
|
+
## API Discovery
|
|
149
|
+
|
|
150
|
+
Inspect installed `@tanstack/svelte-table/src` for Table APIs and `@tanstack/svelte-virtual/src` for the exact virtualizer options. Use the maintained Svelte examples for layout combinations.
|
|
@@ -51,7 +51,6 @@ export type SvelteTable<
|
|
|
51
51
|
* const table = createTable(
|
|
52
52
|
* {
|
|
53
53
|
* features,
|
|
54
|
-
* rowModels: {},
|
|
55
54
|
* columns,
|
|
56
55
|
* data,
|
|
57
56
|
* },
|
|
@@ -73,7 +72,7 @@ export function createTable<
|
|
|
73
72
|
// 1. Merge reactivity into options using mergeObjects (preserves getters)
|
|
74
73
|
const mergedOptions = mergeObjects(tableOptions, {
|
|
75
74
|
features: {
|
|
76
|
-
|
|
75
|
+
coreReactivityFeature: svelteReactivity(),
|
|
77
76
|
...tableOptions.features,
|
|
78
77
|
},
|
|
79
78
|
}) as TableOptions<TFeatures, TData>
|
|
@@ -376,6 +376,84 @@ export type AppSvelteTable<
|
|
|
376
376
|
FlexRender: typeof FlexRenderSvelte
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
export interface CreateTableHookResult<
|
|
380
|
+
TFeatures extends TableFeatures,
|
|
381
|
+
TTableComponents extends Record<string, ComponentType<any>>,
|
|
382
|
+
TCellComponents extends Record<string, ComponentType<any>>,
|
|
383
|
+
THeaderComponents extends Record<string, ComponentType<any>>,
|
|
384
|
+
> {
|
|
385
|
+
/** The features object that was passed to `createTableHook`. */
|
|
386
|
+
appFeatures: TFeatures
|
|
387
|
+
/**
|
|
388
|
+
* A column helper pre-bound to `TFeatures` and the registered components, so
|
|
389
|
+
* the cell/header/footer render props expose the bound components.
|
|
390
|
+
*/
|
|
391
|
+
createAppColumnHelper: <TData extends RowData>() => AppColumnHelper<
|
|
392
|
+
TFeatures,
|
|
393
|
+
TData,
|
|
394
|
+
TCellComponents,
|
|
395
|
+
THeaderComponents
|
|
396
|
+
>
|
|
397
|
+
/**
|
|
398
|
+
* Creates a table with the `App*` wrapper components and registered
|
|
399
|
+
* `tableComponents` attached. `TData` is inferred from the `data` option.
|
|
400
|
+
*/
|
|
401
|
+
createAppTable: <TData extends RowData, TSelected = TableState<TFeatures>>(
|
|
402
|
+
tableOptions: Omit<TableOptions<TFeatures, TData>, 'features'>,
|
|
403
|
+
selector?: (state: TableState<TFeatures>) => TSelected,
|
|
404
|
+
) => AppSvelteTable<
|
|
405
|
+
TFeatures,
|
|
406
|
+
TData,
|
|
407
|
+
TSelected,
|
|
408
|
+
TTableComponents,
|
|
409
|
+
TCellComponents,
|
|
410
|
+
THeaderComponents
|
|
411
|
+
>
|
|
412
|
+
/**
|
|
413
|
+
* Reads the table provided by the nearest `<table.AppTable>`. This is the same
|
|
414
|
+
* extended instance `createAppTable` returns, so the `App*` components and your
|
|
415
|
+
* `tableComponents` are available on it.
|
|
416
|
+
*
|
|
417
|
+
* Pass `TSelected` to match the selector you gave `createAppTable`, so
|
|
418
|
+
* `table.state` is typed as the selected slice. It cannot be inferred
|
|
419
|
+
* automatically (context does not carry the provider's generics), so it
|
|
420
|
+
* defaults to the full table state, which is correct for the common case of
|
|
421
|
+
* `createAppTable` without a selector.
|
|
422
|
+
*/
|
|
423
|
+
useTableContext: <
|
|
424
|
+
TData extends RowData = RowData,
|
|
425
|
+
TSelected = TableState<TFeatures>,
|
|
426
|
+
>() => AppSvelteTable<
|
|
427
|
+
TFeatures,
|
|
428
|
+
TData,
|
|
429
|
+
TSelected,
|
|
430
|
+
TTableComponents,
|
|
431
|
+
TCellComponents,
|
|
432
|
+
THeaderComponents
|
|
433
|
+
>
|
|
434
|
+
/**
|
|
435
|
+
* Reads the cell provided by the nearest `<table.AppCell>`, extended with your
|
|
436
|
+
* `cellComponents` and a context-bound `FlexRender`.
|
|
437
|
+
*/
|
|
438
|
+
useCellContext: <TValue extends CellData = CellData>() => Cell<
|
|
439
|
+
TFeatures,
|
|
440
|
+
any,
|
|
441
|
+
TValue
|
|
442
|
+
> &
|
|
443
|
+
TCellComponents & { FlexRender: typeof FlexRenderSvelte }
|
|
444
|
+
/**
|
|
445
|
+
* Reads the header provided by the nearest `<table.AppHeader>` /
|
|
446
|
+
* `<table.AppFooter>`, extended with your `headerComponents` and a
|
|
447
|
+
* context-bound `FlexRender`.
|
|
448
|
+
*/
|
|
449
|
+
useHeaderContext: <TValue extends CellData = CellData>() => Header<
|
|
450
|
+
TFeatures,
|
|
451
|
+
any,
|
|
452
|
+
TValue
|
|
453
|
+
> &
|
|
454
|
+
THeaderComponents & { FlexRender: typeof FlexRenderSvelte }
|
|
455
|
+
}
|
|
456
|
+
|
|
379
457
|
// =============================================================================
|
|
380
458
|
// createTableHook Factory
|
|
381
459
|
// =============================================================================
|
|
@@ -404,12 +482,12 @@ export type AppSvelteTable<
|
|
|
404
482
|
* rowPaginationFeature,
|
|
405
483
|
* rowSortingFeature,
|
|
406
484
|
* columnFilteringFeature,
|
|
407
|
-
* }),
|
|
408
|
-
* rowModels: {
|
|
409
485
|
* paginatedRowModel: createPaginatedRowModel(),
|
|
410
|
-
* sortedRowModel: createSortedRowModel(
|
|
411
|
-
* filteredRowModel: createFilteredRowModel(
|
|
412
|
-
*
|
|
486
|
+
* sortedRowModel: createSortedRowModel(),
|
|
487
|
+
* filteredRowModel: createFilteredRowModel(),
|
|
488
|
+
* sortFns,
|
|
489
|
+
* filterFns,
|
|
490
|
+
* }),
|
|
413
491
|
* tableComponents: { PaginationControls, RowCount },
|
|
414
492
|
* cellComponents: { TextCell, NumberCell },
|
|
415
493
|
* headerComponents: { SortIndicator, ColumnFilter },
|
|
@@ -431,7 +509,12 @@ export function createTableHook<
|
|
|
431
509
|
TTableComponents,
|
|
432
510
|
TCellComponents,
|
|
433
511
|
THeaderComponents
|
|
434
|
-
>)
|
|
512
|
+
>): CreateTableHookResult<
|
|
513
|
+
TFeatures,
|
|
514
|
+
TTableComponents,
|
|
515
|
+
TCellComponents,
|
|
516
|
+
THeaderComponents
|
|
517
|
+
> {
|
|
435
518
|
/**
|
|
436
519
|
* Create a column helper pre-bound to the features and components configured in this table hook.
|
|
437
520
|
* The cell, header, and footer contexts include pre-bound components (e.g., `cell.TextCell`).
|
|
@@ -455,9 +538,16 @@ export function createTableHook<
|
|
|
455
538
|
* Use this in custom `tableComponents` passed to `createTableHook`.
|
|
456
539
|
* TFeatures is already known from the createTableHook call.
|
|
457
540
|
*/
|
|
458
|
-
function useTableContext<
|
|
541
|
+
function useTableContext<
|
|
542
|
+
TData extends RowData = RowData,
|
|
543
|
+
TSelected = TableState<TFeatures>,
|
|
544
|
+
>(): AppSvelteTable<
|
|
459
545
|
TFeatures,
|
|
460
|
-
TData
|
|
546
|
+
TData,
|
|
547
|
+
TSelected,
|
|
548
|
+
TTableComponents,
|
|
549
|
+
TCellComponents,
|
|
550
|
+
THeaderComponents
|
|
461
551
|
> {
|
|
462
552
|
const table = getContext(tableContextKey)
|
|
463
553
|
|
|
@@ -468,7 +558,17 @@ export function createTableHook<
|
|
|
468
558
|
)
|
|
469
559
|
}
|
|
470
560
|
|
|
471
|
-
|
|
561
|
+
// `<table.AppTable>` provides the extended table (the App* wrapper
|
|
562
|
+
// components and `tableComponents` are Object.assign-ed onto the same
|
|
563
|
+
// instance `createAppTable` returns), so this asserts the runtime shape.
|
|
564
|
+
return table as unknown as AppSvelteTable<
|
|
565
|
+
TFeatures,
|
|
566
|
+
TData,
|
|
567
|
+
TSelected,
|
|
568
|
+
TTableComponents,
|
|
569
|
+
TCellComponents,
|
|
570
|
+
THeaderComponents
|
|
571
|
+
>
|
|
472
572
|
}
|
|
473
573
|
|
|
474
574
|
/**
|
|
@@ -480,7 +580,8 @@ export function createTableHook<
|
|
|
480
580
|
TFeatures,
|
|
481
581
|
any,
|
|
482
582
|
TValue
|
|
483
|
-
>
|
|
583
|
+
> &
|
|
584
|
+
TCellComponents & { FlexRender: typeof FlexRenderSvelte } {
|
|
484
585
|
const cell = getContext(cellContextKey)
|
|
485
586
|
|
|
486
587
|
if (!cell) {
|
|
@@ -490,7 +591,11 @@ export function createTableHook<
|
|
|
490
591
|
)
|
|
491
592
|
}
|
|
492
593
|
|
|
493
|
-
|
|
594
|
+
// `<table.AppCell>` Object.assign-es `cellComponents` and `FlexRender` onto
|
|
595
|
+
// the same cell instance it puts in context, so this asserts the runtime
|
|
596
|
+
// shape.
|
|
597
|
+
return cell as unknown as Cell<TFeatures, any, TValue> &
|
|
598
|
+
TCellComponents & { FlexRender: typeof FlexRenderSvelte }
|
|
494
599
|
}
|
|
495
600
|
|
|
496
601
|
/**
|
|
@@ -502,7 +607,8 @@ export function createTableHook<
|
|
|
502
607
|
TFeatures,
|
|
503
608
|
any,
|
|
504
609
|
TValue
|
|
505
|
-
>
|
|
610
|
+
> &
|
|
611
|
+
THeaderComponents & { FlexRender: typeof FlexRenderSvelte } {
|
|
506
612
|
const header = getContext(headerContextKey)
|
|
507
613
|
|
|
508
614
|
if (!header) {
|
|
@@ -511,7 +617,10 @@ export function createTableHook<
|
|
|
511
617
|
)
|
|
512
618
|
}
|
|
513
619
|
|
|
514
|
-
|
|
620
|
+
// `<table.AppHeader>` / `<table.AppFooter>` Object.assign `headerComponents`
|
|
621
|
+
// and `FlexRender` onto the same header instance they put in context.
|
|
622
|
+
return header as unknown as Header<TFeatures, any, TValue> &
|
|
623
|
+
THeaderComponents & { FlexRender: typeof FlexRenderSvelte }
|
|
515
624
|
}
|
|
516
625
|
|
|
517
626
|
/**
|
|
@@ -527,10 +636,7 @@ export function createTableHook<
|
|
|
527
636
|
TData extends RowData,
|
|
528
637
|
TSelected = TableState<TFeatures>,
|
|
529
638
|
>(
|
|
530
|
-
tableOptions: Omit<
|
|
531
|
-
TableOptions<TFeatures, TData>,
|
|
532
|
-
'features' | 'rowModels'
|
|
533
|
-
>,
|
|
639
|
+
tableOptions: Omit<TableOptions<TFeatures, TData>, 'features'>,
|
|
534
640
|
selector?: (state: TableState<TFeatures>) => TSelected,
|
|
535
641
|
): AppSvelteTable<
|
|
536
642
|
TFeatures,
|
|
@@ -573,7 +679,7 @@ export function createTableHook<
|
|
|
573
679
|
return AppTableSvelte(internal, { ...props })
|
|
574
680
|
}) as Component<{ children: Snippet }>
|
|
575
681
|
|
|
576
|
-
const AppCell = ((internal: any, { children, cell
|
|
682
|
+
const AppCell = ((internal: any, { children, cell }: any) => {
|
|
577
683
|
setContext(cellContextKey, cell)
|
|
578
684
|
return AppCellSvelte(internal, {
|
|
579
685
|
cell,
|
|
@@ -585,7 +691,7 @@ export function createTableHook<
|
|
|
585
691
|
children: Snippet<[any]>
|
|
586
692
|
}>
|
|
587
693
|
|
|
588
|
-
const AppHeader = ((internal: any, { children, header
|
|
694
|
+
const AppHeader = ((internal: any, { children, header }: any) => {
|
|
589
695
|
setContext(headerContextKey, header)
|
|
590
696
|
return AppHeaderSvelte(internal, {
|
|
591
697
|
header,
|
|
@@ -598,7 +704,7 @@ export function createTableHook<
|
|
|
598
704
|
}>
|
|
599
705
|
|
|
600
706
|
// AppFooter reuses AppHeaderSvelte (footers use Header type in table-core)
|
|
601
|
-
const AppFooter = ((internal: any, { children, header
|
|
707
|
+
const AppFooter = ((internal: any, { children, header }: any) => {
|
|
602
708
|
setContext(headerContextKey, header)
|
|
603
709
|
return AppHeaderSvelte(internal, {
|
|
604
710
|
header,
|
|
@@ -629,7 +735,7 @@ export function createTableHook<
|
|
|
629
735
|
}
|
|
630
736
|
|
|
631
737
|
return {
|
|
632
|
-
appFeatures: defaultTableOptions.features
|
|
738
|
+
appFeatures: defaultTableOptions.features,
|
|
633
739
|
createAppColumnHelper,
|
|
634
740
|
createAppTable,
|
|
635
741
|
useTableContext,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@tanstack/table-core/experimental-worker-plugin'
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type {
|
|
|
14
14
|
AppSvelteTable,
|
|
15
15
|
ComponentType,
|
|
16
16
|
CreateTableHookOptions,
|
|
17
|
+
CreateTableHookResult,
|
|
17
18
|
} from './createTableHook.svelte'
|
|
18
19
|
export { createTableState } from './createTableState.svelte'
|
|
19
20
|
export { default as FlexRender } from './FlexRender.svelte'
|
|
@@ -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.
|