argent-grid 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.github/workflows/pages.yml +68 -0
  2. package/AGENTS.md +179 -0
  3. package/README.md +222 -0
  4. package/demo-app/README.md +70 -0
  5. package/demo-app/angular.json +78 -0
  6. package/demo-app/e2e/benchmark.spec.ts +53 -0
  7. package/demo-app/e2e/demo-page.spec.ts +77 -0
  8. package/demo-app/e2e/grid-features.spec.ts +269 -0
  9. package/demo-app/package-lock.json +14023 -0
  10. package/demo-app/package.json +36 -0
  11. package/demo-app/playwright-test-menu.js +19 -0
  12. package/demo-app/playwright.config.ts +23 -0
  13. package/demo-app/src/app/app.component.ts +10 -0
  14. package/demo-app/src/app/app.config.ts +13 -0
  15. package/demo-app/src/app/app.routes.ts +7 -0
  16. package/demo-app/src/app/demo-page/demo-page.component.css +313 -0
  17. package/demo-app/src/app/demo-page/demo-page.component.html +124 -0
  18. package/demo-app/src/app/demo-page/demo-page.component.ts +366 -0
  19. package/demo-app/src/index.html +19 -0
  20. package/demo-app/src/main.ts +6 -0
  21. package/demo-app/tsconfig.json +31 -0
  22. package/ng-package.json +8 -0
  23. package/package.json +60 -0
  24. package/plan.md +131 -0
  25. package/setup-vitest.ts +18 -0
  26. package/src/lib/argent-grid.module.ts +21 -0
  27. package/src/lib/components/argent-grid.component.css +483 -0
  28. package/src/lib/components/argent-grid.component.html +320 -0
  29. package/src/lib/components/argent-grid.component.spec.ts +189 -0
  30. package/src/lib/components/argent-grid.component.ts +1188 -0
  31. package/src/lib/directives/ag-grid-compatibility.directive.ts +92 -0
  32. package/src/lib/rendering/canvas-renderer.ts +962 -0
  33. package/src/lib/rendering/render/blit.spec.ts +453 -0
  34. package/src/lib/rendering/render/blit.ts +393 -0
  35. package/src/lib/rendering/render/cells.ts +369 -0
  36. package/src/lib/rendering/render/index.ts +105 -0
  37. package/src/lib/rendering/render/lines.ts +363 -0
  38. package/src/lib/rendering/render/theme.spec.ts +282 -0
  39. package/src/lib/rendering/render/theme.ts +201 -0
  40. package/src/lib/rendering/render/types.ts +279 -0
  41. package/src/lib/rendering/render/walk.spec.ts +360 -0
  42. package/src/lib/rendering/render/walk.ts +360 -0
  43. package/src/lib/rendering/utils/damage-tracker.spec.ts +444 -0
  44. package/src/lib/rendering/utils/damage-tracker.ts +423 -0
  45. package/src/lib/rendering/utils/index.ts +7 -0
  46. package/src/lib/services/grid.service.spec.ts +1039 -0
  47. package/src/lib/services/grid.service.ts +1284 -0
  48. package/src/lib/types/ag-grid-types.ts +970 -0
  49. package/src/public-api.ts +22 -0
  50. package/tsconfig.json +32 -0
  51. package/tsconfig.lib.json +11 -0
  52. package/tsconfig.spec.json +8 -0
  53. package/vitest.config.ts +55 -0
