Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/react/mantine-react-table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@tanstack/react-table": "^9.0.0-alpha.41",
"@tanstack/react-virtual": "^3.13.24",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"dayjs": "^1.11.20",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
Expand Down
2 changes: 1 addition & 1 deletion examples/react/with-tanstack-form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@tanstack/react-table": "^9.0.0-alpha.41",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"zod": "^4.4.1"
"zod": "^4.4.2"
},
"devDependencies": {
"@rolldown/plugin-babel": "^0.2.3",
Expand Down
2 changes: 1 addition & 1 deletion examples/react/with-tanstack-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@faker-js/faker": "^10.4.0",
"@rolldown/plugin-babel": "^0.2.3",
"@rollup/plugin-replace": "^6.0.3",
"@tanstack/router-vite-plugin": "^1.166.46",
"@tanstack/router-vite-plugin": "^1.166.47",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
Expand Down
2 changes: 1 addition & 1 deletion examples/solid/with-tanstack-form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@
"@tanstack/solid-form": "^1.29.1",
"@tanstack/solid-table": "^9.0.0-alpha.41",
"solid-js": "^1.9.12",
"zod": "^4.4.1"
"zod": "^4.4.2"
}
}
2 changes: 1 addition & 1 deletion examples/solid/with-tanstack-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"license": "MIT",
"devDependencies": {
"@faker-js/faker": "^10.4.0",
"@tanstack/router-vite-plugin": "^1.166.46",
"@tanstack/router-vite-plugin": "^1.166.47",
"typescript": "6.0.3",
"vite": "^8.0.10",
"vite-plugin-solid": "^2.11.12"
Expand Down
2 changes: 1 addition & 1 deletion examples/svelte/with-tanstack-form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@
"@tanstack/form-core": "^1.29.1",
"@tanstack/svelte-form": "^1.29.1",
"@tanstack/svelte-table": "^9.0.0-alpha.41",
"zod": "^4.4.1"
"zod": "^4.4.2"
}
}
2 changes: 1 addition & 1 deletion examples/vue/with-tanstack-form/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@tanstack/vue-form": "^1.29.1",
"@tanstack/vue-table": "^9.0.0-alpha.41",
"vue": "^3.5.33",
"zod": "^4.4.1"
"zod": "^4.4.2"
},
"devDependencies": {
"@types/node": "^25.6.0",
Expand Down
1 change: 0 additions & 1 deletion packages/angular-table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
"test:build": "publint --strict",
"test:eslint": "eslint ./src",
"test:lib": "vitest",
"test:benchmark": "vitest bench",
"test:lib:dev": "vitest --watch",
"test:types": "tsc && vitest --typecheck"
},
Expand Down
126 changes: 37 additions & 89 deletions packages/angular-table/src/injectTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,11 @@ import {
computed,
effect,
inject,
signal,
untracked,
} from '@angular/core'
import {
constructReactivityFeature,
constructTable,
} from '@tanstack/table-core'
import { injectSelector } from '@tanstack/angular-store'
import { constructTable } from '@tanstack/table-core'
import { lazyInit } from './lazySignalInitializer'
import { angularReactivity } from './signals'
import type { Atom, ReadonlyAtom } from '@tanstack/angular-store'
import type {
RowData,
Expand Down Expand Up @@ -60,10 +56,13 @@ export type AngularTable<
*/
readonly value: Signal<AngularTable<TFeatures, TData, TSelected>>
/**
* Alias: **`Subscribe`** — same function reference as `computed` (naming parity with other adapters).
* Creates a computed that subscribe to changes in the table store with a custom selector.
* Default equality function is "shallow".
*/
computed: AngularTableComputed<TFeatures>
Subscribe: AngularTableComputed<TFeatures>
computed: <TSubSelected = {}>(props: {
selector: (state: TableState<TFeatures>) => TSubSelected
equal?: ValueEqualityFn<TSubSelected>
}) => Signal<Readonly<TSubSelected>>
}

