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()) {
- |
+
+
+
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) {
-
-}
+ }
+
+
+
+ |
+
+ |
+
+ Page Rows ({{ table.getRowModel().rows.length }})
+ |
+
+
+
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
+}