@tanstack/angular-table 9.0.0-alpha.49 → 9.0.0-alpha.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.
@@ -2,8 +2,8 @@
2
2
  name: angular/compose-with-tanstack-store
3
3
  description: >
4
4
  Compose TanStack Table v9 with `@tanstack/angular-store`. TanStack Table v9 is itself built on
5
- TanStack Store — each state slice is an atom. Three read surfaces: `table.atoms.<slice>` (per-slice
6
- readonly, signal-backed), `table.store` (flat readonly view), and `table.baseAtoms.<slice>`
5
+ TanStack Store — each state slice is an atom. Read surfaces include `table.atoms.<slice>`
6
+ (per-slice readonly, signal-backed), `table.state` / `table.store` (flat readonly views), and `table.baseAtoms.<slice>`
7
7
  (writable). The `atoms` table option lets you replace an internal slice with an external
8
8
  TanStack Store atom for cross-app sharing (URL sync, persistence, multi-table coordination).
9
9
  In Angular, native signals + `state` + `on[State]Change` is the default; reach for external
@@ -42,11 +42,12 @@ sources:
42
42
 
43
43
  Every TanStack Table instance exposes its state at three layers:
44
44
 
45
- | Surface | Shape | Angular reactivity | Use when |
46
- | ------------------------- | --------------------------------- | ------------------------------- | --------------------------------------------------- |
47
- | `table.baseAtoms.<slice>` | Writable `Atom<T>` | Backed by an Angular `signal` | Direct internal writes; rare |
48
- | `table.atoms.<slice>` | Readonly `Atom<T>` (derived) | Backed by an Angular `computed` | Reading or driving Angular reactivity per-slice |
49
- | `table.store` | Readonly flat `Store<TableState>` | Backed by an Angular `computed` | Reading multiple slices in one go (devtools, debug) |
45
+ | Surface | Shape | Angular reactivity | Use when |
46
+ | ------------------------- | --------------------------------- | ------------------------------- | ----------------------------------------------- |
47
+ | `table.baseAtoms.<slice>` | Writable `Atom<T>` | Backed by an Angular `signal` | Direct internal writes; rare |
48
+ | `table.atoms.<slice>` | Readonly `Atom<T>` (derived) | Backed by an Angular `computed` | Reading or driving Angular reactivity per-slice |
49
+ | `table.state` | Readonly flat proxy | Reads from slice atoms | Full-state JSON/debug output |
50
+ | `table.store` | Readonly flat `Store<TableState>` | Backed by an Angular `computed` | Low-level flat store access; rare |
50
51
 
51
52
  All three are populated only for **registered features** (`_features`). All
52
53
  three are signal-backed via `angularReactivity(injector)`:
@@ -60,7 +61,7 @@ tracks the dependency.
60
61
  ```ts
61
62
  this.table.atoms.pagination.get() // current value (reactive)
62
63
  this.table.atoms.pagination.subscribe(obs) // RxJS observer form
63
- this.table.store.state.pagination // flat snapshot read
64
+ this.table.state.pagination // flat proxy read; prefer atoms for narrow reads
64
65
  this.table.baseAtoms.pagination.set(...) // direct internal write (avoid)
65
66
  ```
66
67
 
@@ -3,8 +3,8 @@ name: angular/migrate-v8-to-v9
3
3
  description: >
4
4
  Mechanical v8 → v9 migration for `@tanstack/angular-table`: `createAngularTable` →
5
5
  `injectTable`, `get*RowModel()` options → `_rowModels` factories with explicit `*Fns`,
6
- required `_features` via `tableFeatures()`, `state` access via `table.store.state` instead
7
- of `table.getState()`, `createColumnHelper<TFeatures, TData>()` generic-order flip, every
6
+ required `_features` via `tableFeatures()`, state access via `table.atoms.<slice>.get()`
7
+ or `table.state` instead of `table.getState()`, `createColumnHelper<TFeatures, TData>()` generic-order flip, every
8
8
  type now requires `TFeatures`, `enablePinning` split into `enableColumnPinning` /
