-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Expand file tree
/
Copy pathtable.svelte.ts
More file actions
119 lines (112 loc) · 3.57 KB
/
table.svelte.ts
File metadata and controls
119 lines (112 loc) · 3.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import {
createTable,
type RowData,
type TableOptions,
type TableOptionsResolved,
type TableState
} from "@tanstack/table-core";
/**
* Merges objects while preserving property getters for lazy evaluation.
* Properties are defined as getters that look up values from sources in
* reverse order at access time. This is critical: it means reading a
* property from the merged result doesn't happen until the property is
* actually accessed, not when mergeObjects is called.
*/
export function mergeObjects(...sources: any): any {
const target: Record<string, any> = {};
for (let i = 0; i < sources.length; i++) {
let source = sources[i];
if (typeof source === "function") source = source();
if (source) {
const descriptors = Object.getOwnPropertyDescriptors(source);
for (const key in descriptors) {
if (key in target) continue;
Object.defineProperty(target, key, {
enumerable: true,
get() {
for (let j = sources.length - 1; j >= 0; j--) {
let s = sources[j];
if (typeof s === "function") s = s();
const v = (s || {})[key];
if (v !== undefined) return v;
}
}
});
}
}
}
return target;
}
/**
* Creates a reactive TanStack Table for Svelte 5.
*
* The reactivity works through mergeObjects' lazy getters:
* - $effect.pre calls table.setOptions() with a merged object
* - The merged object has lazy getters for `data`, `columns`, `state`, etc.
* - TanStack stores this object but doesn't deep-read all properties immediately
* - When getRowModel() is called (in $derived), TanStack reads `data` and `columns`
* through the lazy getters, which read the reactive $state/$derived values
* - onStateChange fires when TanStack mutates its own state → bumps version
* - version is read by getRowModel/getHeaderGroups → $derived re-evaluates
*/
export function createSvelteTable<TData extends RowData>(
options: TableOptions<TData>
) {
const resolvedOptions: TableOptionsResolved<TData> = mergeObjects(
{
state: {},
onStateChange() {},
renderFallbackValue: null,
mergeOptions: (
defaultOptions: TableOptions<TData>,
opts: Partial<TableOptions<TData>>
) => {
return mergeObjects(defaultOptions, opts);
}
},
options
);
const table = createTable(resolvedOptions);
let state = $state<Partial<TableState>>(table.initialState);
let version = $state(0);
function updateOptions(): void {
table.setOptions((prev) => {
// Eagerly resolve prev into a plain object to prevent
// unbounded getter chains. Each setOptions call wraps prev
// in a new mergeObjects layer; without flattening, property
// lookups traverse every previous layer, causing stack overflow.
return mergeObjects({ ...prev }, options, {
state: mergeObjects(state, options.state || {}),
onStateChange: (updater: any) => {
if (updater instanceof Function) state = updater(state);
else state = mergeObjects(state, updater);
version += 1;
options.onStateChange?.(updater);
}
});
});
}
// Initial sync
updateOptions();
// Re-sync when options change. Because mergeObjects uses lazy getters,
// this effect's tracked dependencies are only the properties that
// table.setOptions() eagerly reads (which is minimal — mostly just
// checking if the options object reference changed).
$effect.pre(() => {
updateOptions();
});
return {
getRowModel: () => {
void version;
return table.getRowModel();
},
getHeaderGroups: () => {
void version;
return table.getHeaderGroups();
},
getColumn: (id: string) => {
void version;
return table.getColumn(id);
}
};
}