From 5c6cf2d5fb9a940205ae8d637b38d140fc5ebd96 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 24 Nov 2024 15:50:42 +0100 Subject: [PATCH 01/19] angular v19 table adapter --- .../angular/basic/src/app/app.component.html | 2 +- .../angular/basic/src/app/app.component.ts | 13 +- .../column-ordering/src/app/app.component.ts | 7 +- .../src/app/app.component.ts | 22 +- .../column-pinning/src/app/app.component.ts | 15 +- .../grouping/src/app/app.component.html | 2 +- .../angular/grouping/src/app/app.component.ts | 63 ++--- .../src/app/app.component.html | 3 +- .../src/app/app.component.ts | 18 +- .../row-selection-signal/src/app/filter.ts | 6 +- .../src/app/selection-column.component.ts | 10 +- .../row-selection/src/app/app.component.html | 5 +- .../row-selection/src/app/app.component.ts | 146 ++++++------ .../angular/row-selection/src/app/filter.ts | 4 +- .../src/app/selection-column.component.ts | 9 +- .../person-table/person-table.component.ts | 20 +- examples/react/basic/src/main.tsx | 5 +- packages/angular-table/package.json | 1 + .../angular-table/src/constructTableHelper.ts | 109 +++++++++ .../angular-table/src/createTableHelper.ts | 68 ++++++ packages/angular-table/src/flex-render.ts | 19 +- packages/angular-table/src/index.ts | 1 + packages/angular-table/src/injectTable.ts | 38 ++- .../tests/createAngularTable.test.ts | 91 -------- .../tests/createTableHelper.test-d.ts | 63 +++++ .../angular-table/tests/flex-render.test.ts | 8 +- .../angular-table/tests/injectTable.test.ts | 220 ++++++++++++++++++ .../angular-table/tests/lazy-init.test.ts | 6 +- packages/angular-table/tests/test-utils.ts | 42 +++- packages/angular-table/vite.config.ts | 4 + pnpm-lock.yaml | 50 +++- 31 files changed, 817 insertions(+), 253 deletions(-) create mode 100644 packages/angular-table/src/constructTableHelper.ts create mode 100644 packages/angular-table/src/createTableHelper.ts delete mode 100644 packages/angular-table/tests/createAngularTable.test.ts create mode 100644 packages/angular-table/tests/createTableHelper.test-d.ts create mode 100644 packages/angular-table/tests/injectTable.test.ts diff --git a/examples/angular/basic/src/app/app.component.html b/examples/angular/basic/src/app/app.component.html index 68c81953a6..8f105b6da5 100644 --- a/examples/angular/basic/src/app/app.component.html +++ b/examples/angular/basic/src/app/app.component.html @@ -24,7 +24,7 @@ @for (row of table.getRowModel().rows; track row.id) { - @for (cell of row.getVisibleCells(); track cell.id) { + @for (cell of row.getAllCells(); track cell.id) { ([]) + readonly data = signal(makeData(10000)) + readonly grouping = signal([]) - stringifiedGrouping = computed(() => JSON.stringify(this.grouping(), null, 2)) - - tableOptions = computed(() => - tableOptions({ - data: this.data(), - columns: columns, - state: { - grouping: this.grouping(), - }, - onGroupingChange: (updaterOrValue: Updater) => { - const groupingState = - typeof updaterOrValue === 'function' - ? updaterOrValue([...this.grouping()]) - : updaterOrValue - this.grouping.set(groupingState) - }, - getExpandedRowModel: createExpandedRowModel(), - getGroupedRowModel: createGroupedRowModel(), - getCoreRowModel: createCoreRowModel(), - getPaginatedRowModel: createPaginatedRowModel(), - getFilteredRowModel: createFilteredRowModel(), - debugTable: true, - }), + readonly stringifiedGrouping = computed(() => + JSON.stringify(this.grouping(), null, 2), ) - table = injectTable(this.tableOptions) + readonly table = injectTable(() => ({ + data: this.data(), + columns: columns, + initialState: { + pagination: { pageSize: 20, pageIndex: 0 }, + }, + state: { + grouping: this.grouping(), + }, + _features: { + columnGroupingFeature, + rowPaginationFeature, + columnFilteringFeature, + rowExpandingFeature, + }, + _rowModels: { + groupedRowModel: createGroupedRowModel(), + expandedRowModel: createExpandedRowModel(), + paginatedRowModel: createPaginatedRowModel(), + filteredRowModel: createFilteredRowModel(), + }, + onGroupingChange: (updaterOrValue: Updater) => { + const groupingState = + typeof updaterOrValue === 'function' + ? updaterOrValue([...this.grouping()]) + : updaterOrValue + this.grouping.set(groupingState) + }, + })) onPageInputChange(event: any): void { const page = event.target.value ? Number(event.target.value) - 1 : 0 diff --git a/examples/angular/row-selection-signal/src/app/app.component.html b/examples/angular/row-selection-signal/src/app/app.component.html index c2dfb725f7..f50c2d0052 100644 --- a/examples/angular/row-selection-signal/src/app/app.component.html +++ b/examples/angular/row-selection-signal/src/app/app.component.html @@ -20,8 +20,9 @@ @if (header.column.getCanFilter()) {
+
diff --git a/examples/angular/row-selection-signal/src/app/app.component.ts b/examples/angular/row-selection-signal/src/app/app.component.ts index 908f667cdf..8c480ee9d2 100644 --- a/examples/angular/row-selection-signal/src/app/app.component.ts +++ b/examples/angular/row-selection-signal/src/app/app.component.ts @@ -7,10 +7,13 @@ import { } from '@angular/core' import { FlexRenderDirective, - createCoreRowModel, + columnFilteringFeature, + columnVisibilityFeature, createFilteredRowModel, createPaginatedRowModel, injectTable, + rowPaginationFeature, + rowSelectionFeature, } from '@tanstack/angular-table' import { FilterComponent } from './filter' import { makeData } from './makeData' @@ -97,6 +100,16 @@ export class AppComponent { table = injectTable(() => ({ data: this.data(), + _features: { + rowSelectionFeature, + rowPaginationFeature, + columnFilteringFeature, + columnVisibilityFeature, + }, + _rowModels: { + filteredRowModel: createFilteredRowModel(), + paginatedRowModel: createPaginatedRowModel(), + }, columns: this.columns, state: { rowSelection: this.rowSelection(), @@ -110,9 +123,6 @@ export class AppComponent { : updaterOrValue, ) }, - getCoreRowModel: createCoreRowModel(), - getFilteredRowModel: createFilteredRowModel(), - getPaginatedRowModel: createPaginatedRowModel(), debugTable: true, })) diff --git a/examples/angular/row-selection-signal/src/app/filter.ts b/examples/angular/row-selection-signal/src/app/filter.ts index bd3319c687..c21fcc3807 100644 --- a/examples/angular/row-selection-signal/src/app/filter.ts +++ b/examples/angular/row-selection-signal/src/app/filter.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common' import { Component, input } from '@angular/core' import type { OnInit } from '@angular/core' -import type { Column, Table } from '@tanstack/angular-table' +import type { Column, RowData, Table } from '@tanstack/angular-table' @Component({ selector: 'app-table-filter', @@ -39,10 +39,10 @@ import type { Column, Table } from '@tanstack/angular-table' standalone: true, imports: [CommonModule], }) -export class FilterComponent implements OnInit { +export class FilterComponent implements OnInit { column = input.required>() - table = input.required>() + table = input.required>() columnType!: string diff --git a/examples/angular/row-selection-signal/src/app/selection-column.component.ts b/examples/angular/row-selection-signal/src/app/selection-column.component.ts index 50d057b84c..2a6cb75670 100644 --- a/examples/angular/row-selection-signal/src/app/selection-column.component.ts +++ b/examples/angular/row-selection-signal/src/app/selection-column.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core' -import type { Row, Table } from '@tanstack/angular-table' +import type { Row, RowData, Table } from '@tanstack/angular-table' @Component({ template: ` @@ -16,7 +16,7 @@ import type { Row, Table } from '@tanstack/angular-table' standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TableHeadSelectionComponent { +export class TableHeadSelectionComponent { // Your component should also reflect the fields you use as props in flexRenderer directive. // Define the fields as input you want to use in your component // ie. In this case, you are passing HeaderContext object as props in flexRenderer directive. @@ -25,7 +25,7 @@ export class TableHeadSelectionComponent { // column = input.required>() // header = input.required>() - table = input.required>() + table = input.required>() } @Component({ @@ -42,6 +42,6 @@ export class TableHeadSelectionComponent { standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TableRowSelectionComponent { - row = input.required>() +export class TableRowSelectionComponent { + row = input.required>() } diff --git a/examples/angular/row-selection/src/app/app.component.html b/examples/angular/row-selection/src/app/app.component.html index c2dfb725f7..109bc93f63 100644 --- a/examples/angular/row-selection/src/app/app.component.html +++ b/examples/angular/row-selection/src/app/app.component.html @@ -20,8 +20,9 @@ @if (header.column.getCanFilter()) {
+
@@ -35,7 +36,7 @@ @for (row of table.getRowModel().rows; track row.id) { - @for (cell of row.getVisibleCells(); track cell.id) { + @for (cell of row.getAllCells(); track cell.id) { = Omit< + TableOptions>, + 'columns' | 'data' | 'state' +> & { + _features: TFeatures + TData?: TData // provide a cast for the TData type +} + +/** + * Internal type that each adapter package will build off of to create a table helper + */ +export type TableHelper_Core< + TFeatures extends TableFeatures, + TData extends RowData = any, +> = { + columnHelper: ColumnHelper + createColumnHelper: () => ColumnHelper< + TFeatures, + TData + > + features: TFeatures + options: Omit, 'columns' | 'data' | 'state'> + tableCreator: ( + tableOptions: () => Omit< + TableOptions, + '_features' | '_rowModels' | '_rowModelFns' + >, + ) => Table +} + +/** + * Internal function to create a table helper that each adapter package will use to create their own table helper + */ +export function constructTableHelper< + TFeatures extends TableFeatures, + TData extends RowData = any, +>( + tableCreator: ( + tableOptions: () => TableOptions, + ) => Table & Signal>, + tableHelperOptions: TableHelperOptions, +): TableHelper_Core { + const { TData: _TData, ..._tableHelperOptions } = tableHelperOptions + return { + columnHelper: createColumnHelper(), + createColumnHelper, + features: tableHelperOptions._features, + options: _tableHelperOptions as any, + tableCreator: (tableOptions) => + // @ts-expect-error Fix this + tableCreator(() => ({ + ...tableHelperOptions, + ...tableOptions(), + })), + } +} + +// test + +// // eslint-disable-next-line import/first, import/order +// import { constructTable } from '../core/table/constructTable' +// // eslint-disable-next-line import/first, import/order +// import { type ColumnDef } from '../types/ColumnDef' + +// type Person = { +// firstName: string +// lastName: string +// age: number +// } + +// const tableHelper = constructTableHelper(constructTable, { +// _features: { rowSelectionFeature: {} }, +// _rowModels: {}, +// TData: {} as Person, +// }) + +// const columns = [ +// tableHelper.columnHelper.accessor('firstName', { +// header: 'First Name', +// cell: (info) => info.getValue(), +// }), +// tableHelper.columnHelper.accessor('lastName', { header: 'Last Name' }), +// tableHelper.columnHelper.accessor('age', { header: 'Age' }), +// tableHelper.columnHelper.display({ header: 'Actions', id: 'actions' }), +// ] as Array> + +// const data: Array = [] + +// tableHelper.tableCreator({ +// columns, +// data, +// }) diff --git a/packages/angular-table/src/createTableHelper.ts b/packages/angular-table/src/createTableHelper.ts new file mode 100644 index 0000000000..b4f6d2a787 --- /dev/null +++ b/packages/angular-table/src/createTableHelper.ts @@ -0,0 +1,68 @@ +import { Signal } from '@angular/core' +import { constructTableHelper } from './constructTableHelper' +import { injectTable } from './injectTable' +import type { + RowData, + Table, + TableFeatures, + TableHelperOptions, + TableHelper_Core, + TableOptions, +} from '@tanstack/table-core' + +export type TableHelper< + TFeatures extends TableFeatures, + TData extends RowData = any, +> = Omit, 'tableCreator'> & { + injectTable: ( + tableOptions: () => Omit< + TableOptions, + '_features' | '_rowModels' | '_rowModelFns' + >, + ) => Table +} + +export function createTableHelper< + TFeatures extends TableFeatures, + TData extends RowData = any, +>( + tableHelperOptions: TableHelperOptions, +): TableHelper { + const tableHelper = constructTableHelper( + injectTable as unknown as ( + tableOptions: () => TableOptions, + ) => Table & Signal>, + tableHelperOptions, + ) + return { + ...tableHelper, + injectTable: tableHelper.tableCreator, + } as any +} + +// test + +// type Person = { +// firstName: string +// lastName: string +// age: number +// } + +// const tableHelper = createTableHelper({ +// _features: { rowSelectionFeature: {} }, +// TData: {} as Person, +// }) + +// const columns = [ +// tableHelper.columnHelper.accessor('firstName', { header: 'First Name' }), +// tableHelper.columnHelper.accessor('lastName', { header: 'Last Name' }), +// tableHelper.columnHelper.accessor('age', { header: 'Age' }), +// tableHelper.columnHelper.display({ header: 'Actions', id: 'actions' }), +// ] as Array> + +// const data: Array = [] + +// tableHelper.createTable({ +// columns, +// data, +// }) diff --git a/packages/angular-table/src/flex-render.ts b/packages/angular-table/src/flex-render.ts index a584af7ac6..70b36a829a 100644 --- a/packages/angular-table/src/flex-render.ts +++ b/packages/angular-table/src/flex-render.ts @@ -13,7 +13,8 @@ import { inject, isSignal, } from '@angular/core' -import type { OnChanges, SimpleChanges } from '@angular/core' +import type { DoCheck, OnChanges, SimpleChanges } from '@angular/core' +import type { Table } from '@tanstack/table-core' export type FlexRenderContent> = | string @@ -29,7 +30,7 @@ export type FlexRenderContent> = standalone: true, }) export class FlexRenderDirective> - implements OnChanges + implements OnChanges, DoCheck { @Input({ required: true, alias: 'flexRender' }) content: @@ -45,6 +46,8 @@ export class FlexRenderDirective> @Input({ required: false, alias: 'flexRenderInjector' }) injector: Injector = inject(Injector) + ref?: ComponentRef | EmbeddedViewRef | null = null + constructor( @Inject(ViewContainerRef) private readonly viewContainerRef: ViewContainerRef, @@ -52,12 +55,13 @@ export class FlexRenderDirective> private readonly templateRef: TemplateRef, ) {} - ref?: ComponentRef | EmbeddedViewRef | null = null - - ngOnChanges(changes: SimpleChanges) { + ngDoCheck(): void { if (this.ref instanceof ComponentRef) { this.ref.injector.get(ChangeDetectorRef).markForCheck() } + } + + ngOnChanges(changes: SimpleChanges) { if (!changes['content']) { return } @@ -69,12 +73,11 @@ export class FlexRenderDirective> const { content, props } = this if (content === null || content === undefined) { this.ref = null - return } if (typeof content === 'function') { - return this.renderContent(content(props)) + this.ref = this.renderContent(content(props)) } else { - return this.renderContent(content) + this.ref = this.renderContent(content) } } diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts index b0b52b8882..f9bd9c806d 100644 --- a/packages/angular-table/src/index.ts +++ b/packages/angular-table/src/index.ts @@ -4,3 +4,4 @@ export * from './flex-render' export * from './proxy' export * from './lazy-signal-initializer' export * from './injectTable' +export * from './createTableHelper' diff --git a/packages/angular-table/src/injectTable.ts b/packages/angular-table/src/injectTable.ts index 748299f34b..847d8c9958 100644 --- a/packages/angular-table/src/injectTable.ts +++ b/packages/angular-table/src/injectTable.ts @@ -7,40 +7,52 @@ import { } from '@tanstack/table-core' import { lazyInit } from './lazy-signal-initializer' import { proxifyTable } from './proxy' -import type { Signal } from '@angular/core' import type { + CreateRowModels_All, RowData, Table, TableFeatures, TableOptions, TableState, } from '@tanstack/table-core' +import type { Signal } from '@angular/core' + +export type AngularTableOptions< + TFeatures extends TableFeatures, + TData extends RowData, +> = Omit, '_rowModels'> & { + _rowModels: CreateRowModels_All + // TODO: no exported + // _rowModelsFns: RowModelFns +} export function injectTable< TFeatures extends TableFeatures, TData extends RowData, >( - options: () => TableOptions, + options: () => AngularTableOptions, ): Table & Signal> { return lazyInit(() => { - const resolvedOptions = { - ...options(), - _features: { + const features = () => { + return { ...coreFeatures, ...options()._features, - }, + } } - const table = constructTable(resolvedOptions) - // By default, manage table state here using the table's initial state const state = signal>( - getInitialTableState( - resolvedOptions._features, - resolvedOptions.initialState, - ), + getInitialTableState(features(), options().initialState), ) + const resolvedOptions: TableOptions = { + ...options(), + _features: features(), + state: { ...state(), ...options().state }, + } as TableOptions + + const table = constructTable(resolvedOptions) + // Compose table options using computed. // This is to allow `tableSignal` to listen and set table option const updatedOptions = computed>(() => { @@ -48,10 +60,12 @@ export function injectTable< const tableState = state() // listen to input options changed const tableOptions = options() + return { ...table.options, ...resolvedOptions, ...tableOptions, + _features: features(), state: { ...tableState, ...tableOptions.state }, onStateChange: (updater) => { const value = isFunction(updater) ? updater(tableState) : updater diff --git a/packages/angular-table/tests/createAngularTable.test.ts b/packages/angular-table/tests/createAngularTable.test.ts deleted file mode 100644 index 1ab38422df..0000000000 --- a/packages/angular-table/tests/createAngularTable.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { Component, input, isSignal, signal, untracked } from '@angular/core' -import { TestBed } from '@angular/core/testing' -import { createCoreRowModel, injectTable } from '../src/injectTable' -import { setSignalInputs } from './test-utils' -import type { ColumnDef, Table } from '../src/injectTable' - -describe('injectTable', () => { - test('should render with required signal inputs', () => { - @Component({ - selector: 'app-fake', - template: ``, - standalone: true, - }) - class FakeComponent { - data = input.required>() - - table = injectTable(() => ({ - data: this.data(), - columns: [], - getCoreRowModel: createCoreRowModel(), - })) - } - - const fixture = TestBed.createComponent(FakeComponent) - setSignalInputs(fixture.componentInstance, { - data: [], - }) - - fixture.detectChanges() - }) - - describe('Proxy table', () => { - type Data = { id: string; title: string } - const data = signal>([{ id: '1', title: 'Title' }]) - const columns: Array> = [ - { id: 'id', header: 'Id', cell: (context) => context.getValue() }, - { id: 'title', header: 'Title', cell: (context) => context.getValue() }, - ] - const table = injectTable(() => ({ - data: data(), - columns: columns, - getCoreRowModel: createCoreRowModel(), - getRowId: (row) => row.id, - })) - const tablePropertyKeys = Object.keys(table()) - - test('table must be a signal', () => { - expect(isSignal(table)).toEqual(true) - }) - - test('supports "in" operator', () => { - expect('getCoreRowModel' in table).toBe(true) - expect('options' in table).toBe(true) - expect('notFound' in table).toBe(false) - }) - - test('supports "Object.keys"', () => { - const keys = Object.keys(table()) - expect(Object.keys(table)).toEqual(keys) - }) - - test.each( - tablePropertyKeys.map((property) => [ - property, - testShouldBeComputedProperty(untracked(table), property), - ]), - )('property (%s) is computed -> (%s)', (name, expected) => { - const tableProperty = table[name as keyof typeof table] - expect(isSignal(tableProperty)).toEqual(expected) - }) - }) -}) - -const testShouldBeComputedProperty = ( - table: Table, - propertyName: string, -) => { - if (propertyName.endsWith('Handler') || propertyName.endsWith('Model')) { - return false - } - - if (propertyName.startsWith('get')) { - // Only properties with no arguments are computed - const fn = table[propertyName as keyof Table] - // Cannot test if is lazy computed since we return the unwrapped value - return fn instanceof Function && fn.length === 0 - } - - return false -} diff --git a/packages/angular-table/tests/createTableHelper.test-d.ts b/packages/angular-table/tests/createTableHelper.test-d.ts new file mode 100644 index 0000000000..e8a22c9555 --- /dev/null +++ b/packages/angular-table/tests/createTableHelper.test-d.ts @@ -0,0 +1,63 @@ +import { expectTypeOf, test } from 'vitest' +import { + createPaginatedRowModel, + createTableHelper, + stockFeatures, +} from '../src' +import type { ColumnDef, StockTableFeatures, Table, TableHelper } from '../src' + +test('infer data type from TData', () => { + type TestDataType = { firstName: string; lastName: string; age: number } + + const tableHelper = createTableHelper({ + _features: stockFeatures, + _rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + TData: {} as TestDataType, + }) + + expectTypeOf().toEqualTypeOf< + TableHelper, TestDataType> + >() + + expectTypeOf<(typeof tableHelper)['features']>().toEqualTypeOf< + Required + >() + + const columns = [ + tableHelper.columnHelper.accessor('firstName', { header: 'First Name' }), + tableHelper.columnHelper.accessor('lastName', { header: 'Last Name' }), + tableHelper.columnHelper.accessor('age', { header: 'Age' }), + tableHelper.columnHelper.display({ header: 'Actions', id: 'actions' }), + ] as const + + expectTypeOf().toMatchTypeOf< + ReadonlyArray> + >() +}) + +test('infer data type given by injectTable', () => { + type TestDataType = { firstName: string; lastName: string } + + const tableHelper = createTableHelper({ + _features: stockFeatures, + _rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + }) + + expectTypeOf().toEqualTypeOf< + TableHelper, any> + >() + + const injectTable = tableHelper.injectTable + const table = injectTable(() => ({ + data: [] as Array, + columns: [], + })) + + expectTypeOf().toEqualTypeOf< + Table, TestDataType> + >() +}) diff --git a/packages/angular-table/tests/flex-render.test.ts b/packages/angular-table/tests/flex-render.test.ts index 02e3c33003..c88bbc1e16 100644 --- a/packages/angular-table/tests/flex-render.test.ts +++ b/packages/angular-table/tests/flex-render.test.ts @@ -1,5 +1,5 @@ -import { Component, ViewChild, input, type TemplateRef } from '@angular/core' -import { TestBed, type ComponentFixture } from '@angular/core/testing' +import { Component, ViewChild, input } from '@angular/core' +import { TestBed } from '@angular/core/testing' import { createColumnHelper } from '@tanstack/table-core' import { describe, expect, test } from 'vitest' import { @@ -8,6 +8,8 @@ import { injectFlexRenderContext, } from '../src/flex-render' import { setFixtureSignalInput, setFixtureSignalInputs } from './test-utils' +import type { ComponentFixture } from '@angular/core/testing' +import type { TemplateRef } from '@angular/core' interface Data { id: string @@ -18,7 +20,7 @@ interface Data { } describe('FlexRenderDirective', () => { - const helper = createColumnHelper() + const helper = createColumnHelper<{}, Data>() test('should render primitives', async () => { const fixture = TestBed.createComponent(TestRenderComponent) diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts new file mode 100644 index 0000000000..61db8e8ed8 --- /dev/null +++ b/packages/angular-table/tests/injectTable.test.ts @@ -0,0 +1,220 @@ +import { describe, expect, test } from 'vitest' +import { Component, input, isSignal, signal, untracked } from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { ColumnDef, stockFeatures } from '@tanstack/table-core' +import { injectTable } from '../src/injectTable' +import { + experimentalReactivity_testShouldBeComputedProperty, + setSignalInputs, + testShouldBeComputedProperty, +} from './test-utils' + +describe('injectTable', () => { + test('should render with required signal inputs', () => { + @Component({ + selector: 'app-fake', + template: ``, + standalone: true, + }) + class FakeComponent { + data = input.required>() + + table = injectTable(() => ({ + data: this.data(), + _features: stockFeatures, + columns: [], + })) + } + + const fixture = TestBed.createComponent(FakeComponent) + setSignalInputs(fixture.componentInstance, { + data: [], + }) + + fixture.detectChanges() + }) + + describe('Proxy table', () => { + type Data = { id: string; title: string } + const data = signal>([{ id: '1', title: 'Title' }]) + const columns: Array> = [ + { id: 'id', header: 'Id', cell: (context) => context.getValue() }, + { id: 'title', header: 'Title', cell: (context) => context.getValue() }, + ] + const table = injectTable(() => ({ + data: data(), + _features: stockFeatures, + columns: columns, + getRowId: (row) => row.id, + })) + const tablePropertyKeys = Object.keys(table()) + + test('table must be a signal', () => { + expect(isSignal(table)).toEqual(true) + }) + + test('supports "in" operator', () => { + expect('_features' in table).toBe(true) + expect('options' in table).toBe(true) + expect('notFound' in table).toBe(false) + }) + + test('supports "Object.keys"', () => { + const keys = Object.keys(table()) + expect(Object.keys(table)).toEqual(keys) + }) + + test.each( + tablePropertyKeys.map((property) => [ + property, + testShouldBeComputedProperty(untracked(table), property), + ]), + )('property (%s) is computed -> (%s)', (name, expected) => { + const tableProperty = table[name as keyof typeof table] + expect(isSignal(tableProperty)).toEqual(expected) + }) + }) +}) + +describe('injectTable - Experimental reactivity', () => { + type Data = { id: string; title: string } + const data = signal>([{ id: '1', title: 'Title' }]) + const columns: Array> = [ + { id: 'id', header: 'Id', cell: (context) => context.getValue() }, + { id: 'title', header: 'Title', cell: (context) => context.getValue() }, + ] + const table = injectTable(() => ({ + data: data(), + _features: { ...stockFeatures }, + columns: columns, + getRowId: (row) => row.id, + enableExperimentalReactivity: true, + })) + const tablePropertyKeys = Object.keys(table) + + describe('Proxy', () => { + test('table must be a signal', () => { + expect(isSignal(table)).toEqual(true) + }) + + test('supports "in" operator', () => { + expect('_features' in table).toBe(true) + expect('options' in table).toBe(true) + expect('notFound' in table).toBe(false) + }) + + test('supports "Object.keys"', () => { + const keys = Object.keys(table) + expect(Object.keys(table)).toEqual(keys) + }) + }) + + describe('Table property reactivity', () => { + test.each( + tablePropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty(table, property), + ]), + )('property (%s) is computed -> (%s)', (name, expected) => { + const tableProperty = table[name as keyof typeof table] + expect(isSignal(tableProperty)).toEqual(expected) + }) + }) + + describe('Header property reactivity', () => { + const headers = table.getHeaderGroups() + headers.forEach((headerGroup, index) => { + const headerPropertyKeys = Object.keys(headerGroup) + test.each( + headerPropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty( + headerGroup, + property, + ), + ]), + )( + `HeaderGroup ${headerGroup.id} (${index}) - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = headerGroup[name as keyof typeof headerGroup] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + + const headers = headerGroup.headers + headers.forEach((header, cellIndex) => { + const headerPropertyKeys = Object.keys(header) + test.each( + headerPropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty( + header, + property, + ), + ]), + )( + `HeaderGroup ${headerGroup.id} (${index}) / Header ${header.id} - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = header[name as keyof typeof header] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + }) + }) + }) + + describe('Column property reactivity', () => { + const columns = table.getAllColumns() + columns.forEach((column, index) => { + const columnPropertyKeys = Object.keys(column) + test.each( + columnPropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty(column, property), + ]), + )( + `Column ${column.id} (${index}) - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = column[name as keyof typeof column] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + }) + }) + + describe('Row property reactivity', () => { + const flatRows = table.getRowModel().flatRows + flatRows.forEach((row, index) => { + const rowsPropertyKeys = Object.keys(row) + test.each( + rowsPropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty(row, property), + ]), + )( + `Row ${row.id} (${index}) - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = row[name as keyof typeof row] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + + const cells = row.getAllCells() + cells.forEach((cell, cellIndex) => { + const cellPropertyKeys = Object.keys(cell) + test.each( + cellPropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty(cell, property), + ]), + )( + `Row ${row.id} (${index}) / Cell ${cell.id} - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = cell[name as keyof typeof cell] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + }) + }) + }) +}) diff --git a/packages/angular-table/tests/lazy-init.test.ts b/packages/angular-table/tests/lazy-init.test.ts index fa3a43f553..ba9854fb2b 100644 --- a/packages/angular-table/tests/lazy-init.test.ts +++ b/packages/angular-table/tests/lazy-init.test.ts @@ -2,7 +2,6 @@ import { describe, expect, test, vi } from 'vitest' import { ChangeDetectionStrategy, Component, - type WritableSignal, computed, effect, input, @@ -11,6 +10,7 @@ import { import { TestBed } from '@angular/core/testing' import { lazyInit } from '../src/lazy-signal-initializer' import { flushQueue, setFixtureSignalInputs } from './test-utils' +import type { WritableSignal } from '@angular/core' describe('lazyInit', () => { test('should init lazily in next tick when not accessing manually', async () => { @@ -51,7 +51,7 @@ describe('lazyInit', () => { test('should init lazily and only once', async () => { const initCallFn = vi.fn() - const registerDataValue = vi.fn<[number]>() + const registerDataValue = vi.fn<(arg0: number) => void>() let value!: { data: WritableSignal } const outerSignal = signal(0) @@ -84,7 +84,7 @@ describe('lazyInit', () => { await flushQueue() expect(initCallFn).toHaveBeenCalledTimes(1) - expect(registerDataValue).toHaveBeenCalledTimes(2) + expect(registerDataValue).toHaveBeenCalledTimes(1) }) test('should support required signal input', async () => { diff --git a/packages/angular-table/tests/test-utils.ts b/packages/angular-table/tests/test-utils.ts index 28b788e0bf..443f091a06 100644 --- a/packages/angular-table/tests/test-utils.ts +++ b/packages/angular-table/tests/test-utils.ts @@ -1,6 +1,7 @@ -import type { InputSignal } from '@angular/core' import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals' +import type { InputSignal } from '@angular/core' import type { ComponentFixture } from '@angular/core/testing' +import { Table } from '@tanstack/table-core' type ToSignalInputUpdatableMap = { [K in keyof T as T[K] extends InputSignal @@ -60,3 +61,42 @@ function componentHasSignalInputProperty( export async function flushQueue() { await new Promise(setImmediate) } + +export const experimentalReactivity_testShouldBeComputedProperty = ( + testObj: any, + propertyName: string, +) => { + if (propertyName.startsWith('_rootNotifier')) { + return true + } + if (propertyName.endsWith('Handler')) { + return false + } + + if (propertyName.startsWith('get')) { + // Only properties with no arguments are computed + const fn = testObj[propertyName] + // Cannot test if is lazy computed since we return the unwrapped value + return fn instanceof Function && fn.length === 0 + } + + return false +} + +export const testShouldBeComputedProperty = ( + testObj: any, + propertyName: string, +) => { + if (propertyName.endsWith('Handler') || propertyName.endsWith('Model')) { + return false + } + + if (propertyName.startsWith('get')) { + // Only properties with no arguments are computed + const fn = testObj[propertyName] + // Cannot test if is lazy computed since we return the unwrapped value + return fn instanceof Function && fn.length === 0 + } + + return false +} diff --git a/packages/angular-table/vite.config.ts b/packages/angular-table/vite.config.ts index 523b22e583..1ba318b695 100644 --- a/packages/angular-table/vite.config.ts +++ b/packages/angular-table/vite.config.ts @@ -9,5 +9,9 @@ export default defineConfig({ environment: 'jsdom', setupFiles: ['./tests/test-setup.ts'], globals: true, + typecheck: { + enabled: true, + tsconfig: './tsconfig.json', + }, }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7051a5c73d..f5baad1c9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,7 +70,7 @@ importers: version: 5.4.11(@types/node@22.9.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0) vitest: specifier: ^2.1.5 - version: 2.1.5(@types/node@22.9.1)(jsdom@25.0.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0) + version: 2.1.5(@types/node@22.9.1)(@vitest/ui@2.1.5)(jsdom@25.0.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0) examples/angular/basic: dependencies: @@ -2955,6 +2955,9 @@ importers: '@angular/platform-browser-dynamic': specifier: ^19.0.0 version: 19.0.0(@angular/common@19.0.0(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.0.0(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.0.0(@angular/animations@19.0.0(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/common@19.0.0(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0))) + '@vitest/ui': + specifier: ^2.1.5 + version: 2.1.5(vitest@2.1.5) ng-packagr: specifier: ^19.0.0 version: 19.0.0(@angular/compiler-cli@19.0.0(@angular/compiler@19.0.0(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.6.3))(tslib@2.8.1)(typescript@5.6.3) @@ -5102,6 +5105,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@polka/url@1.0.0-next.28': + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -5812,6 +5818,11 @@ packages: '@vitest/spy@2.1.5': resolution: {integrity: sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==} + '@vitest/ui@2.1.5': + resolution: {integrity: sha512-ERgKkDMTfngrZip6VG5h8L9B5D0AH/4+bga4yR1UzGH7c2cxv3LWogw2Dvuwr9cP3/iKDHYys7kIFLDKpxORTg==} + peerDependencies: + vitest: 2.1.5 + '@vitest/utils@2.1.5': resolution: {integrity: sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==} @@ -7182,6 +7193,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -9394,6 +9408,10 @@ packages: simple-git@3.27.0: resolution: {integrity: sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==} + sirv@3.0.0: + resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + engines: {node: '>=18'} + size-limit@11.1.6: resolution: {integrity: sha512-S5ux2IB8rU26xwVgMskmknGMFkieaIAqDLuwgKiypk6oa4lFsie8yFPrzRFV+yrLDY2GddjXuCaVk5PveVOHiQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -9761,6 +9779,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@5.0.0: resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} engines: {node: '>=16'} @@ -12697,6 +12719,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@polka/url@1.0.0-next.28': {} + '@popperjs/core@2.11.8': {} '@rollup/plugin-json@6.1.0(rollup@4.26.0)': @@ -13516,6 +13540,17 @@ snapshots: dependencies: tinyspy: 3.0.2 + '@vitest/ui@2.1.5(vitest@2.1.5)': + dependencies: + '@vitest/utils': 2.1.5 + fflate: 0.8.2 + flatted: 3.3.1 + pathe: 1.1.2 + sirv: 3.0.0 + tinyglobby: 0.2.10 + tinyrainbow: 1.2.0 + vitest: 2.1.5(@types/node@22.9.1)(@vitest/ui@2.1.5)(jsdom@25.0.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0) + '@vitest/utils@2.1.5': dependencies: '@vitest/pretty-format': 2.1.5 @@ -15217,6 +15252,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fflate@0.8.2: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -17633,6 +17670,12 @@ snapshots: transitivePeerDependencies: - supports-color + sirv@3.0.0: + dependencies: + '@polka/url': 1.0.0-next.28 + mrmime: 2.0.0 + totalist: 3.0.1 + size-limit@11.1.6: dependencies: bytes-iec: 3.1.1 @@ -18030,6 +18073,8 @@ snapshots: toidentifier@1.0.1: {} + totalist@3.0.1: {} + tough-cookie@5.0.0: dependencies: tldts: 6.1.61 @@ -18350,7 +18395,7 @@ snapshots: optionalDependencies: vite: 5.4.11(@types/node@22.9.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0) - vitest@2.1.5(@types/node@22.9.1)(jsdom@25.0.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0): + vitest@2.1.5(@types/node@22.9.1)(@vitest/ui@2.1.5)(jsdom@25.0.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0): dependencies: '@vitest/expect': 2.1.5 '@vitest/mocker': 2.1.5(vite@5.4.11(@types/node@22.9.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0)) @@ -18374,6 +18419,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.9.1 + '@vitest/ui': 2.1.5(vitest@2.1.5) jsdom: 25.0.1 transitivePeerDependencies: - less From a036c18987c27629556d0610002071327ab275cd Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 24 Nov 2024 15:53:56 +0100 Subject: [PATCH 02/19] angular v19 table adapter --- packages/angular-table/src/injectTable.ts | 2 +- packages/angular-table/src/proxy.ts | 15 +- .../angular-table/tests/injectTable.test.ts | 143 ------------------ 3 files changed, 9 insertions(+), 151 deletions(-) diff --git a/packages/angular-table/src/injectTable.ts b/packages/angular-table/src/injectTable.ts index 847d8c9958..f53b3ab95e 100644 --- a/packages/angular-table/src/injectTable.ts +++ b/packages/angular-table/src/injectTable.ts @@ -21,7 +21,7 @@ export type AngularTableOptions< TFeatures extends TableFeatures, TData extends RowData, > = Omit, '_rowModels'> & { - _rowModels: CreateRowModels_All + _rowModels?: CreateRowModels_All // TODO: no exported // _rowModelsFns: RowModelFns } diff --git a/packages/angular-table/src/proxy.ts b/packages/angular-table/src/proxy.ts index 86a65434ab..0706f5eadb 100644 --- a/packages/angular-table/src/proxy.ts +++ b/packages/angular-table/src/proxy.ts @@ -19,9 +19,9 @@ export function proxifyTable< apply() { return tableSignal() }, - get(target, property: keyof Table): any { - if (target[property]) { - return target[property] + get(target, property): any { + if (Reflect.has(target, property)) { + return Reflect.get(target, property) } const table = untracked(tableSignal) /** @@ -29,25 +29,26 @@ export function proxifyTable< * excluding handlers as they do not retain any reactive value */ if ( + typeof property === 'string' && property.startsWith('get') && !property.endsWith('Handler') && !property.endsWith('Model') ) { - const maybeFn = table[property] as Function | never + const maybeFn = (table as any)[property] as Function | never if (typeof maybeFn === 'function') { Object.defineProperty(target, property, { value: toComputed(tableSignal, maybeFn), configurable: true, enumerable: true, }) - return target[property] + return (target as any)[property] } } // @ts-expect-error return (target[property] = table[property]) }, - has(_, prop: keyof Table) { - return !!untracked(tableSignal)[prop] + has(_, prop) { + return Reflect.has(untracked(tableSignal), prop) }, ownKeys() { return Reflect.ownKeys(untracked(tableSignal)) diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts index 61db8e8ed8..55630f0eb5 100644 --- a/packages/angular-table/tests/injectTable.test.ts +++ b/packages/angular-table/tests/injectTable.test.ts @@ -75,146 +75,3 @@ describe('injectTable', () => { }) }) }) - -describe('injectTable - Experimental reactivity', () => { - type Data = { id: string; title: string } - const data = signal>([{ id: '1', title: 'Title' }]) - const columns: Array> = [ - { id: 'id', header: 'Id', cell: (context) => context.getValue() }, - { id: 'title', header: 'Title', cell: (context) => context.getValue() }, - ] - const table = injectTable(() => ({ - data: data(), - _features: { ...stockFeatures }, - columns: columns, - getRowId: (row) => row.id, - enableExperimentalReactivity: true, - })) - const tablePropertyKeys = Object.keys(table) - - describe('Proxy', () => { - test('table must be a signal', () => { - expect(isSignal(table)).toEqual(true) - }) - - test('supports "in" operator', () => { - expect('_features' in table).toBe(true) - expect('options' in table).toBe(true) - expect('notFound' in table).toBe(false) - }) - - test('supports "Object.keys"', () => { - const keys = Object.keys(table) - expect(Object.keys(table)).toEqual(keys) - }) - }) - - describe('Table property reactivity', () => { - test.each( - tablePropertyKeys.map((property) => [ - property, - experimentalReactivity_testShouldBeComputedProperty(table, property), - ]), - )('property (%s) is computed -> (%s)', (name, expected) => { - const tableProperty = table[name as keyof typeof table] - expect(isSignal(tableProperty)).toEqual(expected) - }) - }) - - describe('Header property reactivity', () => { - const headers = table.getHeaderGroups() - headers.forEach((headerGroup, index) => { - const headerPropertyKeys = Object.keys(headerGroup) - test.each( - headerPropertyKeys.map((property) => [ - property, - experimentalReactivity_testShouldBeComputedProperty( - headerGroup, - property, - ), - ]), - )( - `HeaderGroup ${headerGroup.id} (${index}) - property (%s) is computed -> (%s)`, - (name, expected) => { - const tableProperty = headerGroup[name as keyof typeof headerGroup] - expect(isSignal(tableProperty)).toEqual(expected) - }, - ) - - const headers = headerGroup.headers - headers.forEach((header, cellIndex) => { - const headerPropertyKeys = Object.keys(header) - test.each( - headerPropertyKeys.map((property) => [ - property, - experimentalReactivity_testShouldBeComputedProperty( - header, - property, - ), - ]), - )( - `HeaderGroup ${headerGroup.id} (${index}) / Header ${header.id} - property (%s) is computed -> (%s)`, - (name, expected) => { - const tableProperty = header[name as keyof typeof header] - expect(isSignal(tableProperty)).toEqual(expected) - }, - ) - }) - }) - }) - - describe('Column property reactivity', () => { - const columns = table.getAllColumns() - columns.forEach((column, index) => { - const columnPropertyKeys = Object.keys(column) - test.each( - columnPropertyKeys.map((property) => [ - property, - experimentalReactivity_testShouldBeComputedProperty(column, property), - ]), - )( - `Column ${column.id} (${index}) - property (%s) is computed -> (%s)`, - (name, expected) => { - const tableProperty = column[name as keyof typeof column] - expect(isSignal(tableProperty)).toEqual(expected) - }, - ) - }) - }) - - describe('Row property reactivity', () => { - const flatRows = table.getRowModel().flatRows - flatRows.forEach((row, index) => { - const rowsPropertyKeys = Object.keys(row) - test.each( - rowsPropertyKeys.map((property) => [ - property, - experimentalReactivity_testShouldBeComputedProperty(row, property), - ]), - )( - `Row ${row.id} (${index}) - property (%s) is computed -> (%s)`, - (name, expected) => { - const tableProperty = row[name as keyof typeof row] - expect(isSignal(tableProperty)).toEqual(expected) - }, - ) - - const cells = row.getAllCells() - cells.forEach((cell, cellIndex) => { - const cellPropertyKeys = Object.keys(cell) - test.each( - cellPropertyKeys.map((property) => [ - property, - experimentalReactivity_testShouldBeComputedProperty(cell, property), - ]), - )( - `Row ${row.id} (${index}) / Cell ${cell.id} - property (%s) is computed -> (%s)`, - (name, expected) => { - const tableProperty = cell[name as keyof typeof cell] - expect(isSignal(tableProperty)).toEqual(expected) - }, - ) - }) - }) - }) -}) From 965be46af7784e8bc098895dd96db055eb0af8a9 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sat, 23 Nov 2024 10:03:40 +0100 Subject: [PATCH 03/19] add angular table helper --- .../angular-table/src/createTableHelper.ts | 62 ++++++++++ .../src/createTableHelperCore.ts | 106 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 packages/angular-table/src/createTableHelper.ts create mode 100644 packages/angular-table/src/createTableHelperCore.ts diff --git a/packages/angular-table/src/createTableHelper.ts b/packages/angular-table/src/createTableHelper.ts new file mode 100644 index 0000000000..a93308848f --- /dev/null +++ b/packages/angular-table/src/createTableHelper.ts @@ -0,0 +1,62 @@ +import { constructTableHelper } from './createTableHelperCore' +import { injectTable } from './injectTable' +import type { + RowData, + Table, + TableFeatures, + TableHelperOptions, + TableHelper_Core, + TableOptions, +} from '@tanstack/table-core' + +export type TableHelper< + TFeatures extends TableFeatures, + TData extends RowData = any, +> = Omit, 'tableCreator'> & { + injectTable: ( + tableOptions: Omit< + TableOptions, + '_features' | '_rowModels' | '_rowModelFns' + >, + ) => Table +} + +export function createTableHelper< + TFeatures extends TableFeatures, + TData extends RowData = any, +>( + tableHelperOptions: TableHelperOptions, +): TableHelper { + const tableHelper = constructTableHelper(injectTable, tableHelperOptions) + return { + ...tableHelper, + injectTable: tableHelper.tableCreator, + } as any +} + +// test + +// type Person = { +// firstName: string +// lastName: string +// age: number +// } + +// const tableHelper = createTableHelper({ +// _features: { rowSelectionFeature: {} }, +// TData: {} as Person, +// }) + +// const columns = [ +// tableHelper.columnHelper.accessor('firstName', { header: 'First Name' }), +// tableHelper.columnHelper.accessor('lastName', { header: 'Last Name' }), +// tableHelper.columnHelper.accessor('age', { header: 'Age' }), +// tableHelper.columnHelper.display({ header: 'Actions', id: 'actions' }), +// ] as Array> + +// const data: Array = [] + +// tableHelper.createTable({ +// columns, +// data, +// }) diff --git a/packages/angular-table/src/createTableHelperCore.ts b/packages/angular-table/src/createTableHelperCore.ts new file mode 100644 index 0000000000..185b44b4f6 --- /dev/null +++ b/packages/angular-table/src/createTableHelperCore.ts @@ -0,0 +1,106 @@ +import { createColumnHelper } from '@tanstack/table-core' +import type { + ColumnHelper, + RowData, + Table, + TableFeatures, + TableOptions, +} from '@tanstack/table-core' +import type { Signal } from '@angular/core' + +/** + * Options for creating a table helper to share common options across multiple tables + * columnsFeature, data, and state are excluded from this type and reserved for only the `useTable`/`createTable` functions + */ +export type TableHelperOptions< + TFeatures extends TableFeatures, + TData extends RowData = any, +> = Omit< + TableOptions>, + 'columns' | 'data' | 'state' +> & { + _features: TFeatures + TData?: TData // provide a cast for the TData type +} + +/** + * Internal type that each adapter package will build off of to create a table helper + */ +export type TableHelper_Core< + TFeatures extends TableFeatures, + TData extends RowData = any, +> = { + columnHelper: ColumnHelper + createColumnHelper: () => ColumnHelper< + TFeatures, + TData + > + features: TFeatures + options: Omit, 'columns' | 'data' | 'state'> + tableCreator: ( + tableOptions: Omit< + TableOptions, + '_features' | '_rowModels' | '_rowModelFns' + >, + ) => Table +} + +/** + * Internal function to create a table helper that each adapter package will use to create their own table helper + */ +export function constructTableHelper< + TFeatures extends TableFeatures, + TData extends RowData = any, +>( + tableCreator: ( + tableOptions: () => TableOptions, + ) => Table & Signal>, + tableHelperOptions: TableHelperOptions, +): TableHelper_Core { + const { TData: _TData, ..._tableHelperOptions } = tableHelperOptions + return { + columnHelper: createColumnHelper(), + createColumnHelper, + features: tableHelperOptions._features, + options: _tableHelperOptions as any, + tableCreator: (tableOptions) => + // @ts-expect-error - TODO: fix this + tableCreator({ ..._tableHelperOptions, ...tableOptions }), + } +} + +// test + +// // eslint-disable-next-line import/first, import/order +// import { constructTable } from '../core/table/constructTable' +// // eslint-disable-next-line import/first, import/order +// import { type ColumnDef } from '../types/ColumnDef' + +// type Person = { +// firstName: string +// lastName: string +// age: number +// } + +// const tableHelper = constructTableHelper(constructTable, { +// _features: { rowSelectionFeature: {} }, +// _rowModels: {}, +// TData: {} as Person, +// }) + +// const columns = [ +// tableHelper.columnHelper.accessor('firstName', { +// header: 'First Name', +// cell: (info) => info.getValue(), +// }), +// tableHelper.columnHelper.accessor('lastName', { header: 'Last Name' }), +// tableHelper.columnHelper.accessor('age', { header: 'Age' }), +// tableHelper.columnHelper.display({ header: 'Actions', id: 'actions' }), +// ] as Array> + +// const data: Array = [] + +// tableHelper.tableCreator({ +// columns, +// data, +// }) From 8d518ddb25cd08c2002275848b1827b08a34be6e Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sat, 23 Nov 2024 10:03:48 +0100 Subject: [PATCH 04/19] wip: fix proxy build --- packages/angular-table/src/proxy.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/angular-table/src/proxy.ts b/packages/angular-table/src/proxy.ts index 86a65434ab..26886f9ed2 100644 --- a/packages/angular-table/src/proxy.ts +++ b/packages/angular-table/src/proxy.ts @@ -19,8 +19,8 @@ export function proxifyTable< apply() { return tableSignal() }, - get(target, property: keyof Table): any { - if (target[property]) { + get(target, property): any { + if (typeof property === 'string' && target[property]) { return target[property] } const table = untracked(tableSignal) @@ -29,6 +29,7 @@ export function proxifyTable< * excluding handlers as they do not retain any reactive value */ if ( + typeof property === 'string' && property.startsWith('get') && !property.endsWith('Handler') && !property.endsWith('Model') @@ -43,11 +44,11 @@ export function proxifyTable< return target[property] } } - // @ts-expect-error return (target[property] = table[property]) }, - has(_, prop: keyof Table) { - return !!untracked(tableSignal)[prop] + has(_, prop) { + const t = untracked(tableSignal) + return !!Reflect.get(tableSignal, prop) }, ownKeys() { return Reflect.ownKeys(untracked(tableSignal)) From e6151c68f9d03ec6580064954d73aeb9916b3062 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sat, 23 Nov 2024 10:04:08 +0100 Subject: [PATCH 05/19] eslint --- packages/angular-table/src/index.ts | 1 + packages/angular-table/tests/flex-render.test.ts | 6 ++++-- packages/angular-table/tests/lazy-init.test.ts | 5 +++-- packages/angular-table/tests/test-utils.ts | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts index b0b52b8882..f9bd9c806d 100644 --- a/packages/angular-table/src/index.ts +++ b/packages/angular-table/src/index.ts @@ -4,3 +4,4 @@ export * from './flex-render' export * from './proxy' export * from './lazy-signal-initializer' export * from './injectTable' +export * from './createTableHelper' diff --git a/packages/angular-table/tests/flex-render.test.ts b/packages/angular-table/tests/flex-render.test.ts index 02e3c33003..1b5a3a1203 100644 --- a/packages/angular-table/tests/flex-render.test.ts +++ b/packages/angular-table/tests/flex-render.test.ts @@ -1,5 +1,5 @@ -import { Component, ViewChild, input, type TemplateRef } from '@angular/core' -import { TestBed, type ComponentFixture } from '@angular/core/testing' +import { Component, ViewChild, input } from '@angular/core' +import { TestBed } from '@angular/core/testing' import { createColumnHelper } from '@tanstack/table-core' import { describe, expect, test } from 'vitest' import { @@ -8,6 +8,8 @@ import { injectFlexRenderContext, } from '../src/flex-render' import { setFixtureSignalInput, setFixtureSignalInputs } from './test-utils' +import type {ComponentFixture} from '@angular/core/testing'; +import type {TemplateRef} from '@angular/core'; interface Data { id: string diff --git a/packages/angular-table/tests/lazy-init.test.ts b/packages/angular-table/tests/lazy-init.test.ts index fa3a43f553..02291d640c 100644 --- a/packages/angular-table/tests/lazy-init.test.ts +++ b/packages/angular-table/tests/lazy-init.test.ts @@ -2,15 +2,16 @@ import { describe, expect, test, vi } from 'vitest' import { ChangeDetectionStrategy, Component, - type WritableSignal, + computed, effect, input, - signal, + signal } from '@angular/core' import { TestBed } from '@angular/core/testing' import { lazyInit } from '../src/lazy-signal-initializer' import { flushQueue, setFixtureSignalInputs } from './test-utils' +import type {WritableSignal} from '@angular/core'; describe('lazyInit', () => { test('should init lazily in next tick when not accessing manually', async () => { diff --git a/packages/angular-table/tests/test-utils.ts b/packages/angular-table/tests/test-utils.ts index 28b788e0bf..bcbf012e43 100644 --- a/packages/angular-table/tests/test-utils.ts +++ b/packages/angular-table/tests/test-utils.ts @@ -1,5 +1,5 @@ -import type { InputSignal } from '@angular/core' import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals' +import type { InputSignal } from '@angular/core' import type { ComponentFixture } from '@angular/core/testing' type ToSignalInputUpdatableMap = { From 517f779c5c6e82341a2ec86d9d13d099aa4ce4ab Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sat, 23 Nov 2024 10:04:35 +0100 Subject: [PATCH 06/19] fix basic example --- examples/angular/basic/src/app/app.component.html | 2 +- examples/angular/basic/src/app/app.component.ts | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/angular/basic/src/app/app.component.html b/examples/angular/basic/src/app/app.component.html index 68c81953a6..8f105b6da5 100644 --- a/examples/angular/basic/src/app/app.component.html +++ b/examples/angular/basic/src/app/app.component.html @@ -24,7 +24,7 @@ @for (row of table.getRowModel().rows; track row.id) { - @for (cell of row.getVisibleCells(); track cell.id) { + @for (cell of row.getAllCells(); track cell.id) { @@ -35,7 +36,7 @@ @for (row of table.getRowModel().rows; track row.id) { - @for (cell of row.getVisibleCells(); track cell.id) { + @for (cell of row.getAllCells(); track cell.id) { ([]) + readonly data = signal(makeData(10000)) + readonly grouping = signal([]) - stringifiedGrouping = computed(() => JSON.stringify(this.grouping(), null, 2)) - - tableOptions = computed(() => - tableOptions({ - data: this.data(), - columns: columns, - state: { - grouping: this.grouping(), - }, - onGroupingChange: (updaterOrValue: Updater) => { - const groupingState = - typeof updaterOrValue === 'function' - ? updaterOrValue([...this.grouping()]) - : updaterOrValue - this.grouping.set(groupingState) - }, - getExpandedRowModel: createExpandedRowModel(), - getGroupedRowModel: createGroupedRowModel(), - getCoreRowModel: createCoreRowModel(), - getPaginatedRowModel: createPaginatedRowModel(), - getFilteredRowModel: createFilteredRowModel(), - debugTable: true, - }), + readonly stringifiedGrouping = computed(() => + JSON.stringify(this.grouping(), null, 2), ) - table = injectTable(this.tableOptions) + readonly table = injectTable(() => ({ + data: this.data(), + columns: columns, + enableExperimentalReactivity: true, + initialState: { + pagination: { pageSize: 20, pageIndex: 0 }, + }, + state: { + grouping: this.grouping(), + }, + _features: { + columnGroupingFeature, + rowPaginationFeature, + columnFilteringFeature, + rowExpandingFeature, + }, + _rowModels: { + // @ts-expect-error Fix type + groupedRowModel: createGroupedRowModel(), + // @ts-expect-error Fix type + expandedRowModel: createExpandedRowModel(), + // @ts-expect-error Fix type + paginatedRowModel: createPaginatedRowModel(), + // @ts-expect-error Fix type + filteredRowModel: createFilteredRowModel(), + }, + onGroupingChange: (updaterOrValue: Updater) => { + const groupingState = + typeof updaterOrValue === 'function' + ? updaterOrValue([...this.grouping()]) + : updaterOrValue + this.grouping.set(groupingState) + }, + })) onPageInputChange(event: any): void { const page = event.target.value ? Number(event.target.value) - 1 : 0 diff --git a/examples/angular/row-selection-signal/src/app/app.component.html b/examples/angular/row-selection-signal/src/app/app.component.html index c2dfb725f7..f50c2d0052 100644 --- a/examples/angular/row-selection-signal/src/app/app.component.html +++ b/examples/angular/row-selection-signal/src/app/app.component.html @@ -20,8 +20,9 @@ @if (header.column.getCanFilter()) {
+
diff --git a/examples/angular/row-selection-signal/src/app/app.component.ts b/examples/angular/row-selection-signal/src/app/app.component.ts index 908f667cdf..599724e905 100644 --- a/examples/angular/row-selection-signal/src/app/app.component.ts +++ b/examples/angular/row-selection-signal/src/app/app.component.ts @@ -7,10 +7,13 @@ import { } from '@angular/core' import { FlexRenderDirective, - createCoreRowModel, + columnFilteringFeature, + columnVisibilityFeature, createFilteredRowModel, createPaginatedRowModel, injectTable, + rowPaginationFeature, + rowSelectionFeature, } from '@tanstack/angular-table' import { FilterComponent } from './filter' import { makeData } from './makeData' @@ -97,6 +100,19 @@ export class AppComponent { table = injectTable(() => ({ data: this.data(), + enableExperimentalReactivity: true, + _features: { + rowSelectionFeature, + rowPaginationFeature, + columnFilteringFeature, + columnVisibilityFeature, + }, + _rowModels: { + // @ts-expect-error Fix type + filteredRowModel: createFilteredRowModel(), + // @ts-expect-error Fix type + paginatedRowModel: createPaginatedRowModel(), + }, columns: this.columns, state: { rowSelection: this.rowSelection(), @@ -110,9 +126,6 @@ export class AppComponent { : updaterOrValue, ) }, - getCoreRowModel: createCoreRowModel(), - getFilteredRowModel: createFilteredRowModel(), - getPaginatedRowModel: createPaginatedRowModel(), debugTable: true, })) diff --git a/examples/angular/row-selection-signal/src/app/filter.ts b/examples/angular/row-selection-signal/src/app/filter.ts index bd3319c687..c21fcc3807 100644 --- a/examples/angular/row-selection-signal/src/app/filter.ts +++ b/examples/angular/row-selection-signal/src/app/filter.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common' import { Component, input } from '@angular/core' import type { OnInit } from '@angular/core' -import type { Column, Table } from '@tanstack/angular-table' +import type { Column, RowData, Table } from '@tanstack/angular-table' @Component({ selector: 'app-table-filter', @@ -39,10 +39,10 @@ import type { Column, Table } from '@tanstack/angular-table' standalone: true, imports: [CommonModule], }) -export class FilterComponent implements OnInit { +export class FilterComponent implements OnInit { column = input.required>() - table = input.required>() + table = input.required>() columnType!: string diff --git a/examples/angular/row-selection-signal/src/app/selection-column.component.ts b/examples/angular/row-selection-signal/src/app/selection-column.component.ts index 50d057b84c..2a6cb75670 100644 --- a/examples/angular/row-selection-signal/src/app/selection-column.component.ts +++ b/examples/angular/row-selection-signal/src/app/selection-column.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core' -import type { Row, Table } from '@tanstack/angular-table' +import type { Row, RowData, Table } from '@tanstack/angular-table' @Component({ template: ` @@ -16,7 +16,7 @@ import type { Row, Table } from '@tanstack/angular-table' standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TableHeadSelectionComponent { +export class TableHeadSelectionComponent { // Your component should also reflect the fields you use as props in flexRenderer directive. // Define the fields as input you want to use in your component // ie. In this case, you are passing HeaderContext object as props in flexRenderer directive. @@ -25,7 +25,7 @@ export class TableHeadSelectionComponent { // column = input.required>() // header = input.required>() - table = input.required>() + table = input.required>() } @Component({ @@ -42,6 +42,6 @@ export class TableHeadSelectionComponent { standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TableRowSelectionComponent { - row = input.required>() +export class TableRowSelectionComponent { + row = input.required>() } diff --git a/examples/angular/row-selection/src/app/app.component.ts b/examples/angular/row-selection/src/app/app.component.ts index 3b4b5a64cc..08f88e1a3a 100644 --- a/examples/angular/row-selection/src/app/app.component.ts +++ b/examples/angular/row-selection/src/app/app.component.ts @@ -124,6 +124,7 @@ export class AppComponent { state: { rowSelection: this.rowSelection(), }, + enableExperimentalReactivity: true, enableRowSelection: true, // enable row selection for all rows // enableRowSelection: row => row.original.age > 18, // or enable row selection conditionally per row onRowSelectionChange: (updaterOrValue) => { diff --git a/examples/angular/signal-input/src/app/person-table/person-table.component.ts b/examples/angular/signal-input/src/app/person-table/person-table.component.ts index a04aaeb5f4..3431cd045f 100644 --- a/examples/angular/signal-input/src/app/person-table/person-table.component.ts +++ b/examples/angular/signal-input/src/app/person-table/person-table.component.ts @@ -1,16 +1,29 @@ import { ChangeDetectionStrategy, Component, input, model } from '@angular/core' import { FlexRenderDirective, + columnVisibilityFeature, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createPaginatedRowModel, + createTableHelper, injectTable, + rowPaginationFeature, } from '@tanstack/angular-table' import type { ColumnDef, PaginationState } from '@tanstack/angular-table' import type { Person } from '../makeData' +const tableHelper = createTableHelper({ + _features: { + rowPaginationFeature, + columnVisibilityFeature, + }, + _rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, +}) + @Component({ selector: 'app-person-table', templateUrl: 'person-table.component.html', @@ -37,7 +50,7 @@ export class PersonTableComponent { }, ] - table = injectTable(() => { + readonly table = tableHelper.injectTable(() => { return { data: this.data(), columns: this.columns, @@ -49,11 +62,6 @@ export class PersonTableComponent { ? this.pagination.update(updaterOrValue) : this.pagination.set(updaterOrValue) }, - getExpandedRowModel: createExpandedRowModel(), - getGroupedRowModel: createGroupedRowModel(), - getCoreRowModel: createCoreRowModel(), - getPaginatedRowModel: createPaginatedRowModel(), - getFilteredRowModel: createFilteredRowModel(), debugTable: true, } }) diff --git a/packages/angular-table/package.json b/packages/angular-table/package.json index 161699803d..1417755551 100644 --- a/packages/angular-table/package.json +++ b/packages/angular-table/package.json @@ -60,6 +60,7 @@ "@angular/core": "^19.0.0", "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", + "@vitest/ui": "^2.1.5", "ng-packagr": "^19.0.0" }, "peerDependencies": { diff --git a/packages/angular-table/src/flex-render.ts b/packages/angular-table/src/flex-render.ts index 272b5eb541..ecddf13462 100644 --- a/packages/angular-table/src/flex-render.ts +++ b/packages/angular-table/src/flex-render.ts @@ -14,7 +14,12 @@ import { inject, isSignal, } from '@angular/core' -import type { EffectRef, OnChanges, SimpleChanges } from '@angular/core' +import type { + DoCheck, + EffectRef, + OnChanges, + SimpleChanges, +} from '@angular/core' import type { Table } from '@tanstack/table-core' export type FlexRenderContent> = @@ -31,7 +36,7 @@ export type FlexRenderContent> = standalone: true, }) export class FlexRenderDirective> - implements OnChanges + implements OnChanges, DoCheck { @Input({ required: true, alias: 'flexRender' }) content: @@ -47,6 +52,10 @@ export class FlexRenderDirective> @Input({ required: false, alias: 'flexRenderInjector' }) injector: Injector = inject(Injector) + ref?: ComponentRef | EmbeddedViewRef | null = null + + experimentalReactivity = false + constructor( @Inject(ViewContainerRef) private readonly viewContainerRef: ViewContainerRef, @@ -54,12 +63,26 @@ export class FlexRenderDirective> private readonly templateRef: TemplateRef, ) {} - ref?: ComponentRef | EmbeddedViewRef | null = null + ngDoCheck(): void { + if ( + this.experimentalReactivity === false && + this.ref instanceof ComponentRef + ) { + this.ref.injector.get(ChangeDetectorRef).markForCheck() + } + } ngOnChanges(changes: SimpleChanges) { if (!changes['content']) { return } + + if ('table' in this.props) { + this.experimentalReactivity = + (this.props.table as Partial>).options + ?.enableExperimentalReactivity ?? false + } + this.render() } diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts index f9bd9c806d..d84d942fa7 100644 --- a/packages/angular-table/src/index.ts +++ b/packages/angular-table/src/index.ts @@ -5,3 +5,4 @@ export * from './proxy' export * from './lazy-signal-initializer' export * from './injectTable' export * from './createTableHelper' +export * from './reactivity' diff --git a/packages/angular-table/src/injectTable.ts b/packages/angular-table/src/injectTable.ts index 3bc70ff5d4..a334bcd0f8 100644 --- a/packages/angular-table/src/injectTable.ts +++ b/packages/angular-table/src/injectTable.ts @@ -78,7 +78,7 @@ export function injectTable< }, ) - table._setRootNotifier?.(tableSignal as unknown as Signal>) + table._setRootNotifier?.(tableSignal as any) // proxify Table instance to provide ability for consumer to listen to any table state changes return proxifyTable(tableSignal) diff --git a/packages/angular-table/src/proxy.ts b/packages/angular-table/src/proxy.ts index 16f06541ce..fc7689b8f2 100644 --- a/packages/angular-table/src/proxy.ts +++ b/packages/angular-table/src/proxy.ts @@ -22,7 +22,11 @@ export function proxifyTable< return new Proxy(internalState, { apply() { - return tableSignal() + const signal = untracked(tableSignal) + const impl = signal.options.enableExperimentalReactivity + ? proxyTargetImplementation.experimental + : proxyTargetImplementation.default + return impl.apply() }, get(target, property, receiver): any { const signal = untracked(tableSignal) @@ -77,13 +81,14 @@ export function toComputed< void signal() return fn() }, - { equal: () => false, debugName }, + { debugName }, ) } const computedCache: Record> = {} - return (...argsArray: Array) => { + return (arg0: any, ...otherArgs: Array) => { + const argsArray = [arg0, ...otherArgs] const serializedArgs = serializeArgs(...argsArray) if (computedCache.hasOwnProperty(serializedArgs)) { return computedCache[serializedArgs]?.() @@ -115,18 +120,42 @@ function getDefaultProxyHandler< return tableSignal() }, get(target, property, receiver): any { - if (typeof property === 'string' && Reflect.has(target, property)) { + if (Reflect.has(target, property)) { return Reflect.get(target, property) } const table = untracked(tableSignal) - return (target[property] = table[property]) + /** + * Attempt to convert all accessors into computed ones, + * excluding handlers as they do not retain any reactive value + */ + if ( + typeof property === 'string' && + property.startsWith('get') && + !property.endsWith('Handler') && + !property.endsWith('Model') + ) { + const maybeFn = table[property as keyof typeof target] as + | Function + | never + if (typeof maybeFn === 'function') { + Object.defineProperty(target, property, { + value: toComputed(tableSignal, maybeFn), + configurable: true, + enumerable: true, + }) + return target[property as keyof typeof target] + } + } + return ((target as any)[property] = (table as any)[property]) }, has(_, prop) { - const t = untracked(tableSignal) - return !!Reflect.get(tableSignal, prop) + return ( + Reflect.has(untracked(tableSignal), prop) || + Reflect.has(tableSignal, prop) + ) }, ownKeys() { - return Reflect.ownKeys(untracked(tableSignal)) + return [...Reflect.ownKeys(untracked(tableSignal))] }, getOwnPropertyDescriptor() { return { @@ -146,15 +175,17 @@ function getExperimentalProxyHandler< return tableSignal() }, get(target, property, receiver): any { - if (typeof property === 'string' && Reflect.has(target, property)) { + if (Reflect.has(target, property)) { return Reflect.get(target, property) } const table = untracked(tableSignal) - return (target[property] = table[property]) + return ((target as any)[property] = (table as any)[property]) }, - has(_, prop) { - const t = untracked(tableSignal) - return !!Reflect.get(tableSignal, prop) + has(_, property) { + return ( + Reflect.has(untracked(tableSignal), property) || + Reflect.has(tableSignal, property) + ) }, ownKeys() { return Reflect.ownKeys(untracked(tableSignal)) diff --git a/packages/angular-table/src/reactivity.ts b/packages/angular-table/src/reactivity.ts new file mode 100644 index 0000000000..d63ad89c78 --- /dev/null +++ b/packages/angular-table/src/reactivity.ts @@ -0,0 +1,115 @@ +import { computed, signal } from '@angular/core' +import { toComputed } from './proxy' +import type { Signal } from '@angular/core' +import type { Table, TableFeature } from '@tanstack/table-core' + +declare module '@tanstack/table-core' { + interface TableOptions_Plugins { + enableExperimentalReactivity?: boolean + } + + interface Table_Plugins { + _rootNotifier?: Signal> + _setRootNotifier?: (signal: Signal>) => void + } +} + +export const reactivityFeature: TableFeature = { + getDefaultTableOptions(table) { + return { enableExperimentalReactivity: false } + }, + constructTableAPIs: (table) => { + if (!table.options.enableExperimentalReactivity) { + return + } + const rootNotifier = signal | null>(null) + + table._rootNotifier = computed(() => rootNotifier()?.(), { + equal: () => false, + }) as any + + table._setRootNotifier = (notifier) => { + rootNotifier.set(notifier) + } + + setReactiveProps(table._rootNotifier!, table, { + skipProperty: skipBaseProperties, + }) + }, + + constructCellAPIs(cell) { + if ( + !cell.table.options.enableExperimentalReactivity || + !cell.table._rootNotifier + ) { + return + } + setReactiveProps(cell.table._rootNotifier, cell, { + skipProperty: skipBaseProperties, + }) + }, + + constructColumnAPIs(column) { + if ( + !column.table.options.enableExperimentalReactivity || + !column.table._rootNotifier + ) { + return + } + setReactiveProps(column.table._rootNotifier, column, { + skipProperty: skipBaseProperties, + }) + }, + + constructHeaderAPIs(header) { + if ( + !header.table.options.enableExperimentalReactivity || + !header.table._rootNotifier + ) { + return + } + setReactiveProps(header.table._rootNotifier, header, { + skipProperty: skipBaseProperties, + }) + }, + + constructRowAPIs(row) { + if ( + !row.table.options.enableExperimentalReactivity || + !row.table._rootNotifier + ) { + return + } + setReactiveProps(row.table._rootNotifier, row, { + skipProperty: skipBaseProperties, + }) + }, +} + +function skipBaseProperties(property: string): boolean { + return property.endsWith('Handler') || !property.startsWith('get') +} + +export function setReactiveProps( + notifier: Signal>, + obj: { [key: string]: any }, + options: { + skipProperty: (property: string) => boolean + }, +) { + const { skipProperty } = options + for (const property in obj) { + const value = obj[property] + if (typeof value !== 'function') { + continue + } + if (skipProperty(property)) { + continue + } + Object.defineProperty(obj, property, { + enumerable: true, + configurable: false, + value: toComputed(notifier, value, property), + }) + } +} diff --git a/packages/angular-table/tests/createAngularTable.test.ts b/packages/angular-table/tests/createAngularTable.test.ts deleted file mode 100644 index 1ab38422df..0000000000 --- a/packages/angular-table/tests/createAngularTable.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { Component, input, isSignal, signal, untracked } from '@angular/core' -import { TestBed } from '@angular/core/testing' -import { createCoreRowModel, injectTable } from '../src/injectTable' -import { setSignalInputs } from './test-utils' -import type { ColumnDef, Table } from '../src/injectTable' - -describe('injectTable', () => { - test('should render with required signal inputs', () => { - @Component({ - selector: 'app-fake', - template: ``, - standalone: true, - }) - class FakeComponent { - data = input.required>() - - table = injectTable(() => ({ - data: this.data(), - columns: [], - getCoreRowModel: createCoreRowModel(), - })) - } - - const fixture = TestBed.createComponent(FakeComponent) - setSignalInputs(fixture.componentInstance, { - data: [], - }) - - fixture.detectChanges() - }) - - describe('Proxy table', () => { - type Data = { id: string; title: string } - const data = signal>([{ id: '1', title: 'Title' }]) - const columns: Array> = [ - { id: 'id', header: 'Id', cell: (context) => context.getValue() }, - { id: 'title', header: 'Title', cell: (context) => context.getValue() }, - ] - const table = injectTable(() => ({ - data: data(), - columns: columns, - getCoreRowModel: createCoreRowModel(), - getRowId: (row) => row.id, - })) - const tablePropertyKeys = Object.keys(table()) - - test('table must be a signal', () => { - expect(isSignal(table)).toEqual(true) - }) - - test('supports "in" operator', () => { - expect('getCoreRowModel' in table).toBe(true) - expect('options' in table).toBe(true) - expect('notFound' in table).toBe(false) - }) - - test('supports "Object.keys"', () => { - const keys = Object.keys(table()) - expect(Object.keys(table)).toEqual(keys) - }) - - test.each( - tablePropertyKeys.map((property) => [ - property, - testShouldBeComputedProperty(untracked(table), property), - ]), - )('property (%s) is computed -> (%s)', (name, expected) => { - const tableProperty = table[name as keyof typeof table] - expect(isSignal(tableProperty)).toEqual(expected) - }) - }) -}) - -const testShouldBeComputedProperty = ( - table: Table, - propertyName: string, -) => { - if (propertyName.endsWith('Handler') || propertyName.endsWith('Model')) { - return false - } - - if (propertyName.startsWith('get')) { - // Only properties with no arguments are computed - const fn = table[propertyName as keyof Table] - // Cannot test if is lazy computed since we return the unwrapped value - return fn instanceof Function && fn.length === 0 - } - - return false -} diff --git a/packages/angular-table/tests/createTableHelper.test-d.ts b/packages/angular-table/tests/createTableHelper.test-d.ts new file mode 100644 index 0000000000..e8a22c9555 --- /dev/null +++ b/packages/angular-table/tests/createTableHelper.test-d.ts @@ -0,0 +1,63 @@ +import { expectTypeOf, test } from 'vitest' +import { + createPaginatedRowModel, + createTableHelper, + stockFeatures, +} from '../src' +import type { ColumnDef, StockTableFeatures, Table, TableHelper } from '../src' + +test('infer data type from TData', () => { + type TestDataType = { firstName: string; lastName: string; age: number } + + const tableHelper = createTableHelper({ + _features: stockFeatures, + _rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + TData: {} as TestDataType, + }) + + expectTypeOf().toEqualTypeOf< + TableHelper, TestDataType> + >() + + expectTypeOf<(typeof tableHelper)['features']>().toEqualTypeOf< + Required + >() + + const columns = [ + tableHelper.columnHelper.accessor('firstName', { header: 'First Name' }), + tableHelper.columnHelper.accessor('lastName', { header: 'Last Name' }), + tableHelper.columnHelper.accessor('age', { header: 'Age' }), + tableHelper.columnHelper.display({ header: 'Actions', id: 'actions' }), + ] as const + + expectTypeOf().toMatchTypeOf< + ReadonlyArray> + >() +}) + +test('infer data type given by injectTable', () => { + type TestDataType = { firstName: string; lastName: string } + + const tableHelper = createTableHelper({ + _features: stockFeatures, + _rowModels: { + paginatedRowModel: createPaginatedRowModel(), + }, + }) + + expectTypeOf().toEqualTypeOf< + TableHelper, any> + >() + + const injectTable = tableHelper.injectTable + const table = injectTable(() => ({ + data: [] as Array, + columns: [], + })) + + expectTypeOf().toEqualTypeOf< + Table, TestDataType> + >() +}) diff --git a/packages/angular-table/tests/flex-render.test.ts b/packages/angular-table/tests/flex-render.test.ts index 1b5a3a1203..c88bbc1e16 100644 --- a/packages/angular-table/tests/flex-render.test.ts +++ b/packages/angular-table/tests/flex-render.test.ts @@ -1,5 +1,5 @@ -import { Component, ViewChild, input } from '@angular/core' -import { TestBed } from '@angular/core/testing' +import { Component, ViewChild, input } from '@angular/core' +import { TestBed } from '@angular/core/testing' import { createColumnHelper } from '@tanstack/table-core' import { describe, expect, test } from 'vitest' import { @@ -8,8 +8,8 @@ import { injectFlexRenderContext, } from '../src/flex-render' import { setFixtureSignalInput, setFixtureSignalInputs } from './test-utils' -import type {ComponentFixture} from '@angular/core/testing'; -import type {TemplateRef} from '@angular/core'; +import type { ComponentFixture } from '@angular/core/testing' +import type { TemplateRef } from '@angular/core' interface Data { id: string @@ -20,7 +20,7 @@ interface Data { } describe('FlexRenderDirective', () => { - const helper = createColumnHelper() + const helper = createColumnHelper<{}, Data>() test('should render primitives', async () => { const fixture = TestBed.createComponent(TestRenderComponent) diff --git a/packages/angular-table/tests/injectTable.test.ts b/packages/angular-table/tests/injectTable.test.ts new file mode 100644 index 0000000000..61db8e8ed8 --- /dev/null +++ b/packages/angular-table/tests/injectTable.test.ts @@ -0,0 +1,220 @@ +import { describe, expect, test } from 'vitest' +import { Component, input, isSignal, signal, untracked } from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { ColumnDef, stockFeatures } from '@tanstack/table-core' +import { injectTable } from '../src/injectTable' +import { + experimentalReactivity_testShouldBeComputedProperty, + setSignalInputs, + testShouldBeComputedProperty, +} from './test-utils' + +describe('injectTable', () => { + test('should render with required signal inputs', () => { + @Component({ + selector: 'app-fake', + template: ``, + standalone: true, + }) + class FakeComponent { + data = input.required>() + + table = injectTable(() => ({ + data: this.data(), + _features: stockFeatures, + columns: [], + })) + } + + const fixture = TestBed.createComponent(FakeComponent) + setSignalInputs(fixture.componentInstance, { + data: [], + }) + + fixture.detectChanges() + }) + + describe('Proxy table', () => { + type Data = { id: string; title: string } + const data = signal>([{ id: '1', title: 'Title' }]) + const columns: Array> = [ + { id: 'id', header: 'Id', cell: (context) => context.getValue() }, + { id: 'title', header: 'Title', cell: (context) => context.getValue() }, + ] + const table = injectTable(() => ({ + data: data(), + _features: stockFeatures, + columns: columns, + getRowId: (row) => row.id, + })) + const tablePropertyKeys = Object.keys(table()) + + test('table must be a signal', () => { + expect(isSignal(table)).toEqual(true) + }) + + test('supports "in" operator', () => { + expect('_features' in table).toBe(true) + expect('options' in table).toBe(true) + expect('notFound' in table).toBe(false) + }) + + test('supports "Object.keys"', () => { + const keys = Object.keys(table()) + expect(Object.keys(table)).toEqual(keys) + }) + + test.each( + tablePropertyKeys.map((property) => [ + property, + testShouldBeComputedProperty(untracked(table), property), + ]), + )('property (%s) is computed -> (%s)', (name, expected) => { + const tableProperty = table[name as keyof typeof table] + expect(isSignal(tableProperty)).toEqual(expected) + }) + }) +}) + +describe('injectTable - Experimental reactivity', () => { + type Data = { id: string; title: string } + const data = signal>([{ id: '1', title: 'Title' }]) + const columns: Array> = [ + { id: 'id', header: 'Id', cell: (context) => context.getValue() }, + { id: 'title', header: 'Title', cell: (context) => context.getValue() }, + ] + const table = injectTable(() => ({ + data: data(), + _features: { ...stockFeatures }, + columns: columns, + getRowId: (row) => row.id, + enableExperimentalReactivity: true, + })) + const tablePropertyKeys = Object.keys(table) + + describe('Proxy', () => { + test('table must be a signal', () => { + expect(isSignal(table)).toEqual(true) + }) + + test('supports "in" operator', () => { + expect('_features' in table).toBe(true) + expect('options' in table).toBe(true) + expect('notFound' in table).toBe(false) + }) + + test('supports "Object.keys"', () => { + const keys = Object.keys(table) + expect(Object.keys(table)).toEqual(keys) + }) + }) + + describe('Table property reactivity', () => { + test.each( + tablePropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty(table, property), + ]), + )('property (%s) is computed -> (%s)', (name, expected) => { + const tableProperty = table[name as keyof typeof table] + expect(isSignal(tableProperty)).toEqual(expected) + }) + }) + + describe('Header property reactivity', () => { + const headers = table.getHeaderGroups() + headers.forEach((headerGroup, index) => { + const headerPropertyKeys = Object.keys(headerGroup) + test.each( + headerPropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty( + headerGroup, + property, + ), + ]), + )( + `HeaderGroup ${headerGroup.id} (${index}) - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = headerGroup[name as keyof typeof headerGroup] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + + const headers = headerGroup.headers + headers.forEach((header, cellIndex) => { + const headerPropertyKeys = Object.keys(header) + test.each( + headerPropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty( + header, + property, + ), + ]), + )( + `HeaderGroup ${headerGroup.id} (${index}) / Header ${header.id} - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = header[name as keyof typeof header] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + }) + }) + }) + + describe('Column property reactivity', () => { + const columns = table.getAllColumns() + columns.forEach((column, index) => { + const columnPropertyKeys = Object.keys(column) + test.each( + columnPropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty(column, property), + ]), + )( + `Column ${column.id} (${index}) - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = column[name as keyof typeof column] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + }) + }) + + describe('Row property reactivity', () => { + const flatRows = table.getRowModel().flatRows + flatRows.forEach((row, index) => { + const rowsPropertyKeys = Object.keys(row) + test.each( + rowsPropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty(row, property), + ]), + )( + `Row ${row.id} (${index}) - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = row[name as keyof typeof row] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + + const cells = row.getAllCells() + cells.forEach((cell, cellIndex) => { + const cellPropertyKeys = Object.keys(cell) + test.each( + cellPropertyKeys.map((property) => [ + property, + experimentalReactivity_testShouldBeComputedProperty(cell, property), + ]), + )( + `Row ${row.id} (${index}) / Cell ${cell.id} - property (%s) is computed -> (%s)`, + (name, expected) => { + const tableProperty = cell[name as keyof typeof cell] + expect(isSignal(tableProperty)).toEqual(expected) + }, + ) + }) + }) + }) +}) diff --git a/packages/angular-table/tests/lazy-init.test.ts b/packages/angular-table/tests/lazy-init.test.ts index 02291d640c..ba9854fb2b 100644 --- a/packages/angular-table/tests/lazy-init.test.ts +++ b/packages/angular-table/tests/lazy-init.test.ts @@ -2,16 +2,15 @@ import { describe, expect, test, vi } from 'vitest' import { ChangeDetectionStrategy, Component, - computed, effect, input, - signal + signal, } from '@angular/core' import { TestBed } from '@angular/core/testing' import { lazyInit } from '../src/lazy-signal-initializer' import { flushQueue, setFixtureSignalInputs } from './test-utils' -import type {WritableSignal} from '@angular/core'; +import type { WritableSignal } from '@angular/core' describe('lazyInit', () => { test('should init lazily in next tick when not accessing manually', async () => { @@ -52,7 +51,7 @@ describe('lazyInit', () => { test('should init lazily and only once', async () => { const initCallFn = vi.fn() - const registerDataValue = vi.fn<[number]>() + const registerDataValue = vi.fn<(arg0: number) => void>() let value!: { data: WritableSignal } const outerSignal = signal(0) @@ -85,7 +84,7 @@ describe('lazyInit', () => { await flushQueue() expect(initCallFn).toHaveBeenCalledTimes(1) - expect(registerDataValue).toHaveBeenCalledTimes(2) + expect(registerDataValue).toHaveBeenCalledTimes(1) }) test('should support required signal input', async () => { diff --git a/packages/angular-table/tests/test-utils.ts b/packages/angular-table/tests/test-utils.ts index bcbf012e43..443f091a06 100644 --- a/packages/angular-table/tests/test-utils.ts +++ b/packages/angular-table/tests/test-utils.ts @@ -1,6 +1,7 @@ import { SIGNAL, signalSetFn } from '@angular/core/primitives/signals' import type { InputSignal } from '@angular/core' import type { ComponentFixture } from '@angular/core/testing' +import { Table } from '@tanstack/table-core' type ToSignalInputUpdatableMap = { [K in keyof T as T[K] extends InputSignal @@ -60,3 +61,42 @@ function componentHasSignalInputProperty( export async function flushQueue() { await new Promise(setImmediate) } + +export const experimentalReactivity_testShouldBeComputedProperty = ( + testObj: any, + propertyName: string, +) => { + if (propertyName.startsWith('_rootNotifier')) { + return true + } + if (propertyName.endsWith('Handler')) { + return false + } + + if (propertyName.startsWith('get')) { + // Only properties with no arguments are computed + const fn = testObj[propertyName] + // Cannot test if is lazy computed since we return the unwrapped value + return fn instanceof Function && fn.length === 0 + } + + return false +} + +export const testShouldBeComputedProperty = ( + testObj: any, + propertyName: string, +) => { + if (propertyName.endsWith('Handler') || propertyName.endsWith('Model')) { + return false + } + + if (propertyName.startsWith('get')) { + // Only properties with no arguments are computed + const fn = testObj[propertyName] + // Cannot test if is lazy computed since we return the unwrapped value + return fn instanceof Function && fn.length === 0 + } + + return false +} diff --git a/packages/angular-table/vite.config.ts b/packages/angular-table/vite.config.ts index 523b22e583..1ba318b695 100644 --- a/packages/angular-table/vite.config.ts +++ b/packages/angular-table/vite.config.ts @@ -9,5 +9,9 @@ export default defineConfig({ environment: 'jsdom', setupFiles: ['./tests/test-setup.ts'], globals: true, + typecheck: { + enabled: true, + tsconfig: './tsconfig.json', + }, }, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7051a5c73d..f5baad1c9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,7 +70,7 @@ importers: version: 5.4.11(@types/node@22.9.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0) vitest: specifier: ^2.1.5 - version: 2.1.5(@types/node@22.9.1)(jsdom@25.0.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0) + version: 2.1.5(@types/node@22.9.1)(@vitest/ui@2.1.5)(jsdom@25.0.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0) examples/angular/basic: dependencies: @@ -2955,6 +2955,9 @@ importers: '@angular/platform-browser-dynamic': specifier: ^19.0.0 version: 19.0.0(@angular/common@19.0.0(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/compiler@19.0.0(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0))(@angular/platform-browser@19.0.0(@angular/animations@19.0.0(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0)))(@angular/common@19.0.0(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0))) + '@vitest/ui': + specifier: ^2.1.5 + version: 2.1.5(vitest@2.1.5) ng-packagr: specifier: ^19.0.0 version: 19.0.0(@angular/compiler-cli@19.0.0(@angular/compiler@19.0.0(@angular/core@19.0.0(rxjs@7.8.1)(zone.js@0.15.0)))(typescript@5.6.3))(tslib@2.8.1)(typescript@5.6.3) @@ -5102,6 +5105,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@polka/url@1.0.0-next.28': + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -5812,6 +5818,11 @@ packages: '@vitest/spy@2.1.5': resolution: {integrity: sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==} + '@vitest/ui@2.1.5': + resolution: {integrity: sha512-ERgKkDMTfngrZip6VG5h8L9B5D0AH/4+bga4yR1UzGH7c2cxv3LWogw2Dvuwr9cP3/iKDHYys7kIFLDKpxORTg==} + peerDependencies: + vitest: 2.1.5 + '@vitest/utils@2.1.5': resolution: {integrity: sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==} @@ -7182,6 +7193,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -9394,6 +9408,10 @@ packages: simple-git@3.27.0: resolution: {integrity: sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==} + sirv@3.0.0: + resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + engines: {node: '>=18'} + size-limit@11.1.6: resolution: {integrity: sha512-S5ux2IB8rU26xwVgMskmknGMFkieaIAqDLuwgKiypk6oa4lFsie8yFPrzRFV+yrLDY2GddjXuCaVk5PveVOHiQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -9761,6 +9779,10 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tough-cookie@5.0.0: resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} engines: {node: '>=16'} @@ -12697,6 +12719,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@polka/url@1.0.0-next.28': {} + '@popperjs/core@2.11.8': {} '@rollup/plugin-json@6.1.0(rollup@4.26.0)': @@ -13516,6 +13540,17 @@ snapshots: dependencies: tinyspy: 3.0.2 + '@vitest/ui@2.1.5(vitest@2.1.5)': + dependencies: + '@vitest/utils': 2.1.5 + fflate: 0.8.2 + flatted: 3.3.1 + pathe: 1.1.2 + sirv: 3.0.0 + tinyglobby: 0.2.10 + tinyrainbow: 1.2.0 + vitest: 2.1.5(@types/node@22.9.1)(@vitest/ui@2.1.5)(jsdom@25.0.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0) + '@vitest/utils@2.1.5': dependencies: '@vitest/pretty-format': 2.1.5 @@ -15217,6 +15252,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fflate@0.8.2: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -17633,6 +17670,12 @@ snapshots: transitivePeerDependencies: - supports-color + sirv@3.0.0: + dependencies: + '@polka/url': 1.0.0-next.28 + mrmime: 2.0.0 + totalist: 3.0.1 + size-limit@11.1.6: dependencies: bytes-iec: 3.1.1 @@ -18030,6 +18073,8 @@ snapshots: toidentifier@1.0.1: {} + totalist@3.0.1: {} + tough-cookie@5.0.0: dependencies: tldts: 6.1.61 @@ -18350,7 +18395,7 @@ snapshots: optionalDependencies: vite: 5.4.11(@types/node@22.9.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0) - vitest@2.1.5(@types/node@22.9.1)(jsdom@25.0.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0): + vitest@2.1.5(@types/node@22.9.1)(@vitest/ui@2.1.5)(jsdom@25.0.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0): dependencies: '@vitest/expect': 2.1.5 '@vitest/mocker': 2.1.5(vite@5.4.11(@types/node@22.9.1)(less@4.2.0)(sass@1.80.7)(terser@5.36.0)) @@ -18374,6 +18419,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.9.1 + '@vitest/ui': 2.1.5(vitest@2.1.5) jsdom: 25.0.1 transitivePeerDependencies: - less From c08e862a00a2e063cb85dc6bb31690feff77f134 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Sun, 24 Nov 2024 15:18:31 +0100 Subject: [PATCH 16/19] fix types --- examples/angular/grouping/src/app/app.component.ts | 4 ---- .../row-selection-signal/src/app/app.component.ts | 2 -- ...eTableHelperCore.ts => constructTableHelper.ts} | 0 packages/angular-table/src/createTableHelper.ts | 10 ++++++++-- packages/angular-table/src/injectTable.ts | 14 ++++++++++++-- 5 files changed, 20 insertions(+), 10 deletions(-) rename packages/angular-table/src/{createTableHelperCore.ts => constructTableHelper.ts} (100%) diff --git a/examples/angular/grouping/src/app/app.component.ts b/examples/angular/grouping/src/app/app.component.ts index ec2f6218f5..023f5663f7 100644 --- a/examples/angular/grouping/src/app/app.component.ts +++ b/examples/angular/grouping/src/app/app.component.ts @@ -56,13 +56,9 @@ export class AppComponent { rowExpandingFeature, }, _rowModels: { - // @ts-expect-error Fix type groupedRowModel: createGroupedRowModel(), - // @ts-expect-error Fix type expandedRowModel: createExpandedRowModel(), - // @ts-expect-error Fix type paginatedRowModel: createPaginatedRowModel(), - // @ts-expect-error Fix type filteredRowModel: createFilteredRowModel(), }, onGroupingChange: (updaterOrValue: Updater) => { diff --git a/examples/angular/row-selection-signal/src/app/app.component.ts b/examples/angular/row-selection-signal/src/app/app.component.ts index 599724e905..4a4a048462 100644 --- a/examples/angular/row-selection-signal/src/app/app.component.ts +++ b/examples/angular/row-selection-signal/src/app/app.component.ts @@ -108,9 +108,7 @@ export class AppComponent { columnVisibilityFeature, }, _rowModels: { - // @ts-expect-error Fix type filteredRowModel: createFilteredRowModel(), - // @ts-expect-error Fix type paginatedRowModel: createPaginatedRowModel(), }, columns: this.columns, diff --git a/packages/angular-table/src/createTableHelperCore.ts b/packages/angular-table/src/constructTableHelper.ts similarity index 100% rename from packages/angular-table/src/createTableHelperCore.ts rename to packages/angular-table/src/constructTableHelper.ts diff --git a/packages/angular-table/src/createTableHelper.ts b/packages/angular-table/src/createTableHelper.ts index 6f4738072c..b4f6d2a787 100644 --- a/packages/angular-table/src/createTableHelper.ts +++ b/packages/angular-table/src/createTableHelper.ts @@ -1,4 +1,5 @@ -import { constructTableHelper } from './createTableHelperCore' +import { Signal } from '@angular/core' +import { constructTableHelper } from './constructTableHelper' import { injectTable } from './injectTable' import type { RowData, @@ -27,7 +28,12 @@ export function createTableHelper< >( tableHelperOptions: TableHelperOptions, ): TableHelper { - const tableHelper = constructTableHelper(injectTable, tableHelperOptions) + const tableHelper = constructTableHelper( + injectTable as unknown as ( + tableOptions: () => TableOptions, + ) => Table & Signal>, + tableHelperOptions, + ) return { ...tableHelper, injectTable: tableHelper.tableCreator, diff --git a/packages/angular-table/src/injectTable.ts b/packages/angular-table/src/injectTable.ts index a334bcd0f8..8b74b84b25 100644 --- a/packages/angular-table/src/injectTable.ts +++ b/packages/angular-table/src/injectTable.ts @@ -9,6 +9,7 @@ import { lazyInit } from './lazy-signal-initializer' import { proxifyTable } from './proxy' import { reactivityFeature } from './reactivity' import type { + CreateRowModels_All, RowData, Table, TableFeatures, @@ -17,11 +18,20 @@ import type { } from '@tanstack/table-core' import type { Signal } from '@angular/core' +export type AngularTableOptions< + TFeatures extends TableFeatures, + TData extends RowData, +> = Omit, '_rowModels'> & { + _rowModels: CreateRowModels_All + // TODO: no exported + // _rowModelsFns: RowModelFns +} + export function injectTable< TFeatures extends TableFeatures, TData extends RowData, >( - options: () => TableOptions, + options: () => AngularTableOptions, ): Table & Signal> { return lazyInit(() => { const features = () => { @@ -41,7 +51,7 @@ export function injectTable< ...options(), _features: features(), state: { ...state(), ...options().state }, - } + } as TableOptions const table = constructTable(resolvedOptions) From f235c08592d53a806b4f49412609224e76312c22 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 25 Nov 2024 13:42:23 +0100 Subject: [PATCH 17/19] angular v19 table adapter --- .../angular/column-ordering/src/app/app.component.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/angular/column-ordering/src/app/app.component.ts b/examples/angular/column-ordering/src/app/app.component.ts index 8840601d07..8e7d827b8c 100644 --- a/examples/angular/column-ordering/src/app/app.component.ts +++ b/examples/angular/column-ordering/src/app/app.component.ts @@ -71,6 +71,11 @@ const defaultColumns: Array> = [ }, ] +const _features = tableFeatures({ + columnVisibilityFeature, + columnOrderingFeature, +}) + @Component({ selector: 'app-root', standalone: true, @@ -84,17 +89,14 @@ export class AppComponent { readonly columnOrder = signal([]) readonly table = injectTable(() => ({ + _features, data: this.data(), - _features: { - columnVisibilityFeature, - columnOrderingFeature, - }, columns: defaultColumns, - data: this.data(), state: { columnOrder: this.columnOrder(), columnVisibility: this.columnVisibility(), }, + enableExperimentalReactivity: true, onColumnVisibilityChange: (updaterOrValue) => { typeof updaterOrValue === 'function' ? this.columnVisibility.update(updaterOrValue) From 89e7ef8e606773f84ba8b2e286d17d8a96b417b7 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 25 Nov 2024 13:43:38 +0100 Subject: [PATCH 18/19] angular v19 table adapter --- packages/angular-table/src/proxy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/angular-table/src/proxy.ts b/packages/angular-table/src/proxy.ts index fc7689b8f2..1e2ef1bb69 100644 --- a/packages/angular-table/src/proxy.ts +++ b/packages/angular-table/src/proxy.ts @@ -87,6 +87,7 @@ export function toComputed< const computedCache: Record> = {} + // Declare at least a static argument in order to detect fns `length` > 0 return (arg0: any, ...otherArgs: Array) => { const argsArray = [arg0, ...otherArgs] const serializedArgs = serializeArgs(...argsArray) From 10e1fceb2fc9d84c37bdbf24e89669ddf3dd6216 Mon Sep 17 00:00:00 2001 From: riccardoperra Date: Mon, 25 Nov 2024 13:44:55 +0100 Subject: [PATCH 19/19] angular v19 table adapter --- examples/angular/column-ordering/src/app/app.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/angular/column-ordering/src/app/app.component.ts b/examples/angular/column-ordering/src/app/app.component.ts index 8e7d827b8c..e1a9fcc62d 100644 --- a/examples/angular/column-ordering/src/app/app.component.ts +++ b/examples/angular/column-ordering/src/app/app.component.ts @@ -9,6 +9,7 @@ import { columnOrderingFeature, columnVisibilityFeature, injectTable, + tableFeatures, } from '@tanstack/angular-table' import { faker } from '@faker-js/faker' import { makeData } from './makeData'