@@ -0,0 +1,366 @@
1
+ import { Component, OnInit, OnDestroy, AfterViewInit, ViewChild, ChangeDetectorRef } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { ArgentGridModule, ArgentGridComponent } from 'argent-grid';
4
+ import { GridApi, ColDef, IRowNode } from 'argent-grid';
5
+
6
+ interface Employee {
7
+ id: number;
8
+ name: string;
9
+ department: string;
10
+ role: string;
11
+ salary: number;
12
+ salaryTrend: number[];
13
+ location: string;
14
+ startDate: string;
15
+ performance: number;
16
+ }
17
+
18
+ @Component({
19
+ selector: 'app-demo-page',
20
+ standalone: true,
21
+ imports: [CommonModule, ArgentGridModule],
22
+ templateUrl: './demo-page.component.html',
23
+ styleUrls: ['./demo-page.component.css'],
24
+ })
25
+ export class DemoPageComponent implements OnInit, AfterViewInit, OnDestroy {
26
+ @ViewChild(ArgentGridComponent) gridComponent!: ArgentGridComponent<Employee>;
27
+
28
+ rowData: Employee[] = [];
29
+ renderTime = 0;
30
+ canvasFrameTime = 0;
31
+ fps = 0;
32
+ isLoading = false;
33
+ rowCount = 100000;
34
+ isGrouped = false;
35
+ isFloatingFilterShown = true;
36
+ isBenchmarking = false;
37
+ isMasterDetail = false;
38
+ isPivotMode = false;
39
+ isSideBarVisible = true;
40
+ isFiltered = false;
41
+ benchmarkResults: any = null;
42
+
43
+ columnDefs: ColDef<Employee>[] = [
44
+ { field: 'id', headerName: 'ID', width: 80, sortable: true, filter: 'number' },
45
+ { field: 'name', headerName: 'Name', width: 200, sortable: true, filter: 'text' },
46
+ { field: 'department', headerName: 'Department', width: 180, sortable: true, filter: 'text', rowGroup: false },
47
+ { field: 'role', headerName: 'Role', width: 250, filter: 'text' },
48
+ {
49
+ field: 'salary',
50
+ headerName: 'Salary',
51
+ width: 120,
52
+ sortable: true,
53
+ filter: 'number',
54
+ valueFormatter: (params: any) => `$${params.value?.toLocaleString()}`,
55
+ },
56
+ {
57
+ field: 'salaryTrend',
58
+ headerName: 'Salary Trend',
59
+ width: 150,
60
+ sparklineOptions: {
61
+ type: 'area',
62
+ area: {
63
+ fill: 'rgba(74, 222, 128, 0.2)',
64
+ stroke: '#4ade80',
65
+ strokeWidth: 2
66
+ }
67
+ }
68
+ },
69
+ { field: 'location', headerName: 'Location', width: 150, filter: 'text' },
70
+ { field: 'startDate', headerName: 'Start Date', width: 130, filter: 'date' },
71
+ {
72
+ field: 'performance',
73
+ headerName: 'Performance',
74
+ width: 120,
75
+ filter: 'number',
76
+ cellRenderer: (params: any) => {
77
+ const value = params.value;
78
+ const color = value >= 80 ? '#22c55e' : value >= 60 ? '#eab308' : '#ef4444';
79
+ return `<span style="color: ${color}; font-weight: bold; padding: 4px 8px; background: ${color}20; border-radius: 4px;">${value}%</span>`;
80
+ },
81
+ },
82
+ ];
83
+
84
+ gridOptions: any = {
85
+ floatingFilter: true,
86
+ enableRangeSelection: true,
87
+ sideBar: true,
88
+ autoGroupColumnDef: {
89
+ headerName: 'Organization',
90
+ width: 250,
91
+ pinned: 'left'
92
+ }
93
+ };
94
+
95
+ private gridApi?: GridApi<Employee>;
96
+ private fpsInterval?: number;
97
+ private lastFrameTime = 0;
98
+ private fpsUpdateTimer = 0;
99
+
100
+ constructor(
101
+ private cdr: ChangeDetectorRef
102
+ ) {}
103
+
104
+ ngOnInit(): void {
105
+ this.loadData(100000);
106
+ this.startFPSCounter();
107
+ }
108
+
109
+ ngAfterViewInit(): void {
110
+ // Grid is ready after view init
111
+ }
112
+
113
+ toggleGrouping(): void {
114
+ this.isGrouped = !this.isGrouped;
115
+ this.columnDefs = this.columnDefs.map(col => {
116
+ if (col.field === 'department') {
117
+ return { ...col, rowGroup: this.isGrouped };
118
+ }
119
+ return col;
120
+ });
121
+
122
+ if (this.gridApi) {
123
+ this.gridApi.setColumnDefs(this.columnDefs);
124
+ this.gridApi.onFilterChanged(); // Trigger re-processing
125
+ }
126
+
127
+ this.cdr.detectChanges();
128
+ }
129
+
130
+ toggleFloatingFilter(): void {
131
+ this.isFloatingFilterShown = !this.isFloatingFilterShown;
132
+ if (this.gridApi) {
133
+ this.gridApi.setGridOption('floatingFilter', this.isFloatingFilterShown);
134
+ }
135
+ this.cdr.detectChanges();
136
+ }
137
+
138
+ toggleMasterDetail(): void {
139
+ this.isMasterDetail = !this.isMasterDetail;
140
+ if (this.gridApi) {
141
+ this.gridApi.setGridOption('masterDetail', this.isMasterDetail);
142
+ this.gridApi.setGridOption('isRowMaster', (data: any) => data.id % 2 === 0);
143
+ this.gridApi.setRowData([...this.rowData]); // Force refresh
144
+ }
145
+ this.cdr.detectChanges();
146
+ }
147
+
148
+ togglePivotMode(): void {
149
+ this.isPivotMode = !this.isPivotMode;
150
+ if (this.gridApi) {
151
+ this.columnDefs = this.columnDefs.map(col => {
152
+ if (col.field === 'location') {
153
+ return { ...col, pivot: this.isPivotMode };
154
+ }
155
+ if (col.field === 'salary') {
156
+ return { ...col, aggFunc: 'sum' };
157
+ }
158
+ return col;
159
+ });
160
+ this.gridApi.setColumnDefs(this.columnDefs);
161
+ this.gridApi.setPivotMode(this.isPivotMode);
162
+ }
163
+ this.cdr.detectChanges();
164
+ }
165
+
166
+ toggleSideBar(): void {
167
+ this.isSideBarVisible = !this.isSideBarVisible;
168
+ if (this.gridApi) {
169
+ this.gridApi.setGridOption('sideBar', this.isSideBarVisible);
170
+ }
171
+ this.cdr.detectChanges();
172
+ }
173
+
174
+ toggleFilter(): void {
175
+ this.isFiltered = !this.isFiltered;
176
+ if (this.gridApi) {
177
+ if (this.isFiltered) {
178
+ this.gridApi.setFilterModel({
179
+ department: { filterType: 'text', type: 'contains', filter: 'Eng' }
180
+ });
181
+ } else {
182
+ this.gridApi.setFilterModel({});
183
+ }
184
+ }
185
+ this.cdr.detectChanges();
186
+ }
187
+
188
+ startFPSCounter(): void {
189
+ const countFPS = () => {
190
+ const now = performance.now();
191
+ let changed = false;
192
+
193
+ if (this.lastFrameTime) {
194
+ const delta = now - this.lastFrameTime;
195
+
196
+ // Update metrics only every 500ms to reduce change detection cycles
197
+ if (now - this.fpsUpdateTimer > 500) {
198
+ const newFps = Math.round(1000 / delta);
199
+ if (newFps !== this.fps) {
200
+ this.fps = newFps;
201
+ changed = true;
202
+ }
203
+
204
+ // Update canvas frame time if available
205
+ if (this.gridComponent) {
206
+ const newFrameTime = Number(this.gridComponent.getLastFrameTime().toFixed(2));
207
+ if (newFrameTime !== this.canvasFrameTime) {
208
+ this.canvasFrameTime = newFrameTime;
209
+ changed = true;
210
+ }
211
+ }
212
+
213
+ this.fpsUpdateTimer = now;
214
+ }
215
+ }
216
+ this.lastFrameTime = now;
217
+
218
+ // In zoneless mode, we manually trigger change detection for these async updates
219
+ if (changed) {
220
+ this.cdr.detectChanges();
221
+ }
222
+
223
+ this.fpsInterval = requestAnimationFrame(countFPS);
224
+ };
225
+ this.fpsInterval = requestAnimationFrame(countFPS);
226
+ }
227
+
228
+ runBenchmark(): void {
229
+ if (!this.gridApi || !this.gridComponent) return;
230
+
231
+ this.isBenchmarking = true;
232
+ this.benchmarkResults = null;
233
+ this.cdr.detectChanges();
234
+
235
+ const results = {
236
+ initialRender: 0,
237
+ scrollFrameAverage: 0,
238
+ selectionUpdateTime: 0,
239
+ groupingUpdateTime: 0,
240
+ totalTime: 0
241
+ };
242
+
243
+ const startTime = performance.now();
244
+
245
+ // 1. Initial render time (cached from last frame)
246
+ results.initialRender = this.gridComponent.getLastFrameTime();
247
+
248
+ // 2. Selection Update Time
249
+ const selStart = performance.now();
250
+ this.gridApi.selectAll();
251
+ // Wait for render cycle
252
+ setTimeout(() => {
253
+ results.selectionUpdateTime = Number((performance.now() - selStart).toFixed(2));
254
+ this.gridApi?.deselectAll();
255
+
256
+ // 3. Grouping Update Time (Toggle grouping twice)
257
+ const groupStart = performance.now();
258
+ if (!this.isGrouped) {
259
+ this.toggleGrouping(); // Turn on
260
+ setTimeout(() => {
261
+ this.toggleGrouping(); // Turn off
262
+ results.groupingUpdateTime = Number((performance.now() - groupStart).toFixed(2));
263
+
264
+ startScrolling();
265
+ }, 100);
266
+ } else {
267
+ this.toggleGrouping(); // Turn off
268
+ setTimeout(() => {
269
+ this.toggleGrouping(); // Turn on
270
+ results.groupingUpdateTime = Number((performance.now() - groupStart).toFixed(2));
271
+
272
+ startScrolling();
273
+ }, 100);
274
+ }
275
+
276
+ // 4. Scroll performance (programmatic scroll)
277
+ const startScrolling = () => {
278
+ const frameTimes: number[] = [];
279
+ let scrollCount = 0;
280
+ const totalScrollFrames = 30;
281
+ const viewport = this.gridComponent.viewportRef.nativeElement;
282
+
283
+ const runScroll = () => {
284
+ if (scrollCount < totalScrollFrames) {
285
+ viewport.scrollTop += 100;
286
+ frameTimes.push(this.gridComponent.getLastFrameTime());
287
+ scrollCount++;
288
+ requestAnimationFrame(runScroll);
289
+ } else {
290
+ // Finished scroll
291
+ results.scrollFrameAverage = Number((frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length).toFixed(2));
292
+ results.totalTime = Number((performance.now() - startTime).toFixed(2));
293
+ this.benchmarkResults = results;
294
+ this.isBenchmarking = false;
295
+
296
+ // Scroll back up
297
+ viewport.scrollTop = 0;
298
+ this.cdr.detectChanges();
299
+ }
300
+ };
301
+ requestAnimationFrame(runScroll);
302
+ };
303
+ }, 100);
304
+ }
305
+
306
+ loadData(count: number): void {
307
+ this.isLoading = true;
308
+ const startTime = performance.now();
309
+
310
+ const departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance', 'Operations', 'Support', 'Product'];
311
+ const roles = [
312
+ 'Software Engineer', 'Senior Engineer', 'Staff Engineer', 'Principal Engineer',
313
+ 'Engineering Manager', 'Sales Rep', 'Account Executive', 'Marketing Manager',
314
+ 'HR Specialist', 'Financial Analyst', 'Operations Manager', 'Support Specialist',
315
+ 'Product Manager', 'Senior PM', 'Director',
316
+ ];
317
+ const locations = ['New York', 'San Francisco', 'London', 'Singapore', 'Tokyo', 'Berlin', 'Remote'];
318
+
319
+ const data: Employee[] = [];
320
+
321
+ for (let i = 0; i < count; i++) {
322
+ const dept = departments[Math.floor(Math.random() * departments.length)];
323
+ const role = roles[Math.floor(Math.random() * roles.length)];
324
+ const location = locations[Math.floor(Math.random() * locations.length)];
325
+
326
+ data.push({
327
+ id: i + 1,
328
+ name: `Employee ${i + 1}`,
329
+ department: dept,
330
+ role: `${dept} - ${role}`,
331
+ salary: Math.floor(Math.random() * 150000) + 50000,
332
+ salaryTrend: Array.from({ length: 10 }, () => Math.floor(Math.random() * 100)),
333
+ location,
334
+ startDate: new Date(Date.now() - Math.random() * 5 * 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
335
+ performance: Math.floor(Math.random() * 40) + 60,
336
+ });
337
+ }
338
+
339
+ setTimeout(() => {
340
+ this.rowData = data;
341
+ const endTime = performance.now();
342
+ this.renderTime = Math.round(endTime - startTime);
343
+ this.rowCount = count;
344
+ this.isLoading = false;
345
+
346
+ this.cdr.detectChanges();
347
+ console.log(`Loaded ${count} rows in ${this.renderTime}ms`);
348
+ }, 100);
349
+ }
350
+
351
+ onGridReady(api: GridApi<Employee>): void {
352
+ this.gridApi = api;
353
+ (window as any).gridApi = api;
354
+ console.log('Grid ready:', api);
355
+ }
356
+
357
+ onRowClicked(event: { data: Employee; node: IRowNode<Employee> }): void {
358
+ console.log('Row clicked:', event.data);
359
+ }
360
+
361
+ ngOnDestroy(): void {
362
+ if (this.fpsInterval) {
363
+ cancelAnimationFrame(this.fpsInterval);
364
+ }
365
+ }
366
+ }
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>ArgentGrid - Canvas-Based High-Performance Grid Demo</title>
6
+ <base href="/">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <meta name="description" content="Live demo of ArgentGrid - Canvas-based Angular grid with 1M+ rows and 60fps scrolling">
9
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
11
+ <style>
12
+ * { margin: 0; padding: 0; box-sizing: border-box; }
13
+ html, body { height: 100%; font-family: 'Inter', sans-serif; background: #0f0f23; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <app-root></app-root>
18
+ </body>
19
+ </html>
@@ -0,0 +1,6 @@
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { appConfig } from './app/app.config';
3
+ import { AppComponent } from './app/app.component';
4
+
5
+ bootstrapApplication(AppComponent, appConfig)
6
+ .catch((err) => console.error(err));
@@ -0,0 +1,31 @@
1
+ {
2
+ "compileOnSave": false,
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "outDir": "./dist/out-tsc",
6
+ "strict": false,
7
+ "noImplicitOverride": true,
8
+ "noPropertyAccessFromIndexSignature": true,
9
+ "noImplicitReturns": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "skipLibCheck": true,
12
+ "esModuleInterop": true,
13
+ "sourceMap": true,
14
+ "declaration": false,
15
+ "experimentalDecorators": true,
16
+ "moduleResolution": "bundler",
17
+ "importHelpers": true,
18
+ "target": "ES2022",
19
+ "module": "ES2022",
20
+ "lib": ["ES2022", "dom"],
21
+ "paths": {}
22
+ },
23
+ "include": ["src/**/*"],
24
+ "exclude": ["src/**/*.spec.ts"],
25
+ "angularCompilerOptions": {
26
+ "enableI18nLegacyMessageIdFormat": false,
27
+ "strictInjectionParameters": true,
28
+ "strictInputAccessModifiers": true,
29
+ "strictTemplates": true
30
+ }
31
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "dist",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts"
6
+ },
7
+ "allowedNonPeerDependencies": ["exceljs"]
8
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "argent-grid",
3
+ "version": "0.1.0",
4
+ "description": "A free, high-performance alternative to AG Grid Enterprise",
5
+ "author": "hainzhao",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/HainanZhao/ArgentGrid.git"
10
+ },
11
+ "keywords": [
12
+ "angular",
13
+ "grid",
14
+ "data-grid",
15
+ "ag-grid",
16
+ "table",
17
+ "canvas",
18
+ "performance"
19
+ ],
20
+ "peerDependencies": {
21
+ "@angular/cdk": "^18.0.0",
22
+ "@angular/common": "^18.0.0",
23
+ "@angular/core": "^18.0.0"
24
+ },
25
+ "dependencies": {
26
+ "exceljs": "^4.4.0",
27
+ "tslib": "^2.6.0"
28
+ },
29
+ "devDependencies": {
30
+ "@angular-devkit/build-angular": "^18.0.0",
31
+ "@angular/animations": "^18.0.0",
32
+ "@angular/cdk": "^18.0.0",
33
+ "@angular/cli": "^18.0.0",
34
+ "@angular/common": "^18.0.0",
35
+ "@angular/compiler": "^18.0.0",
36
+ "@angular/compiler-cli": "^18.0.0",
37
+ "@angular/core": "^18.0.0",
38
+ "@angular/platform-browser": "^18.0.0",
39
+ "@angular/platform-browser-dynamic": "^18.0.0",
40
+ "@types/exceljs": "^1.3.2",
41
+ "@types/node": "^20.0.0",
42
+ "@vitest/coverage-v8": "^3.0.0",
43
+ "jsdom": "^26.0.0",
44
+ "ng-packagr": "^18.0.0",
45
+ "rxjs": "~7.8.0",
46
+ "typescript": "~5.4.2",
47
+ "vite": "^6.0.0",
48
+ "vitest": "^3.0.0"
49
+ },
50
+ "scripts": {
51
+ "build": "ng-packagr -p ng-package.json",
52
+ "build:watch": "ng-packagr -p ng-package.json -w",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "test:coverage": "vitest run --coverage",
56
+ "test:e2e": "cd demo-app && npx playwright test",
57
+ "lint": "eslint src/**/*.ts",
58
+ "clean": "rm -rf dist"
59
+ }
60
+ }
package/plan.md ADDED
@@ -0,0 +1,131 @@
1
+ # ArgentGrid Project Plan
2
+
3
+ > **Goal:** Build a free, high-performance alternative to AG Grid Enterprise using Canvas rendering and a headless logic layer.
4
+
5
+ ## βš–οΈ AG Grid Comparison Matrix
6
+
7
+ | Feature Category | AG Grid Community | AG Grid Enterprise | **ArgentGrid (Current)** |
8
+ | :--- | :--- | :--- | :--- |
9
+ | **Rendering Engine** | DOM-based | DOM-based | **Canvas-based (GPU Opt)** |
10
+ | **Data Volume Limit** | ~100k rows | Millions (SSRM) | **1M+ rows (Client-side)** |
11
+ | **Row Models** | Client-side only | Client, **SSRM, Infinite** | **Client-side only** |
12
+ | **Custom Components** | Header, Cell, Filter | Header, Cell, Filter | **Hardcoded / String-based** |
13
+ | **Sorting & Filtering**| Yes (Basic) | Yes (Advanced) | **Yes (Client-side)** |
14
+ | **Filter Types** | Text, Num, Date | + **Set Filter**, Multi | **Text, Num, Date, Boolean** |
15
+ | **Row Grouping** | No | Yes | **Yes (Hierarchical)** |
16
+ | **Aggregation** | No | Yes | **Yes (Sum/Avg/Min/Max/Cnt)** |
17
+ | **Pivoting** | No | Yes | **Yes (Basic)** |
18
+ | **Master/Detail** | No | Yes | **Yes (Basic)** |
19
+ | **Tree Data** | Basic | Advanced | **Planned (Phase IV)** |
20
+ | **Selection** | Row only | Row + **Range** | **Row + Range (Basic)** |
21
+ | **Excel Export** | No (CSV only) | True .xlsx | **True .xlsx & CSV** |
22
+ | **Context Menu** | No | Yes | **Yes (Basic)** |
23
+ | **Header Menus** | Basic | Advanced | **Yes (Sort, Hide, Pin)** |
24
+ | **Side Bar** | No | Yes | **Yes (Columns, Filters)** |
25
+ | **Keyboard Nav** | Yes (Cell-level) | Yes (Advanced) | **Basic (Editing only)** |
26
+ | **State Persistence** | No | Yes | **No** |
27
+ | **Integrated Charts** | No | Yes | **Planned (Phase IV)** |
28
+ | **Sparklines** | No | Yes | **Yes (Area, Line, Bar)** |
29
+ | **Accessibility (ARIA)**| Yes | Yes | **Partial (Headers only)** |
30
+
31
+
32
+ ## πŸš€ Status: Phase V Complete - Transitioning to Enterprise Maturity
33
+
34
+ ArgentGrid has met its initial implementation goals. We are now entering a maturity phase to bridge the final gaps with AG Grid Enterprise, including Server-Side Row Models and deep Angular component integration.
35
+
36
+ ---
37
+
38
+ ## πŸ—ΊοΈ Roadmap
39
+
40
+ ### Phase I: API Extraction & Architecture βœ…
41
+ - [x] Map AG Grid `GridOptions`, `ColDef`, and `GridApi` interfaces.
42
+ - [x] Bootstrap Angular library project.
43
+ - [x] Hybrid Architecture: DOM Headers + Canvas Viewport.
44
+ - [x] Virtual Scrolling engine (60fps at 1M rows).
45
+
46
+ ### Phase II: Core Grid Logic βœ…
47
+ - [x] Client-side Sorting.
48
+ - [x] Advanced Filtering (Text, Number, Date, Boolean).
49
+ - [x] Cell Editing (Inline with valueParser/valueSetter).
50
+ - [x] Selection (Single/Multi with Checkbox support).
51
+ - [x] Column & Row Pinning (Left/Right, Top/Bottom).
52
+
53
+ ### Phase III: Enterprise Features βœ…
54
+ - [x] **Row Grouping**: Hierarchical data with expand/collapse.
55
+ - [x] **Aggregation**: sum, avg, min, max, count, and custom functions.
56
+ - [x] **Export**: CSV and HTML-based Excel export.
57
+
58
+ ### Phase IV: UI Interactivity & UX βœ…
59
+ - [x] **Column Re-ordering (Drag & Drop)**:
60
+ - [x] Implement drag handle in DOM headers.
61
+ - [x] Sidebar column re-ordering via tool panel.
62
+ - [x] Update `columnDefs` and `GridApi` on drop.
63
+ - [x] Animate column movement on Canvas.
64
+ - [x] **Column Resizing**:
65
+ - [x] Add resize handles to DOM header cells.
66
+ - [x] Implement drag-to-resize logic.
67
+ - [x] Update `columnDefs` and `GridApi` on resize completion.
68
+ - [x] **Header Menus**:
69
+ - [x] Add "hamburger" or "ellipsis" menu to column headers.
70
+ - [x] Support Sort, Filter, and "Hide Column" actions from menu.
71
+ - [x] Integrate with existing `GridApi`.
72
+ - [ ] **Context Menus**:
73
+ - [x] Right-click cell interaction.
74
+ - [x] Default actions: Copy, Export, Reset Columns.
75
+ - [ ] Support for user-defined custom context menu items.
76
+ - [x] **Excel-like Range Selection**:
77
+ - [x] Drag-to-select rectangular ranges of cells.
78
+ - [x] Visual selection box rendered on Canvas.
79
+ - [x] "Copy with Headers" support.
80
+
81
+ ### Phase V: Advanced Data Analysis βœ…
82
+ - [x] **Pivoting**: Excel-style pivot tables (cross-tabulation).
83
+ - [x] **Tool Panels & Sidebars**: Dedicated UI for column management and global filtering.
84
+ - [x] **Master/Detail**: Expandable rows to reveal nested grids or custom templates.
85
+ - [x] **True Excel Export**: Implementation using `exceljs` for native `.xlsx` files with styles.
86
+ - [x] **Integrated Sparklines**: Mini-charts rendered directly in cells using the Canvas engine.
87
+
88
+ ### Phase VI: UX Polish & Extensibility 🚧
89
+ - [ ] **Custom Angular Components**: Support for using real Angular components as cell renderers/editors inside the Canvas.
90
+ - [x] **Context Menu API**: Full implementation of `getContextMenuItems` to allow dynamic, user-defined menu actions.
91
+ - [ ] **Advanced Keyboard Navigation**: Full cell-to-cell navigation (Arrows, Tab, Page Up/Down) matching AG Grid behavior.
92
+ - [ ] **State Persistence**: Save/Restore user grid state (order, width, filters) to LocalStorage.
93
+ - [ ] **Advanced Filtering**: Set Filter (Excel-style checkboxes) and Multi-Filter support.
94
+
95
+ ### Phase VII: Enterprise Row Models
96
+ - [ ] **Server-Side Row Model (SSRM)**: Loading and aggregating millions of rows on the server.
97
+ - [ ] **Infinite Row Model**: Standard infinite scrolling for large flat datasets.
98
+ - [ ] **Tree Data**: Advanced hierarchical structures with path-based navigation.
99
+
100
+ ### Phase VIII: Final Polish
101
+ - [ ] **Advanced Accessibility**: Full ARIA compliance and screen reader optimization for the Canvas viewport.
102
+ - [ ] **Touch & Mobile Support**: Optimized interactions for mobile devices.
103
+ - [ ] **Web Workers**: Move data processing to background threads for even better responsiveness.
104
+
105
+ ## πŸŽ‰ Project Milestone: CORE PHASES COMPLETE!
106
+
107
+ ArgentGrid has reached its initial goal of providing a high-performance, Enterprise-compatible Angular data grid.
108
+
109
+ ### Future Considerations
110
+ - [ ] **Web Workers**: Move data processing to background threads for even better responsiveness.
111
+ - [ ] **Touch & Mobile Support**: Optimized interactions for mobile devices.
112
+ - [ ] **Integrated Charts**: User-creatable charts directly from range selections.
113
+ - [ ] **Tree Data**: Advanced hierarchical structures with path-based navigation.
114
+
115
+
116
+ ---
117
+
118
+ ## πŸ› οΈ Implementation Strategy
119
+
120
+ 1. **Hybrid Rendering Strategy**:
121
+ - Keep headers as DOM elements for easy Drag-and-Drop implementation (using Angular CDK) and native browser menus.
122
+ - Maintain the data viewport on Canvas for infinite performance.
123
+ - Synchronize horizontal scroll between DOM header and Canvas viewport.
124
+
125
+ 2. **State Management**:
126
+ - Use a centralized `GridService` to maintain the source of truth.
127
+ - Trigger partial Canvas repaints on state changes to maximize performance.
128
+
129
+ 3. **Test-Driven Development (TDD)**:
130
+ - Every new UI feature must have a corresponding Playwright E2E test in the `demo-app`.
131
+ - Logic changes must be verified by Vitest unit tests in `src/lib/services/grid.service.spec.ts`.
@@ -0,0 +1,18 @@
1
+ import { vi } from 'vitest';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
4
+ import { provideExperimentalZonelessChangeDetection } from '@angular/core';
5
+
6
+ // Initialize Angular test environment
7
+ TestBed.initTestEnvironment(
8
+ BrowserDynamicTestingModule,
9
+ platformBrowserDynamicTesting(),
10
+ {
11
+ provide: [
12
+ provideExperimentalZonelessChangeDetection()
13
+ ]
14
+ }
15
+ );
16
+
17
+ // Setup fake timers
18
+ vi.useFakeTimers();
@@ -0,0 +1,21 @@
1
+ import { NgModule } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { DragDropModule } from '@angular/cdk/drag-drop';
4
+ import { ArgentGridComponent } from './components/argent-grid.component';
5
+ import { AgGridCompatibilityDirective } from './directives/ag-grid-compatibility.directive';
6
+
7
+ @NgModule({
8
+ declarations: [
9
+ ArgentGridComponent,
10
+ AgGridCompatibilityDirective
11
+ ],
12
+ imports: [
13
+ CommonModule,
14
+ DragDropModule
15
+ ],
16
+ exports: [
17
+ ArgentGridComponent,
18
+ AgGridCompatibilityDirective
19
+ ]
20
+ })
21
+ export class ArgentGridModule {}