diff --git a/examples/angular/basic/angular.json b/examples/angular/basic/angular.json index c23a9f5e89..2c71bfb7c0 100644 --- a/examples/angular/basic/angular.json +++ b/examples/angular/basic/angular.json @@ -4,6 +4,11 @@ "newProjectRoot": "projects", "projects": { "basic": { + "cli": { + "cache": { + "enabled": false + } + }, "projectType": "application", "schematics": { "@schematics/angular:component": { diff --git a/examples/angular/basic/src/app/app.component.html b/examples/angular/basic/src/app/app.component.html index e79dcf8ca5..4777fa1d55 100644 --- a/examples/angular/basic/src/app/app.component.html +++ b/examples/angular/basic/src/app/app.component.html @@ -1,65 +1,61 @@ -@if (table) { - - - @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { - - @for (header of headerGroup.headers; track header.id) { - @if (!header.isPlaceholder) { - + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + @if (!header.isPlaceholder) { + - } + > + {{ header }} + + } - - } - - - @for (row of table.getRowModel().rows; track row.id) { - - @for (cell of row.getVisibleCells(); track cell.id) { - - } - - } - - - @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { - - @for (footer of footerGroup.headers; track footer.id) { - - } - - } - -
- +
+ - {{ header }} - -
- - {{ cell }} - -
- - {{ footer }} - -
-} @else { -

loading...

-} + > + {{ footer }} + + + } + + } + + diff --git a/examples/angular/basic/src/app/app.component.ts b/examples/angular/basic/src/app/app.component.ts index a119ee5364..487f596d98 100644 --- a/examples/angular/basic/src/app/app.component.ts +++ b/examples/angular/basic/src/app/app.component.ts @@ -1,10 +1,14 @@ -import { Component } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + type OnInit, + signal, +} from '@angular/core' import { RouterOutlet } from '@angular/router' import { ColumnDef, - FlexRenderDirective, - Table, createAngularTable, + FlexRenderDirective, getCoreRowModel, } from '@tanstack/angular-table' @@ -87,16 +91,18 @@ const defaultData: Person[] = [ imports: [RouterOutlet, FlexRenderDirective], templateUrl: './app.component.html', styleUrl: './app.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush }) -export class AppComponent { - data: Person[] = [] - table!: Table +export class AppComponent implements OnInit { + data = signal([]) + + table = createAngularTable(() => ({ + data: this.data(), + columns: defaultColumns, + getCoreRowModel: getCoreRowModel(), + })) + ngOnInit() { - this.data = [...defaultData] - this.table = createAngularTable({ - data: this.data, - columns: defaultColumns, - getCoreRowModel: getCoreRowModel(), - }) + this.data.set(defaultData) } } diff --git a/examples/angular/grouping/angular.json b/examples/angular/grouping/angular.json index 7fd7367537..a97add3d36 100644 --- a/examples/angular/grouping/angular.json +++ b/examples/angular/grouping/angular.json @@ -82,5 +82,11 @@ } } } + }, + "cli": { + "analytics": false, + "cache": { + "enabled": false + } } } diff --git a/examples/angular/grouping/src/app/app.component.html b/examples/angular/grouping/src/app/app.component.html index 3c42725e52..fe4a7f67b0 100644 --- a/examples/angular/grouping/src/app/app.component.html +++ b/examples/angular/grouping/src/app/app.component.html @@ -1,17 +1,16 @@

Grouping

- @if (table) { - - - @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { - - @for (header of headerGroup.headers; track header.id) { - + + +
- @if (!header.isPlaceholder && header.column.getCanGroup()) { -
@@ -137,12 +135,14 @@

Grouping

class="pagination-select" (change)="onPageSizeChange($event)" > - + @for (pageSize of [10, 20, 30, 40, 50]; track pageSize) { + + } +
diff --git a/examples/angular/grouping/src/app/app.component.ts b/examples/angular/grouping/src/app/app.component.ts index 6efb0c9b05..b6f7f47feb 100644 --- a/examples/angular/grouping/src/app/app.component.ts +++ b/examples/angular/grouping/src/app/app.component.ts @@ -1,104 +1,92 @@ -import { CommonModule, NgFor, NgIf } from '@angular/common' -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - OnDestroy, - OnInit, -} from '@angular/core' +import {CommonModule} from '@angular/common' +import {ChangeDetectionStrategy, Component, computed, effect, signal} from '@angular/core' import { + createAngularTable, ExpandedState, FlexRenderDirective, - GroupingState, - PaginationState, - Table, - Updater, - createAngularTable, getCoreRowModel, getExpandedRowModel, getFilteredRowModel, getGroupedRowModel, getPaginationRowModel, + GroupingState, + PaginationState, + Updater, } from '@tanstack/angular-table' - -import { BehaviorSubject, Subject, combineLatest, takeUntil } from 'rxjs' -import { Person, columns } from './columns' -import { mockData } from './mockdata' +import {columns} from './columns' +import {mockData} from './mockdata' @Component({ selector: 'app-root', standalone: true, - imports: [NgIf, NgFor, FlexRenderDirective, CommonModule], + imports: [FlexRenderDirective, CommonModule], templateUrl: './app.component.html', styleUrl: './app.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AppComponent implements OnInit, OnDestroy { +export class AppComponent { title = 'grouping' - - private destroy$ = new Subject() - data = mockData(10000) - groupingState = new BehaviorSubject([]) - expandedState = new BehaviorSubject({}) - paginationState = new BehaviorSubject({ + data = signal(mockData(10000)) + groupingState = signal([]) + expandedState = signal({}) + paginationState = signal({ pageIndex: 0, pageSize: 10, } as PaginationState) - table!: Table - expanded: ExpandedState = {} - constructor(private cdr: ChangeDetectorRef) {} + expanded = signal({}) - ngOnInit() { - this.createTable() - combineLatest([ - this.expandedState, - this.groupingState, - this.paginationState, - ]) - .pipe(takeUntil(this.destroy$)) - .subscribe(([expandedState, groupingState, paginationState]) => { - this.table.options.state.grouping = groupingState - this.table.options.state.expanded = expandedState - this.table.options.state.pagination = paginationState - this.cdr.detectChanges() - }) - } + table = createAngularTable(() => ({ + data: this.data(), + columns: columns, + state: { + grouping: this.groupingState(), + expanded: this.expandedState(), + pagination: this.paginationState(), + }, + onGroupingChange: (updaterOrValue: Updater) => { + const group = + typeof updaterOrValue === 'function' + ? updaterOrValue([...this.groupingState()]) + : updaterOrValue + this.groupingState.set(group) + }, + onExpandedChange: updater => { + const expand = + typeof updater === 'function' ? updater(this.expandedState()) : updater + this.expandedState.set(expand) + }, + onPaginationChange: val => { + const page = typeof val === 'function' ? val(this.paginationState()) : val + this.paginationState.set(page) + }, + debugTable: true, + getExpandedRowModel: getExpandedRowModel(), + getGroupedRowModel: getGroupedRowModel(), + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + })); - createTable() { - this.table = createAngularTable({ - data: this.data, - columns: columns, - state: { - grouping: this.groupingState.getValue(), - expanded: this.expandedState.getValue(), - pagination: this.paginationState.getValue(), - }, - onGroupingChange: (updaterOrValue: Updater) => { - const group = - typeof updaterOrValue === 'function' - ? updaterOrValue([...this.groupingState.getValue()]) - : updaterOrValue - this.groupingState.next(group) - }, - onExpandedChange: updater => { - const expand = - typeof updater === 'function' - ? updater(this.expandedState.getValue()) - : updater - this.expandedState.next(expand) - }, - debugTable: true, - onPaginationChange: val => { - const page = - typeof val === 'function' ? val(this.paginationState.getValue()) : val - this.paginationState.next(page) - }, - getExpandedRowModel: getExpandedRowModel(), - getGroupedRowModel: getGroupedRowModel(), - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getFilteredRowModel: getFilteredRowModel(), - }) + constructor() { + effect(() => { + // run on every state/option change + this.table().getPageOptions(); + }); + + // more granular state, run on every "getPageOptions" change (e.g. only when pagination state change) + effect(() => { + this.table.getPageOptions() + }); + + // granular state is still possible using computed manually, but it must be done manually + const pageOptions = computed(() => this.table().getPageOptions()); + effect(() => { + pageOptions() + }); + + // these two are lines does the same thing, they access to the same table instance property. + this.table().setPageSize(...) + this.table.setPageSize() // this one is evaluated lazily with proxy } onPageInputChange(event: any): void { @@ -111,10 +99,6 @@ export class AppComponent implements OnInit, OnDestroy { } refreshData() { - this.table.options.data = mockData(1000) - } - ngOnDestroy(): void { - this.destroy$.next() - this.destroy$.complete() + this.data.set(mockData(1000)) } } diff --git a/examples/angular/row-selection/angular.json b/examples/angular/row-selection/angular.json index 8c777a97ac..76c68d93ac 100644 --- a/examples/angular/row-selection/angular.json +++ b/examples/angular/row-selection/angular.json @@ -84,6 +84,9 @@ } }, "cli": { - "analytics": false + "analytics": false, + "cache": { + "enabled": false + } } } diff --git a/examples/angular/row-selection/src/app/app.component.html b/examples/angular/row-selection/src/app/app.component.html index 4263ebb092..cbb547fdf6 100644 --- a/examples/angular/row-selection/src/app/app.component.html +++ b/examples/angular/row-selection/src/app/app.component.html @@ -1,107 +1,105 @@

Select

-@if (table) { - - - @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { - - @for (header of headerGroup.headers; track header.id) { - + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + - } - - } - - - @for (row of table.getRowModel().rows; track row.id) { - - @for (cell of row.getVisibleCells(); track cell.id) { - - } - - } - - - - - + > + {{ renderCell }} + + } + + } - -
- @if (!header.isPlaceholder) { - @if (header.id == 'select') { - - } @else { - +
+ @if (!header.isPlaceholder) { + @if (header.id == 'select') { + + } @else { + - {{ headerCell }} - @if (header.column.getCanSort()) { - + > + {{ headerCell }} + @if (header.column.getCanSort()) { + {{ - header.column.getIsSorted() === 'asc' - ? 'arrow_drop_up' - : header.column.getIsSorted() === 'desc' - ? 'arrow_drop_down' - : '' - }} + header.column.getIsSorted() === 'asc' + ? 'arrow_drop_up' + : header.column.getIsSorted() === 'desc' + ? 'arrow_drop_down' + : '' + }} - } - - @if (header.column.getCanFilter()) { - } + + @if (header.column.getCanFilter()) { + } } -
- @if (cell.id.endsWith('select')) { - - } @else { - + } @else { + - {{ renderCell }} - - } -
- - - Page Rows ({{ table.getRowModel().rows.length }}) -
-} + } + + + + + + + + Page Rows ({{ table.getRowModel().rows.length }}) + + + +
- {{ getRowSelectionLength() | async }} of + {{ rowSelectionLength() }} of {{ table.getPreFilteredRowModel().rows.length }} Total Rows Selected
@@ -154,9 +152,11 @@

Select

class="pagination-select" (change)="onPageSizeChange($event)" > - + @for (pageSize of [10, 20, 30, 40, 50]; track pageSize) { + + }
diff --git a/examples/angular/row-selection/src/app/app.component.ts b/examples/angular/row-selection/src/app/app.component.ts index 9ce0594d28..c3b9e09d0d 100644 --- a/examples/angular/row-selection/src/app/app.component.ts +++ b/examples/angular/row-selection/src/app/app.component.ts @@ -1,107 +1,85 @@ -import { AsyncPipe, NgFor, NgIf } from '@angular/common' -import { ChangeDetectorRef, Component, OnInit } from '@angular/core' +import { Component, computed, signal } from '@angular/core' import { ColumnFiltersState, - FlexRenderDirective, - PaginationState, - RowSelectionState, - SortingState, - Table, createAngularTable, + FlexRenderDirective, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, + PaginationState, + RowSelectionState, + SortingState, } from '@tanstack/angular-table' -import { BehaviorSubject, Subject, combineLatest, map, takeUntil } from 'rxjs' -import { Person, columns } from './columns' +import { columns, Person } from './columns' import { FilterComponent } from './filter' import { mockData } from './mockdata' @Component({ selector: 'app-root', standalone: true, - imports: [NgFor, NgIf, AsyncPipe, FilterComponent, FlexRenderDirective], + imports: [FilterComponent, FlexRenderDirective], templateUrl: './app.component.html', styleUrl: './app.component.scss', }) -export class AppComponent implements OnInit { - table!: Table - private destroy$ = new Subject() - private rowSelectionState = new BehaviorSubject({}) - private paginationState = new BehaviorSubject({ +export class AppComponent { + private rowSelectionState = signal({}) + private paginationState = signal({ pageIndex: 0, pageSize: 10, - } as PaginationState) - private columnFilterState = new BehaviorSubject([]) - sortingState = new BehaviorSubject([]) + }) + private columnFilterState = signal([]) + sortingState = signal([]) data: Person[] = mockData(10000) - constructor(private cdr: ChangeDetectorRef) {} + table = createAngularTable(() => ({ + data: this.data, + columns: columns, + state: { + rowSelection: this.rowSelectionState(), + pagination: this.paginationState(), + columnFilters: this.columnFilterState(), + sorting: this.sortingState(), + }, + enableRowSelection: true, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + debugTable: true, + onRowSelectionChange: updaterOrValue => { + this.rowSelectionState.set( + typeof updaterOrValue === 'function' + ? updaterOrValue(this.rowSelectionState()) + : updaterOrValue + ) + }, + onPaginationChange: updaterOrValue => { + this.paginationState.set( + typeof updaterOrValue === 'function' + ? updaterOrValue(this.paginationState()) + : updaterOrValue + ) + }, + onColumnFiltersChange: updaterOrValue => { + this.columnFilterState.set( + typeof updaterOrValue === 'function' + ? updaterOrValue(this.columnFilterState()) + : updaterOrValue + ) + }, + onSortingChange: updaterOrValue => { + this.sortingState.set( + typeof updaterOrValue === 'function' + ? updaterOrValue(this.sortingState()) + : updaterOrValue + ) + }, + })) - ngOnInit() { - this.createTable() - combineLatest([ - this.rowSelectionState, - this.paginationState, - this.sortingState, - ]) - .pipe(takeUntil(this.destroy$)) - .subscribe(([rowSelectionState, paginationState, sortingState]) => { - this.table.options.state.rowSelection = rowSelectionState - this.table.options.state.pagination = paginationState - this.table.options.state.sorting = sortingState - this.cdr.detectChanges() - }) - } - createTable() { - this.table = createAngularTable({ - data: this.data, - columns: columns, - state: { - rowSelection: this.rowSelectionState.getValue(), - pagination: this.paginationState.getValue(), - columnFilters: this.columnFilterState.getValue(), - sorting: this.sortingState.getValue(), - }, - enableRowSelection: true, - onRowSelectionChange: updaterOrValue => { - this.rowSelectionState.next( - typeof updaterOrValue === 'function' - ? updaterOrValue(this.rowSelectionState.getValue()) - : updaterOrValue - ) - }, - onPaginationChange: Updater => { - const newvalue = - typeof Updater === 'function' - ? Updater(this.paginationState.getValue()) - : Updater - this.table.options.state.pagination = newvalue - this.paginationState.next(newvalue) - }, - onColumnFiltersChange: updater => { - const filter = - typeof updater === 'function' - ? updater(this.columnFilterState.getValue()) - : updater - this.table.options.state.columnFilters = filter - this.columnFilterState.next(filter) - }, - onSortingChange: updaterOrValue => { - const sorting = - typeof updaterOrValue == 'function' - ? updaterOrValue([...this.sortingState.getValue()]) - : updaterOrValue - this.sortingState.next(sorting) - }, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - debugTable: true, - }) - } + rowSelectionLength = computed( + () => Object.keys(this.rowSelectionState()).length + ) onPageInputChange(event: Event) { const inputElement = event.target as HTMLInputElement @@ -112,12 +90,4 @@ export class AppComponent implements OnInit { onPageSizeChange(event: any) { this.table.setPageSize(Number(event.target.value)) } - - getRowSelectionLength() { - return this.rowSelectionState.pipe(map(val => Object.keys(val).length)) - } - ngOnDestroy(): void { - this.destroy$.next() - this.destroy$.complete() - } } diff --git a/packages/angular-table/ng-package.json b/packages/angular-table/ng-package.json index 392237ec0f..fabab36e39 100644 --- a/packages/angular-table/ng-package.json +++ b/packages/angular-table/ng-package.json @@ -4,5 +4,6 @@ "lib": { "entryFile": "src/index.ts" }, + "deleteDestPath": false, "allowedNonPeerDependencies": ["@tanstack/table-core"] } diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts index fac3a44ba3..0887b67441 100644 --- a/packages/angular-table/src/index.ts +++ b/packages/angular-table/src/index.ts @@ -1,16 +1,24 @@ import { + computed, Directive, + effect, + inject, + Injector, Input, OnInit, + runInInjectionContext, + signal, TemplateRef, + untracked, ViewContainerRef, } from '@angular/core' import { + createTable, RowData, TableOptions, TableOptionsResolved, - createTable, } from '@tanstack/table-core' +import { proxifyTable, type TableResult } from './proxy' export * from '@tanstack/table-core' @@ -67,31 +75,52 @@ export class FlexRenderDirective implements OnInit { } export function createAngularTable( - options: TableOptions -) { - const resolvedOptions: TableOptionsResolved = { - state: {}, - onStateChange: () => {}, - renderFallbackValue: null, - ...options, - } + options: () => TableOptions +): TableResult { + const injector = inject(Injector) + return runInInjectionContext(injector, () => { + const resolvedOptionsSignal = computed>(() => { + return { + state: {}, + onStateChange: () => {}, + renderFallbackValue: null, + ...options(), + } + }) - let table = createTable(resolvedOptions) - let state = table.initialState - // Compose the default state above with any user state. - table.setOptions((prev: any) => { - return { - ...prev, - ...options, - state: { - ...state, - ...options.state, - }, - onStateChange: (updater: any) => { - options.onStateChange?.(updater) - }, + const table = signal(createTable(untracked(resolvedOptionsSignal)), { + equal: () => false, + }) + const state = signal(untracked(table).initialState) + + function updateOptions() { + const tableState = state() + const resolvedOptions = resolvedOptionsSignal() + untracked(() => { + table().setOptions(prev => ({ + ...prev, + ...resolvedOptions, + state: { ...tableState, ...resolvedOptions.state }, + onStateChange: updater => { + const value = + updater instanceof Function ? updater(tableState) : updater + state.set(value) + resolvedOptions.onStateChange?.(updater) + }, + })) + }) } - }) - return table + updateOptions() + + effect(() => { + void [state(), resolvedOptionsSignal()] + untracked(() => { + updateOptions() + table.update(value => ({ ...value })) + }) + }) + + return proxifyTable(table.asReadonly()) + }) } diff --git a/packages/angular-table/src/proxy.ts b/packages/angular-table/src/proxy.ts new file mode 100644 index 0000000000..4e441e1c70 --- /dev/null +++ b/packages/angular-table/src/proxy.ts @@ -0,0 +1,97 @@ +import { computed, type Signal, untracked } from '@angular/core' +import { type Table } from '@tanstack/table-core' + +type Prettify = { + [K in keyof T]: T[K] +} & {} + +type TableProxyAccessor = T extends (...args: any[]) => any + ? T + : T extends () => infer U + ? Signal + : never + +type TableProxy> = Prettify< + { + [K in keyof T]: K extends `get${string}` ? TableProxyAccessor : T[K] + } & { + options: Signal + } +> + +export type TableResult = Signal> & TableProxy> + +// WIP +export function proxifyTable(tableSignal: Signal>): TableResult { + const proxyTable = new Proxy(tableSignal, { + get(target: Signal>, property: keyof Table): any { + const untypedTarget = target as any + if (untypedTarget[property]) { + return untypedTarget[property] + } + + const table = untracked(target) + if (!(property in table)) { + return untypedTarget[property] + } + + // Attempt to convert all accessors into computed ones, + // excluding handlers as they do not retain any value. + if (property.startsWith('get') && !property.endsWith('Handler')) { + const maybeFn = table[property] as Function | never + if (typeof maybeFn === 'function') { + // Accessors with no arguments should be treated as computed functions + if (maybeFn.length === 0) { + Object.defineProperty(untypedTarget, property, { + value: computed(() => untypedTarget()[property]()), + configurable: true, + enumerable: true, + }) + } + + // Accessors with one argument could be a memoized fn (e.g. `getHeaderGroups(memoArgs)`) + // or a fn with some dependent parameter in their signature (e.g. `getIsSomeRowsPinned(position)`) + // TODO: need to check better this, since there are some fns that have an optional argument. + // This may work also for fns with more than 1 argument. + if (maybeFn.length === 1) { + const computedCache: Record> = {} + const computedTrap = new Proxy(maybeFn, { + apply(target: any, thisArg: any, argArray: any[]): any { + const args = JSON.stringify(argArray) + if (computedCache[args]) { + return computedCache[args]?.() + } + const computedSignal = computed(() => { + untypedTarget()[property]() + return Reflect.apply(target, thisArg, argArray) + }) + computedCache[args] = computedSignal + return computedSignal() + }, + }) + Object.defineProperty(untypedTarget, property, { + value: computedTrap, + configurable: true, + enumerable: true, + }) + } + + // Accessors with arguments could be impure functions, so we can't memoize the value + if (maybeFn.length > 1) { + Object.defineProperty(untypedTarget, property, { + value: target()[property], + configurable: true, + enumerable: true, + }) + } + } + } + + return untypedTarget[property] || table[property] + }, + }) + + return Object.assign(proxyTable, { + options: computed(() => tableSignal().options), + }) as TableResult +}