diff --git a/examples/angular/basic/src/app/app.component.html b/examples/angular/basic/src/app/app.component.html index cef2752dc6..68c81953a6 100644 --- a/examples/angular/basic/src/app/app.component.html +++ b/examples/angular/basic/src/app/app.component.html @@ -13,7 +13,7 @@ let header " > - {{ header }} +
} @@ -33,7 +33,7 @@ let cell " > - {{ cell }} +
} diff --git a/examples/angular/basic/src/app/app.component.ts b/examples/angular/basic/src/app/app.component.ts index 6ef9e63a95..4d3f8059f6 100644 --- a/examples/angular/basic/src/app/app.component.ts +++ b/examples/angular/basic/src/app/app.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, - type OnInit, + Injectable, signal, } from '@angular/core' import { RouterOutlet } from '@angular/router' @@ -88,11 +88,10 @@ const defaultColumns: ColumnDef[] = [ standalone: true, imports: [RouterOutlet, FlexRenderDirective], templateUrl: './app.component.html', - styleUrl: './app.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AppComponent implements OnInit { - data = signal([]) +export class AppComponent { + data = signal(defaultData) table = createAngularTable(() => ({ data: this.data(), @@ -101,11 +100,7 @@ export class AppComponent implements OnInit { debugTable: true, })) - ngOnInit() { - this.data.set(defaultData) - } - rerender() { - this.data.set(defaultData) + this.data.set([...defaultData.sort(() => -1)]) } } diff --git a/examples/angular/column-ordering/.editorconfig b/examples/angular/column-ordering/.editorconfig new file mode 100644 index 0000000000..59d9a3a3e7 --- /dev/null +++ b/examples/angular/column-ordering/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/examples/angular/column-ordering/.gitignore b/examples/angular/column-ordering/.gitignore new file mode 100644 index 0000000000..0711527ef9 --- /dev/null +++ b/examples/angular/column-ordering/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/column-ordering/.vscode/extensions.json b/examples/angular/column-ordering/.vscode/extensions.json new file mode 100644 index 0000000000..77b374577d --- /dev/null +++ b/examples/angular/column-ordering/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/examples/angular/column-ordering/.vscode/launch.json b/examples/angular/column-ordering/.vscode/launch.json new file mode 100644 index 0000000000..925af83705 --- /dev/null +++ b/examples/angular/column-ordering/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/examples/angular/column-ordering/.vscode/tasks.json b/examples/angular/column-ordering/.vscode/tasks.json new file mode 100644 index 0000000000..a298b5bd87 --- /dev/null +++ b/examples/angular/column-ordering/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/examples/angular/column-ordering/README.md b/examples/angular/column-ordering/README.md new file mode 100644 index 0000000000..5da97a87d1 --- /dev/null +++ b/examples/angular/column-ordering/README.md @@ -0,0 +1,27 @@ +# Basic + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.1.2. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/column-ordering/angular.json b/examples/angular/column-ordering/angular.json new file mode 100644 index 0000000000..add3c87198 --- /dev/null +++ b/examples/angular/column-ordering/angular.json @@ -0,0 +1,83 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "basic": { + "cli": { + "cache": { + "enabled": false + } + }, + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/column-ordering", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "basic:build:production" + }, + "development": { + "buildTarget": "basic:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "basic:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing"], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + } + } + } + } + }, + "cli": { + "analytics": "73f296b8-52f2-4044-acca-9178df581487" + } +} diff --git a/examples/angular/column-ordering/package.json b/examples/angular/column-ordering/package.json new file mode 100644 index 0000000000..2e0c5944cb --- /dev/null +++ b/examples/angular/column-ordering/package.json @@ -0,0 +1,38 @@ +{ + "name": "tanstack-table-example-angular-column-ordering", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^17.3.1", + "@angular/common": "^17.3.1", + "@angular/compiler": "^17.3.1", + "@angular/core": "^17.3.1", + "@angular/forms": "^17.3.1", + "@angular/platform-browser": "^17.3.1", + "@angular/platform-browser-dynamic": "^17.3.1", + "@tanstack/angular-table": "^8.14.0", + "rxjs": "~7.8.1", + "zone.js": "~0.14.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.3.1", + "@angular/cli": "^17.3.1", + "@angular/compiler-cli": "^17.3.1", + "@types/jasmine": "~5.1.4", + "jasmine-core": "~5.1.2", + "karma": "~6.4.3", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.1", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "tslib": "^2.6.2", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/column-ordering/src/app/app.component.html b/examples/angular/column-ordering/src/app/app.component.html new file mode 100644 index 0000000000..fca57e650a --- /dev/null +++ b/examples/angular/column-ordering/src/app/app.component.html @@ -0,0 +1,102 @@ +
+
+
+ +
+ + @for (column of table.getAllLeafColumns(); track column.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) { + + } + + } + + + @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { + + @for (header of footerGroup.headers; track header.id) { + + } + + } + +
+ @if (!header.isPlaceholder) { + + {{ header }} + + } +
+ + {{ cell }} + +
+ @if (!header.isPlaceholder) { + + {{ header }} + + } +
+ +
+
{{ stringifiedColumnOrdering() }}
+
diff --git a/examples/angular/column-ordering/src/app/app.component.ts b/examples/angular/column-ordering/src/app/app.component.ts new file mode 100644 index 0000000000..2f447c6f61 --- /dev/null +++ b/examples/angular/column-ordering/src/app/app.component.ts @@ -0,0 +1,118 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + signal, +} from '@angular/core' +import { + ColumnDef, + type ColumnOrderState, + createAngularTable, + FlexRenderDirective, + getCoreRowModel, + type VisibilityState, +} from '@tanstack/angular-table' +import { makeData, type Person } from './makeData' +import { faker } from '@faker-js/faker' + +const defaultColumns: ColumnDef[] = [ + { + header: 'Name', + footer: props => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: info => info.getValue(), + footer: props => props.column.id, + }, + { + accessorFn: row => row.lastName, + id: 'lastName', + cell: info => info.getValue(), + header: () => 'Last Name', + footer: props => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: props => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: props => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: props => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: props => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: props => props.column.id, + }, + ], + }, + ], + }, +] + +@Component({ + selector: 'app-root', + standalone: true, + imports: [FlexRenderDirective], + templateUrl: './app.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppComponent { + readonly data = signal(makeData(20)) + readonly columnVisibility = signal({}) + readonly columnOrder = signal([]) + + readonly table = createAngularTable(() => ({ + data: this.data(), + columns: defaultColumns, + state: { + columnOrder: this.columnOrder(), + columnVisibility: this.columnVisibility(), + }, + getCoreRowModel: getCoreRowModel(), + onColumnVisibilityChange: updaterOrValue => { + typeof updaterOrValue === 'function' + ? this.columnVisibility.update(updaterOrValue) + : this.columnVisibility.set(updaterOrValue) + }, + onColumnOrderChange: updaterOrValue => { + typeof updaterOrValue === 'function' + ? this.columnOrder.update(updaterOrValue) + : this.columnOrder.set(updaterOrValue) + }, + debugTable: true, + debugHeaders: true, + debugColumns: true, + })) + + readonly stringifiedColumnOrdering = computed(() => { + return JSON.stringify(this.table.getState().columnOrder) + }) + + randomizeColumns() { + this.table.setColumnOrder( + faker.helpers.shuffle(this.table.getAllLeafColumns().map(d => d.id)) + ) + } + + rerender() { + this.data.set([...makeData(20)]) + } +} diff --git a/examples/angular/column-ordering/src/app/app.config.ts b/examples/angular/column-ordering/src/app/app.config.ts new file mode 100644 index 0000000000..f27099f33c --- /dev/null +++ b/examples/angular/column-ordering/src/app/app.config.ts @@ -0,0 +1,5 @@ +import { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [], +} diff --git a/examples/angular/column-ordering/src/app/makeData.ts b/examples/angular/column-ordering/src/app/makeData.ts new file mode 100644 index 0000000000..331dd1eb19 --- /dev/null +++ b/examples/angular/column-ordering/src/app/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Person[] +} + +const range = (len: number) => { + const arr: number[] = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0]!, + } +} + +export function makeData(...lens: number[]) { + const makeDataLevel = (depth = 0): Person[] => { + const len = lens[depth]! + return range(len).map((d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/basic/src/app/app.component.scss b/examples/angular/column-ordering/src/assets/.gitkeep similarity index 100% rename from examples/angular/basic/src/app/app.component.scss rename to examples/angular/column-ordering/src/assets/.gitkeep diff --git a/examples/angular/column-ordering/src/favicon.ico b/examples/angular/column-ordering/src/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/column-ordering/src/favicon.ico differ diff --git a/examples/angular/column-ordering/src/index.html b/examples/angular/column-ordering/src/index.html new file mode 100644 index 0000000000..a4bb987648 --- /dev/null +++ b/examples/angular/column-ordering/src/index.html @@ -0,0 +1,14 @@ + + + + + Basic + + + + + + + + + diff --git a/examples/angular/column-ordering/src/main.ts b/examples/angular/column-ordering/src/main.ts new file mode 100644 index 0000000000..0c3b92057c --- /dev/null +++ b/examples/angular/column-ordering/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)) diff --git a/examples/angular/column-ordering/src/styles.scss b/examples/angular/column-ordering/src/styles.scss new file mode 100644 index 0000000000..93034cdd1b --- /dev/null +++ b/examples/angular/column-ordering/src/styles.scss @@ -0,0 +1,35 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td:last-child { + border-right: 0; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} diff --git a/examples/angular/column-ordering/tsconfig.app.json b/examples/angular/column-ordering/tsconfig.app.json new file mode 100644 index 0000000000..84f1f992d2 --- /dev/null +++ b/examples/angular/column-ordering/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/column-ordering/tsconfig.json b/examples/angular/column-ordering/tsconfig.json new file mode 100644 index 0000000000..b58d3efc71 --- /dev/null +++ b/examples/angular/column-ordering/tsconfig.json @@ -0,0 +1,31 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "src", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/column-ordering/tsconfig.spec.json b/examples/angular/column-ordering/tsconfig.spec.json new file mode 100644 index 0000000000..47e3dd7551 --- /dev/null +++ b/examples/angular/column-ordering/tsconfig.spec.json @@ -0,0 +1,9 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/examples/angular/column-pinning-sticky/.editorconfig b/examples/angular/column-pinning-sticky/.editorconfig new file mode 100644 index 0000000000..59d9a3a3e7 --- /dev/null +++ b/examples/angular/column-pinning-sticky/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/examples/angular/column-pinning-sticky/.gitignore b/examples/angular/column-pinning-sticky/.gitignore new file mode 100644 index 0000000000..0711527ef9 --- /dev/null +++ b/examples/angular/column-pinning-sticky/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/column-pinning-sticky/.vscode/extensions.json b/examples/angular/column-pinning-sticky/.vscode/extensions.json new file mode 100644 index 0000000000..77b374577d --- /dev/null +++ b/examples/angular/column-pinning-sticky/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/examples/angular/column-pinning-sticky/.vscode/launch.json b/examples/angular/column-pinning-sticky/.vscode/launch.json new file mode 100644 index 0000000000..925af83705 --- /dev/null +++ b/examples/angular/column-pinning-sticky/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/examples/angular/column-pinning-sticky/.vscode/tasks.json b/examples/angular/column-pinning-sticky/.vscode/tasks.json new file mode 100644 index 0000000000..a298b5bd87 --- /dev/null +++ b/examples/angular/column-pinning-sticky/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/examples/angular/column-pinning-sticky/README.md b/examples/angular/column-pinning-sticky/README.md new file mode 100644 index 0000000000..5da97a87d1 --- /dev/null +++ b/examples/angular/column-pinning-sticky/README.md @@ -0,0 +1,27 @@ +# Basic + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.1.2. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/column-pinning-sticky/angular.json b/examples/angular/column-pinning-sticky/angular.json new file mode 100644 index 0000000000..c9fe837fe5 --- /dev/null +++ b/examples/angular/column-pinning-sticky/angular.json @@ -0,0 +1,83 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "basic": { + "cli": { + "cache": { + "enabled": false + } + }, + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/column-pinning-sticky", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "basic:build:production" + }, + "development": { + "buildTarget": "basic:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "basic:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing"], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + } + } + } + } + }, + "cli": { + "analytics": "73f296b8-52f2-4044-acca-9178df581487" + } +} diff --git a/examples/angular/column-pinning-sticky/package.json b/examples/angular/column-pinning-sticky/package.json new file mode 100644 index 0000000000..e6a128598b --- /dev/null +++ b/examples/angular/column-pinning-sticky/package.json @@ -0,0 +1,38 @@ +{ + "name": "tanstack-table-example-angular-column-pinning-sticky", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^17.3.1", + "@angular/common": "^17.3.1", + "@angular/compiler": "^17.3.1", + "@angular/core": "^17.3.1", + "@angular/forms": "^17.3.1", + "@angular/platform-browser": "^17.3.1", + "@angular/platform-browser-dynamic": "^17.3.1", + "@tanstack/angular-table": "^8.14.0", + "rxjs": "~7.8.1", + "zone.js": "~0.14.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.3.1", + "@angular/cli": "^17.3.1", + "@angular/compiler-cli": "^17.3.1", + "@types/jasmine": "~5.1.4", + "jasmine-core": "~5.1.2", + "karma": "~6.4.3", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.1", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "tslib": "^2.6.2", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/column-pinning-sticky/src/app/app.component.html b/examples/angular/column-pinning-sticky/src/app/app.component.html new file mode 100644 index 0000000000..141fce97c0 --- /dev/null +++ b/examples/angular/column-pinning-sticky/src/app/app.component.html @@ -0,0 +1,137 @@ +
+
+
+ +
+ + @for (column of table.getAllLeafColumns(); track column.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) { + + } + + } + +
+
+ @if (!header.isPlaceholder) { + + {{ headerValue }} + + } + + {{ + header.column.getIndex( + header.column.getIsPinned() || 'center' + ) + }} +
+ + @if (!header.isPlaceholder && header.column.getCanPin()) { +
+ @if (header.column.getIsPinned() !== 'left') { + + } + + @if (header.column.getIsPinned()) { + + } + + @if (header.column.getIsPinned() !== 'right') { + + } +
+ } + + +
+
+ + {{ cellValue }} + +
+
+
+ +
+
{{ stringifiedColumnPinning() }}
diff --git a/examples/angular/column-pinning-sticky/src/app/app.component.ts b/examples/angular/column-pinning-sticky/src/app/app.component.ts new file mode 100644 index 0000000000..c5ac8ada05 --- /dev/null +++ b/examples/angular/column-pinning-sticky/src/app/app.component.ts @@ -0,0 +1,138 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + signal, +} from '@angular/core' +import { + Column, + ColumnDef, + type ColumnOrderState, + type ColumnPinningState, + createAngularTable, + FlexRenderDirective, + getCoreRowModel, + type VisibilityState, +} from '@tanstack/angular-table' +import { makeData } from './makeData' +import { faker } from '@faker-js/faker' +import { NgStyle, NgTemplateOutlet, SlicePipe } from '@angular/common' + +type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +const defaultColumns: ColumnDef[] = [ + { + accessorKey: 'firstName', + id: 'firstName', + header: 'First Name', + cell: info => info.getValue(), + footer: props => props.column.id, + size: 180, + }, + { + accessorFn: row => row.lastName, + id: 'lastName', + cell: info => info.getValue(), + header: () => 'Last Name', + footer: props => props.column.id, + size: 180, + }, + { + accessorKey: 'age', + id: 'age', + header: 'Age', + footer: props => props.column.id, + size: 180, + }, + { + accessorKey: 'visits', + id: 'visits', + header: 'Visits', + footer: props => props.column.id, + size: 180, + }, + { + accessorKey: 'status', + id: 'status', + header: 'Status', + footer: props => props.column.id, + size: 180, + }, + { + accessorKey: 'progress', + id: 'progress', + header: 'Profile Progress', + footer: props => props.column.id, + size: 180, + }, +] + +@Component({ + selector: 'app-root', + standalone: true, + imports: [FlexRenderDirective, SlicePipe, NgTemplateOutlet, NgStyle], + templateUrl: './app.component.html', +}) +export class AppComponent { + readonly columns = signal([...defaultColumns]) + readonly data = signal(makeData(30)) + readonly columnVisibility = signal({}) + readonly columnOrder = signal([]) + readonly columnPinning = signal({}) + readonly split = signal(false) + + table = createAngularTable(() => ({ + data: this.data(), + columns: this.columns(), + getCoreRowModel: getCoreRowModel(), + debugTable: true, + debugHeaders: true, + debugColumns: true, + columnResizeMode: 'onChange', + })) + + stringifiedColumnPinning = computed(() => { + return JSON.stringify(this.table.getState().columnPinning) + }) + + readonly getCommonPinningStyles = ( + column: Column + ): Record => { + const isPinned = column.getIsPinned() + const isLastLeftPinnedColumn = + isPinned === 'left' && column.getIsLastColumn('left') + const isFirstRightPinnedColumn = + isPinned === 'right' && column.getIsFirstColumn('right') + + return { + boxShadow: isLastLeftPinnedColumn + ? '-4px 0 4px -4px gray inset' + : isFirstRightPinnedColumn + ? '4px 0 4px -4px gray inset' + : undefined, + left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined, + right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined, + opacity: isPinned ? 0.95 : 1, + position: isPinned ? 'sticky' : 'relative', + width: column.getSize(), + zIndex: isPinned ? 1 : 0, + } + } + + randomizeColumns() { + this.table.setColumnOrder( + faker.helpers.shuffle(this.table.getAllLeafColumns().map(d => d.id)) + ) + } + + rerender() { + this.data.set(makeData(5000)) + } +} diff --git a/examples/angular/column-pinning-sticky/src/app/app.config.ts b/examples/angular/column-pinning-sticky/src/app/app.config.ts new file mode 100644 index 0000000000..f27099f33c --- /dev/null +++ b/examples/angular/column-pinning-sticky/src/app/app.config.ts @@ -0,0 +1,5 @@ +import { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [], +} diff --git a/examples/angular/column-pinning-sticky/src/app/makeData.ts b/examples/angular/column-pinning-sticky/src/app/makeData.ts new file mode 100644 index 0000000000..331dd1eb19 --- /dev/null +++ b/examples/angular/column-pinning-sticky/src/app/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Person[] +} + +const range = (len: number) => { + const arr: number[] = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0]!, + } +} + +export function makeData(...lens: number[]) { + const makeDataLevel = (depth = 0): Person[] => { + const len = lens[depth]! + return range(len).map((d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/column-visibility/src/app/app.component.scss b/examples/angular/column-pinning-sticky/src/assets/.gitkeep similarity index 100% rename from examples/angular/column-visibility/src/app/app.component.scss rename to examples/angular/column-pinning-sticky/src/assets/.gitkeep diff --git a/examples/angular/column-pinning-sticky/src/favicon.ico b/examples/angular/column-pinning-sticky/src/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/column-pinning-sticky/src/favicon.ico differ diff --git a/examples/angular/column-pinning-sticky/src/index.html b/examples/angular/column-pinning-sticky/src/index.html new file mode 100644 index 0000000000..a4bb987648 --- /dev/null +++ b/examples/angular/column-pinning-sticky/src/index.html @@ -0,0 +1,14 @@ + + + + + Basic + + + + + + + + + diff --git a/examples/angular/column-pinning-sticky/src/main.ts b/examples/angular/column-pinning-sticky/src/main.ts new file mode 100644 index 0000000000..0c3b92057c --- /dev/null +++ b/examples/angular/column-pinning-sticky/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)) diff --git a/examples/angular/column-pinning-sticky/src/styles.scss b/examples/angular/column-pinning-sticky/src/styles.scss new file mode 100644 index 0000000000..2e804931bd --- /dev/null +++ b/examples/angular/column-pinning-sticky/src/styles.scss @@ -0,0 +1,50 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +.table-container { + border: 1px solid lightgray; + overflow-x: scroll; + width: 100%; + max-width: 960px; + position: relative; +} + +table { + /* box-shadow and borders will not work with positon: sticky otherwise */ + border-collapse: separate !important; + border-spacing: 0; +} + +th { + background-color: lightgray; + border-bottom: 1px solid lightgray; + font-weight: bold; + height: 30px; + padding: 2px 4px; + position: relative; + text-align: center; +} + +td { + background-color: white; + padding: 2px 4px; +} + +.resizer { + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + height: 100%; + position: absolute; + right: 0; + top: 0; + touch-action: none; + user-select: none; + width: 5px; +} + +.resizer.isResizing { + background: blue; + opacity: 1; +} diff --git a/examples/angular/column-pinning-sticky/tsconfig.app.json b/examples/angular/column-pinning-sticky/tsconfig.app.json new file mode 100644 index 0000000000..84f1f992d2 --- /dev/null +++ b/examples/angular/column-pinning-sticky/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/column-pinning-sticky/tsconfig.json b/examples/angular/column-pinning-sticky/tsconfig.json new file mode 100644 index 0000000000..b58d3efc71 --- /dev/null +++ b/examples/angular/column-pinning-sticky/tsconfig.json @@ -0,0 +1,31 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "src", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/column-pinning-sticky/tsconfig.spec.json b/examples/angular/column-pinning-sticky/tsconfig.spec.json new file mode 100644 index 0000000000..47e3dd7551 --- /dev/null +++ b/examples/angular/column-pinning-sticky/tsconfig.spec.json @@ -0,0 +1,9 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/examples/angular/column-pinning/.editorconfig b/examples/angular/column-pinning/.editorconfig new file mode 100644 index 0000000000..59d9a3a3e7 --- /dev/null +++ b/examples/angular/column-pinning/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/examples/angular/column-pinning/.gitignore b/examples/angular/column-pinning/.gitignore new file mode 100644 index 0000000000..0711527ef9 --- /dev/null +++ b/examples/angular/column-pinning/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/column-pinning/.vscode/extensions.json b/examples/angular/column-pinning/.vscode/extensions.json new file mode 100644 index 0000000000..77b374577d --- /dev/null +++ b/examples/angular/column-pinning/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/examples/angular/column-pinning/.vscode/launch.json b/examples/angular/column-pinning/.vscode/launch.json new file mode 100644 index 0000000000..925af83705 --- /dev/null +++ b/examples/angular/column-pinning/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/examples/angular/column-pinning/.vscode/tasks.json b/examples/angular/column-pinning/.vscode/tasks.json new file mode 100644 index 0000000000..a298b5bd87 --- /dev/null +++ b/examples/angular/column-pinning/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/examples/angular/column-pinning/README.md b/examples/angular/column-pinning/README.md new file mode 100644 index 0000000000..5da97a87d1 --- /dev/null +++ b/examples/angular/column-pinning/README.md @@ -0,0 +1,27 @@ +# Basic + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.1.2. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/column-pinning/angular.json b/examples/angular/column-pinning/angular.json new file mode 100644 index 0000000000..7eaee1b793 --- /dev/null +++ b/examples/angular/column-pinning/angular.json @@ -0,0 +1,83 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "basic": { + "cli": { + "cache": { + "enabled": false + } + }, + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/column-pinning", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "basic:build:production" + }, + "development": { + "buildTarget": "basic:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "basic:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing"], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + } + } + } + } + }, + "cli": { + "analytics": "73f296b8-52f2-4044-acca-9178df581487" + } +} diff --git a/examples/angular/column-pinning/package.json b/examples/angular/column-pinning/package.json new file mode 100644 index 0000000000..7ddb6dc241 --- /dev/null +++ b/examples/angular/column-pinning/package.json @@ -0,0 +1,38 @@ +{ + "name": "tanstack-table-example-angular-column-pinning", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^17.3.1", + "@angular/common": "^17.3.1", + "@angular/compiler": "^17.3.1", + "@angular/core": "^17.3.1", + "@angular/forms": "^17.3.1", + "@angular/platform-browser": "^17.3.1", + "@angular/platform-browser-dynamic": "^17.3.1", + "@tanstack/angular-table": "^8.14.0", + "rxjs": "~7.8.1", + "zone.js": "~0.14.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.3.1", + "@angular/cli": "^17.3.1", + "@angular/compiler-cli": "^17.3.1", + "@types/jasmine": "~5.1.4", + "jasmine-core": "~5.1.2", + "karma": "~6.4.3", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.1", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "tslib": "^2.6.2", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/column-pinning/src/app/app.component.html b/examples/angular/column-pinning/src/app/app.component.html new file mode 100644 index 0000000000..e002af2b70 --- /dev/null +++ b/examples/angular/column-pinning/src/app/app.component.html @@ -0,0 +1,261 @@ +
+
+
+ +
+ + @for (column of table.getAllLeafColumns(); track column.id) { +
+ +
+ } +
+ +
+ +
+ + +
+
+
+ +
+ +
+ + @if (split()) { + + + @for ( + headerGroup of table.getLeftHeaderGroups(); + track headerGroup.id + ) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + + @for (row of table.getRowModel().rows | slice: 0 : 20; track row.id) { + + @for (cell of row.getLeftVisibleCells(); track cell.id) { + + } + + } + +
+
+ @if (!header.isPlaceholder) { + + {{ headerValue }} + + } +
+ + +
+ + {{ cellValue }} + +
+ } + + + + + @if ( + split() ? table.getCenterHeaderGroups() : table.getHeaderGroups(); + as headerGroups + ) { + @for (headerGroup of headerGroups; track headerGroup.id) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + } + + + @for (row of table.getRowModel().rows | slice: 0 : 20; track row.id) { + @if ( + split() ? row.getCenterVisibleCells() : row.getVisibleCells(); + as cells + ) { + + @for (cell of cells; track cell.id) { + + } + + } + } + +
+
+ @if (!header.isPlaceholder) { + + {{ headerValue }} + + } + + +
+
+ + {{ cellValue }} + +
+ + + @if (split()) { + + + @for ( + headerGroup of table.getRightHeaderGroups(); + track headerGroup.id + ) { + + @for (header of headerGroup.headers; track header.id) { + + } + + } + + + + @for (row of table.getRowModel().rows | slice: 0 : 20; track row.id) { + + @for (cell of row.getRightVisibleCells(); track cell.id) { + + } + + } + +
+
+ @if (!header.isPlaceholder) { + + {{ headerValue }} + + } +
+ + +
+ + {{ cellValue }} + +
+ } +
+ +
+
{{ stringifiedColumnPinning() }}
+
+ + + @if (!header.isPlaceholder && header.column.getCanPin()) { +
+ @if (header.column.getIsPinned() !== 'left') { + + } + + @if (header.column.getIsPinned()) { + + } + + @if (header.column.getIsPinned() !== 'right') { + + } +
+ } +
diff --git a/examples/angular/column-pinning/src/app/app.component.ts b/examples/angular/column-pinning/src/app/app.component.ts new file mode 100644 index 0000000000..3784a0782c --- /dev/null +++ b/examples/angular/column-pinning/src/app/app.component.ts @@ -0,0 +1,137 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + signal, +} from '@angular/core' +import { + ColumnDef, + type ColumnOrderState, + type ColumnPinningState, + createAngularTable, + FlexRenderDirective, + getCoreRowModel, + type VisibilityState, +} from '@tanstack/angular-table' +import { makeData } from './makeData' +import { faker } from '@faker-js/faker' +import { NgTemplateOutlet, SlicePipe } from '@angular/common' + +type Person = { + firstName: string + lastName: string + age: number + visits: number + status: string + progress: number +} + +const defaultColumns: ColumnDef[] = [ + { + header: 'Name', + footer: props => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: info => info.getValue(), + footer: props => props.column.id, + }, + { + accessorFn: row => row.lastName, + id: 'lastName', + cell: info => info.getValue(), + header: () => 'Last Name', + footer: props => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: props => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => 'Age', + footer: props => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: props => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: props => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: props => props.column.id, + }, + ], + }, + ], + }, +] + +@Component({ + selector: 'app-root', + standalone: true, + imports: [FlexRenderDirective, SlicePipe, NgTemplateOutlet], + templateUrl: './app.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppComponent { + readonly data = signal(makeData(5000)) + readonly columnVisibility = signal({}) + readonly columnOrder = signal([]) + readonly columnPinning = signal({}) + readonly split = signal(false) + + table = createAngularTable(() => ({ + data: this.data(), + columns: defaultColumns, + state: { + columnVisibility: this.columnVisibility(), + columnOrder: this.columnOrder(), + columnPinning: this.columnPinning(), + }, + onColumnVisibilityChange: updaterOrValue => { + typeof updaterOrValue === 'function' + ? this.columnVisibility.update(updaterOrValue) + : this.columnVisibility.set(updaterOrValue) + }, + onColumnOrderChange: updaterOrValue => { + typeof updaterOrValue === 'function' + ? this.columnOrder.update(updaterOrValue) + : this.columnOrder.set(updaterOrValue) + }, + onColumnPinningChange: updaterOrValue => { + typeof updaterOrValue === 'function' + ? this.columnPinning.update(updaterOrValue) + : this.columnPinning.set(updaterOrValue) + }, + getCoreRowModel: getCoreRowModel(), + debugTable: true, + debugHeaders: true, + debugColumns: true, + })) + + stringifiedColumnPinning = computed(() => { + return JSON.stringify(this.table.getState().columnPinning) + }) + + randomizeColumns() { + this.table.setColumnOrder( + faker.helpers.shuffle(this.table.getAllLeafColumns().map(d => d.id)) + ) + } + + rerender() { + this.data.set(makeData(5000)) + } +} diff --git a/examples/angular/column-pinning/src/app/app.config.ts b/examples/angular/column-pinning/src/app/app.config.ts new file mode 100644 index 0000000000..f27099f33c --- /dev/null +++ b/examples/angular/column-pinning/src/app/app.config.ts @@ -0,0 +1,5 @@ +import { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [], +} diff --git a/examples/angular/column-pinning/src/app/makeData.ts b/examples/angular/column-pinning/src/app/makeData.ts new file mode 100644 index 0000000000..331dd1eb19 --- /dev/null +++ b/examples/angular/column-pinning/src/app/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Person[] +} + +const range = (len: number) => { + const arr: number[] = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0]!, + } +} + +export function makeData(...lens: number[]) { + const makeDataLevel = (depth = 0): Person[] => { + const len = lens[depth]! + return range(len).map((d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/column-pinning/src/assets/.gitkeep b/examples/angular/column-pinning/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/angular/column-pinning/src/favicon.ico b/examples/angular/column-pinning/src/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/column-pinning/src/favicon.ico differ diff --git a/examples/angular/column-pinning/src/index.html b/examples/angular/column-pinning/src/index.html new file mode 100644 index 0000000000..a4bb987648 --- /dev/null +++ b/examples/angular/column-pinning/src/index.html @@ -0,0 +1,14 @@ + + + + + Basic + + + + + + + + + diff --git a/examples/angular/column-pinning/src/main.ts b/examples/angular/column-pinning/src/main.ts new file mode 100644 index 0000000000..0c3b92057c --- /dev/null +++ b/examples/angular/column-pinning/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)) diff --git a/examples/angular/column-pinning/src/styles.scss b/examples/angular/column-pinning/src/styles.scss new file mode 100644 index 0000000000..93034cdd1b --- /dev/null +++ b/examples/angular/column-pinning/src/styles.scss @@ -0,0 +1,35 @@ +html { + font-family: sans-serif; + font-size: 14px; +} + +table { + border: 1px solid lightgray; +} + +tbody { + border-bottom: 1px solid lightgray; +} + +th { + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td { + border-right: 1px solid lightgray; + padding: 2px 4px; +} + +td:last-child { + border-right: 0; +} + +tfoot { + color: gray; +} + +tfoot th { + font-weight: normal; +} diff --git a/examples/angular/column-pinning/tsconfig.app.json b/examples/angular/column-pinning/tsconfig.app.json new file mode 100644 index 0000000000..84f1f992d2 --- /dev/null +++ b/examples/angular/column-pinning/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/column-pinning/tsconfig.json b/examples/angular/column-pinning/tsconfig.json new file mode 100644 index 0000000000..b58d3efc71 --- /dev/null +++ b/examples/angular/column-pinning/tsconfig.json @@ -0,0 +1,31 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "src", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/column-pinning/tsconfig.spec.json b/examples/angular/column-pinning/tsconfig.spec.json new file mode 100644 index 0000000000..47e3dd7551 --- /dev/null +++ b/examples/angular/column-pinning/tsconfig.spec.json @@ -0,0 +1,9 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/examples/angular/column-visibility/angular.json b/examples/angular/column-visibility/angular.json index 2c71bfb7c0..42b0d75bb7 100644 --- a/examples/angular/column-visibility/angular.json +++ b/examples/angular/column-visibility/angular.json @@ -22,7 +22,7 @@ "build": { "builder": "@angular-devkit/build-angular:application", "options": { - "outputPath": "dist/basic", + "outputPath": "dist/column-visibility", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], @@ -34,18 +34,7 @@ }, "configurations": { "production": { - "budgets": [ - { - "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "2kb", - "maximumError": "4kb" - } - ], + "budgets": [], "outputHashing": "all" }, "development": { diff --git a/examples/angular/column-visibility/src/app/app.component.ts b/examples/angular/column-visibility/src/app/app.component.ts index e1f7b4327b..c38a366b3c 100644 --- a/examples/angular/column-visibility/src/app/app.component.ts +++ b/examples/angular/column-visibility/src/app/app.component.ts @@ -106,7 +106,6 @@ const defaultColumns: ColumnDef[] = [ standalone: true, imports: [FlexRenderDirective], templateUrl: './app.component.html', - styleUrl: './app.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppComponent implements OnInit { diff --git a/examples/angular/filters/.editorconfig b/examples/angular/filters/.editorconfig new file mode 100644 index 0000000000..59d9a3a3e7 --- /dev/null +++ b/examples/angular/filters/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/examples/angular/filters/.gitignore b/examples/angular/filters/.gitignore new file mode 100644 index 0000000000..0711527ef9 --- /dev/null +++ b/examples/angular/filters/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/examples/angular/filters/.vscode/extensions.json b/examples/angular/filters/.vscode/extensions.json new file mode 100644 index 0000000000..77b374577d --- /dev/null +++ b/examples/angular/filters/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/examples/angular/filters/.vscode/launch.json b/examples/angular/filters/.vscode/launch.json new file mode 100644 index 0000000000..925af83705 --- /dev/null +++ b/examples/angular/filters/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/examples/angular/filters/.vscode/tasks.json b/examples/angular/filters/.vscode/tasks.json new file mode 100644 index 0000000000..a298b5bd87 --- /dev/null +++ b/examples/angular/filters/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/examples/angular/filters/README.md b/examples/angular/filters/README.md new file mode 100644 index 0000000000..73a201f1eb --- /dev/null +++ b/examples/angular/filters/README.md @@ -0,0 +1,27 @@ +# Selection + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.1.2. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/examples/angular/filters/angular.json b/examples/angular/filters/angular.json new file mode 100644 index 0000000000..9287417602 --- /dev/null +++ b/examples/angular/filters/angular.json @@ -0,0 +1,81 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "selection": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/filters", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "selection:build:production" + }, + "development": { + "buildTarget": "selection:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "selection:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": ["zone.js", "zone.js/testing"], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": ["src/favicon.ico", "src/assets"], + "styles": ["src/styles.scss"], + "scripts": [] + } + } + } + } + }, + "cli": { + "analytics": false, + "cache": { + "enabled": false + } + } +} diff --git a/examples/angular/filters/package.json b/examples/angular/filters/package.json new file mode 100644 index 0000000000..ba9c89a99a --- /dev/null +++ b/examples/angular/filters/package.json @@ -0,0 +1,39 @@ +{ + "name": "tanstack-table-example-angular-filters", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "dependencies": { + "@angular/animations": "^17.3.1", + "@angular/common": "^17.3.1", + "@angular/compiler": "^17.3.1", + "@angular/core": "^17.3.1", + "@angular/forms": "^17.3.1", + "@angular/platform-browser": "^17.3.1", + "@angular/platform-browser-dynamic": "^17.3.1", + "@faker-js/faker": "^8.4.1", + "@tanstack/angular-table": "^8.14.0", + "rxjs": "~7.8.1", + "tslib": "^2.6.2", + "zone.js": "~0.14.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^17.3.1", + "@angular/cli": "^17.3.1", + "@angular/compiler-cli": "^17.3.1", + "@types/jasmine": "~5.1.4", + "jasmine-core": "~5.1.2", + "karma": "~6.4.3", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.1", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "5.4.5" + } +} diff --git a/examples/angular/filters/src/app/app.component.html b/examples/angular/filters/src/app/app.component.html new file mode 100644 index 0000000000..e4f5d993f0 --- /dev/null +++ b/examples/angular/filters/src/app/app.component.html @@ -0,0 +1,138 @@ +
+
+ + + + @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) { + + } + + } + +
+ @if (!header.isPlaceholder) { +
+ + {{ headerCell }} + + + @if (header.column.getIsSorted() === 'asc') { + 🔼 + } + @if (header.column.getIsSorted() === 'desc') { + 🔽 + } +
+ + @if (header.column.getCanFilter()) { +
+ +
+ } + } +
+ + {{ renderCell }} + +
+ +
+
+ + + + + +
Page
+ + {{ table.getState().pagination.pageIndex + 1 }} of + {{ table.getPageCount() }} + +
+ + | Go to page: + + + + +
+
{{ table.getPrePaginationRowModel().rows.length }} Rows
+
+ +
+
+
{{ stringifiedFilters() }}
+
+
+ + + Age 🥳 + diff --git a/examples/angular/filters/src/app/app.component.ts b/examples/angular/filters/src/app/app.component.ts new file mode 100644 index 0000000000..8ad9a1f7b0 --- /dev/null +++ b/examples/angular/filters/src/app/app.component.ts @@ -0,0 +1,117 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + signal, +} from '@angular/core' +import { + ColumnDef, + type ColumnFiltersState, + createAngularTable, + FlexRenderDirective, + getCoreRowModel, + getFacetedMinMaxValues, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, +} from '@tanstack/angular-table' +import { FilterComponent } from './table-filter.component' +import { makeData, type Person } from './makeData' +import { FormsModule } from '@angular/forms' +import { NgClass } from '@angular/common' + +@Component({ + selector: 'app-root', + standalone: true, + imports: [FilterComponent, FlexRenderDirective, FormsModule, NgClass], + templateUrl: './app.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppComponent { + readonly columnFilters = signal([]) + readonly data = signal(makeData(5000)) + + readonly columns: ColumnDef[] = [ + { + accessorKey: 'firstName', + cell: info => info.getValue(), + }, + { + accessorFn: row => row.lastName, + id: 'lastName', + cell: info => info.getValue(), + header: () => 'Last Name', + }, + { + accessorKey: 'age', + header: () => 'Age', + meta: { + filterVariant: 'range', + }, + }, + { + accessorKey: 'visits', + header: () => 'Visits', + meta: { + filterVariant: 'range', + }, + }, + { + accessorKey: 'status', + header: 'Status', + meta: { + filterVariant: 'select', + }, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + meta: { + filterVariant: 'range', + }, + }, + ] + + table = createAngularTable(() => ({ + columns: this.columns, + data: this.data(), + state: { + columnFilters: this.columnFilters(), + }, + onColumnFiltersChange: updaterOrValue => { + typeof updaterOrValue === 'function' + ? this.columnFilters.update(updaterOrValue) + : this.columnFilters.set(updaterOrValue) + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), //client-side filtering + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFacetedRowModel: getFacetedRowModel(), // client-side faceting + getFacetedUniqueValues: getFacetedUniqueValues(), // generate unique values for select filter/autocomplete + getFacetedMinMaxValues: getFacetedMinMaxValues(), // generate min/max values for range filter + debugTable: true, + debugHeaders: true, + debugColumns: false, + })) + + readonly stringifiedFilters = computed(() => + JSON.stringify(this.columnFilters(), null, 2) + ) + + onPageInputChange(event: Event): void { + const inputElement = event.target as HTMLInputElement + const page = inputElement.value ? Number(inputElement.value) - 1 : 0 + this.table.setPageIndex(page) + } + + onPageSizeChange(event: any): void { + this.table.setPageSize(Number(event.target.value)) + } + + refreshData(): void { + this.data.set(makeData(100_000)) // stress test + } +} diff --git a/examples/angular/filters/src/app/app.config.ts b/examples/angular/filters/src/app/app.config.ts new file mode 100644 index 0000000000..f27099f33c --- /dev/null +++ b/examples/angular/filters/src/app/app.config.ts @@ -0,0 +1,5 @@ +import { ApplicationConfig } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [], +} diff --git a/examples/angular/filters/src/app/debounced-input.directive.ts b/examples/angular/filters/src/app/debounced-input.directive.ts new file mode 100644 index 0000000000..19d5ef1b55 --- /dev/null +++ b/examples/angular/filters/src/app/debounced-input.directive.ts @@ -0,0 +1,37 @@ +import { Directive, ElementRef, inject, input, NgZone } from '@angular/core' +import { + debounceTime, + fromEvent, + type MonoTypeOperatorFunction, + Observable, + switchMap, +} from 'rxjs' +import { outputFromObservable, toObservable } from '@angular/core/rxjs-interop' + +export function runOutsideAngular( + zone: NgZone +): MonoTypeOperatorFunction { + return source => + new Observable(subscriber => + zone.runOutsideAngular(() => source.subscribe(subscriber)) + ) +} + +@Directive({ + standalone: true, + selector: 'input[debouncedInput]', +}) +export class DebouncedInputDirective { + #ref = inject(ElementRef).nativeElement as HTMLInputElement + + readonly debounce = input(500) + readonly debounce$ = toObservable(this.debounce) + + readonly changeEvent = outputFromObservable( + this.debounce$.pipe( + switchMap(debounce => { + return fromEvent(this.#ref, 'change').pipe(debounceTime(debounce)) + }) + ) + ) +} diff --git a/examples/angular/filters/src/app/makeData.ts b/examples/angular/filters/src/app/makeData.ts new file mode 100644 index 0000000000..331dd1eb19 --- /dev/null +++ b/examples/angular/filters/src/app/makeData.ts @@ -0,0 +1,48 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' + subRows?: Person[] +} + +const range = (len: number) => { + const arr: number[] = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0]!, + } +} + +export function makeData(...lens: number[]) { + const makeDataLevel = (depth = 0): Person[] => { + const len = lens[depth]! + return range(len).map((d): Person => { + return { + ...newPerson(), + subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined, + } + }) + } + + return makeDataLevel() +} diff --git a/examples/angular/filters/src/app/table-filter.component.ts b/examples/angular/filters/src/app/table-filter.component.ts new file mode 100644 index 0000000000..2c897cfe7d --- /dev/null +++ b/examples/angular/filters/src/app/table-filter.component.ts @@ -0,0 +1,136 @@ +import { CommonModule } from '@angular/common' +import { Component, computed, input, OnInit } from '@angular/core' +import type { Column, RowData, Table } from '@tanstack/angular-table' +import { DebouncedInputDirective } from './debounced-input.directive' + +declare module '@tanstack/angular-table' { + //allows us to define custom properties for our columns + interface ColumnMeta { + filterVariant?: 'text' | 'range' | 'select' + } +} + +@Component({ + selector: 'app-table-filter', + template: ` + @if (filterVariant() === 'range') { +
+
+ + + +
+
+
+ } @else if (filterVariant() === 'select') { + + } @else { + + @for (value of sortedUniqueValues(); track value) { + + } + + +
+ } + `, + standalone: true, + imports: [CommonModule, DebouncedInputDirective], +}) +export class FilterComponent { + column = input.required>() + + table = input.required>() + + readonly filterVariant = computed(() => { + return (this.column().columnDef.meta ?? {}).filterVariant + }) + + readonly columnFilterValue = computed(() => + this.column().getFilterValue() + ) + + readonly minRangePlaceholder = computed(() => { + return `Min ${ + this.column().getFacetedMinMaxValues()?.[0] !== undefined + ? `(${this.column().getFacetedMinMaxValues()?.[0]})` + : '' + }` + }) + + readonly maxRangePlaceholder = computed(() => { + return `Max ${ + this.column().getFacetedMinMaxValues()?.[1] + ? `(${this.column().getFacetedMinMaxValues()?.[1]})` + : '' + }` + }) + + readonly sortedUniqueValues = computed(() => { + const filterVariant = this.filterVariant() + const column = this.column() + if (filterVariant === 'range') { + return [] + } + return Array.from(column.getFacetedUniqueValues().keys()) + .sort() + .slice(0, 5000) + }) + + readonly changeMinRangeValue = (event: Event) => { + const value = (event.target as HTMLInputElement).value + this.column().setFilterValue((old: [number, number]) => { + return [value, old?.[1]] + }) + } + + readonly changeMaxRangeValue = (event: Event) => { + const value = (event.target as HTMLInputElement).value + this.column().setFilterValue((old: [number, number]) => { + return [old?.[0], value] + }) + } +} diff --git a/examples/angular/filters/src/assets/.gitkeep b/examples/angular/filters/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/angular/filters/src/favicon.ico b/examples/angular/filters/src/favicon.ico new file mode 100644 index 0000000000..57614f9c96 Binary files /dev/null and b/examples/angular/filters/src/favicon.ico differ diff --git a/examples/angular/filters/src/index.html b/examples/angular/filters/src/index.html new file mode 100644 index 0000000000..27917d2b28 --- /dev/null +++ b/examples/angular/filters/src/index.html @@ -0,0 +1,14 @@ + + + + + Selection + + + + + + + + + diff --git a/examples/angular/filters/src/main.ts b/examples/angular/filters/src/main.ts new file mode 100644 index 0000000000..0c3b92057c --- /dev/null +++ b/examples/angular/filters/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser' +import { appConfig } from './app/app.config' +import { AppComponent } from './app/app.component' + +bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)) diff --git a/examples/angular/row-selection/src/app/app.component.scss b/examples/angular/filters/src/styles.scss similarity index 81% rename from examples/angular/row-selection/src/app/app.component.scss rename to examples/angular/filters/src/styles.scss index cda3113f7d..43c09e0f6b 100644 --- a/examples/angular/row-selection/src/app/app.component.scss +++ b/examples/angular/filters/src/styles.scss @@ -24,9 +24,3 @@ tfoot { tfoot th { font-weight: normal; } - -.pagination-actions { - margin: 10px; - display: flex; - gap: 10px; -} diff --git a/examples/angular/filters/tsconfig.app.json b/examples/angular/filters/tsconfig.app.json new file mode 100644 index 0000000000..84f1f992d2 --- /dev/null +++ b/examples/angular/filters/tsconfig.app.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"] +} diff --git a/examples/angular/filters/tsconfig.json b/examples/angular/filters/tsconfig.json new file mode 100644 index 0000000000..fd2d87ac26 --- /dev/null +++ b/examples/angular/filters/tsconfig.json @@ -0,0 +1,30 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": ["ES2022", "dom"] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/examples/angular/filters/tsconfig.spec.json b/examples/angular/filters/tsconfig.spec.json new file mode 100644 index 0000000000..47e3dd7551 --- /dev/null +++ b/examples/angular/filters/tsconfig.spec.json @@ -0,0 +1,9 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": ["jasmine"] + }, + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/examples/angular/row-selection/src/app/app.component.html b/examples/angular/row-selection/src/app/app.component.html index 6842ffd3f4..c2dfb725f7 100644 --- a/examples/angular/row-selection/src/app/app.component.html +++ b/examples/angular/row-selection/src/app/app.component.html @@ -8,32 +8,23 @@ @for (header of headerGroup.headers; track header.id) { @if (!header.isPlaceholder) { - @if (header.id == 'select') { - - } @else { - - {{ headerCell }} - + + {{ headerCell }} + - @if (header.column.getCanFilter()) { -
- -
- } + @if (header.column.getCanFilter()) { +
+ +
} } @@ -46,25 +37,15 @@ @for (cell of row.getVisibleCells(); track cell.id) { - @if (cell.id.endsWith('select')) { - - } @else { - - {{ renderCell }} - - } + + {{ renderCell }} + } @@ -165,3 +146,7 @@
{{ stringifiedRowSelection() }}
+ + + Age 🥳 + diff --git a/examples/angular/row-selection/src/app/app.component.ts b/examples/angular/row-selection/src/app/app.component.ts index a1b2c691fb..8711fd3959 100644 --- a/examples/angular/row-selection/src/app/app.component.ts +++ b/examples/angular/row-selection/src/app/app.component.ts @@ -1,32 +1,109 @@ -import { Component, computed, signal } from '@angular/core' import { + ChangeDetectionStrategy, + Component, + computed, + signal, + TemplateRef, + viewChild, +} from '@angular/core' +import { + ColumnDef, createAngularTable, + FlexRenderComponent, FlexRenderDirective, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, RowSelectionState, } from '@tanstack/angular-table' -import { columns } from './columns' import { FilterComponent } from './filter' -import { makeData } from './makeData' +import { makeData, type Person } from './makeData' import { FormsModule } from '@angular/forms' +import { + TableHeadSelectionComponent, + TableRowSelectionComponent, +} from './selection-column.component' @Component({ selector: 'app-root', standalone: true, imports: [FilterComponent, FlexRenderDirective, FormsModule], templateUrl: './app.component.html', - styleUrl: './app.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppComponent { private readonly rowSelection = signal({}) readonly globalFilter = signal('') readonly data = signal(makeData(10_000)) + readonly ageHeaderCell = + viewChild.required>('ageHeaderCell') + + readonly columns: ColumnDef[] = [ + { + id: 'select', + header: () => { + return new FlexRenderComponent(TableHeadSelectionComponent) + }, + cell: () => { + return new FlexRenderComponent(TableRowSelectionComponent) + }, + }, + { + header: 'Name', + footer: props => props.column.id, + columns: [ + { + accessorKey: 'firstName', + cell: info => info.getValue(), + footer: props => props.column.id, + header: 'First name', + }, + { + accessorFn: row => row.lastName, + id: 'lastName', + cell: info => info.getValue(), + header: () => 'Last Name', + footer: props => props.column.id, + }, + ], + }, + { + header: 'Info', + footer: props => props.column.id, + columns: [ + { + accessorKey: 'age', + header: () => this.ageHeaderCell(), + footer: props => props.column.id, + }, + { + header: 'More Info', + columns: [ + { + accessorKey: 'visits', + header: () => 'Visits', + footer: props => props.column.id, + }, + { + accessorKey: 'status', + header: 'Status', + footer: props => props.column.id, + }, + { + accessorKey: 'progress', + header: 'Profile Progress', + footer: props => props.column.id, + }, + ], + }, + ], + }, + ] + table = createAngularTable(() => ({ data: this.data(), - columns: columns, + columns: this.columns, state: { rowSelection: this.rowSelection(), }, @@ -45,10 +122,6 @@ export class AppComponent { debugTable: true, })) - constructor() { - console.log('table', this.table) - } - readonly stringifiedRowSelection = computed(() => JSON.stringify(this.rowSelection(), null, 2) ) diff --git a/examples/angular/row-selection/src/app/columns.ts b/examples/angular/row-selection/src/app/columns.ts deleted file mode 100644 index 9be530ef80..0000000000 --- a/examples/angular/row-selection/src/app/columns.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ColumnDef } from '@tanstack/angular-table' - -export type Person = { - firstName: string - lastName: string - age: number - visits: number - progress: number - status: 'relationship' | 'complicated' | 'single' - subRows?: Person[] -} -export const columns: ColumnDef[] = [ - { - id: 'select', - header: ({ table }) => table, - cell: ({ row }) => row, - }, - { - header: 'Name', - footer: props => props.column.id, - columns: [ - { - accessorKey: 'firstName', - cell: info => info.getValue(), - footer: props => props.column.id, - header: 'First Name', - }, - { - accessorFn: row => row.lastName, - id: 'lastName', - cell: info => info.getValue(), - header: () => 'Last Name', - footer: props => props.column.id, - }, - ], - }, - { - header: 'Info', - footer: props => props.column.id, - columns: [ - { - accessorKey: 'age', - header: () => 'Age', - footer: props => props.column.id, - }, - { - header: 'More Info', - columns: [ - { - accessorKey: 'visits', - header: () => 'Visits', - footer: props => props.column.id, - }, - { - accessorKey: 'status', - header: 'Status', - footer: props => props.column.id, - }, - { - accessorKey: 'progress', - header: 'Profile Progress', - footer: props => props.column.id, - }, - ], - }, - ], - }, -] diff --git a/examples/angular/row-selection/src/app/selection-column.component.ts b/examples/angular/row-selection/src/app/selection-column.component.ts new file mode 100644 index 0000000000..b4f3e1c008 --- /dev/null +++ b/examples/angular/row-selection/src/app/selection-column.component.ts @@ -0,0 +1,43 @@ +import { + type CellContext, + type HeaderContext, + injectFlexRenderContext, +} from '@tanstack/angular-table' +import { ChangeDetectionStrategy, Component } from '@angular/core' + +@Component({ + template: ` + + `, + host: { + class: 'px-1 block', + }, + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TableHeadSelectionComponent { + context = injectFlexRenderContext>() +} + +@Component({ + template: ` + + `, + host: { + class: 'px-1 block', + }, + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TableRowSelectionComponent { + context = injectFlexRenderContext>() +} diff --git a/examples/angular/signal-input/angular.json b/examples/angular/signal-input/angular.json index 4a48e1faf9..1ce36507d6 100644 --- a/examples/angular/signal-input/angular.json +++ b/examples/angular/signal-input/angular.json @@ -17,7 +17,7 @@ "build": { "builder": "@angular-devkit/build-angular:application", "options": { - "outputPath": "dist/grouping", + "outputPath": "dist/signal-input", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], 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 daad82c9ac..29a8622ce9 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,12 +1,4 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - effect, - inject, - input, - model, -} from '@angular/core' +import { ChangeDetectionStrategy, Component, input, model } from '@angular/core' import type { Person } from '../makeData' import { ColumnDef, @@ -19,6 +11,7 @@ import { getPaginationRowModel, PaginationState, } from '@tanstack/angular-table' + @Component({ selector: 'app-person-table', templateUrl: 'person-table.component.html', @@ -74,10 +67,4 @@ export class PersonTableComponent { onPageSizeChange(event: any) { this.table.setPageSize(Number(event.target.value)) } - - constructor() { - setTimeout(() => { - console.log({ ...this.table }) - }, 1000) - } } diff --git a/packages/angular-table/src/flex-render.ts b/packages/angular-table/src/flex-render.ts index de2a021df4..e5d710a065 100644 --- a/packages/angular-table/src/flex-render.ts +++ b/packages/angular-table/src/flex-render.ts @@ -1,50 +1,149 @@ import { + ChangeDetectorRef, + ComponentRef, Directive, + type DoCheck, + EmbeddedViewRef, + inject, + InjectionToken, + Injector, Input, type OnInit, TemplateRef, + type Type, ViewContainerRef, } from '@angular/core' +type FlexRenderContent> = + | string + | FlexRenderComponent + | TemplateRef<{ $implicit: TProps }> + @Directive({ selector: '[flexRender]', standalone: true, }) -export class FlexRenderDirective implements OnInit { - @Input({ required: true }) - flexRender!: any | ((props: any) => any) +export class FlexRenderDirective> + implements OnInit, DoCheck +{ + @Input({ required: true, alias: 'flexRender' }) + content: string | ((props: TProps) => FlexRenderContent) | undefined = + undefined + + @Input({ required: true, alias: 'flexRenderProps' }) + props: TProps = {} as TProps - @Input({ required: true }) - flexRenderProps!: any + @Input({ required: false, alias: 'flexRenderInjector' }) + injector: Injector = inject(Injector) constructor( private viewContainerRef: ViewContainerRef, private templateRef: TemplateRef ) {} + ref?: ComponentRef | EmbeddedViewRef | null = null + ngOnInit(): void { - this.renderComponent() + this.ref = this.render() + } + + ngDoCheck() { + if (this.ref instanceof ComponentRef) { + this.ref.injector.get(ChangeDetectorRef).markForCheck() + } else if (this.ref instanceof EmbeddedViewRef) { + this.ref.markForCheck() + } } - renderComponent() { + render() { this.viewContainerRef.clear() - if (!this.flexRender) { + const { content, props } = this + if (!this.content) { return null } - if (typeof this.flexRender === 'string') { - const getContext = () => this.flexRender - this.viewContainerRef.createEmbeddedView(this.templateRef, { - get $implicit() { - return getContext() - }, - }) - } else if (typeof this.flexRender === 'function') { - const getContext = () => this.flexRender(this.flexRenderProps) - this.viewContainerRef.createEmbeddedView(this.templateRef, { - get $implicit() { - return getContext() - }, - }) + + if (typeof content === 'string') { + return this.renderStringContent() + } + if (typeof content === 'function') { + return this.renderContent(content(props)) + } + return null + } + + private renderContent(content: FlexRenderContent) { + if (typeof content === 'string') { + return this.renderStringContent() + } + if (content instanceof TemplateRef) { + return this.viewContainerRef.createEmbeddedView( + content, + this.getTemplateRefContext() + ) + } + return this.renderComponent(content) + } + + private renderStringContent() { + const context = () => { + return typeof this.content === 'string' + ? this.content + : this.content?.(this.props) + } + return this.viewContainerRef.createEmbeddedView(this.templateRef, { + get $implicit() { + return context() + }, + }) + } + + private renderComponent(flexRenderComponent: FlexRenderComponent) { + const { component, inputs, injector } = flexRenderComponent + + const getContext = () => this.props + + const proxy = new Proxy(this.props, { + get: (_, key) => getContext()?.[key as keyof typeof _], + }) + + const componentInjector = Injector.create({ + parent: injector ?? this.injector, + providers: [{ provide: FlexRenderComponentProps, useValue: proxy }], + }) + + const componentRef = this.viewContainerRef.createComponent(component, { + injector: componentInjector, + }) + for (const prop in inputs) { + if (componentRef.instance?.hasOwnProperty(prop)) { + componentRef.setInput(prop, inputs[prop]) + } + } + return componentRef + } + + private getTemplateRefContext() { + const getContext = () => this.props + return { + get $implicit() { + return getContext() + }, } } } + +export class FlexRenderComponent> { + constructor( + readonly component: Type, + readonly inputs: T = {} as T, + readonly injector?: Injector + ) {} +} + +const FlexRenderComponentProps = new InjectionToken>( + '[@tanstack/angular-table] Flex render component context props' +) + +export function injectFlexRenderContext>(): T { + return inject(FlexRenderComponentProps) +} diff --git a/packages/angular-table/src/index.ts b/packages/angular-table/src/index.ts index f2d6734a9c..20e2db99b3 100644 --- a/packages/angular-table/src/index.ts +++ b/packages/angular-table/src/index.ts @@ -4,6 +4,7 @@ import { inject, Injector, runInInjectionContext, + type Signal, signal, untracked, } from '@angular/core' @@ -19,11 +20,15 @@ import { lazyInit } from './lazy-signal-initializer' export * from '@tanstack/table-core' -export { FlexRenderDirective } from './flex-render' +export { + FlexRenderDirective, + FlexRenderComponent, + injectFlexRenderContext, +} from './flex-render' export function createAngularTable( options: () => TableOptions -): Table { +): Table & Signal> { const injector = inject(Injector) return lazyInit(() => @@ -44,8 +49,8 @@ export function createAngularTable( const state = signal(table.initialState) function updateOptions() { - const tableState = state() - const resolvedOptions = resolvedOptionsSignal() + const tableState = untracked(state) + const resolvedOptions = untracked(resolvedOptionsSignal) untracked(() => { table.setOptions(prev => ({ ...prev, @@ -63,11 +68,11 @@ export function createAngularTable( updateOptions() - let skip = true + let firstRender = true effect(() => { void [state(), resolvedOptionsSignal()] - if (skip) { - return (skip = false) + if (firstRender) { + return (firstRender = false) } untracked(() => { updateOptions() @@ -75,7 +80,7 @@ export function createAngularTable( }) }) - const tableValue = computed( + const tableSignal = computed( () => { notifier() return table @@ -85,7 +90,7 @@ export function createAngularTable( } ) - return proxifyTable(tableValue) + return proxifyTable(tableSignal) }) ) } diff --git a/packages/angular-table/src/lazy-signal-initializer.ts b/packages/angular-table/src/lazy-signal-initializer.ts index 576d745a3d..65ca4037cd 100644 --- a/packages/angular-table/src/lazy-signal-initializer.ts +++ b/packages/angular-table/src/lazy-signal-initializer.ts @@ -15,7 +15,16 @@ export function lazyInit(initializer: () => T): T { queueMicrotask(() => initializeObject()) - return new Proxy({} as T, { + function table() {} + + return new Proxy(table as T, { + apply(target: T, thisArg: any, argArray: any[]): any { + initializeObject() + if (typeof object === 'function') { + return Reflect.apply(object, thisArg, argArray) + } + return Reflect.apply(target as any, thisArg, argArray) + }, get(_, prop, receiver) { initializeObject() return Reflect.get(object as T, prop, receiver) diff --git a/packages/angular-table/src/proxy.ts b/packages/angular-table/src/proxy.ts index e39873e49c..93de38aa7d 100644 --- a/packages/angular-table/src/proxy.ts +++ b/packages/angular-table/src/proxy.ts @@ -1,11 +1,18 @@ import { computed, type Signal, untracked } from '@angular/core' import { type Table } from '@tanstack/table-core' -export function proxifyTable(tableSignal: Signal>): Table { - const internalState = {} as Table +type TableSignal = Table & Signal> + +export function proxifyTable( + tableSignal: Signal> +): Table & Signal> { + const internalState = tableSignal as TableSignal return new Proxy(internalState, { - get(target: Table, property: keyof Table): any { + apply() { + return tableSignal() + }, + get(target, property: keyof Table): any { if (target[property]) { return target[property] } @@ -28,7 +35,6 @@ export function proxifyTable(tableSignal: Signal>): Table { return target[property] } } - // @ts-expect-error return (target[property] = table[property]) }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98099223dd..212913d9ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -178,6 +178,216 @@ importers: specifier: 5.4.5 version: 5.4.5 + examples/angular/column-ordering: + dependencies: + '@angular/animations': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1) + '@angular/common': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1)(rxjs@7.8.1) + '@angular/compiler': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1) + '@angular/core': + specifier: ^17.3.1 + version: 17.3.1(rxjs@7.8.1)(zone.js@0.14.4) + '@angular/forms': + specifier: ^17.3.1 + version: 17.3.6(@angular/common@17.3.6)(@angular/core@17.3.1)(@angular/platform-browser@17.3.6)(rxjs@7.8.1) + '@angular/platform-browser': + specifier: ^17.3.1 + version: 17.3.6(@angular/animations@17.3.6)(@angular/common@17.3.6)(@angular/core@17.3.1) + '@angular/platform-browser-dynamic': + specifier: ^17.3.1 + version: 17.3.6(@angular/common@17.3.6)(@angular/compiler@17.3.6)(@angular/core@17.3.1)(@angular/platform-browser@17.3.6) + '@tanstack/angular-table': + specifier: ^8.14.0 + version: link:../../../packages/angular-table + rxjs: + specifier: ~7.8.1 + version: 7.8.1 + zone.js: + specifier: ~0.14.4 + version: 0.14.4 + devDependencies: + '@angular-devkit/build-angular': + specifier: ^17.3.1 + version: 17.3.6(@angular/compiler-cli@17.3.6)(@types/node@20.12.7)(karma@6.4.3)(ng-packagr@17.3.0)(typescript@5.4.5) + '@angular/cli': + specifier: ^17.3.1 + version: 17.3.6 + '@angular/compiler-cli': + specifier: ^17.3.1 + version: 17.3.6(@angular/compiler@17.3.6)(typescript@5.4.5) + '@types/jasmine': + specifier: ~5.1.4 + version: 5.1.4 + jasmine-core: + specifier: ~5.1.2 + version: 5.1.2 + karma: + specifier: ~6.4.3 + version: 6.4.3 + karma-chrome-launcher: + specifier: ~3.2.0 + version: 3.2.0 + karma-coverage: + specifier: ~2.2.1 + version: 2.2.1 + karma-jasmine: + specifier: ~5.1.0 + version: 5.1.0(karma@6.4.3) + karma-jasmine-html-reporter: + specifier: ~2.1.0 + version: 2.1.0(jasmine-core@5.1.2)(karma-jasmine@5.1.0)(karma@6.4.3) + tslib: + specifier: ^2.6.2 + version: 2.6.2 + typescript: + specifier: 5.4.5 + version: 5.4.5 + + examples/angular/column-pinning: + dependencies: + '@angular/animations': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1) + '@angular/common': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1)(rxjs@7.8.1) + '@angular/compiler': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1) + '@angular/core': + specifier: ^17.3.1 + version: 17.3.1(rxjs@7.8.1)(zone.js@0.14.4) + '@angular/forms': + specifier: ^17.3.1 + version: 17.3.6(@angular/common@17.3.6)(@angular/core@17.3.1)(@angular/platform-browser@17.3.6)(rxjs@7.8.1) + '@angular/platform-browser': + specifier: ^17.3.1 + version: 17.3.6(@angular/animations@17.3.6)(@angular/common@17.3.6)(@angular/core@17.3.1) + '@angular/platform-browser-dynamic': + specifier: ^17.3.1 + version: 17.3.6(@angular/common@17.3.6)(@angular/compiler@17.3.6)(@angular/core@17.3.1)(@angular/platform-browser@17.3.6) + '@tanstack/angular-table': + specifier: ^8.14.0 + version: link:../../../packages/angular-table + rxjs: + specifier: ~7.8.1 + version: 7.8.1 + zone.js: + specifier: ~0.14.4 + version: 0.14.4 + devDependencies: + '@angular-devkit/build-angular': + specifier: ^17.3.1 + version: 17.3.6(@angular/compiler-cli@17.3.6)(@types/node@20.12.7)(karma@6.4.3)(ng-packagr@17.3.0)(typescript@5.4.5) + '@angular/cli': + specifier: ^17.3.1 + version: 17.3.6 + '@angular/compiler-cli': + specifier: ^17.3.1 + version: 17.3.6(@angular/compiler@17.3.6)(typescript@5.4.5) + '@types/jasmine': + specifier: ~5.1.4 + version: 5.1.4 + jasmine-core: + specifier: ~5.1.2 + version: 5.1.2 + karma: + specifier: ~6.4.3 + version: 6.4.3 + karma-chrome-launcher: + specifier: ~3.2.0 + version: 3.2.0 + karma-coverage: + specifier: ~2.2.1 + version: 2.2.1 + karma-jasmine: + specifier: ~5.1.0 + version: 5.1.0(karma@6.4.3) + karma-jasmine-html-reporter: + specifier: ~2.1.0 + version: 2.1.0(jasmine-core@5.1.2)(karma-jasmine@5.1.0)(karma@6.4.3) + tslib: + specifier: ^2.6.2 + version: 2.6.2 + typescript: + specifier: 5.4.5 + version: 5.4.5 + + examples/angular/column-pinning-sticky: + dependencies: + '@angular/animations': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1) + '@angular/common': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1)(rxjs@7.8.1) + '@angular/compiler': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1) + '@angular/core': + specifier: ^17.3.1 + version: 17.3.1(rxjs@7.8.1)(zone.js@0.14.4) + '@angular/forms': + specifier: ^17.3.1 + version: 17.3.6(@angular/common@17.3.6)(@angular/core@17.3.1)(@angular/platform-browser@17.3.6)(rxjs@7.8.1) + '@angular/platform-browser': + specifier: ^17.3.1 + version: 17.3.6(@angular/animations@17.3.6)(@angular/common@17.3.6)(@angular/core@17.3.1) + '@angular/platform-browser-dynamic': + specifier: ^17.3.1 + version: 17.3.6(@angular/common@17.3.6)(@angular/compiler@17.3.6)(@angular/core@17.3.1)(@angular/platform-browser@17.3.6) + '@tanstack/angular-table': + specifier: ^8.14.0 + version: link:../../../packages/angular-table + rxjs: + specifier: ~7.8.1 + version: 7.8.1 + zone.js: + specifier: ~0.14.4 + version: 0.14.4 + devDependencies: + '@angular-devkit/build-angular': + specifier: ^17.3.1 + version: 17.3.6(@angular/compiler-cli@17.3.6)(@types/node@20.12.7)(karma@6.4.3)(ng-packagr@17.3.0)(typescript@5.4.5) + '@angular/cli': + specifier: ^17.3.1 + version: 17.3.6 + '@angular/compiler-cli': + specifier: ^17.3.1 + version: 17.3.6(@angular/compiler@17.3.6)(typescript@5.4.5) + '@types/jasmine': + specifier: ~5.1.4 + version: 5.1.4 + jasmine-core: + specifier: ~5.1.2 + version: 5.1.2 + karma: + specifier: ~6.4.3 + version: 6.4.3 + karma-chrome-launcher: + specifier: ~3.2.0 + version: 3.2.0 + karma-coverage: + specifier: ~2.2.1 + version: 2.2.1 + karma-jasmine: + specifier: ~5.1.0 + version: 5.1.0(karma@6.4.3) + karma-jasmine-html-reporter: + specifier: ~2.1.0 + version: 2.1.0(jasmine-core@5.1.2)(karma-jasmine@5.1.0)(karma@6.4.3) + tslib: + specifier: ^2.6.2 + version: 2.6.2 + typescript: + specifier: 5.4.5 + version: 5.4.5 + examples/angular/column-visibility: dependencies: '@angular/animations': @@ -248,6 +458,79 @@ importers: specifier: 5.4.5 version: 5.4.5 + examples/angular/filters: + dependencies: + '@angular/animations': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1) + '@angular/common': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1)(rxjs@7.8.1) + '@angular/compiler': + specifier: ^17.3.1 + version: 17.3.6(@angular/core@17.3.1) + '@angular/core': + specifier: ^17.3.1 + version: 17.3.1(rxjs@7.8.1)(zone.js@0.14.4) + '@angular/forms': + specifier: ^17.3.1 + version: 17.3.6(@angular/common@17.3.6)(@angular/core@17.3.1)(@angular/platform-browser@17.3.6)(rxjs@7.8.1) + '@angular/platform-browser': + specifier: ^17.3.1 + version: 17.3.6(@angular/animations@17.3.6)(@angular/common@17.3.6)(@angular/core@17.3.1) + '@angular/platform-browser-dynamic': + specifier: ^17.3.1 + version: 17.3.6(@angular/common@17.3.6)(@angular/compiler@17.3.6)(@angular/core@17.3.1)(@angular/platform-browser@17.3.6) + '@faker-js/faker': + specifier: ^8.4.1 + version: 8.4.1 + '@tanstack/angular-table': + specifier: ^8.14.0 + version: link:../../../packages/angular-table + rxjs: + specifier: ~7.8.1 + version: 7.8.1 + tslib: + specifier: ^2.6.2 + version: 2.6.2 + zone.js: + specifier: ~0.14.4 + version: 0.14.4 + devDependencies: + '@angular-devkit/build-angular': + specifier: ^17.3.1 + version: 17.3.6(@angular/compiler-cli@17.3.6)(@types/node@20.12.7)(karma@6.4.3)(ng-packagr@17.3.0)(typescript@5.4.5) + '@angular/cli': + specifier: ^17.3.1 + version: 17.3.6 + '@angular/compiler-cli': + specifier: ^17.3.1 + version: 17.3.6(@angular/compiler@17.3.6)(typescript@5.4.5) + '@types/jasmine': + specifier: ~5.1.4 + version: 5.1.4 + jasmine-core: + specifier: ~5.1.2 + version: 5.1.2 + karma: + specifier: ~6.4.3 + version: 6.4.3 + karma-chrome-launcher: + specifier: ~3.2.0 + version: 3.2.0 + karma-coverage: + specifier: ~2.2.1 + version: 2.2.1 + karma-jasmine: + specifier: ~5.1.0 + version: 5.1.0(karma@6.4.3) + karma-jasmine-html-reporter: + specifier: ~2.1.0 + version: 2.1.0(jasmine-core@5.1.2)(karma-jasmine@5.1.0)(karma@6.4.3) + typescript: + specifier: 5.4.5 + version: 5.4.5 + examples/angular/grouping: dependencies: '@angular/animations': @@ -3916,7 +4199,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.24.0 - '@babel/helper-create-class-features-plugin': 7.24.1(@babel/core@7.24.0) + '@babel/helper-create-class-features-plugin': 7.24.4(@babel/core@7.24.0) '@babel/helper-plugin-utils': 7.24.0 dev: true @@ -14039,7 +14322,7 @@ packages: peerDependencies: solid-js: ^1.3 dependencies: - '@babel/generator': 7.24.1 + '@babel/generator': 7.24.4 '@babel/helper-module-imports': 7.24.3 '@babel/types': 7.24.0 solid-js: 1.8.17