/**
Expand Down Expand Up @@ -133,104 +132,53 @@ export function injectTable<
): AngularTable<TFeatures, TData, TSelected> {
assertInInjectionContext(injectTable)
const injector = inject(Injector)
const stateNotifier = signal(0)
const angularReactivityFeature = constructReactivityFeature({
stateNotifier: () => stateNotifier(),
})

return lazyInit(() => {
const resolvedOptions: TableOptions<TFeatures, TData> = {
const table = constructTable({
...options(),
_features: {
...options()._features,
angularReactivityFeature,
},
}

const table = constructTable(resolvedOptions) as AngularTable<
TFeatures,
TData,
TSelected
>
const tableState = injectSelector(table.store, (state) => state, {
injector,
})
const tableOptions = injectSelector(table.optionsStore, (state) => state, {
injector,
})

const updatedOptions = computed<TableOptions<TFeatures, TData>>(() => {
const tableOptionsValue = options()
const result: TableOptions<TFeatures, TData> = {
...untracked(() => table.options),
...tableOptionsValue,
_features: { ...tableOptionsValue._features, angularReactivityFeature },
}
if (tableOptionsValue.state) {
result.state = tableOptionsValue.state
}
return result
})

effect(
() => {
const newOptions = updatedOptions()
untracked(() => table.setOptions(newOptions))
},
{ injector, debugName: 'tableOptionsUpdate' },
)
reactivity: angularReactivity(injector),
}) as AngularTable<TFeatures, TData, TSelected>

let isMount = true
effect(
() => {
void [tableOptions(), tableState()]
if (!isMount) untracked(() => stateNotifier.update((n) => n + 1))
isMount && (isMount = false)
const newOptions = options()
if (isMount) {
isMount = false
return
}
untracked(() =>
table.setOptions((previous) => ({
...previous,
...newOptions,
})),
)
},
{ injector, debugName: 'tableStateNotifier' },
{ injector, debugName: 'tableOptionsUpdate' },
)

const computedFn = function computedSubscribe(props: {
source?: Atom<unknown> | ReadonlyAtom<unknown>
selector?: (state: unknown) => unknown
equal?: ValueEqualityFn<unknown>
table.computed = function Subscribe<TSubSelected = {}>(props: {
selector: (state: TableState<TFeatures>) => TSubSelected
equal?: ValueEqualityFn<TSubSelected>
}) {
if (props.source !== undefined) {
return injectSelector(
props.source,
props.selector ?? ((value) => value),
{
injector,
...(props.equal && { compare: props.equal }),
},
)
}
return injectSelector(table.store, props.selector, {
injector,
...(props.equal && { compare: props.equal }),
return computed(() => props.selector(table.store.get()), {
equal: props.equal,
})
}
table.computed = computedFn as AngularTable<
TFeatures,
TData,
TSelected
>['computed']
table.Subscribe = computedFn as AngularTable<
TFeatures,
TData,
TSelected
>['Subscribe']

Object.defineProperty(table, 'state', {
value: injectSelector(table.store, selector, { injector }),
value: computed(() => selector(table.store.get())),
})

Object.defineProperty(table, 'value', {
value: computed(() => {
tableOptions()
tableState()
return table
}),
value: computed(
() => {
table.store.get()
table.optionsStore.get()
return table
},
{ equal: () => false },
),
})

return table
Expand Down
2 changes: 1 addition & 1 deletion packages/angular-table/src/lazySignalInitializer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { untracked } from '@angular/core'
import { effect, untracked } from '@angular/core'

/**
* Implementation from @tanstack/angular-query
Expand Down
66 changes: 66 additions & 0 deletions packages/angular-table/src/signals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { computed, signal, untracked } from '@angular/core'
import { toObservable } from '@angular/core/rxjs-interop'
import { batch } from '@tanstack/angular-store'
import type { Atom, Observer, ReadonlyAtom } from '@tanstack/angular-store'
import type {
TableAtomOptions,
TableReactivityBindings,
} from '@tanstack/table-core'
import type { Injector, Signal, WritableSignal } from '@angular/core'

function signalToReadonlyAtom<T>(
signal: Signal<T>,
injector: Injector,
): ReadonlyAtom<T> {
return Object.assign(signal, {
get: () => signal(),
subscribe: (observer: Observer<T>) => {
return toObservable(computed(signal), { injector: injector }).subscribe(
observer,
)
},
})
}

function signalToWritableAtom<T>(
signal: WritableSignal<T>,
injector: Injector,
): Atom<T> {
return Object.assign(signal.asReadonly(), {
set: (updater: T | ((prevVal: T) => T)) => {
typeof updater === 'function'
? signal.update(updater as (val: T) => T)
: signal.set(updater)
},
get: () => signal(),
subscribe: (observer: Observer<T>) => {
return toObservable(computed(signal), { injector: injector }).subscribe(
observer,
)
},
})
}

export function angularReactivity(injector: Injector): TableReactivityBindings {
return {
createReadonlyAtom: <T>(fn: () => T, options?: TableAtomOptions<T>) => {
const signal = computed(() => fn(), {
equal: options?.compare,
debugName: options?.debugName,
})
return signalToReadonlyAtom(signal, injector)
},
createWritableAtom: <T>(
value: T,
options?: TableAtomOptions<T>,
): Atom<T> => {
const writableSignal = signal(value, {
equal: options?.compare,
debugName: options?.debugName,
})
return signalToWritableAtom(writableSignal, injector)
},
untrack: untracked,
batch: (fn) => fn(),
}
}
36 changes: 9 additions & 27 deletions packages/angular-table/tests/angularReactivityFeature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,6 @@ describe('angularReactivityFeature', () => {
_features: { ...stockFeatures },
columns: columns,
getRowId: (row) => row.id,
reactivity: {
column: true,
cell: true,
row: true,
header: true,
},
})),
)
}
Expand All @@ -44,7 +38,7 @@ describe('angularReactivityFeature', () => {

describe('Integration', () => {
// TODO this switches between 1 and 2 calls on every other run, so it's not a reliable test
test.skip('methods within effect will be re-trigger when options/state changes', () => {
test('methods within effect will be re-trigger when options/state changes', () => {
Comment on lines 40 to +41
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Address test flakiness before unskipping.

The TODO comment indicates this test "switches between 1 and 2 calls on every other run." Re-enabling a known flaky test may cause intermittent CI failures. Consider either resolving the underlying timing issue or adding a more robust synchronization mechanism before unskipping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-table/tests/angularReactivityFeature.test.ts` around lines
40 - 41, The test "methods within effect will be re-trigger when options/state
changes" is flaky due to a race between state changes and assertion timing; make
it deterministic by synchronizing on the effect completion (e.g., await the next
microtask/tick or use a test utility like waitFor/waitForNextUpdate) and
stabilize call counting by resetting mocks before the test and asserting after
you run pending timers (jest.runAllTimers or await act/nextTick) so the
method-call count is consistently observed; update the test to explicitly wait
for the re-triggered effect and then assert the exact number of calls rather
than relying on timing-sensitive immediate assertions.

const data = signal<Array<Data>>([{ id: '1', title: 'Title' }])
const table = createTestTable(data)
const isSelectedRow1Captor = vi.fn<(val: boolean) => void>()
Expand Down Expand Up @@ -86,35 +80,23 @@ describe('angularReactivityFeature', () => {
TestBed.tick()
expect(isSelectedRow1Captor).toHaveBeenCalledTimes(2)
expect(cellGetValueCaptor).toHaveBeenCalledTimes(1)
expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(2)
expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(1)

data.set([{ id: '1', title: 'Title 3' }])
TestBed.tick()
// Row/cell instances are memoized by id in the atoms-based table, so a
// data change that preserves ids does not emit a new cell reference.
// `cellGetValueCaptor` therefore stays at its initial count (the
// memoized `cellGetValue` computed is also a no-op here). Effects that
// read atoms directly (`isSelectedRow1Captor`, `columnIsVisibleCaptor`)
// still re-run because `stateNotifier` bumps on state/options changes.
expect(isSelectedRow1Captor).toHaveBeenCalledTimes(3)
expect(cellGetValueCaptor).toHaveBeenCalledTimes(1)
expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(3)
expect(cellGetValueCaptor).toHaveBeenCalledTimes(2)
expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(2)

cell().column.toggleVisibility(false)
TestBed.tick()
expect(isSelectedRow1Captor).toHaveBeenCalledTimes(4)
expect(cellGetValueCaptor).toHaveBeenCalledTimes(1)
expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(4)
expect(isSelectedRow1Captor).toHaveBeenCalledTimes(3)
expect(cellGetValueCaptor).toHaveBeenCalledTimes(2)
expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(3)

expect(isSelectedRow1Captor.mock.calls).toEqual([
[false],
[true],
[true],
[true],
])
expect(cellGetValueCaptor.mock.calls).toEqual([['1']])
expect(isSelectedRow1Captor.mock.calls).toEqual([[false], [true], [true]])
expect(cellGetValueCaptor.mock.calls).toEqual([['1'], ['1']])
expect(columnIsVisibleCaptor.mock.calls).toEqual([
[true],
[true],
[true],
[false],
Expand Down
Loading
Loading