@tanstack/angular-table 9.0.0-alpha.10 → 9.0.0-alpha.12

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.
@@ -0,0 +1,87 @@
1
+ import { Directive, InjectionToken, inject, input } from '@angular/core'
2
+ import { RowData, TableFeatures, TableState } from '@tanstack/table-core'
3
+ import { AngularTable } from '../injectTable'
4
+ import type { Signal } from '@angular/core'
5
+
6
+ /**
7
+ * Injection token that provides access to the current {@link AngularTable} instance.
8
+ *
9
+ * This token is provided by the {@link TanStackTable} directive.
10
+ */
11
+ export const TanStackTableToken = new InjectionToken<
12
+ Signal<AngularTable<any, any>>
13
+ >('[TanStack Table] Table Context')
14
+
15
+ /**
16
+ * Provides a TanStack Table instance (`AngularTable`) in Angular DI.
17
+ *
18
+ * The table can be injected by:
19
+ * - any descendant of an element using `[tanStackTable]="..."`
20
+ * - any component instantiated by `*flexRender` when the render props contains `table`
21
+ *
22
+ * @example
23
+ * ```html
24
+ * <div [tanStackTable]="table">
25
+ * <app-pagination />
26
+ * </div>
27
+ * ```
28
+ *
29
+ * ```ts
30
+ * @Component({
31
+ * selector: 'app-pagination',
32
+ * template: `
33
+ * <button (click)="prev()" [disabled]="!table().getCanPreviousPage()">Prev</button>
34
+ * <button (click)="next()" [disabled]="!table().getCanNextPage()">Next</button>
35
+ * `,
36
+ * })
37
+ * export class PaginationComponent {
38
+ * readonly table = injectTableContext()
39
+ *
40
+ * prev() {
41
+ * this.table().previousPage()
42
+ * }
43
+ * next() {
44
+ * this.table().nextPage()
45
+ * }
46
+ * }
47
+ * ```
48
+ */
49
+ @Directive({
50
+ selector: '[tanStackTable]',
51
+ exportAs: 'table',
52
+ providers: [
53
+ {
54
+ provide: TanStackTableToken,
55
+ useFactory: () => inject(TanStackTable).table,
56
+ },
57
+ ],
58
+ })
59
+ export class TanStackTable<
60
+ TFeatures extends TableFeatures,
61
+ TData extends RowData,
62
+ TSelected extends {} = TableState<TFeatures>,
63
+ > {
64
+ /**
65
+ * The current TanStack Table instance.
66
+ *
67
+ * Provided as a required signal input so DI consumers always read the latest value.
68
+ */
69
+ readonly table = input.required<AngularTable<TFeatures, TData, TSelected>>({
70
+ alias: 'tanStackTable',
71
+ })
72
+ }
73
+
74
+ /**
75
+ * Injects the current TanStack Table instance signal.
76
+ *
77
+ * Available when:
78
+ * - there is a nearest `[tanStackTable]` directive in the DI tree, or
79
+ * - the caller is rendered via `*flexRender` with render props containing `table`
80
+ */
81
+ export function injectTableContext<
82
+ TFeatures extends TableFeatures,
83
+ TData extends RowData,
84
+ TSelected extends {} = TableState<TFeatures>,
85
+ >(): Signal<AngularTable<TFeatures, TData, TSelected>> {
86
+ return inject(TanStackTableToken)
87
+ }
package/src/index.ts CHANGED
@@ -1,73 +1,25 @@
1
- import { computed, type Signal, signal } from '@angular/core'
2
- import {
3
- RowData,
4
- TableOptions,
5
- TableOptionsResolved,
6
- TableState,
7
- _createTable,
8
- type Table,
9
- } from '@tanstack/table-core'
10
- import { lazyInit } from './lazy-signal-initializer'
11
- import { proxifyTable } from './proxy'
1
+ import { FlexRenderCell } from './helpers/flexRenderCell'
2
+ import { FlexRenderDirective } from './flexRender'
12
3
 
13
4
  export * from '@tanstack/table-core'
14
5
 
