Skip to content

Commit 48c70bd

Browse files
committed
refactor(injectTable): improve type definitions and enhance reactivity in table subscription
1 parent 7bd52bc commit 48c70bd

4 files changed

Lines changed: 268 additions & 204 deletions

File tree

packages/angular-table/src/injectTable.ts

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import { constructTable } from '@tanstack/table-core'
1010
import { injectStore } from '@tanstack/angular-store'
1111
import { lazyInit } from './lazySignalInitializer'
1212
import { angularReactivityFeature } from './angularReactivityFeature'
13-
import type { Signal } from '@angular/core'
1413
import type {
1514
RowData,
1615
Table,
1716
TableFeatures,
1817
TableOptions,
1918
TableState,
2019
} from '@tanstack/table-core'
20+
import type { Signal, ValueEqualityFn } from '@angular/core'
2121

2222
export type AngularTable<
2323
TFeatures extends TableFeatures,
@@ -33,8 +33,8 @@ export type AngularTable<
3333
*/
3434
Subscribe: <TSubSelected = {}>(props: {
3535
selector: (state: TableState<TFeatures>) => TSubSelected
36-
children: ((state: Signal<Readonly<TSubSelected>>) => any) | any
37-
}) => any
36+
equal?: ValueEqualityFn<TSubSelected>
37+
}) => Signal<Readonly<TSubSelected>>
3838
}
3939

4040
export function injectTable<
@@ -58,11 +58,9 @@ export function injectTable<
5858
},
5959
} as TableOptions<TFeatures, TData>
6060

61-
const table = constructTable(resolvedOptions) as AngularTable<
62-
TFeatures,
63-
TData,
64-
TSelected
65-
>
61+
const table: AngularTable<TFeatures, TData, TSelected> = constructTable(
62+
resolvedOptions,
63+
) as AngularTable<TFeatures, TData, TSelected>
6664