9
9
  `enableRowPinning`, `sortingFn` → `sortFn` rename pile, `ColumnSizingInfo` → `ColumnResizing`
10
10
  split, removal of `_`-prefixed internals, signal-backed atoms replacing v8 memoized accessors,
@@ -124,24 +124,26 @@ Row-model and feature lookup tables → [`references/v8-to-v9-mapping.md`](refer
124
124
 
125
125
  ---
126
126
 
127
- ## 3. State access: `getState()` → `table.store.state` (and atoms)
127
+ ## 3. State access: `getState()` → atoms or `table.state`
128
128
 
129
129
  ```ts
130
130
  // v8
131
131
  const { sorting, pagination } = table.getState()
132
132
 
133
- // v9 — flat snapshot
134
- const { sorting, pagination } = table.store.state
133
+ // v9 — full-state flat proxy
134
+ const { sorting, pagination } = table.state
135
135
 
136
- // v9 — per slice (signal-backed in Angular)
136
+ // v9 — per slice (preferred for Angular render code)
137
137
  const sorting = table.atoms.sorting.get()
138
138
  const pagination = table.atoms.pagination.get()
139
139
  ```
140
140
 
141
- In Angular, all three (`table.atoms.<slice>`, `table.store.state`,
141
+ In Angular, all three (`table.atoms.<slice>`, `table.state`,
142
142
  `table.baseAtoms.<slice>`) are signal-backed — reading them inside a template,
143
143
  `computed(...)`, or `effect(...)` registers an Angular dependency
144
- automatically. No `toSignal(...)` wrappers needed.
144
+ automatically. No `toSignal(...)` wrappers needed. Prefer direct atom reads for
145
+ specific slices; keep `table.state` for full-state JSON/debug output or code
146
+ that genuinely wants the flat state shape.
145
147
 
146
148
  See `tanstack-table/angular/table-state` for the full state surface mental
147
149
  model.
@@ -275,7 +277,8 @@ v8 backed reactivity with manual memoized getters. v9's adapter
275
277
  `computed` and every writable atom with an Angular `signal`. Consequences:
276
278
 
277
279
  - **No `toSignal(...)` adapters around table state.** Read `table.atoms.x.get()`
278
- / `table.store.state.x` directly inside templates, `computed`, `effect`.
280
+ directly inside templates, `computed`, `effect` for specific slices. Use
281
+ `table.state` when you need the full-state flat shape.
279
282
  - **`computed(...)` is for derivation / equality, not for "make it reactive".**
280
283
  Use `{ equal: shallow }` from `@tanstack/angular-table` on object/array
281
284
  slices to skip downstream work on no-op updates.
@@ -306,8 +309,8 @@ v8 backed reactivity with manual memoized getters. v9's adapter
306
309
  - [ ] Update `createColumnHelper<Person>()` → `createColumnHelper<typeof _features, Person>()`.
307
310
  - [ ] Update every `ColumnDef<Person>` / `Cell<Person, X>` etc. to include
308
311
  `TFeatures`.
309
- - [ ] Replace `table.getState()` reads with `table.store.state` (or
310
- `table.atoms.<slice>.get()` for per-slice reactivity).
312
+ - [ ] Replace `table.getState().slice` reads with `table.atoms.<slice>.get()`
313
+ where possible; use `table.state` for full-state/debug reads.
311
314
  - [ ] Remove any usage of the v8 single `onStateChange` — split into per-slice
312
315
  `on[State]Change`.
313
316
  - [ ] In `on[State]Change` callbacks, handle both value and updater-fn shapes.
@@ -366,12 +369,13 @@ _rowModels: {
366
369
  Same for sorting, pagination, expanding, grouping, faceting. Selection,
367
370
  visibility, ordering, pinning, sizing, resizing do **not** need a row model.
368
371
 
369
- ### 4. (HIGH) `getState()` → `table.store.state` text replacement loses reactivity
372
+ ### 4. (HIGH) `getState()` → `table.state` text replacement can be too broad
370
373
 
371
- Bulk-replacing `table.getState().x` with `table.store.state.x` works for _current
372
- value_ reads, but if you used a `computed`/`memo` around `getState()` for
373
- reactivity, switch to `table.atoms.x.get()` — it's already signal-backed and
374
- needs no wrapper.
374
+ Bulk-replacing `table.getState().x` with `table.state.x` works for _current
375
+ value_ reads, but it hides which slice the component depends on. For Angular
376
+ render code and reactive derivations, switch to `table.atoms.x.get()` — it's
377
+ already signal-backed and needs no wrapper. Keep `table.state` for full-state
378
+ debug output.
375
379
 
376
380
  ### 5. (HIGH) Stale `sortingFn` / `sortingFns` references in column defs
377
381
 
@@ -6,7 +6,7 @@ description: >
6
6
  stable references OUTSIDE the `injectTable` initializer; pass only the `*Fns` your data needs
7
7
  to `createSortedRowModel` / `createFilteredRowModel` / `createGroupedRowModel`; use
8
8
  `ChangeDetectionStrategy.OnPush`; lean on signal-backed atoms (`table.atoms.<slice>.get()`)
9
- instead of broad `table.store.state` reads where granularity matters; use `{ equal: shallow }`
9
+ instead of broad `table.state` reads where granularity matters; use `{ equal: shallow }`
10
10
  on object/array `computed` selectors; set `getRowId` for stable identity; track by `id` in
11
11
  every `@for`; defer cell components with `flexRenderComponent` only when you need its options;
12
12
  scope DI tokens via `[tanStackTable*]` directives to kill prop drilling.
@@ -148,20 +148,21 @@ All `examples/angular/*` use `OnPush`. Match that.
148
148
 
149
149
  ---
150
150
 
151
- ## 4. Read narrowly — `table.atoms.<slice>.get()` over `table.store.state`
151
+ ## 4. Read narrowly — `table.atoms.<slice>.get()` over `table.state`
152
152
 
153
153
  Both surfaces are signal-backed. The difference is _which signal_ gets read.
154
154
 
155
155
  ```ts
156
- // Wider — depends on the flat snapshot signal (recomputes when ANY registered slice changes)
157
- const pageIndex = computed(() => this.table.store.state.pagination.pageIndex)
156
+ // Wider — reads through the flat state proxy
157
+ const pageIndex = computed(() => this.table.state.pagination.pageIndex)
158
158
 
159
159
  // Narrower — depends only on the pagination atom
160
160
  const pageIndex = computed(() => this.table.atoms.pagination.get().pageIndex)
161
161
  ```
162
162
 
163
- For most apps the difference is negligible. For high-frequency atoms or
164
- deeply-derived components, prefer per-atom reads.
163
+ For most apps the difference is negligible. For render code, high-frequency
164
+ atoms, or deeply-derived components, prefer per-atom reads. Keep `table.state`
165
+ for places that genuinely want the full-state shape, such as debug JSON.
165
166
 
166
167
  ---
167
168
 
@@ -347,7 +348,7 @@ the wiring cost. Pick exactly one source of truth per slice (see
347
348
  - [ ] Cell render fns return primitives or component classes when possible;
348
349
  `flexRenderComponent(...)` reserved for explicit-option cases.
349
350
  - [ ] Reading state inside `effect`s / heavy `computed`s uses
350
- `table.atoms.<slice>.get()` (not the flat snapshot).
351
+ `table.atoms.<slice>.get()` (not the flat state proxy).
351
352
  - [ ] Object/array `computed` selectors that feed expensive downstream use
352
353
  `{ equal: shallow }`.
353
354
  - [ ] No `cell` / `header` / `table` inputs drilled through multiple
@@ -30,6 +30,7 @@ sources:
30
30
  > on top of TanStack Store (`alien-signals`). In Angular, the adapter bridges those
31
31
  > atoms to native Angular signals, so reading `table.atoms.<slice>.get()` from a
32
32
  > template, `computed(...)`, or `effect(...)` participates in Angular reactivity.
33
+ > Prefer that direct atom read when you need a specific state slice.
33
34
  >
34
35
  > This is the prerequisite for every other Angular Table skill. Don't skip it.
35
36
 
@@ -145,17 +146,20 @@ A table instance has three ways to look at its state:
145
146
  | ------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------- |
146
147
  | `table.baseAtoms.<slice>` | writable TanStack Store atom (always exists for registered slices) | low-level direct write; rare |
147
148
  | `table.atoms.<slice>` | **readonly** derived atom per registered feature; backed by Angular `computed` | reading current value or driving reactivity |
148
- | `table.store.state` | flat snapshot object of every registered slice; backed by Angular `computed` | reading multiple slices at once, devtools |
149
+ | `table.state` | flat proxy object over every registered slice | full-state JSON/debug output |
149
150
 
150
151
  All three are signal-backed in Angular. Reading any of them inside a template,
151
- `computed(...)`, or `effect(...)` registers an Angular dependency.
152
+ `computed(...)`, or `effect(...)` registers an Angular dependency. For normal
153
+ render code, prefer `table.atoms.<slice>.get()` so the read is explicit and
154
+ limited to the slice the component needs. Use `table.state` when you actually
155
+ need the flat full-state shape, such as `JSON.stringify(table.state, null, 2)`.
152
156
 
153
157
  ```ts
154
158
  // Read current value (anywhere)
155
159
  const pagination = this.table.atoms.pagination.get()
156
160
 
157
- // Same value, flat shape
158
- const pagination2 = this.table.store.state.pagination
161
+ // Same value through the flat proxy; use mainly for full-state debug output
162
+ const pagination2 = this.table.state.pagination
159
163
 
160
164
  // Reactive derivation with custom equality
161
165
  import { computed } from '@angular/core'
@@ -20,6 +20,7 @@ import type {
20
20
  Table,
21
21
  TableFeatures,
22
22
  TableOptions,
23
+ TableState,
23
24
  } from '@tanstack/table-core'
24
25
 
25
26
  export type SubscribeSource<TValue> =
@@ -31,7 +32,70 @@ export type SubscribeSource<TValue> =
31
32
  export type AngularTable<
32
33
  TFeatures extends TableFeatures,
33
34
  TData extends RowData,
34
- > = Table<TFeatures, TData>
35
+ > = Table<TFeatures, TData> & {
36
+ /**
37
+ * @deprecated Prefer `table.atoms.<slice>.get()` for template/render reads
38
+ * of a specific state slice, `table.state` for full-state debug snapshots, or
39
+ * Angular computed values around explicit selectors. `table.store.state` is a
40
+ * current-value snapshot and is easy to misuse in render code.
41
+ */
42
+ readonly store: Table<TFeatures, TData>['store']
43
+ /**
44
+ * The current table state exposed as a flat proxy. Prefer
45
+ * `table.atoms.<slice>.get()` when reading a specific slice.
46
+ */
47
+ readonly state: Readonly<TableState<TFeatures>>
48
+ }
49
+
50
+ function createStateProxy<
51
+ TFeatures extends TableFeatures,
52
+ TData extends RowData,
53
+ >(table: Table<TFeatures, TData>): Readonly<TableState<TFeatures>> {
54
+ const getSnapshot = () => {
55
+ const snapshot = {} as TableState<TFeatures>
56
+ const stateKeys = Object.keys(table.initialState) as Array<
57
+ keyof TableState<TFeatures>
58
+ >
59
+
60
+ for (const key of stateKeys) {
61
+ snapshot[key] = table.atoms[key].get()
62
+ }
63
+
64
+ return snapshot
65
+ }
66
+
67
+ const target = {} as TableState<TFeatures>
68
+
69
+ return new Proxy(target, {
70
+ get(target, prop, receiver) {
71
+ if (prop === 'toJSON') {
72
+ return getSnapshot
73
+ }
74
+
75
+ if (typeof prop === 'string' && prop in table.initialState) {
76
+ return table.atoms[prop as keyof TableState<TFeatures>]?.get()
77
+ }
78
+
79
+ return Reflect.get(target, prop, receiver)
80
+ },
81
+ has(_, prop) {
82
+ return typeof prop === 'string' && prop in table.initialState
83
+ },
84
+ ownKeys() {
85
+ return Reflect.ownKeys(table.initialState)
86
+ },
87
+ getOwnPropertyDescriptor(_, prop) {
88
+ if (typeof prop !== 'string' || !(prop in table.initialState)) {
89
+ return undefined
90
+ }
91
+
92
+ return {
93
+ enumerable: true,
94
+ configurable: true,
95
+ }
96
+ },
97
+ })
98
+ }
35
99
 
36
100
  /**
37
101
  * Creates and returns an Angular-reactive table instance.
@@ -107,6 +171,15 @@ export function injectTable<
107
171
  coreReativityFeature: angularReactivity(injector),
108
172
  ...options()._features,
109
173
  },
174
+ }) as AngularTable<TFeatures, TData>
175
+ const stateProxy = createStateProxy(table)
176
+
177
+ Object.defineProperty(table, 'state', {
178
+ get() {
179
+ return stateProxy
180
+ },
181
+ configurable: true,
182
+ enumerable: true,
110
183
  })
111
184
 
112
185
  let isMount = true
package/src/reactivity.ts CHANGED
@@ -1,5 +1,12 @@
1
- import { NgZone, computed, signal, untracked } from '@angular/core'
2
- import { toObservable } from '@angular/core/rxjs-interop'
1
+ import {
2
+ DestroyRef,
3
+ NgZone,
4
+ computed,
5
+ effect,
6
+ signal,
7
+ untracked,
8
+ } from '@angular/core'
9
+ import { batch, createAtom } from '@tanstack/angular-store'
3
10
  import type { Atom, Observer, ReadonlyAtom } from '@tanstack/angular-store'
4
11
  import type {
5
12
  TableAtomOptions,
@@ -7,70 +14,134 @@ import type {
7
14
  } from '@tanstack/table-core/reactivity'
8
15
  import type { Injector, Signal, WritableSignal } from '@angular/core'
9
16
 
17
+ const optionsStoreDebugName = 'table/optionsStore'
18
+
19
+ function observerToCallback<T>(
20
+ observerOrNext: Observer<T> | ((value: T) => void),
21
+ ): (value: T) => void {
22
+ return typeof observerOrNext === 'function'
23
+ ? observerOrNext
24
+ : (value) => observerOrNext.next?.(value)
25
+ }
26
+
10
27
  function signalToReadonlyAtom<T>(
11
- signal: Signal<T>,
12
- injector: Injector,
28
+ source: Signal<T>,
29
+ getSource: () => T,
30
+ subscribeSource: (observerOrNext: Observer<T> | ((value: T) => void)) => {
31
+ unsubscribe: () => void
32
+ },
13
33
  ): ReadonlyAtom<T> {
14
- return Object.assign(signal, {
15
- get: () => signal(),
16
- subscribe: (observer: Observer<T>) => {
17
- return toObservable(computed(signal), { injector: injector }).subscribe(
18
- observer,
19
- )
34
+ return Object.assign(source, {
35
+ get: () => {
36
+ const value = getSource()
37
+ source()
38
+ return value
20
39
  },
40
+ subscribe: subscribeSource as ReadonlyAtom<T>['subscribe'],
21
41
  })
22
42
  }
23
43
 
24
44
  function signalToWritableAtom<T>(
25
- signal: WritableSignal<T>,
45
+ source: WritableSignal<T>,
26
46
  injector: Injector,
27
47
  ): Atom<T> {
28
- return Object.assign(signal.asReadonly(), {
48
+ const observers = new Set<(value: T) => void>()
49
+ let observed = false
50
+ effect(
51
+ () => {
52
+ const value = source()
53
+ if (!observed) {
54
+ observed = true
55
+ return
56
+ }
57
+ observers.forEach((observer) => observer(value))
58
+ },
59
+ { injector },
60
+ )
61
+
62
+ return Object.assign(source.asReadonly(), {
29
63
  set: (updater: T | ((prevVal: T) => T)) => {
30
64
  typeof updater === 'function'
31
- ? signal.update(updater as (val: T) => T)
32
- : signal.set(updater)
33
- },
34
- get: () => signal(),
35
- subscribe: (observer: Observer<T>) => {
36
- return toObservable(computed(signal), { injector: injector }).subscribe(
37
- observer,
38
- )
65
+ ? source.update(updater as (val: T) => T)
66
+ : source.set(updater)
39
67
  },
68
+ get: () => source(),
69
+ subscribe: ((observerOrNext: Observer<T> | ((value: T) => void)) => {
70
+ const observer = observerToCallback(observerOrNext)
71
+ observers.add(observer)
72
+
73
+ return {
74
+ unsubscribe: () => observers.delete(observer),
75
+ }
76
+ }) as Atom<T>['subscribe'],
40
77
  })
41
78
  }
42
79
 
43
80
  /**
44
81
  * Creates the table-core reactivity bindings used by the Angular adapter.
45
82
  *
46
- * Readonly table atoms are backed by Angular `computed` signals and writable
47
- * atoms by Angular `signal`. Subscriptions bridge through `toObservable` with
48
- * the caller's injector so table APIs can be consumed from Angular `computed`
49
- * and `effect` calls.
83
+ * Table state atoms are backed by TanStack Store atoms. The options store stays
84
+ * framework-native because row-model APIs read `table.options` directly during
85
+ * render. Readonly table atoms bridge Store dependency tracking into Angular
86
+ * computed signals.
50
87
  */
51
88
  export function angularReactivity(injector: Injector): TableReactivityBindings {
52
89
  const ngZone = injector.get(NgZone)
90
+ const destroyRef = injector.get(DestroyRef)
91
+
53
92
  return {
54
93
  createOptionsStore: true,
55
94
  schedule: (fn) => ngZone.runOutsideAngular(() => queueMicrotask(fn)),
56
95
  createReadonlyAtom: <T>(fn: () => T, options?: TableAtomOptions<T>) => {
57
- const signal = computed(() => fn(), {
58
- equal: options?.compare,
59
- debugName: options?.debugName,
96
+ const storeAtom = createAtom(() => fn(), {
97
+ compare: options?.compare,
98
+ })
99
+ const version = signal(0, {
100
+ equal: () => false,
101
+ })
102
+ const subscription = storeAtom.subscribe(() => {
103
+ version.update((value) => value + 1)
60
104
  })
61
- return signalToReadonlyAtom(signal, injector)
105
+ destroyRef.onDestroy(() => subscription.unsubscribe())
106
+
107
+ const value = computed(
108
+ () => {
109
+ version()
110
+ return storeAtom.get()
111
+ },
112
+ {
113
+ equal: options?.compare,
114
+ debugName: options?.debugName,
115
+ },
116
+ )
117
+ return signalToReadonlyAtom(
118
+ value,
119
+ () => storeAtom.get(),
120
+ (observerOrNext) => {
121
+ const observer = observerToCallback(observerOrNext)
122
+ return storeAtom.subscribe(() => {
123
+ observer(storeAtom.get())
124
+ })
125
+ },
126
+ )
62
127
  },
63
128
  createWritableAtom: <T>(
64
129
  value: T,
65
130
  options?: TableAtomOptions<T>,
66
131
  ): Atom<T> => {
67
- const writableSignal = signal(value, {
68
- equal: options?.compare,
69
- debugName: options?.debugName,
132
+ if (options?.debugName === optionsStoreDebugName) {
133
+ const writableSignal = signal(value, {
134
+ equal: options.compare,
135
+ debugName: options.debugName,
136
+ })
137
+ return signalToWritableAtom(writableSignal, injector)
138
+ }
139
+
140
+ return createAtom(value, {
141
+ compare: options?.compare,
70
142
  })
71
- return signalToWritableAtom(writableSignal, injector)
72
143
  },
73
144
  untrack: untracked,
74
- batch: (fn) => fn(),
145
+ batch,
75
146
  }
76
147
  }