15
- export {
16
- type FlexRenderContent,
17
- FlexRenderComponent,
18
- FlexRenderDirective,
19
- injectFlexRenderContext,
20
- } from './flex-render'
21
-
22
- export function injectTable<TData extends RowData>(
23
- options: () => TableOptions<TData>
24
- ): Table<TData> & Signal<Table<TData>> {
25
- return lazyInit(() => {
26
- const resolvedOptions = {
27
- state: {},
28
- onStateChange: () => {},
29
- renderFallbackValue: null,
30
- ...options(),
31
- }
32
-
33
- const table = _createTable(resolvedOptions)
34
-
35
- // By default, manage table state here using the table's initial state
36
- const state = signal<TableState>(table.initialState)
37
-
38
- // Compose table options using computed.
39
- // This is to allow `tableSignal` to listen and set table option
40
- const updatedOptions = computed<TableOptionsResolved<TData>>(() => {
41
- // listen to table state changed
42
- const tableState = state()
43
- // listen to input options changed
44
- const tableOptions = options()
45
- return {
46
- ...table.options,
47
- ...resolvedOptions,
48
- ...tableOptions,
49
- state: { ...tableState, ...tableOptions.state },
50
- onStateChange: updater => {
51
- const value =
52
- updater instanceof Function ? updater(tableState) : updater
53
- state.set(value)
54
- resolvedOptions.onStateChange?.(updater)
55
- },
56
- }
57
- })
58
-
59
- // convert table instance to signal for proxify to listen to any table state and options changes
60
- const tableSignal = computed(
61
- () => {
62
- table.setOptions(updatedOptions())
63
- return table
64
- },
65
- {
66
- equal: () => false,
67
- }
68
- )
69
-
70
- // proxify Table instance to provide ability for consumer to listen to any table state changes
71
- return proxifyTable(tableSignal)
72
- })
73
- }
6
+ export * from './angularReactivityFeature'
7
+ export * from './flexRender'
8
+ export * from './injectTable'
9
+ export * from './flex-render/flexRenderComponent'
10
+
11
+ export * from './helpers/cell'
12
+ export * from './helpers/header'
13
+ export * from './helpers/table'
14
+ export * from './helpers/createTableHook'
15
+ export * from './helpers/flexRenderCell'
16
+
17
+ /**
18
+ * Constant helper to import FlexRender directives.
19
+ *
20
+ * You should prefer to use this constant over importing the directives separately,
21
+ * as it ensures you always have the correct set of directives over library updates.
22
+ *
23
+ * @see {@link FlexRenderDirective} and {@link FlexRenderCell} for more details on the directives included in this export.
24
+ */
25
+ export const FlexRender = [FlexRenderDirective, FlexRenderCell] as const
@@ -0,0 +1,170 @@
1
+ import {
2
+ Injector,
3
+ assertInInjectionContext,
4
+ computed,
5
+ inject,
6
+ untracked,
7
+ } from '@angular/core'
8
+ import { constructTable } from '@tanstack/table-core'
9
+ import { injectStore } from '@tanstack/angular-store'
10
+ import { lazyInit } from './lazySignalInitializer'
11
+ import { angularReactivityFeature } from './angularReactivityFeature'
12
+ import type {
13
+ RowData,
14
+ Table,
15
+ TableFeatures,
16
+ TableOptions,
17
+ TableState,
18
+ } from '@tanstack/table-core'
19
+ import type { Signal, ValueEqualityFn } from '@angular/core'
20
+
21
+ export type AngularTable<
22
+ TFeatures extends TableFeatures,
23
+ TData extends RowData,
24
+ TSelected = TableState<TFeatures>,
25
+ > = Table<TFeatures, TData> & {
26
+ /**
27
+ * The selected state from the table store, based on the selector provided.
28
+ */
29
+ readonly state: Signal<Readonly<TSelected>>
30
+ /**
31
+ * Subscribe to changes in the table store with a custom selector.
32
+ */
33
+ Subscribe: <TSubSelected = {}>(props: {
34
+ selector: (state: TableState<TFeatures>) => TSubSelected
35
+ equal?: ValueEqualityFn<TSubSelected>
36
+ }) => Signal<Readonly<TSubSelected>>
37
+ }
38
+
39
+ /**
40
+ * Creates and returns an Angular-reactive table instance.
41
+ *
42
+ * The initializer is intentionally re-evaluated whenever any signal read inside it changes.
43
+ * This is how the adapter keeps the table in sync with Angular's reactivity model.
44
+ *
45
+ * Because of that behavior, keep expensive/static values (for example `columns`, feature setup, row models)
46
+ * as stable references outside the initializer, and only read reactive state (`data()`, pagination/filter/sorting signals, etc.)
47
+ * inside it.
48
+ *
49
+ * The returned table is also signal-reactive: table state and table APIs are wired for Angular signals, so you can safely consume table methods inside `computed(...)` and `effect(...)`.
50
+ *
51
+ * @example
52
+ * 1. Register the table features you need
53
+ * ```ts
54
+ * // Register only the features you need
55
+ * import {tableFeatures, rowPaginationFeature} from '@tanstack/angular-table';
56
+ * const _features = tableFeatures({
57
+ * rowPaginationFeature,
58
+ * // ...all other features you need
59
+ * })
60
+ *
61
+ * // Use all table core features
62
+ * import {stockFeatures} from '@tanstack/angular-table';
63
+ * const _features = tableFeatures(stockFeatures);
64
+ * ```
65
+ * 2. Prepare the table columns
66
+ * ```ts
67
+ * import {ColumnDef} from '@tanstack/angular-table';
68
+ *
69
+ * type MyData = {}
70
+ *
71
+ * const columns: ColumnDef<typeof _features, MyData>[] = [
72
+ * // ...column definitions
73
+ * ]
74
+ *
75
+ * // or using createColumnHelper
76
+ * import {createColumnHelper} from '@tanstack/angular-table';
77
+ * const columnHelper = createColumnHelper<typeof _features, MyData>();
78
+ * const columns = columnHelper.columns([
79
+ * columnHelper.accessor(...),
80
+ * // ...other columns
81
+ * ])
82
+ * ```
83
+ * 3. Create the table instance with `injectTable`
84
+ * ```ts
85
+ * const table = injectTable(() => {
86
+ * // ...table options,
87
+ * _features,
88
+ * columns: columns,
89
+ * data: myDataSignal(),
90
+ * })
91
+ * ```
92
+ *
93
+ * @returns An Angular-reactive TanStack Table instance.
94
+ */
95
+ export function injectTable<
96
+ TFeatures extends TableFeatures,
97
+ TData extends RowData,
98
+ TSelected = TableState<TFeatures>,
99
+ >(
100
+ options: () => TableOptions<TFeatures, TData>,
101
+ selector: (state: TableState<TFeatures>) => TSelected = (state) =>
102
+ state as TSelected,
103
+ ): AngularTable<TFeatures, TData, TSelected> {
104
+ assertInInjectionContext(injectTable)
105
+ const injector = inject(Injector)
106
+
107
+ return lazyInit(() => {
108
+ const resolvedOptions: TableOptions<TFeatures, TData> = {
109
+ ...options(),
110
+ _features: {
111
+ ...options()._features,
112
+ angularReactivityFeature,
113
+ },
114
+ } as TableOptions<TFeatures, TData>
115
+
116
+ const table: AngularTable<TFeatures, TData, TSelected> = constructTable(
117
+ resolvedOptions,
118
+ ) as AngularTable<TFeatures, TData, TSelected>
119
+
120
+ const updatedOptions = computed<TableOptions<TFeatures, TData>>(() => {
121
+ const tableOptionsValue = options()
122
+ const result: TableOptions<TFeatures, TData> = {
123
+ ...table.options,
124
+ ...tableOptionsValue,
125
+ _features: {
126
+ ...tableOptionsValue._features,
127
+ angularReactivityFeature,
128
+ },
129
+ }
130
+ if (tableOptionsValue.state) {
131
+ result.state = tableOptionsValue.state
132
+ }
133
+ return result
134
+ })
135
+
136
+ const tableState = injectStore(
137
+ table.store,
138
+ (state: TableState<TFeatures>) => state,
139
+ { injector },
140
+ )
141
+
142
+ const tableSignalNotifier = computed(
143
+ () => {
144
+ tableState()
145
+ table.setOptions(updatedOptions())
146
+ untracked(() => table.baseStore.setState((prev) => ({ ...prev })))
147
+ return table
148
+ },
149
+ { equal: () => false },
150
+ )
151
+
152
+ table.setTableNotifier(tableSignalNotifier)
153
+
154
+ table.Subscribe = function Subscribe<TSubSelected = {}>(props: {
155
+ selector: (state: TableState<TFeatures>) => TSubSelected
156
+ equal?: ValueEqualityFn<TSubSelected>
157
+ }) {
158
+ return injectStore(table.store, props.selector, {
159
+ injector,
160
+ equal: props.equal,
161
+ })
162
+ }
163
+
164
+ Object.defineProperty(table, 'state', {
165
+ value: injectStore(table.store, selector, { injector }),
166
+ })
167
+
168
+ return table
169
+ })
170
+ }
@@ -2,7 +2,7 @@ import { untracked } from '@angular/core'
2
2
 