6765
const updatedOptions = computed<TableOptions<TFeatures, TData>>(() => {
6866
const tableOptionsValue = options()
@@ -96,12 +94,9 @@ export function injectTable<
9694

9795
const tableSignalNotifier = computed(
9896
() => {
99-
// TODO: replace computed just using effects could be better?
10097
tableState()
10198
table.setOptions(updatedOptions())
102-
untracked(() => {
103-
table.baseStore.setState((prev) => ({ ...prev }))
104-
})
99+
untracked(() => table.baseStore.setState((prev) => ({ ...prev })))
105100
return table
106101
},
107102
{ equal: () => false },
@@ -111,18 +106,17 @@ export function injectTable<
111106

112107
table.Subscribe = function Subscribe<TSubSelected = {}>(props: {
113108
selector: (state: TableState<TFeatures>) => TSubSelected
114-
children: ((state: Signal<Readonly<TSubSelected>>) => any) | any
109+
equal?: ValueEqualityFn<TSubSelected>
115110
}) {
116-
const selected = injectStore(table.store, props.selector, { injector })
117-
if (typeof props.children === 'function') {
118-
return props.children(selected)
119-
}
120-
return props.children
111+
return injectStore(table.store, props.selector, {
112+
injector,
113+
equal: props.equal,
114+
})
121115
}
122116

123-
const stateStore = injectStore(table.store, selector, { injector })
124-
125-
Reflect.set(table, 'state', stateStore)
117+
Object.defineProperty(table, 'state', {
118+
value: injectStore(table.store, selector, { injector }),
119+
})
126120

127121
return table
128122
})
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { describe, expect, test, vi } from 'vitest'
2+
import { computed, effect, isSignal, signal } from '@angular/core'
3+
import { TestBed } from '@angular/core/testing'
4+
import { injectTable, stockFeatures } from '../src'
5+
import { getFnReactiveCache, testShouldBeComputedProperty } from './test-utils'
6+
import type { WritableSignal } from '@angular/core'
7+
import type { ColumnDef } from '../src'
8+
9+
describe('angularReactivityFeature', () => {
10+
type Data = { id: string; title: string }
11+
const data = signal<Array<Data>>([{ id: '1', title: 'Title' }])
12+
const columns: Array<ColumnDef<typeof stockFeatures, Data>> = [
13+
{
14+
id: 'id',
15+
header: 'Id',
16+
accessorKey: 'id',
17+
cell: (context) => context.getValue(),
18+
},
19+
{
20+
id: 'title',
21+
header: 'Title',
22+
accessorKey: 'title',
23+
cell: (context) => context.getValue(),
24+
},
25+
]
26+
27+
function createTestTable(_data: WritableSignal<Array<Data>> = data) {
28+
return TestBed.runInInjectionContext(() =>
29+
injectTable(() => ({
30+
data: _data(),
31+
_features: { ...stockFeatures },
32+
columns: columns,
33+
getRowId: (row) => row.id,
34+
reactivity: {
35+
column: true,
36+
cell: true,
37+
row: true,
38+
header: true,
39+
},
40+
})),
41+
)
42+
}
43+
44+
const table = createTestTable()
45+
const tablePropertyKeys = Object.keys(table)
46+
47+
describe('Table property reactivity', () => {
48+
test.each(
49+
tablePropertyKeys.map((property) => [
50+
property,
51+
testShouldBeComputedProperty(table, property),
52+
]),
53+
)('property (%s) is computed -> (%s)', (name, expected) => {
54+
const tableProperty = table[name as keyof typeof table]
55+
expect(isSignal(tableProperty)).toEqual(expected)
56+
})
57+
58+
describe('will create a computed for non detectable computed properties', () => {
59+
test('getIsSomeRowsPinned', () => {
60+
table.getIsSomeRowsPinned('top')
61+
table.getIsSomeRowsPinned('bottom')
62+
table.getIsSomeRowsPinned()
63+
64+
expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty(
65+
'["top"]',
66+
)
67+
expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty(
68+
'["bottom"]',
69+
)
70+
expect(getFnReactiveCache(table.getIsSomeRowsPinned)).toHaveProperty(
71+
'[]',
72+
)
73+
})
74+
})
75+
})
76+
77+
describe('Header property reactivity', () => {
78+
const headers = table.getHeaderGroups()
79+
headers.forEach((headerGroup, index) => {
80+
const headerPropertyKeys = Object.keys(headerGroup)
81+
test.each(
82+
headerPropertyKeys.map((property) => [
83+
property,
84+
testShouldBeComputedProperty(headerGroup, property),
85+
]),
86+
)(
87+
`HeaderGroup ${headerGroup.id} (${index}) - property (%s) is computed -> (%s)`,
88+
(name, expected) => {
89+
const tableProperty = headerGroup[name as keyof typeof headerGroup]
90+
expect(isSignal(tableProperty)).toEqual(expected)
91+
},
92+
)
93+
94+
const headers = headerGroup.headers
95+
headers.forEach((header, cellIndex) => {
96+
const headerPropertyKeys = Object.keys(header)
97+
test.each(
98+
headerPropertyKeys.map((property) => [
99+
property,
100+
testShouldBeComputedProperty(header, property),
101+
]),
102+
)(
103+
`HeaderGroup ${headerGroup.id} (${index}) / Header ${header.id} - property (%s) is computed -> (%s)`,
104+
(name, expected) => {
105+
const tableProperty = header[name as keyof typeof header]
106+
expect(isSignal(tableProperty)).toEqual(expected)
107+
},
108+
)
109+
})
110+
})
111+
})
112+
113+
describe('Column property reactivity', () => {
114+
const columns = table.getAllColumns()
115+
columns.forEach((column, index) => {
116+
const columnPropertyKeys = Object.keys(column)
117+
test.each(
118+
columnPropertyKeys.map((property) => [
119+
property,
120+
testShouldBeComputedProperty(column, property),
121+
]),
122+
)(
123+
`Column ${column.id} (${index}) - property (%s) is computed -> (%s)`,
124+
(name, expected) => {
125+
const tableProperty = column[name as keyof typeof column]
126+
expect(isSignal(tableProperty)).toEqual(expected)
127+
},
128+
)
129+
})
130+
})
131+
132+
describe('Row property reactivity', () => {
133+
const flatRows = table.getRowModel().flatRows
134+
flatRows.forEach((row, index) => {
135+
const rowsPropertyKeys = Object.keys(row)
136+
test.each(
137+
rowsPropertyKeys.map((property) => [
138+
property,
139+
testShouldBeComputedProperty(row, property),
140+
]),
141+
)(
142+
`Row ${row.id} (${index}) - property (%s) is computed -> (%s)`,
143+
(name, expected) => {
144+
const tableProperty = row[name as keyof typeof row]
145+
expect(isSignal(tableProperty)).toEqual(expected)
146+
},
147+
)
148+
149+
const cells = row.getAllCells()
150+
cells.forEach((cell, cellIndex) => {
151+
const cellPropertyKeys = Object.keys(cell)
152+
test.each(
153+
cellPropertyKeys.map((property) => [
154+
property,
155+
testShouldBeComputedProperty(cell, property),
156+
]),
157+
)(
158+
`Row ${row.id} (${index}) / Cell ${cell.id} - property (%s) is computed -> (%s)`,
159+
(name, expected) => {
160+
const tableProperty = cell[name as keyof typeof cell]
161+
expect(isSignal(tableProperty)).toEqual(expected)
162+
},
163+
)
164+
})
165+
})
166+
})
167+
168+
describe('Integration', () => {
169+
test('methods works will be reactive effects', () => {
170+
const data = signal<Array<Data>>([{ id: '1', title: 'Title' }])
171+
const table = createTestTable(data)
172+
const isSelectedRow1Captor = vi.fn<(val: boolean) => void>()
173+
const cellGetValueCaptor = vi.fn<(val: unknown) => void>()
174+
const columnIsVisibleCaptor = vi.fn<(val: boolean) => void>()
175+
176+
// This will test a case where you put in the effect a single cell property method
177+
// which will trigger effect reschedule only when the value changes, acting like
178+
// its a computed value
179+
const cell = computed(
180+
() => table.getRowModel().rows[0]!.getAllCells()[0]!,
181+
)
182+
183+
TestBed.runInInjectionContext(() => {
184+
effect(() => {
185+
isSelectedRow1Captor(cell().row.getIsSelected())
186+
})
187+
effect(() => {
188+
cellGetValueCaptor(cell().getValue())
189+
})
190+
effect(() => {
191+
columnIsVisibleCaptor(cell().column.getIsVisible())
192+
})
193+
})
194+
195+
TestBed.tick()
196+
expect(isSelectedRow1Captor).toHaveBeenCalledTimes(1)
197+
expect(cellGetValueCaptor).toHaveBeenCalledTimes(1)
198+
expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(1)
199+
200+
cell().row.toggleSelected(true)
201+
TestBed.tick()
202+
expect(isSelectedRow1Captor).toHaveBeenCalledTimes(2)
203+
expect(cellGetValueCaptor).toHaveBeenCalledTimes(1)
204+
expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(1)
205+
206+
data.set([{ id: '1', title: 'Title 3' }])
207+
TestBed.tick()
208+
// In this case it will be called twice since `data` will change and
209+
// the cell instance will be recreated
210+
expect(isSelectedRow1Captor).toHaveBeenCalledTimes(3)
211+
expect(cellGetValueCaptor).toHaveBeenCalledTimes(2)
212+
expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(2)
213+
214+
cell().column.toggleVisibility(false)
215+
TestBed.tick()
216+
expect(isSelectedRow1Captor).toHaveBeenCalledTimes(3)
217+
expect(cellGetValueCaptor).toHaveBeenCalledTimes(2)
218+
expect(columnIsVisibleCaptor).toHaveBeenCalledTimes(3)
219+
220+
expect(isSelectedRow1Captor.mock.calls).toEqual([[false], [true], [true]])
221+
expect(cellGetValueCaptor.mock.calls).toEqual([['1'], ['1']])
222+
expect(columnIsVisibleCaptor.mock.calls).toEqual([
223+
[true],
224+
[true],
225+
[false],
226+
])
227+
})
228+
})
229+
})

0 commit comments

Comments
 (0)