3
3
  /**
4
4
  * Implementation from @tanstack/angular-query
5
- * {@link https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/util/lazy-init/lazy-init.ts}
5
+ * {https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/util/lazy-init/lazy-init.ts}
6
6
  */
7
7
  export function lazyInit<T extends object>(initializer: () => T): T {
8
8
  let object: T | null = null
@@ -18,7 +18,7 @@ export function lazyInit<T extends object>(initializer: () => T): T {
18
18
  const table = () => {}
19
19
 
20
20
  return new Proxy<T>(table as T, {
21
- apply(target: T, thisArg: any, argArray: any[]): any {
21
+ apply(target: T, thisArg: any, argArray: Array<any>): any {
22
22
  initializeObject()
23
23
  if (typeof object === 'function') {
24
24
  return Reflect.apply(object, thisArg, argArray)
@@ -0,0 +1,232 @@
1
+ import { computed, isSignal } from '@angular/core'
2
+ import { $internalMemoFnMeta, getMemoFnMeta } from '@tanstack/table-core'
3
+ import type { MemoFnMeta } from '@tanstack/table-core'
4
+ import type { Signal } from '@angular/core'
5
+
6
+ const $TABLE_REACTIVE = Symbol('reactive')
7
+
8
+ function markReactive<T extends object>(obj: T): void {
9
+ Object.defineProperty(obj, $TABLE_REACTIVE, { value: true })
10
+ }
11
+
12
+ function isReactive<T>(obj: T): boolean {
13
+ return Reflect.get(obj as {}, $TABLE_REACTIVE) === true
14
+ }
15
+
16
+ /**
17
+ * Defines a lazy computed property on an object. The property is initialized
18
+ * with a getter that computes its value only when accessed for the first time.
19
+ * After the first access, the computed value is cached, and the getter is
20
+ * replaced with a direct property assignment for efficiency.
21
+ *
22
+ * @internal should be used only internally
23
+ */
24
+ export function defineLazyComputedProperty<T extends object>(
25
+ notifier: Signal<T>,
26
+ setObjectOptions: {
27
+ originalObject: T
28
+ property: keyof T & string
29
+ valueFn: (...args: any) => any
30
+ overridePrototype?: boolean
31
+ },
32
+ ) {
33
+ const { originalObject, property, overridePrototype, valueFn } =
34
+ setObjectOptions
35
+
36
+ if (overridePrototype) {
37
+ assignReactivePrototypeAPI(notifier, originalObject, property)
38
+ } else {
39
+ Object.defineProperty(originalObject, property, {
40
+ enumerable: true,
41
+ configurable: true,
42
+ get() {
43
+ const computedValue = toComputed(notifier, valueFn, property)
44
+ markReactive(computedValue)
45
+ // Once the property is set the first time, we don't need a getter anymore
46
+ // since we have a computed / cached fn value
47
+ Object.defineProperty(originalObject, property, {
48
+ value: computedValue,
49
+ configurable: true,
50
+ enumerable: true,
51
+ })
52
+ return computedValue
53
+ },
54
+ })
55
+ }
56
+ }
57
+
58
+ /**
59
+ * @internal should be used only internally
60
+ */
61
+ type ComputedFunction<T> = T extends () => infer TReturn
62
+ ? Signal<TReturn>
63
+ : // 1+ args
64
+ T extends (arg0?: any, ...args: Array<any>) => any
65
+ ? T
66
+ : never
67
+
68
+ /**
69
+ * @description Transform a function into a computed that react to given notifier re-computations
70
+ *
71
+ * Here we'll handle all type of accessors:
72
+ * - 0 argument -> e.g. table.getCanNextPage())
73
+ * - 0~1 arguments -> e.g. table.getIsSomeRowsPinned(position?)
74
+ * - 1 required argument -> e.g. table.getColumn(columnId)
75
+ * - 1+ argument -> e.g. table.getRow(id, searchAll?)
76
+ *
77
+ * Since we are not able to detect automatically the accessors parameters,
78
+ * we'll wrap all accessors into a cached function wrapping a computed
79
+ * that return it's value based on the given parameters
80
+ *
81
+ * @internal should be used only internally
82
+ */
83
+ export function toComputed<
84
+ T,
85
+ TReturn,
86
+ TFunction extends (...args: any) => TReturn,
87
+ >(
88
+ notifier: Signal<T>,
89
+ fn: TFunction,
90
+ debugName: string,
91
+ ): ComputedFunction<TFunction> {
92
+ const hasArgs = getFnArgsLength(fn) > 0
93
+ if (!hasArgs) {
94
+ const computedFn = computed(
95
+ () => {
96
+ void notifier()
97
+ return fn()
98
+ },
99
+ { debugName },
100
+ )
101
+ Object.defineProperty(computedFn, 'name', { value: debugName })
102
+ markReactive(computedFn)
103
+ return computedFn as ComputedFunction<TFunction>
104
+ }
105
+
106
+ const computedFn: ((this: unknown, ...argsArray: Array<any>) => unknown) & {
107
+ _reactiveCache?: Record<string, Signal<unknown>>
108
+ } = function (this: unknown, ...argsArray: Array<any>) {
109
+ const cacheable =
110
+ argsArray.length === 0 ||
111
+ argsArray.every((arg) => {
112
+ return (
113
+ arg === null ||
114
+ arg === undefined ||
115
+ typeof arg === 'string' ||
116
+ typeof arg === 'number' ||
117
+ typeof arg === 'boolean' ||
118
+ typeof arg === 'symbol'
119
+ )
120
+ })
121
+ if (!cacheable) {
122
+ return fn.apply(this, argsArray)
123
+ }
124
+ const serializedArgs = serializeArgs(...argsArray)
125
+ if ((computedFn._reactiveCache ??= {})[serializedArgs]) {
126
+ return computedFn._reactiveCache[serializedArgs]()
127
+ }
128
+ const computedSignal = computed(
129
+ () => {
130
+ void notifier()
131
+ return fn.apply(this, argsArray)
132
+ },
133
+ { debugName },
134
+ )
135
+
136
+ computedFn._reactiveCache[serializedArgs] = computedSignal
137
+
138
+ return computedSignal()
139
+ }
140
+
141
+ Object.defineProperty(computedFn, 'name', { value: debugName })
142
+ markReactive(computedFn)
143
+
144
+ return computedFn as ComputedFunction<TFunction>
145
+ }
146
+
147
+ function serializeArgs(...args: Array<any>) {
148
+ return JSON.stringify(args)
149
+ }
150
+
151
+ function getFnArgsLength(
152
+ fn: ((...args: any) => any) & { originalArgsLength?: number },
153
+ ): number {
154
+ return Math.max(0, getMemoFnMeta(fn)?.originalArgsLength ?? fn.length)
155
+ }
156
+
157
+ function assignReactivePrototypeAPI(
158
+ notifier: Signal<unknown>,
159
+ prototype: Record<string, any>,
160
+ fnName: string,
161
+ ) {
162
+ if (isReactive(prototype[fnName])) return
163
+
164
+ const fn = prototype[fnName]
165
+ const originalArgsLength = getFnArgsLength(fn)
166
+
167
+ if (originalArgsLength <= 1) {
168
+ Object.defineProperty(prototype, fnName, {
169
+ enumerable: true,
170
+ configurable: true,
171
+ get(this) {
172
+ const self = this
173
+ // Create a cache in the current prototype to allow the signals
174
+ // to be garbage collected. Shorthand for a WeakMap implementation
175
+ self._reactiveCache ??= {}
176
+ const cached = (self._reactiveCache[`${self.id}${fnName}`] ??= computed(
177
+ () => {
178
+ notifier()
179
+ return fn.apply(self)
180
+ },
181
+ {},
182
+ ))
183
+ markReactive(cached)
184
+ cached[$internalMemoFnMeta] = {
185
+ originalArgsLength,
186
+ } satisfies MemoFnMeta
187
+ return cached
188
+ },
189
+ })
190
+ } else {
191
+ prototype[fnName] = function (this: unknown, ...args: Array<any>) {
192
+ notifier()
193
+ return fn.apply(this, args)
194
+ }
195
+ markReactive(prototype[fnName])
196
+ prototype[fnName][$internalMemoFnMeta] = {
197
+ originalArgsLength,
198
+ } satisfies MemoFnMeta
199
+ }
200
+ }
201
+
202
+ export function setReactivePropertiesOnObject<T extends object>(
203
+ notifier: Signal<T>,
204
+ obj: { [key: string]: any },
205
+ options: {
206
+ overridePrototype?: boolean
207
+ skipProperty: (property: string) => boolean
208
+ },
209
+ ) {
210
+ const { skipProperty } = options
211
+ if (isReactive(obj)) {
212
+ return
213
+ }
214
+ markReactive(obj)
215
+
216
+ for (const property in obj) {
217
+ const value = obj[property]
218
+ if (
219
+ isSignal(value) ||
220
+ typeof value !== 'function' ||
221
+ skipProperty(property)
222
+ ) {
223
+ continue
224
+ }
225
+ defineLazyComputedProperty(notifier, {
226
+ valueFn: value,
227
+ property,
228
+ originalObject: obj,
229
+ overridePrototype: options.overridePrototype,
230
+ })
231
+ }
232
+ }