From a6f750c87451af3180c39c3c79d31234e5fe5df4 Mon Sep 17 00:00:00 2001
From: John White <750350+johnhwhite@users.noreply.github.com>
Date: Fri, 31 Oct 2025 12:56:56 -0400
Subject: [PATCH 01/54] feat(components/ag-grid): create an easy mode for data
grid
---
apps/playground/src/app/app.component.ts | 32 +-
.../src/app/components/components.module.ts | 17 +
.../data-grid/basic/grid.component.html | 183 ++++++++
.../data-grid/basic/grid.component.scss | 5 +
.../data-grid/basic/grid.component.ts | 127 +++++
.../components/data-grid/data-grid.module.ts | 38 ++
.../paging/grid-paging.component.html | 27 ++
.../data-grid/paging/grid-paging.component.ts | 15 +
.../grids/basic/grid.component.html | 240 ++++++++++
.../grids/basic/grid.component.scss | 5 +
.../components/grids/basic/grid.component.ts | 217 +++++++++
.../src/app/components/grids/grids.module.ts | 47 ++
.../grids/paging/grid-paging.component.html | 18 +
.../grids/paging/grid-paging.component.ts | 14 +
.../grids/search/grid-search.component.html | 18 +
.../grids/search/grid-search.component.ts | 14 +
.../list-builder/list-builder.module.ts | 29 ++
.../list-view-grid.component.html | 148 ++++++
.../list-view-grid.component.ts | 110 +++++
apps/playground/src/main.ts | 12 +-
libs/components/ag-grid/documentation.json | 1 +
libs/components/ag-grid/project.json | 22 +-
libs/components/ag-grid/src/index.ts | 3 +
.../ag-grid/header/header.component.html | 11 +-
.../ag-grid/header/header.component.ts | 22 +-
.../fixtures/ag-grid-test.component.html | 151 ++++++
.../fixtures/ag-grid-test.component.ts | 113 +++++
.../sky-ag-grid-column.component.ts | 58 +++
.../sky-ag-grid/sky-ag-grid.component.css | 4 +
.../sky-ag-grid/sky-ag-grid.component.html | 24 +
.../sky-ag-grid/sky-ag-grid.component.spec.ts | 270 +++++++++++
.../sky-ag-grid/sky-ag-grid.component.ts | 432 ++++++++++++++++++
.../modules/ag-grid/types/header-params.ts | 15 +-
.../ag-grid/testing/eslint.config.js | 5 +
libs/components/ag-grid/testing/karma.conf.js | 19 +
.../ag-grid/testing/ng-package.json | 5 +
libs/components/ag-grid/testing/project.json | 47 ++
.../ag-grid-wrapper-harness.filters.ts | 7 +
.../ag-grid-wrapper-harness.spec.ts | 43 ++
.../ag-grid-wrapper-harness.ts | 80 ++++
.../ag-grid/ag-grid-harness.filters.ts | 7 +
.../modules/ag-grid/ag-grid-harness.spec.ts | 99 ++++
.../src/modules/ag-grid/ag-grid-harness.ts | 59 +++
.../fixtures/ag-grid-test.component.html | 151 ++++++
.../fixtures/ag-grid-test.component.ts | 114 +++++
.../ag-grid/testing/src/public-api.ts | 1 +
.../ag-grid/testing/tsconfig.spec.json | 8 +
libs/components/code-examples/src/index.ts | 1 +
.../grid/basic/context-menu.component.html | 31 ++
.../grid/basic/context-menu.component.ts | 36 ++
.../lib/modules/ag-grid/grid/basic/data.ts | 157 +++++++
.../ag-grid/grid/basic/example.component.html | 15 +
.../ag-grid/grid/basic/example.component.ts | 18 +
tsconfig.base.json | 3 +
54 files changed, 3308 insertions(+), 40 deletions(-)
create mode 100644 apps/playground/src/app/components/data-grid/basic/grid.component.html
create mode 100644 apps/playground/src/app/components/data-grid/basic/grid.component.scss
create mode 100644 apps/playground/src/app/components/data-grid/basic/grid.component.ts
create mode 100644 apps/playground/src/app/components/data-grid/data-grid.module.ts
create mode 100644 apps/playground/src/app/components/data-grid/paging/grid-paging.component.html
create mode 100644 apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts
create mode 100644 apps/playground/src/app/components/grids/basic/grid.component.html
create mode 100644 apps/playground/src/app/components/grids/basic/grid.component.scss
create mode 100644 apps/playground/src/app/components/grids/basic/grid.component.ts
create mode 100644 apps/playground/src/app/components/grids/grids.module.ts
create mode 100644 apps/playground/src/app/components/grids/paging/grid-paging.component.html
create mode 100644 apps/playground/src/app/components/grids/paging/grid-paging.component.ts
create mode 100644 apps/playground/src/app/components/grids/search/grid-search.component.html
create mode 100644 apps/playground/src/app/components/grids/search/grid-search.component.ts
create mode 100644 apps/playground/src/app/components/list-builder/list-builder.module.ts
create mode 100644 apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html
create mode 100644 apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts
create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html
create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.ts
create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid-column.component.ts
create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.css
create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.html
create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts
create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts
create mode 100644 libs/components/ag-grid/testing/eslint.config.js
create mode 100644 libs/components/ag-grid/testing/karma.conf.js
create mode 100644 libs/components/ag-grid/testing/ng-package.json
create mode 100644 libs/components/ag-grid/testing/project.json
create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.filters.ts
create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.spec.ts
create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.ts
create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.filters.ts
create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.spec.ts
create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.ts
create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html
create mode 100644 libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.ts
create mode 100644 libs/components/ag-grid/testing/src/public-api.ts
create mode 100644 libs/components/ag-grid/testing/tsconfig.spec.json
create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.html
create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.ts
create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/data.ts
create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.html
create mode 100644 libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.ts
diff --git a/apps/playground/src/app/app.component.ts b/apps/playground/src/app/app.component.ts
index e9e7487c56..5457348d7f 100644
--- a/apps/playground/src/app/app.component.ts
+++ b/apps/playground/src/app/app.component.ts
@@ -1,12 +1,6 @@
-import { Component, Renderer2 } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { Router, RouterLink, RouterOutlet } from '@angular/router';
-import {
- SkyAppViewportService,
- SkyTheme,
- SkyThemeMode,
- SkyThemeService,
- SkyThemeSettings,
-} from '@skyux/theme';
+import { SkyAppViewportService } from '@skyux/theme';
import { SkyThemeSelectorComponent } from './shared/theme-selector/theme-selector.component';
@@ -22,29 +16,17 @@ import { SkyThemeSelectorComponent } from './shared/theme-selector/theme-selecto
export class AppComponent {
public height = 80;
- constructor(
- private router: Router,
- renderer: Renderer2,
- themeService: SkyThemeService,
- viewportService: SkyAppViewportService,
- ) {
- viewportService.reserveSpace({
+ readonly #router = inject(Router);
+
+ constructor() {
+ inject(SkyAppViewportService).reserveSpace({
id: 'playground-controls',
position: 'top',
size: this.height,
});
-
- themeService.init(
- document.body,
- renderer,
- new SkyThemeSettings(
- SkyTheme.presets['default'],
- SkyThemeMode.presets.light,
- ),
- );
}
public isHome(): boolean {
- return this.router.url === '/';
+ return this.#router.url === '/';
}
}
diff --git a/apps/playground/src/app/components/components.module.ts b/apps/playground/src/app/components/components.module.ts
index f6a9962728..8b5680309f 100644
--- a/apps/playground/src/app/components/components.module.ts
+++ b/apps/playground/src/app/components/components.module.ts
@@ -61,6 +61,16 @@ export const componentRoutes: Routes = [
loadChildren: () =>
import('./forms/forms.module').then((m) => m.FormsModule),
},
+ {
+ path: 'grids',
+ loadChildren: () =>
+ import('./grids/grids.module').then((m) => m.GridsModule),
+ },
+ {
+ path: 'data-grid',
+ loadChildren: () =>
+ import('./data-grid/data-grid.module').then((m) => m.DataGridModule),
+ },
{
path: 'help-inline',
loadChildren: () =>
@@ -78,6 +88,13 @@ export const componentRoutes: Routes = [
loadChildren: () =>
import('./layout/layout.module').then((m) => m.LayoutModule),
},
+ {
+ path: 'list-builder',
+ loadChildren: () =>
+ import('./list-builder/list-builder.module').then(
+ (m) => m.ListBuilderModule,
+ ),
+ },
{
path: 'lists',
loadChildren: () =>
diff --git a/apps/playground/src/app/components/data-grid/basic/grid.component.html b/apps/playground/src/app/components/data-grid/basic/grid.component.html
new file mode 100644
index 0000000000..6228918dc1
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/basic/grid.component.html
@@ -0,0 +1,183 @@
+
Grid
+
+ Toggle column 3
+
+
+ No data
+
+
+
+
+
+ @if (!hideCol3()) {
+
+ }
+
+
+
+Grid with multiselect enabled
+
+
+ Select all
+
+
+
+ Clear all
+
+
+
+ Programmatically select rows 101, 103, and 105
+
+
+
+
+
+
+
+ @if (!hideCol3()) {
+
+ }
+
+
+
Selected rows: {{ selectedRowIds | json }}
+
+
+Grid with inline help
+
+
+
+
+
+ @if (!hideCol3()) {
+
+ }
+
+
+
+ This is a numeric column. Click here to learn more.
+
+
+
+ This is a string column. Click here to learn more.
+
+
+
+Grid with scroll bars
+
+
+
+
+
+ @if (!hideCol3()) {
+
+ }
+
+
+
+Grid with row delete
+
+
+
+
+
+
+ @if (!hideCol3()) {
+
+ }
+
+
+
+ This is a numeric column. Click here to learn more.
+
+
+
+Grid w/ aligned columns
+
+
+
+
+
+ @if (!hideCol3()) {
+
+ }
+
+
+
+Grid w/ aligned columns and inline help
+
+
+
+
+
+ @if (!hideCol3()) {
+
+ }
+
+
diff --git a/apps/playground/src/app/components/data-grid/basic/grid.component.scss b/apps/playground/src/app/components/data-grid/basic/grid.component.scss
new file mode 100644
index 0000000000..cf3267ce27
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/basic/grid.component.scss
@@ -0,0 +1,5 @@
+h1 {
+ // Margins throw off the screenshots; use padding for the same effect.
+ margin: 0;
+ padding: 15px 0;
+}
diff --git a/apps/playground/src/app/components/data-grid/basic/grid.component.ts b/apps/playground/src/app/components/data-grid/basic/grid.component.ts
new file mode 100644
index 0000000000..f3963bfd52
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/basic/grid.component.ts
@@ -0,0 +1,127 @@
+import { JsonPipe } from '@angular/common';
+import {
+ ChangeDetectorRef,
+ Component,
+ ViewChild,
+ inject,
+ model,
+} from '@angular/core';
+import {
+ SkyAgGridColumnComponent,
+ SkyAgGridComponent,
+ SkyAgGridRowDeleteCancelArgs,
+ SkyAgGridRowDeleteConfirmArgs,
+} from '@skyux/ag-grid';
+import { SkyGridModule } from '@skyux/grids';
+import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers';
+
+interface RowModel {
+ id: string;
+ column1: string;
+ column2: string;
+ column3: string;
+ myId?: string;
+}
+
+@Component({
+ selector: 'app-data-grid',
+ templateUrl: './grid.component.html',
+ styleUrls: ['./grid.component.scss'],
+ imports: [
+ SkyAgGridComponent,
+ SkyAgGridColumnComponent,
+ SkyPopoverModule,
+ SkyDropdownModule,
+ SkyGridModule,
+ JsonPipe,
+ ],
+})
+export default class GridComponent {
+ public asyncPopover: any;
+
+ public dataForRowDeleteGrid: RowModel[] = [
+ { id: '1', column1: '1', column2: 'Apple', column3: 'aa' },
+ { id: '2', column1: '01', column2: 'Banana', column3: 'bb' },
+ { id: '3', column1: '11', column2: 'Banana', column3: 'cc' },
+ { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' },
+ { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' },
+ { id: '6', column1: '20', column2: 'Fig', column3: 'ff' },
+ { id: '7', column1: '21', column2: 'Grape', column3: 'gg' },
+ ];
+
+ public dataForSimpleGrid: RowModel[] = [
+ { id: '1', column1: '1', column2: 'Apple', column3: 'aa' },
+ { id: '2', column1: '01', column2: 'Banana', column3: 'bb' },
+ { id: '3', column1: '11', column2: 'Banana', column3: 'cc' },
+ { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' },
+ { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' },
+ { id: '6', column1: '20', column2: 'Fig', column3: 'ff' },
+ { id: '7', column1: '21', column2: 'Grape', column3: 'gg' },
+ ];
+
+ public dataForSimpleGridWithMultiselect: RowModel[] = [
+ { id: '1', column1: '1', column2: 'Apple', column3: 'aa', myId: '101' },
+ { id: '2', column1: '01', column2: 'Banana', column3: 'bb', myId: '102' },
+ { id: '3', column1: '11', column2: 'Banana', column3: 'cc', myId: '103' },
+ { id: '4', column1: '12', column2: 'Daikon', column3: 'dd', myId: '104' },
+ { id: '5', column1: '13', column2: 'Edamame', column3: 'ee', myId: '105' },
+ { id: '6', column1: '20', column2: 'Fig', column3: 'ff', myId: '106' },
+ { id: '7', column1: '21', column2: 'Grape', column3: 'gg', myId: '107' },
+ ];
+
+ public selectedRowIds: string[] = [];
+
+ public removeRowIds: string[] = [];
+
+ protected readonly hideCol3 = model(false);
+ protected toggleCol3(): void {
+ this.hideCol3.update((show) => !show);
+ }
+
+ @ViewChild('asyncPopoverRef')
+ private popoverTemplate: any;
+
+ readonly #cdr = inject(ChangeDetectorRef);
+
+ constructor() {
+ setTimeout(() => {
+ this.asyncPopover = this.popoverTemplate;
+ }, 1000);
+ }
+
+ public selectAll(): void {
+ this.selectedRowIds = this.dataForSimpleGridWithMultiselect.map(
+ (item) => item.myId,
+ );
+ this.#cdr.markForCheck();
+ }
+
+ public clearAll(): void {
+ this.selectedRowIds = [];
+ this.#cdr.markForCheck();
+ }
+
+ public cancelRowDelete(cancelArgs: SkyAgGridRowDeleteCancelArgs): void {
+ console.log('Item with id ' + cancelArgs.id + ' has not been deleted');
+ }
+
+ public deleteItem(id: string): void {
+ this.removeRowIds = [id, ...this.removeRowIds];
+ }
+
+ public finishRowDelete(confirmArgs: SkyAgGridRowDeleteConfirmArgs): void {
+ setTimeout(() => {
+ console.log('Item with id ' + confirmArgs.id + ' has been deleted');
+ // IF WORKED
+ this.dataForRowDeleteGrid = this.dataForRowDeleteGrid.filter(
+ (data: any) => data.id !== confirmArgs.id,
+ );
+ this.#cdr.markForCheck();
+ }, 5000);
+ }
+
+ public selectRow(): void {
+ this.selectedRowIds = ['101', '103', '105'];
+ this.#cdr.markForCheck();
+ }
+}
diff --git a/apps/playground/src/app/components/data-grid/data-grid.module.ts b/apps/playground/src/app/components/data-grid/data-grid.module.ts
new file mode 100644
index 0000000000..81e52cba21
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-grid.module.ts
@@ -0,0 +1,38 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { ComponentRouteInfo } from '../../shared/component-info/component-route-info';
+
+const routes: ComponentRouteInfo[] = [
+ {
+ path: 'basic',
+ loadComponent: () => import('./basic/grid.component'),
+ data: {
+ name: 'Data Grid (new)',
+ icon: 'table',
+ library: 'grids',
+ },
+ },
+ {
+ path: 'paging',
+ loadComponent: () => import('./paging/grid-paging.component'),
+ data: {
+ name: 'Data Grid Paging (new)',
+ icon: 'table',
+ library: 'grids',
+ },
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+})
+class DataGridRoutingModule {}
+
+@NgModule({
+ imports: [DataGridRoutingModule],
+})
+export class DataGridModule {
+ public static routes = routes;
+}
diff --git a/apps/playground/src/app/components/data-grid/paging/grid-paging.component.html b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.html
new file mode 100644
index 0000000000..59fb5688fb
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.html
@@ -0,0 +1,27 @@
+Updates query string
+
+
+
+
+
+
+ {{ value.name }}
+
+
+ {{ value.name }}
+
+
+
+No query string, just paging
+
+
+
+
+
+
+ {{ value.name }}
+
+
+ {{ value.name }}
+
+
diff --git a/apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts
new file mode 100644
index 0000000000..db09c0c376
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts
@@ -0,0 +1,15 @@
+import { Component, input } from '@angular/core';
+import { SkyAgGridColumnComponent, SkyAgGridComponent } from '@skyux/ag-grid';
+
+import { AG_GRID_DEMO_DATA } from '../../../shared/data-manager/data-manager-data';
+
+@Component({
+ selector: 'app-data-grid-paging',
+ imports: [SkyAgGridComponent, SkyAgGridColumnComponent],
+ templateUrl: './grid-paging.component.html',
+})
+export default class GridPagingComponent {
+ public readonly page = input();
+
+ protected readonly data = AG_GRID_DEMO_DATA;
+}
diff --git a/apps/playground/src/app/components/grids/basic/grid.component.html b/apps/playground/src/app/components/grids/basic/grid.component.html
new file mode 100644
index 0000000000..754a04e7b3
--- /dev/null
+++ b/apps/playground/src/app/components/grids/basic/grid.component.html
@@ -0,0 +1,240 @@
+Grid
+
+ Toggle column 3
+
+
+
+
+
+
+
+
+
+
+
+ Trigger text highlight
+
+
+ Trigger row highlight
+
+
+
+Grid with multiselect enabled
+
+
+ Select all
+
+
+
+ Clear all
+
+
+
+ Programmatically select rows 101, 103, and 105
+
+
+
+
+
+
+
+
+
+
+
Selected rows:
+
{{ selectedRowIdsDisplay }}
+
+
+Grid with inline help
+
+
+
+
+
+
+
+
+
+ This is a numeric column. Click here to learn more.
+
+
+
+ This is a string column. Click here to learn more.
+
+
+
+Grid with scroll bars
+
+
+
+
+
+
+
+
+
+Grid with row delete
+
+
+
+
+
+
+
+
+
+
+ Delete item
+
+
+
+
+
+
+
+
+
+
+
+
+ This is a numeric column. Click here to learn more.
+
+
+
+ This is a string column. Click here to learn more.
+
+
+
+Grid w/ aligned columns
+
+
+
+
+
+
+
+
+
+Grid w/ aligned columns and inline help
+
+
+
+
+
+
+
+
diff --git a/apps/playground/src/app/components/grids/basic/grid.component.scss b/apps/playground/src/app/components/grids/basic/grid.component.scss
new file mode 100644
index 0000000000..cf3267ce27
--- /dev/null
+++ b/apps/playground/src/app/components/grids/basic/grid.component.scss
@@ -0,0 +1,5 @@
+h1 {
+ // Margins throw off the screenshots; use padding for the same effect.
+ margin: 0;
+ padding: 15px 0;
+}
diff --git a/apps/playground/src/app/components/grids/basic/grid.component.ts b/apps/playground/src/app/components/grids/basic/grid.component.ts
new file mode 100644
index 0000000000..d7d098087e
--- /dev/null
+++ b/apps/playground/src/app/components/grids/basic/grid.component.ts
@@ -0,0 +1,217 @@
+import { CommonModule } from '@angular/common';
+import { Component, ViewChild } from '@angular/core';
+import {
+ SkyGridMessage,
+ SkyGridMessageType,
+ SkyGridModule,
+ SkyGridRowDeleteCancelArgs,
+ SkyGridRowDeleteConfirmArgs,
+ SkyGridSelectedRowsModelChange,
+} from '@skyux/grids';
+import { ListSortFieldSelectorModel } from '@skyux/list-builder-common';
+import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers';
+
+import { Subject } from 'rxjs';
+
+@Component({
+ selector: 'app-grid',
+ templateUrl: './grid.component.html',
+ styleUrls: ['./grid.component.scss'],
+ imports: [CommonModule, SkyGridModule, SkyPopoverModule, SkyDropdownModule],
+})
+export default class GridComponent {
+ public asyncPopover: any;
+
+ public dataForRowDeleteGrid: any = [
+ { id: '1', column1: '1', column2: 'Apple', column3: 'aa' },
+ { id: '2', column1: '01', column2: 'Banana', column3: 'bb' },
+ { id: '3', column1: '11', column2: 'Banana', column3: 'cc' },
+ { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' },
+ { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' },
+ { id: '6', column1: '20', column2: 'Fig', column3: 'ff' },
+ { id: '7', column1: '21', column2: 'Grape', column3: 'gg' },
+ ];
+
+ public dataForSimpleGrid = [
+ { id: '1', column1: '1', column2: 'Apple', column3: 'aa' },
+ { id: '2', column1: '01', column2: 'Banana', column3: 'bb' },
+ { id: '3', column1: '11', column2: 'Banana', column3: 'cc' },
+ { id: '4', column1: '12', column2: 'Daikon', column3: 'dd' },
+ { id: '5', column1: '13', column2: 'Edamame', column3: 'ee' },
+ { id: '6', column1: '20', column2: 'Fig', column3: 'ff' },
+ { id: '7', column1: '21', column2: 'Grape', column3: 'gg' },
+ ];
+
+ public dataForSimpleGridWithMultiselect = [
+ { id: '1', column1: '1', column2: 'Apple', column3: 'aa', myId: '101' },
+ { id: '2', column1: '01', column2: 'Banana', column3: 'bb', myId: '102' },
+ { id: '3', column1: '11', column2: 'Banana', column3: 'cc', myId: '103' },
+ { id: '4', column1: '12', column2: 'Daikon', column3: 'dd', myId: '104' },
+ { id: '5', column1: '13', column2: 'Edamame', column3: 'ee', myId: '105' },
+ { id: '6', column1: '20', column2: 'Fig', column3: 'ff', myId: '106' },
+ { id: '7', column1: '21', column2: 'Grape', column3: 'gg', myId: '107' },
+ ];
+
+ public columnsForSimpleGrid = ['column1', 'column2', 'column3'];
+
+ public sortFieldForSimpleGrid: ListSortFieldSelectorModel = {
+ descending: true,
+ fieldSelector: 'column2',
+ };
+
+ public gridController = new Subject();
+
+ public gridRowDeleteController = new Subject();
+
+ public highlightText: string;
+
+ public rowHighlightedId: string;
+
+ public selectedRowIds: string[];
+
+ public selectedRowIdsDisplay: string[];
+
+ public selectedRows: string;
+
+ @ViewChild('asyncPopoverRef')
+ private popoverTemplate: any;
+
+ constructor() {
+ setTimeout(() => {
+ this.asyncPopover = this.popoverTemplate;
+ }, 1000);
+ }
+
+ public toggleCol3(): void {
+ const col3Index = this.columnsForSimpleGrid.indexOf('column3');
+ if (col3Index === -1) {
+ this.columnsForSimpleGrid.push('column3');
+ } else {
+ this.columnsForSimpleGrid.splice(col3Index, 1);
+ }
+ this.columnsForSimpleGrid = [...this.columnsForSimpleGrid];
+ }
+
+ public sortChangedSimpleGrid(activeSort: ListSortFieldSelectorModel): void {
+ this.dataForSimpleGrid = this.performSort(
+ activeSort,
+ this.dataForSimpleGrid,
+ );
+ }
+
+ public sortChangedMultiselectGrid(
+ activeSort: ListSortFieldSelectorModel,
+ ): void {
+ this.dataForSimpleGridWithMultiselect = this.performSort(
+ activeSort,
+ this.dataForSimpleGridWithMultiselect,
+ );
+ }
+
+ public triggerTextHighlight(): void {
+ if (!this.highlightText) {
+ this.highlightText = 'e';
+ } else {
+ this.highlightText = undefined;
+ }
+ }
+
+ public triggerRowHighlight(): void {
+ if (!this.rowHighlightedId) {
+ this.rowHighlightedId = '2';
+ } else {
+ this.rowHighlightedId = undefined;
+ }
+ }
+
+ public onMultiselectSelectionChange(
+ value: SkyGridSelectedRowsModelChange,
+ ): void {
+ this.selectedRowIdsDisplay = value.selectedRowIds;
+ }
+
+ public selectAll(): void {
+ this.sendMessage(SkyGridMessageType.SelectAll);
+ }
+
+ public clearAll(): void {
+ this.sendMessage(SkyGridMessageType.ClearAll);
+ }
+
+ public cancelRowDelete(cancelArgs: SkyGridRowDeleteCancelArgs): void {
+ console.log('Item with id ' + cancelArgs.id + ' has not been deleted');
+ }
+
+ public deleteItem(id: string): void {
+ this.gridRowDeleteController.next({
+ type: SkyGridMessageType.PromptDeleteRow,
+ data: {
+ promptDeleteRow: {
+ id: id,
+ },
+ },
+ });
+ }
+
+ public finishRowDelete(confirmArgs: SkyGridRowDeleteConfirmArgs): void {
+ setTimeout(() => {
+ console.log('Item with id ' + confirmArgs.id + ' has been deleted');
+ // IF WORKED
+ this.dataForRowDeleteGrid = this.dataForRowDeleteGrid.filter(
+ (data: any) => data.id !== confirmArgs.id,
+ );
+ }, 5000);
+ }
+
+ public selectRow(): void {
+ this.selectedRowIds = ['101', '103', '105'];
+ }
+
+ public onSelectedColumnIdsChange(event: any[]): void {
+ console.log(event);
+ }
+
+ public onColumnWidthChange(event: any): void {
+ console.log(event);
+ }
+
+ private performSort(
+ activeSort: ListSortFieldSelectorModel,
+ data: any[],
+ ): Array {
+ const sortField = activeSort.fieldSelector;
+ const descending = activeSort.descending;
+
+ return data
+ .sort((a: any, b: any) => {
+ let value1 = a[sortField];
+ let value2 = b[sortField];
+
+ if (value1 && typeof value1 === 'string') {
+ value1 = value1.toLowerCase();
+ }
+
+ if (value2 && typeof value2 === 'string') {
+ value2 = value2.toLowerCase();
+ }
+
+ if (value1 === value2) {
+ return 0;
+ }
+
+ let result = value1 > value2 ? 1 : -1;
+
+ if (descending) {
+ result *= -1;
+ }
+
+ return result;
+ })
+ .slice();
+ }
+
+ private sendMessage(type: SkyGridMessageType): void {
+ const message: SkyGridMessage = { type };
+ this.gridController.next(message);
+ }
+}
diff --git a/apps/playground/src/app/components/grids/grids.module.ts b/apps/playground/src/app/components/grids/grids.module.ts
new file mode 100644
index 0000000000..8d59f1a9c4
--- /dev/null
+++ b/apps/playground/src/app/components/grids/grids.module.ts
@@ -0,0 +1,47 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { ComponentRouteInfo } from '../../shared/component-info/component-route-info';
+
+const routes: ComponentRouteInfo[] = [
+ {
+ path: 'basic',
+ loadComponent: () => import('./basic/grid.component'),
+ data: {
+ name: 'Grid',
+ icon: 'table',
+ library: 'grids',
+ },
+ },
+ {
+ path: 'paging',
+ loadComponent: () => import('./paging/grid-paging.component'),
+ data: {
+ name: 'Grid Paging',
+ icon: 'table',
+ library: 'grids',
+ },
+ },
+ {
+ path: 'search',
+ loadComponent: () => import('./search/grid-search.component'),
+ data: {
+ name: 'Grid Search',
+ icon: 'table',
+ library: 'grids',
+ },
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+})
+class GridsRoutingModule {}
+
+@NgModule({
+ imports: [GridsRoutingModule],
+})
+export class GridsModule {
+ public static routes = routes;
+}
diff --git a/apps/playground/src/app/components/grids/paging/grid-paging.component.html b/apps/playground/src/app/components/grids/paging/grid-paging.component.html
new file mode 100644
index 0000000000..4e85feca8e
--- /dev/null
+++ b/apps/playground/src/app/components/grids/paging/grid-paging.component.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ {{ value.name }}
+
+
+ {{ value.name }}
+
+
diff --git a/apps/playground/src/app/components/grids/paging/grid-paging.component.ts b/apps/playground/src/app/components/grids/paging/grid-paging.component.ts
new file mode 100644
index 0000000000..3ecdbc3b7d
--- /dev/null
+++ b/apps/playground/src/app/components/grids/paging/grid-paging.component.ts
@@ -0,0 +1,14 @@
+import { CommonModule } from '@angular/common';
+import { Component } from '@angular/core';
+import { SkyGridModule } from '@skyux/grids';
+
+import { AG_GRID_DEMO_DATA } from '../../../shared/data-manager/data-manager-data';
+
+@Component({
+ selector: 'app-grid-paging',
+ imports: [CommonModule, SkyGridModule],
+ templateUrl: './grid-paging.component.html',
+})
+export default class GridPagingComponent {
+ protected readonly data = AG_GRID_DEMO_DATA;
+}
diff --git a/apps/playground/src/app/components/grids/search/grid-search.component.html b/apps/playground/src/app/components/grids/search/grid-search.component.html
new file mode 100644
index 0000000000..4e85feca8e
--- /dev/null
+++ b/apps/playground/src/app/components/grids/search/grid-search.component.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ {{ value.name }}
+
+
+ {{ value.name }}
+
+
diff --git a/apps/playground/src/app/components/grids/search/grid-search.component.ts b/apps/playground/src/app/components/grids/search/grid-search.component.ts
new file mode 100644
index 0000000000..60a5f46951
--- /dev/null
+++ b/apps/playground/src/app/components/grids/search/grid-search.component.ts
@@ -0,0 +1,14 @@
+import { CommonModule } from '@angular/common';
+import { Component } from '@angular/core';
+import { SkyGridModule } from '@skyux/grids';
+
+import { AG_GRID_DEMO_DATA } from '../../../shared/data-manager/data-manager-data';
+
+@Component({
+ selector: 'app-grid-search',
+ imports: [CommonModule, SkyGridModule],
+ templateUrl: './grid-search.component.html',
+})
+export default class GridSearchComponent {
+ protected readonly data = AG_GRID_DEMO_DATA;
+}
diff --git a/apps/playground/src/app/components/list-builder/list-builder.module.ts b/apps/playground/src/app/components/list-builder/list-builder.module.ts
new file mode 100644
index 0000000000..00a464c2c8
--- /dev/null
+++ b/apps/playground/src/app/components/list-builder/list-builder.module.ts
@@ -0,0 +1,29 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { ComponentRouteInfo } from '../../shared/component-info/component-route-info';
+
+const routes: ComponentRouteInfo[] = [
+ {
+ path: 'grid',
+ loadComponent: () => import('./list-view-grid/list-view-grid.component'),
+ data: {
+ name: 'AAA List Builder Grid',
+ icon: 'table',
+ library: 'list-builder-view-grids',
+ },
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+})
+class ListBuilderRoutingModule {}
+
+@NgModule({
+ imports: [ListBuilderRoutingModule],
+})
+export class ListBuilderModule {
+ public static routes = routes;
+}
diff --git a/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html
new file mode 100644
index 0000000000..d47c89cffd
--- /dev/null
+++ b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Toggle row highlight
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+List-view-grid with inline delete
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts
new file mode 100644
index 0000000000..f1f5b15548
--- /dev/null
+++ b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts
@@ -0,0 +1,110 @@
+import { CommonModule } from '@angular/common';
+import { Component } from '@angular/core';
+import { SkyAlertModule } from '@skyux/indicators';
+import { SkyListModule, SkyListToolbarModule } from '@skyux/list-builder';
+import {
+ SkyListViewGridMessage,
+ SkyListViewGridMessageType,
+ SkyListViewGridModule,
+ SkyListViewGridRowDeleteCancelArgs,
+ SkyListViewGridRowDeleteConfirmArgs,
+} from '@skyux/list-builder-view-grids';
+import { SkyDropdownModule } from '@skyux/popovers';
+
+import { BehaviorSubject, Subject } from 'rxjs';
+
+@Component({
+ selector: 'app-list-view-grid',
+ standalone: true,
+ templateUrl: './list-view-grid.component.html',
+ imports: [
+ CommonModule,
+ SkyAlertModule,
+ SkyDropdownModule,
+ SkyListModule,
+ SkyListViewGridModule,
+ SkyListToolbarModule,
+ ],
+})
+export default class ListViewGridTestComponent {
+ public gridController = new Subject();
+
+ public itemSubject: BehaviorSubject = new BehaviorSubject([]);
+
+ public items = this.itemSubject.asObservable();
+
+ public rowHighlightedId: string;
+
+ public selectedIds: string[];
+
+ private defaultItems = [
+ { id: '1', column1: 101, column2: 'Apple', column3: 'Anne eats apples' },
+ { id: '2', column1: 202, column2: 'Banana', column3: 'Ben eats bananas' },
+ { id: '3', column1: 303, column2: 'Pear', column3: 'Patty eats pears' },
+ { id: '4', column1: 404, column2: 'Grape', column3: 'George eats grapes' },
+ { id: '5', column1: 505, column2: 'Banana', column3: 'Becky eats bananas' },
+ { id: '6', column1: 606, column2: 'Lemon', column3: 'Larry eats lemons' },
+ {
+ id: '7',
+ column1: 707,
+ column2: 'Strawberry',
+ column3: 'Sally eats strawberries',
+ },
+ ];
+
+ constructor() {
+ this.itemSubject.next(this.defaultItems);
+ }
+
+ public onToggleRowHighlightClick(): void {
+ this.rowHighlightedId = this.rowHighlightedId ? undefined : '2';
+ }
+
+ public onSelectedIdsChange(event: any): void {
+ console.log(event);
+ }
+
+ public onRowDeleteCancel(
+ cancelArgs: SkyListViewGridRowDeleteCancelArgs,
+ ): void {
+ this.gridController.next({
+ type: SkyListViewGridMessageType.AbortDeleteRow,
+ data: {
+ abortDeleteRow: {
+ id: cancelArgs.id,
+ },
+ },
+ });
+ }
+
+ public onRowDeleteConfirm(
+ confirmArgs: SkyListViewGridRowDeleteConfirmArgs,
+ ): void {
+ const removeIndex = this.defaultItems
+ .map((item) => {
+ return item.id;
+ })
+ .indexOf(confirmArgs.id);
+
+ if (removeIndex) {
+ setTimeout(() => {
+ this.defaultItems = this.defaultItems.filter(
+ (data: any) => data.id !== confirmArgs.id,
+ );
+ this.itemSubject.next(this.defaultItems);
+ console.log('Item with id ' + confirmArgs.id + ' has been deleted.');
+ }, 1000);
+ }
+ }
+
+ public onDeleteItemClick(id: string): void {
+ this.gridController.next({
+ type: SkyListViewGridMessageType.PromptDeleteRow,
+ data: {
+ promptDeleteRow: {
+ id: id,
+ },
+ },
+ });
+ }
+}
diff --git a/apps/playground/src/main.ts b/apps/playground/src/main.ts
index 52f5d3469c..bf02e81ff8 100644
--- a/apps/playground/src/main.ts
+++ b/apps/playground/src/main.ts
@@ -1,9 +1,13 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
-import { provideRouter, withHashLocation } from '@angular/router';
+import {
+ provideRouter,
+ withComponentInputBinding,
+ withHashLocation,
+} from '@angular/router';
import { SKY_LOG_LEVEL, SkyHelpService, SkyLogLevel } from '@skyux/core';
import { provideIconPreview } from '@skyux/storybook/icon-preview';
-import { SkyThemeService } from '@skyux/theme';
+import { provideInitialTheme } from '@skyux/theme';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
@@ -13,8 +17,8 @@ bootstrapApplication(AppComponent, {
providers: [
provideAnimations(),
provideIconPreview(),
- provideRouter(routes, withHashLocation()),
- SkyThemeService,
+ provideInitialTheme('modern'),
+ provideRouter(routes, withHashLocation(), withComponentInputBinding()),
{ provide: SkyHelpService, useClass: PlaygroundHelpService },
{ provide: SKY_LOG_LEVEL, useValue: SkyLogLevel.Info },
],
diff --git a/libs/components/ag-grid/documentation.json b/libs/components/ag-grid/documentation.json
index 704d9419c2..ae75cc3dad 100644
--- a/libs/components/ag-grid/documentation.json
+++ b/libs/components/ag-grid/documentation.json
@@ -38,6 +38,7 @@
},
"codeExamples": {
"docsIds": [
+ "AgGridBasicExampleComponent",
"AgGridDataGridBasicExampleComponent",
"AgGridDataGridBasicMultiselectExampleComponent",
"AgGridDataGridDataManagerExampleComponent",
diff --git a/libs/components/ag-grid/project.json b/libs/components/ag-grid/project.json
index cd5d54676c..b31ff116cd 100644
--- a/libs/components/ag-grid/project.json
+++ b/libs/components/ag-grid/project.json
@@ -4,6 +4,8 @@
"projectType": "library",
"sourceRoot": "libs/components/ag-grid/src",
"prefix": "sky",
+ "tags": ["component", "npm"],
+ "implicitDependencies": ["phone-field", "validation"],
"targets": {
"build": {
"executor": "@nx/angular:package",
@@ -19,7 +21,21 @@
"tsConfig": "libs/components/ag-grid/tsconfig.lib.json"
}
},
- "defaultConfiguration": "production"
+ "defaultConfiguration": "production",
+ "dependsOn": [
+ "^build",
+ {
+ "projects": ["core"],
+ "target": "build"
+ }
+ ],
+ "inputs": [
+ "buildInputs",
+ "^buildInputs",
+ "{workspaceRoot}/libs/components/ag-grid/testing/src/**/*",
+ "!{workspaceRoot}/libs/components/ag-grid/testing/src/**/*.spec.ts",
+ "!{workspaceRoot}/libs/components/ag-grid/testing/src/**/fixtures/**/*"
+ ]
},
"test": {
"executor": "@angular-devkit/build-angular:karma",
@@ -68,7 +84,5 @@
"command": "ts-node --project ./scripts/tsconfig.json ./scripts/postbuild-ag-grid.ts"
}
}
- },
- "implicitDependencies": ["phone-field", "validation"],
- "tags": ["component", "npm"]
+ }
}
diff --git a/libs/components/ag-grid/src/index.ts b/libs/components/ag-grid/src/index.ts
index f04ebefc6f..12ad65bb84 100644
--- a/libs/components/ag-grid/src/index.ts
+++ b/libs/components/ag-grid/src/index.ts
@@ -30,6 +30,9 @@ export { SkyGetGridOptionsArgs } from './lib/modules/ag-grid/types/sky-grid-opti
export { SkyAgGridTextProperties } from './lib/modules/ag-grid/types/text-properties';
export { SkyAgGridValidatorProperties } from './lib/modules/ag-grid/types/validator-properties';
+export { SkyAgGridComponent } from './lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component';
+export { SkyAgGridColumnComponent } from './lib/modules/ag-grid/sky-ag-grid/sky-ag-grid-column.component';
+
// Components and directives must be exported to support Angular's "partial" Ivy compiler.
// Obscure names are used to indicate types are not part of the public API.
export { SkyAgGridDataManagerAdapterDirective as λ14 } from './lib/modules/ag-grid/ag-grid-data-manager-adapter.directive';
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html
index d5f4e770f1..511e2b7f5d 100644
--- a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.html
@@ -78,6 +78,15 @@
}
}
-
+
+ @if (params()?.helpPopoverContent) {
+
+ }
+
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.ts
index ab0f13e206..12997639ef 100644
--- a/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.ts
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/header/header.component.ts
@@ -17,6 +17,7 @@ import {
SkyDynamicComponentLocation,
SkyDynamicComponentService,
} from '@skyux/core';
+import { SkyHelpInlineModule } from '@skyux/help-inline';
import { SkyI18nModule } from '@skyux/i18n';
import { SkyIconModule } from '@skyux/icon';
import { SkyThemeModule } from '@skyux/theme';
@@ -41,7 +42,13 @@ import { SkyAgGridHeaderParams } from '../types/header-params';
'[attr.aria-label]': 'displayName() || accessibleHeaderText()',
'[attr.role]': '"note"',
},
- imports: [SkyIconModule, SkyThemeModule, AsyncPipe, SkyI18nModule],
+ imports: [
+ AsyncPipe,
+ SkyHelpInlineModule,
+ SkyI18nModule,
+ SkyIconModule,
+ SkyThemeModule,
+ ],
})
export class SkyAgGridHeaderComponent
implements IHeaderAngularComp, OnDestroy, AfterViewInit
@@ -190,7 +197,12 @@ export class SkyAgGridHeaderComponent
return;
}
- const inlineHelpComponent = this.params()?.inlineHelpComponent;
+ const params = this.params();
+ const inlineHelpComponent = params?.inlineHelpComponent;
+
+ if (params?.helpPopoverContent) {
+ return;
+ }
if (
inlineHelpComponent &&
@@ -202,9 +214,9 @@ export class SkyAgGridHeaderComponent
);
const headerInfo = new SkyAgGridHeaderInfo();
- headerInfo.column = this.params()?.column;
- headerInfo.context = this.params()?.context;
- headerInfo.displayName = this.params()?.displayName;
+ headerInfo.column = params?.column;
+ headerInfo.context = params?.context;
+ headerInfo.displayName = params?.displayName;
this.#inlineHelpComponentRef =
this.#dynamicComponentService.createComponent(inlineHelpComponent, {
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html
new file mode 100644
index 0000000000..9333e34bc3
--- /dev/null
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html
@@ -0,0 +1,151 @@
+@if (showAllGrids()) {
+
+ @if (showAllColumns()) {
+
+
+ @if (showCol3()) {
+
+ }
+ }
+
+}
+Grid with multiselect enabled
+
+
+ Select all
+
+
+
+ Clear all
+
+
+
+ Programmatically select rows 101, 103, and 105
+
+
+@if (showAllGrids()) {
+
+ @if (showAllColumns()) {
+
+
+
+ @if (showCol3()) {
+
+ }
+ }
+
+}
+Selected rows: {{ selectedRowIds() | json }}
+
+Grid with row delete
+
+@if (showAllGrids()) {
+
+ @if (showAllColumns()) {
+
+
+
+ @if (row.id) {
+
+
+
+
+ Delete item
+
+
+
+
+ }
+
+
+
+
+ @if (showCol3()) {
+
+ } @else {
+
+ }
+ }
+
+}
+
+ This is a numeric column. Click here to learn more.
+
+
+Grid w/ aligned columns and inline help
+
+@if (showAllGrids()) {
+
+ @if (showAllColumns()) {
+
+
+ @if (showCol3()) {
+
+ }
+ }
+
+}
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.ts
new file mode 100644
index 0000000000..35a6fa36bb
--- /dev/null
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.ts
@@ -0,0 +1,113 @@
+import { JsonPipe } from '@angular/common';
+import {
+ ChangeDetectorRef,
+ Component,
+ inject,
+ input,
+ model,
+} from '@angular/core';
+import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers';
+
+import { SkyAgGridRowDeleteCancelArgs } from '../../types/ag-grid-row-delete-cancel-args';
+import { SkyAgGridRowDeleteConfirmArgs } from '../../types/ag-grid-row-delete-confirm-args';
+import { SkyAgGridColumnComponent } from '../sky-ag-grid-column.component';
+import { SkyAgGridComponent } from '../sky-ag-grid.component';
+
+interface RowModel {
+ id: string;
+ column1: string;
+ column2: string;
+ column3: boolean;
+ myId?: string;
+}
+
+@Component({
+ selector: 'app-ag-grid-test',
+ templateUrl: './ag-grid-test.component.html',
+ imports: [
+ SkyAgGridComponent,
+ SkyAgGridColumnComponent,
+ SkyPopoverModule,
+ SkyDropdownModule,
+ JsonPipe,
+ ],
+})
+export class AgGridTestComponent {
+ public dataForRowDeleteGrid: RowModel[] | undefined = [
+ { id: '1', column1: '1', column2: 'Apple', column3: true },
+ { id: '2', column1: '01', column2: 'Banana', column3: false },
+ { id: '3', column1: '11', column2: 'Banana', column3: true },
+ { id: '4', column1: '12', column2: 'Daikon', column3: false },
+ { id: '5', column1: '13', column2: 'Edamame', column3: true },
+ { id: '6', column1: '20', column2: 'Fig', column3: false },
+ { id: '7', column1: '21', column2: 'Grape', column3: true },
+ ];
+
+ public dataForSimpleGrid: RowModel[] | undefined = [
+ { id: '1', column1: '1', column2: 'Apple', column3: true },
+ { id: '2', column1: '01', column2: 'Banana', column3: false },
+ { id: '3', column1: '11', column2: 'Banana', column3: true },
+ { id: '4', column1: '12', column2: 'Daikon', column3: false },
+ { id: '5', column1: '13', column2: 'Edamame', column3: true },
+ { id: '6', column1: '20', column2: 'Fig', column3: false },
+ { id: '7', column1: '21', column2: 'Grape', column3: true },
+ ];
+
+ public dataForSimpleGridWithMultiselect: RowModel[] | undefined = [
+ { id: '1', column1: '1', column2: 'Apple', column3: true, myId: '101' },
+ { id: '2', column1: '01', column2: 'Banana', column3: false, myId: '102' },
+ { id: '3', column1: '11', column2: 'Banana', column3: true, myId: '103' },
+ { id: '4', column1: '12', column2: 'Daikon', column3: false, myId: '104' },
+ { id: '5', column1: '13', column2: 'Edamame', column3: true, myId: '105' },
+ { id: '6', column1: '20', column2: 'Fig', column3: false, myId: '106' },
+ { id: '7', column1: '21', column2: 'Grape', column3: true, myId: '107' },
+ ];
+
+ public readonly removeRowIds = model([]);
+ public readonly rowHighlightedId = model();
+ public readonly selectedRowIds = model([]);
+ public readonly visibleColumnIds = model([]);
+
+ public page = 1;
+ public pageSize = 0;
+ public pageQueryParam = '';
+
+ protected readonly showAllColumns = input(true);
+ protected readonly showAllGrids = input(true);
+ protected readonly showCol3 = input(true);
+ protected readonly showCol3HeaderText = input(true);
+
+ readonly #cdr = inject(ChangeDetectorRef);
+
+ public selectAll(): void {
+ this.selectedRowIds.set(
+ (this.dataForSimpleGridWithMultiselect ?? []).map(
+ (item) => item.myId as string,
+ ),
+ );
+ this.#cdr.markForCheck();
+ }
+
+ public clearAll(): void {
+ this.selectedRowIds.set([]);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ public cancelRowDelete(_cancelArgs: SkyAgGridRowDeleteCancelArgs): void {
+ // noop
+ }
+
+ public deleteItem(id: string): void {
+ this.removeRowIds.update((removeRowIds) => [id, ...removeRowIds]);
+ }
+
+ public finishRowDelete(confirmArgs: SkyAgGridRowDeleteConfirmArgs): void {
+ this.dataForRowDeleteGrid = (this.dataForRowDeleteGrid ?? []).filter(
+ (data: RowModel) => data.id !== confirmArgs.id,
+ );
+ }
+
+ public selectRow(): void {
+ this.selectedRowIds.set(['101', '103', '105']);
+ }
+}
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid-column.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid-column.component.ts
new file mode 100644
index 0000000000..e8153afe37
--- /dev/null
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid-column.component.ts
@@ -0,0 +1,58 @@
+import {
+ Component,
+ TemplateRef,
+ booleanAttribute,
+ computed,
+ contentChild,
+ input,
+ numberAttribute,
+} from '@angular/core';
+
+@Component({
+ selector: 'sky-ag-grid-column',
+ template: '',
+})
+export class SkyAgGridColumnComponent {
+ public readonly description = input();
+ public readonly field = input();
+ public readonly heading = input();
+ public readonly id = input();
+
+ /**
+ * The title of the help popover. This property only applies when `helpPopoverContent` is
+ * also specified.
+ */
+ public readonly helpPopoverTitle = input();
+
+ /**
+ * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline)
+ * button is added to the column header. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover)
+ * when clicked using the specified content and optional title.
+ */
+ public readonly helpPopoverContent = input>();
+
+ public readonly hidden = input(false, {
+ transform: booleanAttribute,
+ });
+ public readonly isSortable = input(true, {
+ transform: booleanAttribute,
+ });
+ public readonly locked = input(false, {
+ transform: booleanAttribute,
+ });
+ public readonly template = input>();
+ public readonly type = input<'text' | 'number' | 'date' | 'boolean'>('text');
+ public readonly width = input(0, {
+ transform: numberAttribute,
+ });
+
+ protected readonly templateChild = contentChild(TemplateRef);
+
+ public readonly cellTemplate = computed | undefined>(
+ () => {
+ const template = this.template();
+ const templateChild = this.templateChild();
+ return template || templateChild;
+ },
+ );
+}
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.css b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.css
new file mode 100644
index 0000000000..a06b21df93
--- /dev/null
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.css
@@ -0,0 +1,4 @@
+:host {
+ display: block;
+ max-width: 100%;
+}
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.html
new file mode 100644
index 0000000000..de6e2efc2a
--- /dev/null
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.html
@@ -0,0 +1,24 @@
+
+@if (gridOptions(); as gridOptions) {
+
+
+
+
+ @if (pageCount(); as pageCount) {
+
+ }
+}
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts
new file mode 100644
index 0000000000..51586cfcea
--- /dev/null
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts
@@ -0,0 +1,270 @@
+import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
+import { provideLocationMocks } from '@angular/common/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { ActivatedRoute, Router, provideRouter } from '@angular/router';
+import { SkyWaitHarness } from '@skyux/indicators/testing';
+import { SkyPagingHarness } from '@skyux/lists/testing';
+
+import { getGridApi } from 'ag-grid-community';
+
+import { SkyAgGridWrapperComponent } from '../ag-grid-wrapper.component';
+import { SkyAgGridHeaderComponent } from '../header/header.component';
+
+import { AgGridTestComponent } from './fixtures/ag-grid-test.component';
+import { SkyAgGridComponent } from './sky-ag-grid.component';
+
+describe('SkyAgGridComponent', () => {
+ let fixture: ComponentFixture;
+ let component: AgGridTestComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [provideRouter([]), provideLocationMocks()],
+ });
+ fixture = TestBed.createComponent(AgGridTestComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ });
+
+ it('should destroy and recreate grid', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component).toBeTruthy();
+
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridComponent)),
+ ).toEqual([]);
+
+ fixture.componentRef.setInput('showAllGrids', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridComponent)).length,
+ ).toEqual(4);
+ });
+
+ it('should destroy and recreate columns', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component).toBeTruthy();
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent))
+ .length,
+ ).toEqual(4 * 3 + 2); // 4 grids with 3 columns each, plus 2 extra headers for the multi-select and row delete grid
+
+ fixture.componentRef.setInput('showCol3', false);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent))
+ .length,
+ ).toEqual(4 * 2 + 3); // 4 grids with 2 columns each, plus 3 extra headers for multi-select, row delete, and date column
+ expect(component.visibleColumnIds()).toEqual(['column1', 'column2']);
+
+ fixture.componentRef.setInput('showCol3', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent))
+ .length,
+ ).toEqual(4 * 3 + 2); // 4 grids with 3 columns each, plus 2 extra headers for the multi-select and row delete grid
+ expect(component.visibleColumnIds()).toEqual([
+ 'column1',
+ 'column2',
+ 'column3',
+ ]);
+
+ fixture.componentRef.setInput('showAllColumns', false);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent))
+ .length,
+ ).toEqual(0);
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridWrapperComponent))
+ .length,
+ ).toEqual(0);
+ });
+
+ it('should handle empty data', async () => {
+ component.dataForSimpleGrid = undefined;
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ const waitHarness =
+ await TestbedHarnessEnvironment.loader(fixture).getHarness(
+ SkyWaitHarness,
+ );
+ await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(false);
+ expect(
+ Array.from(
+ fixture.nativeElement
+ .querySelector('.ag-viewport')
+ .querySelectorAll('[role="row"]'),
+ ),
+ ).toHaveSize(0);
+ });
+
+ it('should select all rows', async () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ const waitHarness =
+ await TestbedHarnessEnvironment.loader(fixture).getHarness(
+ SkyWaitHarness,
+ );
+ await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(false);
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="multiselect-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getSelectedNodes()).toHaveSize(0);
+ api?.selectAll();
+ expect(api?.getSelectedNodes()).toHaveSize(7);
+ });
+
+ it('should select some rows', async () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const waitHarnesses =
+ await TestbedHarnessEnvironment.loader(fixture).getAllHarnesses(
+ SkyWaitHarness,
+ );
+ await expectAsync(
+ Promise.all(waitHarnesses.map((waitHarness) => waitHarness.isWaiting())),
+ ).toBeResolvedTo([false, false, false, false]);
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="multiselect-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getSelectedNodes()).toHaveSize(0);
+ component.selectedRowIds.set(['102', '104', '106']);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getSelectedNodes()).toHaveSize(3);
+ });
+
+ it('should highlight row', async () => {
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ const waitHarness = await TestbedHarnessEnvironment.loader(
+ fixture,
+ ).getHarness(SkyWaitHarness.with({ ancestor: '[data-sky-id="grid"]' }));
+ await expectAsync(waitHarness.isWaiting()).toBeResolvedTo(false);
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getRowNode('2')?.isSelected()).toBeFalse();
+ fixture.componentRef.setInput('rowHighlightedId', '2');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getRowNode('2')?.isSelected()).toBeTrue();
+ });
+
+ it('should handle paging without url changes', async () => {
+ component.pageSize = 4;
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component).toBeTruthy();
+ const pagingHarness =
+ await TestbedHarnessEnvironment.loader(fixture).getHarness(
+ SkyPagingHarness,
+ );
+ await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1);
+ await pagingHarness.clickNextButton();
+ await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(2);
+ await pagingHarness.clickPreviousButton();
+ await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1);
+ });
+
+ it('should handle paging with url changes', async () => {
+ component.pageSize = 2;
+ component.pageQueryParam = 'page';
+ const router = TestBed.inject(Router);
+ const navSpy = spyOn(router, 'navigate');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component).toBeTruthy();
+ const pagingHarness =
+ await TestbedHarnessEnvironment.loader(fixture).getHarness(
+ SkyPagingHarness,
+ );
+ await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1);
+ await pagingHarness.clickPageButton(2);
+ await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(2);
+ expect(navSpy).toHaveBeenCalledWith(['.'], {
+ relativeTo: jasmine.any(ActivatedRoute),
+ queryParams: { page: 2 },
+ queryParamsHandling: 'merge',
+ });
+ navSpy.calls.reset();
+
+ component.page = 2;
+ fixture.detectChanges();
+
+ await pagingHarness.clickPageButton(1);
+ await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(1);
+ expect(navSpy).toHaveBeenCalledWith(['.'], {
+ relativeTo: jasmine.any(ActivatedRoute),
+ queryParams: { page: null },
+ queryParamsHandling: 'merge',
+ });
+ navSpy.calls.reset();
+
+ component.page = 1;
+ fixture.detectChanges();
+
+ await pagingHarness.clickPageButton(3);
+ await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(3);
+ expect(navSpy).toHaveBeenCalledWith(['.'], {
+ relativeTo: jasmine.any(ActivatedRoute),
+ queryParams: { page: 3 },
+ queryParamsHandling: 'merge',
+ });
+ navSpy.calls.reset();
+
+ component.page = 3;
+ fixture.detectChanges();
+
+ await pagingHarness.clickPageButton(2);
+ await expectAsync(pagingHarness.getCurrentPage()).toBeResolvedTo(2);
+ expect(navSpy).toHaveBeenCalledWith(['.'], {
+ relativeTo: jasmine.any(ActivatedRoute),
+ queryParams: { page: 2 },
+ queryParamsHandling: 'merge',
+ });
+ navSpy.calls.reset();
+
+ component.page = 0;
+ fixture.detectChanges();
+ expect(navSpy).not.toHaveBeenCalledWith(['.'], {
+ relativeTo: jasmine.any(ActivatedRoute),
+ queryParams: { page: 0 },
+ queryParamsHandling: 'merge',
+ });
+
+ component.page = Number.POSITIVE_INFINITY;
+ fixture.detectChanges();
+ expect(navSpy).not.toHaveBeenCalledWith(['.'], {
+ relativeTo: jasmine.any(ActivatedRoute),
+ queryParams: { page: Number.POSITIVE_INFINITY },
+ queryParamsHandling: 'merge',
+ });
+ });
+});
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts
new file mode 100644
index 0000000000..fcca4cf17f
--- /dev/null
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts
@@ -0,0 +1,432 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ booleanAttribute,
+ computed,
+ contentChildren,
+ effect,
+ inject,
+ input,
+ linkedSignal,
+ model,
+ numberAttribute,
+ output,
+ signal,
+ untracked,
+} from '@angular/core';
+import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
+import { ActivatedRoute, Router } from '@angular/router';
+import { SkyWaitModule } from '@skyux/indicators';
+import { SkyPagingModule } from '@skyux/lists';
+
+import { AgGridAngular } from 'ag-grid-angular';
+import {
+ AllCommunityModule,
+ ColDef,
+ DisplayedColumnsChangedEvent,
+ GetRowIdParams,
+ GridApi,
+ GridOptions,
+ GridPreDestroyedEvent,
+ ModuleRegistry,
+ SelectionChangedEvent,
+} from 'ag-grid-community';
+import {
+ distinctUntilChanged,
+ filter,
+ fromEvent,
+ fromEventPattern,
+ map,
+ switchMap,
+ takeUntil,
+} from 'rxjs';
+
+import { SkyAgGridRowDeleteDirective } from '../ag-grid-row-delete.directive';
+import { SkyAgGridWrapperComponent } from '../ag-grid-wrapper.component';
+import { SkyAgGridService } from '../ag-grid.service';
+import { SkyAgGridRowDeleteCancelArgs } from '../types/ag-grid-row-delete-cancel-args';
+import { SkyAgGridRowDeleteConfirmArgs } from '../types/ag-grid-row-delete-confirm-args';
+import { SkyCellType } from '../types/cell-type';
+
+import { SkyAgGridColumnComponent } from './sky-ag-grid-column.component';
+
+ModuleRegistry.registerModules([AllCommunityModule]);
+
+function arraySorted(arr: string[]): string[] {
+ return arr.slice().sort((a, b) => a.localeCompare(b));
+}
+
+function arrayIsEqual(a: string[], b: string[]): boolean {
+ if (a.length !== b.length) {
+ return false;
+ }
+ const bSorted = arraySorted(b);
+ return arraySorted(a).every((v, i) => v === bSorted[i]);
+}
+
+@Component({
+ selector: 'sky-ag-grid',
+ imports: [
+ AgGridAngular,
+ SkyAgGridRowDeleteDirective,
+ SkyAgGridWrapperComponent,
+ SkyPagingModule,
+ SkyWaitModule,
+ ],
+ templateUrl: './sky-ag-grid.component.html',
+ styleUrl: './sky-ag-grid.component.css',
+ host: {
+ '[style.height.px]': 'height() || undefined',
+ '[style.width.px]': 'width() || undefined',
+ },
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SkyAgGridComponent<
+ T extends { id: string } = Record & { id: string },
+> {
+ /**
+ * The data for the grid. Each item requires an `id` and a property that maps
+ * to the `field` or `id` property of each column in the grid.
+ */
+ public readonly data = input();
+
+ /**
+ * The columns to display by default based on the ID or field of the item.
+ */
+ public readonly displayedColumns = input([]);
+
+ /**
+ * Fires when columns change. This includes changes to the displayed columns and changes
+ * to the order of columns. The event emits an array of IDs for the displayed columns that
+ * reflects the column order.
+ */
+ public readonly selectedColumnIdsChange = output();
+
+ /**
+ * The columns to hide by default based on the ID or field of the item.
+ */
+ public readonly hiddenColumns = input([]);
+
+ /**
+ * Whether to enable the multiselect feature to display a column of
+ * checkboxes on the left side of the grid. You can specify a unique ID with
+ * the `multiselectRowId` property, but multiselect defaults to the `id` property on
+ * the `data` object.
+ * @default false
+ */
+ public readonly enableMultiselect = input(false, {
+ transform: booleanAttribute,
+ });
+
+ /**
+ * How the grid fits to its parent. The valid options are `width`,
+ * which fits the grid to the parent's full width, and `scroll`, which allows the grid
+ * to exceed the parent's width. If the grid does not have enough columns to fill
+ * the parent's width, it always stretches to the parent's full width.
+ * @default "width"
+ */
+ public readonly fit = input<'width' | 'scroll'>('width');
+
+ /**
+ * The height of the grid.
+ */
+ public readonly height = input(0, {
+ transform: numberAttribute,
+ });
+
+ /**
+ * The unique ID that matches a property on the `data` object.
+ * By default, this property uses the `id` property.
+ */
+ public readonly multiselectRowId = input('id');
+
+ /**
+ * The current page number of the grid. When using `pageQueryParam`, this value should come from the query parameter.
+ * @default 1
+ */
+ public readonly page = input(1, {
+ transform: numberAttribute,
+ });
+
+ /**
+ * The number of items to display per page. Set to `0` to disable pagination.
+ * @default 0
+ */
+ public readonly pageSize = input(0, {
+ transform: numberAttribute,
+ });
+
+ /**
+ * The query parameter name to use for the current page number. When set, page changes are reflected in the URL.
+ */
+ public readonly pageQueryParam = input();
+
+ /**
+ * The width of the grid in pixels.
+ */
+ public readonly width = input(0, {
+ transform: numberAttribute,
+ });
+
+ /**
+ * The ID of the row to highlight. The ID matches the `multiselectRowId` property
+ * of the `data` object. Typically, this property is used in conjunction with
+ * the flyout component to indicate the currently selected row. Other rows
+ * are de-selected in the grid.
+ */
+ public readonly rowHighlightedId = input();
+
+ /**
+ * The set of IDs for the rows to select in a multiselect grid.
+ * The IDs match the `multiselectRowId` properties of the `data` objects.
+ * Rows with IDs that are not included are de-selected in the grid.
+ */
+ public readonly selectedRowIds = input([]);
+
+ /**
+ * The set of IDs for the rows selected in a multiselect grid.
+ * The IDs match the `multiselectRowId` properties of the `data` objects.
+ */
+ public readonly selectedRowIdsChange = output();
+
+ /**
+ * The set of IDs for the rows to prompt for delete confirmation.
+ * The IDs match the `multiselectRowId` properties of the `data` objects.
+ */
+ protected readonly rowDeleteIds = model([]);
+
+ /**
+ * Fires when users cancel the deletion of a row.
+ */
+ public readonly rowDeleteCancel = output();
+
+ /**
+ * Fires when users confirm the deletion of a row.
+ */
+ public readonly rowDeleteConfirm = output();
+
+ protected readonly columns = contentChildren(SkyAgGridColumnComponent);
+ protected readonly gridReady = signal(false);
+ protected readonly gridApi = signal(undefined);
+ protected readonly pageNumber = linkedSignal(this.page);
+
+ readonly #gridDestroyed = toObservable(this.gridApi).pipe(
+ filter(Boolean),
+ switchMap((api) =>
+ fromEventPattern((handler) =>
+ api.addEventListener('gridPreDestroyed', handler),
+ ),
+ ),
+ );
+ readonly #gridService = inject(SkyAgGridService);
+ readonly #gridSelectedRowIds = toObservable(this.gridApi).pipe(
+ filter(Boolean),
+ switchMap((api) =>
+ fromEvent(api, 'selectionChanged').pipe(
+ takeUntil(this.#gridDestroyed),
+ map((selection) =>
+ (
+ (selection.selectedNodes ?? [])
+ .map((node) => node.id as string)
+ .filter(Boolean) as string[]
+ ).sort((a, b) => a.localeCompare(b)),
+ ),
+ distinctUntilChanged(arrayIsEqual),
+ ),
+ ),
+ );
+ readonly #gridDisplayedColumnIds = toObservable(this.gridApi).pipe(
+ filter(Boolean),
+ switchMap((api) =>
+ fromEvent(
+ api,
+ 'displayedColumnsChanged',
+ ).pipe(
+ takeUntil(this.#gridDestroyed),
+ map((columnsEvent) =>
+ columnsEvent.api
+ .getAllDisplayedColumns()
+ .map((col) => col.getColId()),
+ ),
+ distinctUntilChanged(arrayIsEqual),
+ ),
+ ),
+ );
+ readonly #columnDefs = computed[]>(() => {
+ const columns = this.columns();
+ const displayed = this.displayedColumns().filter(Boolean);
+ const hidden = this.hiddenColumns().filter(Boolean);
+ return columns.map((col): ColDef => {
+ const colDef: ColDef = {
+ headerName: col.heading(),
+ headerComponentParams: {
+ helpPopoverTitle: col.helpPopoverTitle(),
+ helpPopoverContent: col.helpPopoverContent() || col.description(),
+ },
+ initialHide:
+ col.hidden() ||
+ (displayed.length > 0 &&
+ !displayed.includes(col.id() || col.field() || '')) ||
+ hidden.includes(col.id() || col.field() || ''),
+ sortable: col.isSortable(),
+ lockPosition: col.locked(),
+ suppressMovable: col.locked(),
+ type: [],
+ };
+ if (col.field()) {
+ colDef.field = col.field();
+ } else if (col.id()) {
+ colDef.colId = col.id();
+ }
+ if (col.cellTemplate()) {
+ (colDef.type as string[]).push(SkyCellType.Template);
+ colDef.cellRendererParams = { template: col.cellTemplate() };
+ } else if (col.type() === 'date') {
+ (colDef.type as string[]).push(SkyCellType.Date);
+ } else if (col.type() === 'number') {
+ (colDef.type as string[]).push(SkyCellType.Number);
+ } else if (col.type() === 'boolean') {
+ colDef.cellDataType = 'boolean';
+ } else {
+ (colDef.type as string[]).push(SkyCellType.Text);
+ }
+ if (col.width() > 0) {
+ colDef.initialWidth = col.width();
+ colDef.suppressSizeToFit = true;
+ }
+ return colDef;
+ });
+ });
+ readonly #activatedRoute = inject(ActivatedRoute, { optional: true });
+ readonly #router = inject(Router, { optional: true });
+
+ protected readonly gridOptions = computed(() => {
+ const columnDefs = this.#columnDefs();
+ if (columnDefs.length === 0) {
+ return undefined;
+ }
+ return this.#gridService.getGridOptions({
+ gridOptions: {
+ columnDefs,
+ domLayout: this.height() ? 'normal' : 'autoHeight',
+ onGridReady: (args) => {
+ this.gridApi.set(args.api);
+ this.gridReady.set(true);
+ },
+ pagination: this.pageSize() > 0,
+ suppressPaginationPanel: true,
+ paginationPageSize: this.pageSize() || undefined,
+ rowData: this.data(),
+ getRowId: (params: GetRowIdParams) =>
+ params.data[this.multiselectRowId() as keyof T] as string,
+ rowSelection: this.enableMultiselect()
+ ? {
+ checkboxes: true,
+ headerCheckbox: true,
+ mode: 'multiRow',
+ }
+ : {
+ checkboxes: false,
+ mode: 'singleRow',
+ },
+ autoSizeStrategy:
+ this.fit() === 'width' || this.width()
+ ? {
+ type: 'fitGridWidth',
+ }
+ : {
+ type: 'fitCellContents',
+ },
+ },
+ }) as GridOptions;
+ });
+ protected readonly pageCount = computed(() => {
+ const dataLength = this.data()?.length ?? 0;
+ const pageSize = this.pageSize();
+ const gridReady = this.gridReady();
+ if (!gridReady || pageSize === 0) {
+ return 0;
+ }
+ return Math.ceil(dataLength / pageSize);
+ });
+
+ constructor() {
+ effect(() => {
+ const api = untracked(() => this.gridApi());
+ const data = this.data();
+ api?.setGridOption('rowData', data ?? []);
+ });
+ effect(() => {
+ const api = untracked(() => this.gridApi());
+ const columns = this.#columnDefs();
+ api?.setGridOption('columnDefs', columns);
+ });
+ effect(() => {
+ const api = this.gridApi();
+ const selectedRowIds = this.selectedRowIds();
+ this.data();
+ const currentSelectedRowIds =
+ api?.getSelectedNodes().map((node) => node.id as string) ?? [];
+ if (!arrayIsEqual(selectedRowIds, currentSelectedRowIds)) {
+ api?.deselectAll();
+ selectedRowIds.forEach((rowId) =>
+ api?.getRowNode(rowId)?.setSelected(true),
+ );
+ }
+ });
+ effect(() => {
+ const api = this.gridApi();
+ const rowHighlightedId = this.rowHighlightedId();
+ this.data();
+ if (rowHighlightedId) {
+ const rowNode = api?.getRowNode(rowHighlightedId);
+ if (rowNode?.isSelected() === false) {
+ rowNode?.setSelected(true, true);
+ }
+ }
+ });
+ effect(() => {
+ const api = this.gridApi();
+ const pageNumber = this.pageNumber();
+ const pageCount = this.pageCount();
+ if (!pageCount || pageNumber < 1 || pageNumber > pageCount || !api) {
+ return;
+ }
+ api.paginationGoToPage(pageNumber - 1);
+ });
+ this.#gridDestroyed.pipe(takeUntilDestroyed()).subscribe(() => {
+ this.gridApi.set(undefined);
+ this.gridReady.set(false);
+ });
+ this.#gridSelectedRowIds.pipe(takeUntilDestroyed()).subscribe((rowIds) => {
+ this.selectedRowIdsChange.emit(rowIds);
+ });
+ this.#gridDisplayedColumnIds
+ .pipe(takeUntilDestroyed())
+ .subscribe((columnIds) => {
+ this.selectedColumnIdsChange.emit(columnIds);
+ });
+ }
+
+ protected pageChange(page: number): void {
+ const pageQueryParam = this.pageQueryParam();
+ const pageNumber = numberAttribute(page, 1);
+ if (
+ pageQueryParam &&
+ this.#activatedRoute &&
+ this.#activatedRoute.snapshot.queryParamMap.get(pageQueryParam) !==
+ `${page}`
+ ) {
+ // When using a query parameter, send the change through the router.
+ void this.#router?.navigate(['.'], {
+ relativeTo: this.#activatedRoute,
+ queryParams: {
+ [pageQueryParam]: pageNumber === 1 ? null : pageNumber,
+ },
+ queryParamsHandling: 'merge',
+ });
+ } else if (page) {
+ this.pageNumber.set(page);
+ }
+ }
+}
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-params.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-params.ts
index fa342faf28..5a9e4bc7f7 100644
--- a/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-params.ts
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/types/header-params.ts
@@ -1,4 +1,4 @@
-import { Type } from '@angular/core';
+import { TemplateRef, Type } from '@angular/core';
import { IHeaderParams } from 'ag-grid-community';
@@ -20,4 +20,17 @@ export interface SkyAgGridHeaderParams extends IHeaderParams {
* @see SkyAgGridHeaderInfo
*/
inlineHelpComponent?: Type;
+
+ /**
+ * The title of the help popover. This property only applies when `helpPopoverContent` is
+ * also specified.
+ */
+ helpPopoverTitle?: string | undefined;
+
+ /**
+ * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline)
+ * button is added to the column heading. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover)
+ * when clicked using the specified content and optional title.
+ */
+ helpPopoverContent?: string | TemplateRef | undefined;
}
diff --git a/libs/components/ag-grid/testing/eslint.config.js b/libs/components/ag-grid/testing/eslint.config.js
new file mode 100644
index 0000000000..b4c331fb6a
--- /dev/null
+++ b/libs/components/ag-grid/testing/eslint.config.js
@@ -0,0 +1,5 @@
+const prettier = require('eslint-config-prettier');
+const baseConfig = require('../../../../eslint-base.config');
+const overrides = require('../../../../eslint-overrides.config');
+
+module.exports = [...baseConfig, ...overrides, prettier];
diff --git a/libs/components/ag-grid/testing/karma.conf.js b/libs/components/ag-grid/testing/karma.conf.js
new file mode 100644
index 0000000000..2d434ab15e
--- /dev/null
+++ b/libs/components/ag-grid/testing/karma.conf.js
@@ -0,0 +1,19 @@
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+const { join } = require('path');
+const getBaseKarmaConfig = require('../../../../karma.conf');
+
+module.exports = function (config) {
+ const baseConfig = getBaseKarmaConfig();
+ config.set({
+ ...baseConfig,
+ coverageReporter: {
+ ...baseConfig.coverageReporter,
+ dir: join(
+ __dirname,
+ '../../../../coverage/libs/components/ag-grid/testing',
+ ),
+ },
+ });
+};
diff --git a/libs/components/ag-grid/testing/ng-package.json b/libs/components/ag-grid/testing/ng-package.json
new file mode 100644
index 0000000000..fbafcc4448
--- /dev/null
+++ b/libs/components/ag-grid/testing/ng-package.json
@@ -0,0 +1,5 @@
+{
+ "lib": {
+ "entryFile": "src/public-api.ts"
+ }
+}
diff --git a/libs/components/ag-grid/testing/project.json b/libs/components/ag-grid/testing/project.json
new file mode 100644
index 0000000000..78b2919abf
--- /dev/null
+++ b/libs/components/ag-grid/testing/project.json
@@ -0,0 +1,47 @@
+{
+ "name": "ag-grid-testing",
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "library",
+ "sourceRoot": "libs/components/ag-grid/testing/src",
+ "prefix": "sky",
+ "tags": ["testing"],
+ "targets": {
+ "test": {
+ "executor": "@angular-devkit/build-angular:karma",
+ "options": {
+ "tsConfig": "libs/components/ag-grid/testing/tsconfig.spec.json",
+ "karmaConfig": "libs/components/ag-grid/testing/karma.conf.js",
+ "codeCoverage": true,
+ "codeCoverageExclude": ["**/fixtures/**"],
+ "styles": [
+ "libs/components/theme/src/lib/styles/sky.scss",
+ "libs/components/theme/src/lib/styles/themes/modern/styles.scss"
+ ],
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing",
+ "libs/components/packages/src/polyfills.js"
+ ],
+ "inlineStyleLanguage": "scss",
+ "stylePreprocessorOptions": {
+ "includePaths": ["{workspaceRoot}"]
+ }
+ },
+ "configurations": {
+ "ci": {
+ "browsers": "ChromeHeadlessNoSandbox",
+ "codeCoverage": true,
+ "progress": false,
+ "sourceMap": true,
+ "watch": false
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint",
+ "options": {
+ "lintFilePatterns": ["{projectRoot}/src/**/*.ts"]
+ }
+ }
+ }
+}
diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.filters.ts b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.filters.ts
new file mode 100644
index 0000000000..d507802190
--- /dev/null
+++ b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.filters.ts
@@ -0,0 +1,7 @@
+import { SkyHarnessFilters } from '@skyux/core/testing';
+
+/**
+ * A set of criteria that can be used to filter a list of `SkyAgGridWrapperHarness` instances.
+ */
+// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type
+export interface SkyAgGridWrapperHarnessFilters extends SkyHarnessFilters {}
diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.spec.ts b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.spec.ts
new file mode 100644
index 0000000000..72cc88689f
--- /dev/null
+++ b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.spec.ts
@@ -0,0 +1,43 @@
+import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { SkyAgGridModule } from '@skyux/ag-grid';
+
+import { SkyAgGridWrapperHarness } from './ag-grid-wrapper-harness';
+
+@Component({
+ selector: 'app-test',
+ template: ` `,
+ imports: [SkyAgGridModule],
+})
+class TestComponent {}
+
+describe('SkyAgGridWrapperHarness', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ fixture = TestBed.createComponent(TestComponent);
+ fixture.detectChanges();
+ });
+
+ it('should check if the grid is ready', async () => {
+ const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness(
+ SkyAgGridWrapperHarness.with({ dataSkyId: 'wrapper' }),
+ );
+ await expectAsync(harness.isGridReady()).toBeResolvedTo(false);
+ });
+
+ it('should throw error if the grid is not available', async () => {
+ const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness(
+ SkyAgGridWrapperHarness.with({ dataSkyId: 'wrapper' }),
+ );
+ await expectAsync(harness.isGridReady()).toBeResolvedTo(false);
+ await expectAsync(harness.getDisplayedColumnIds()).toBeRejectedWith(
+ 'Unable to retrieve displayed column IDs.',
+ );
+ await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeRejectedWith(
+ 'Unable to retrieve displayed column header names.',
+ );
+ });
+});
diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.ts b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.ts
new file mode 100644
index 0000000000..58d9b47255
--- /dev/null
+++ b/libs/components/ag-grid/testing/src/modules/ag-grid-wrapper/ag-grid-wrapper-harness.ts
@@ -0,0 +1,80 @@
+import { HarnessPredicate } from '@angular/cdk/testing';
+import { UnitTestElement } from '@angular/cdk/testing/testbed';
+import { SkyComponentHarness } from '@skyux/core/testing';
+
+import { GridApi, getGridApi } from 'ag-grid-community';
+
+import { SkyAgGridWrapperHarnessFilters } from './ag-grid-wrapper-harness.filters';
+
+/**
+ * Harness for interacting with SKY UX AG Grid components in tests.
+ */
+export class SkyAgGridWrapperHarness extends SkyComponentHarness {
+ /**
+ * @internal
+ */
+ public static hostSelector = 'sky-ag-grid-wrapper';
+
+ /**
+ * Gets a `HarnessPredicate` that can be used to search for a
+ * `SkyAgGridWrapperHarness` that meets certain criteria
+ */
+ public static with(
+ filters: SkyAgGridWrapperHarnessFilters,
+ ): HarnessPredicate {
+ return SkyAgGridWrapperHarness.getDataSkyIdPredicate(filters);
+ }
+
+ /**
+ * Checks whether the grid is ready.
+ */
+ public async isGridReady(): Promise {
+ const gridReady = this.locatorFactory.locatorFor(
+ '.ag-root.ag-unselectable',
+ );
+
+ return await gridReady()
+ .then((el) => !!el)
+ .catch(() => false);
+ }
+
+ /**
+ * Retrieves the IDs of the currently displayed columns.
+ */
+ public async getDisplayedColumnIds(): Promise {
+ return await this.#getGridApi()
+ .then((api) => api.getAllDisplayedColumns().map((col) => col.getColId()))
+ .catch(() => Promise.reject('Unable to retrieve displayed column IDs.'));
+ }
+
+ /**
+ * Retrieves the header names of the currently displayed columns.
+ */
+ public async getDisplayedColumnHeaderNames(): Promise {
+ return await this.#getGridApi()
+ .then((api) =>
+ api
+ .getAllDisplayedColumns()
+ .map((col) => col.getColDef().headerName || ''),
+ )
+ .catch(() =>
+ Promise.reject('Unable to retrieve displayed column header names.'),
+ );
+ }
+
+ async #getGridApi(): Promise {
+ await this.waitForTasksOutsideAngular();
+ const locator = this.locatorFactory.locatorFor('ag-grid-angular');
+ return await locator().then((grid) => {
+ if (grid instanceof UnitTestElement) {
+ const api = getGridApi(grid.element);
+ if (api) {
+ return api;
+ }
+ }
+ // If this harness were used in an environment that did not provide UnitTestElement.
+ /* istanbul ignore next */
+ throw new Error('Unable to get GridApi from AgGridAngular component.');
+ });
+ }
+}
diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.filters.ts b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.filters.ts
new file mode 100644
index 0000000000..3f0dc980fe
--- /dev/null
+++ b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.filters.ts
@@ -0,0 +1,7 @@
+import { SkyHarnessFilters } from '@skyux/core/testing';
+
+/**
+ * A set of criteria that can be used to filter a list of `SkyAgGridHarness` instances.
+ */
+// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type
+export interface SkyAgGridHarnessFilters extends SkyHarnessFilters {}
diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.spec.ts b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.spec.ts
new file mode 100644
index 0000000000..9a14cda5fa
--- /dev/null
+++ b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.spec.ts
@@ -0,0 +1,99 @@
+import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SkyAgGridHarness } from './ag-grid-harness';
+import { AgGridTestComponent } from './fixtures/ag-grid-test.component';
+
+describe('ag-grid-harness', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ fixture = TestBed.createComponent(AgGridTestComponent);
+ });
+
+ it('should check if the grid is ready', async () => {
+ fixture.componentRef.setInput('showAllColumns', false);
+ fixture.detectChanges();
+
+ const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness(
+ SkyAgGridHarness.with({ dataSkyId: 'grid' }),
+ );
+ await expectAsync(harness.isGridReady()).toBeResolvedTo(false);
+
+ fixture.componentRef.setInput('showAllColumns', true);
+ fixture.detectChanges();
+
+ await expectAsync(harness.isGridReady()).toBeResolvedTo(true);
+ });
+
+ it('should get columns', async () => {
+ fixture.detectChanges();
+
+ const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness(
+ SkyAgGridHarness.with({ dataSkyId: 'grid' }),
+ );
+ await expectAsync(harness.isGridReady()).toBeResolvedTo(true);
+ await expectAsync(harness.getDisplayedColumnIds()).toBeResolvedTo([
+ 'column1',
+ 'column2',
+ 'column3',
+ ]);
+ await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([
+ 'Column1',
+ 'Column2',
+ 'Column3',
+ ]);
+
+ fixture.componentRef.setInput('showCol3', false);
+ fixture.detectChanges();
+
+ await expectAsync(harness.getDisplayedColumnIds()).toBeResolvedTo([
+ 'column1',
+ 'column2',
+ ]);
+ await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([
+ 'Column1',
+ 'Column2',
+ ]);
+ });
+
+ it('should throw error if the grid is not available', async () => {
+ fixture.componentRef.setInput('showAllColumns', false);
+ fixture.detectChanges();
+
+ const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness(
+ SkyAgGridHarness.with({ dataSkyId: 'grid' }),
+ );
+ await expectAsync(harness.isGridReady()).toBeResolvedTo(false);
+ await expectAsync(harness.getDisplayedColumnIds()).toBeRejectedWith(
+ 'Unable to retrieve displayed column IDs.',
+ );
+ await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeRejectedWith(
+ 'Unable to retrieve displayed column header names.',
+ );
+
+ fixture.componentRef.setInput('showAllColumns', true);
+ fixture.detectChanges();
+
+ await expectAsync(harness.getDisplayedColumnIds()).toBeResolvedTo([
+ 'column1',
+ 'column2',
+ 'column3',
+ ]);
+ await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([
+ 'Column1',
+ 'Column2',
+ 'Column3',
+ ]);
+
+ fixture.componentRef.setInput('showCol3HeaderText', false);
+ fixture.detectChanges();
+
+ await expectAsync(harness.getDisplayedColumnHeaderNames()).toBeResolvedTo([
+ 'Column1',
+ 'Column2',
+ '',
+ ]);
+ });
+});
diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.ts b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.ts
new file mode 100644
index 0000000000..4087d658fd
--- /dev/null
+++ b/libs/components/ag-grid/testing/src/modules/ag-grid/ag-grid-harness.ts
@@ -0,0 +1,59 @@
+import { HarnessPredicate } from '@angular/cdk/testing';
+import { SkyQueryableComponentHarness } from '@skyux/core/testing';
+
+import { SkyAgGridWrapperHarness } from '../ag-grid-wrapper/ag-grid-wrapper-harness';
+
+import { SkyAgGridHarnessFilters } from './ag-grid-harness.filters';
+
+/**
+ * Harness for interacting with SKY UX AG Grid components in tests.
+ */
+export class SkyAgGridHarness extends SkyQueryableComponentHarness {
+ /**
+ * @internal
+ */
+ public static hostSelector = 'sky-ag-grid';
+
+ /**
+ * Gets a `HarnessPredicate` that can be used to search for a
+ * `SkyAgGridHarness` that meets certain criteria
+ */
+ public static with(
+ filters: SkyAgGridHarnessFilters,
+ ): HarnessPredicate {
+ return SkyAgGridHarness.getDataSkyIdPredicate(filters);
+ }
+
+ /**
+ * Checks whether the grid is ready.
+ */
+ public async isGridReady(): Promise {
+ return await this.#getGridWrapper()
+ .then(async (grid) => await grid.isGridReady())
+ .catch(() => false);
+ }
+
+ /**
+ * Retrieves the IDs of the currently displayed columns.
+ */
+ public async getDisplayedColumnIds(): Promise {
+ return await this.#getGridWrapper()
+ .then(async (grid) => await grid.getDisplayedColumnIds())
+ .catch(() => Promise.reject('Unable to retrieve displayed column IDs.'));
+ }
+
+ /**
+ * Retrieves the header names of the currently displayed columns.
+ */
+ public async getDisplayedColumnHeaderNames(): Promise {
+ return await this.#getGridWrapper()
+ .then(async (grid) => await grid.getDisplayedColumnHeaderNames())
+ .catch(() =>
+ Promise.reject('Unable to retrieve displayed column header names.'),
+ );
+ }
+
+ async #getGridWrapper(): Promise {
+ return await this.queryHarness(SkyAgGridWrapperHarness);
+ }
+}
diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html
new file mode 100644
index 0000000000..21fc61885e
--- /dev/null
+++ b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html
@@ -0,0 +1,151 @@
+@if (showAllGrids()) {
+
+ @if (showAllColumns()) {
+
+
+ @if (showCol3()) {
+
+ }
+ }
+
+}
+Grid with multiselect enabled
+
+
+ Select all
+
+
+
+ Clear all
+
+
+
+ Programmatically select rows 101, 103, and 105
+
+
+@if (showAllGrids()) {
+
+ @if (showAllColumns()) {
+
+
+
+ @if (showCol3()) {
+
+ }
+ }
+
+}
+Selected rows: {{ selectedRowIds | json }}
+
+Grid with row delete
+
+@if (showAllGrids()) {
+
+ @if (showAllColumns()) {
+
+
+
+ @if (row.id) {
+
+
+
+
+ Delete item
+
+
+
+
+ }
+
+
+
+
+ @if (showCol3()) {
+
+ } @else {
+
+ }
+ }
+
+}
+
+ This is a numeric column. Click here to learn more.
+
+
+Grid w/ aligned columns and inline help
+
+@if (showAllGrids()) {
+
+ @if (showAllColumns()) {
+
+
+ @if (showCol3()) {
+
+ }
+ }
+
+}
diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.ts b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.ts
new file mode 100644
index 0000000000..c5509f237a
--- /dev/null
+++ b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.ts
@@ -0,0 +1,114 @@
+import { JsonPipe } from '@angular/common';
+import {
+ ChangeDetectorRef,
+ Component,
+ inject,
+ input,
+ model,
+} from '@angular/core';
+import {
+ SkyAgGridColumnComponent,
+ SkyAgGridComponent,
+ SkyAgGridRowDeleteCancelArgs,
+ SkyAgGridRowDeleteConfirmArgs,
+} from '@skyux/ag-grid';
+import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers';
+
+interface RowModel {
+ id: string;
+ column1: string;
+ column2: string;
+ column3: boolean;
+ myId?: string;
+}
+
+@Component({
+ selector: 'app-ag-grid-test',
+ templateUrl: './ag-grid-test.component.html',
+ imports: [
+ SkyAgGridComponent,
+ SkyAgGridColumnComponent,
+ SkyPopoverModule,
+ SkyDropdownModule,
+ JsonPipe,
+ ],
+})
+export class AgGridTestComponent {
+ public dataForRowDeleteGrid: RowModel[] | undefined = [
+ { id: '1', column1: '1', column2: 'Apple', column3: true },
+ { id: '2', column1: '01', column2: 'Banana', column3: false },
+ { id: '3', column1: '11', column2: 'Banana', column3: true },
+ { id: '4', column1: '12', column2: 'Daikon', column3: false },
+ { id: '5', column1: '13', column2: 'Edamame', column3: true },
+ { id: '6', column1: '20', column2: 'Fig', column3: false },
+ { id: '7', column1: '21', column2: 'Grape', column3: true },
+ ];
+
+ public dataForSimpleGrid: RowModel[] | undefined = [
+ { id: '1', column1: '1', column2: 'Apple', column3: true },
+ { id: '2', column1: '01', column2: 'Banana', column3: false },
+ { id: '3', column1: '11', column2: 'Banana', column3: true },
+ { id: '4', column1: '12', column2: 'Daikon', column3: false },
+ { id: '5', column1: '13', column2: 'Edamame', column3: true },
+ { id: '6', column1: '20', column2: 'Fig', column3: false },
+ { id: '7', column1: '21', column2: 'Grape', column3: true },
+ ];
+
+ public dataForSimpleGridWithMultiselect: RowModel[] | undefined = [
+ { id: '1', column1: '1', column2: 'Apple', column3: true, myId: '101' },
+ { id: '2', column1: '01', column2: 'Banana', column3: false, myId: '102' },
+ { id: '3', column1: '11', column2: 'Banana', column3: true, myId: '103' },
+ { id: '4', column1: '12', column2: 'Daikon', column3: false, myId: '104' },
+ { id: '5', column1: '13', column2: 'Edamame', column3: true, myId: '105' },
+ { id: '6', column1: '20', column2: 'Fig', column3: false, myId: '106' },
+ { id: '7', column1: '21', column2: 'Grape', column3: true, myId: '107' },
+ ];
+
+ public removeRowIds: string[] = [];
+ public rowHighlightedId: string | undefined;
+ public readonly selectedRowIds = model([]);
+ public visibleColumnIds: string[] = [];
+
+ public page = 1;
+ public pageSize = 0;
+ public pageQueryParam = '';
+
+ protected readonly showAllColumns = input(true);
+ protected readonly showAllGrids = input(true);
+ protected readonly showCol3 = input(true);
+ protected readonly showCol3HeaderText = input(true);
+
+ readonly #cdr = inject(ChangeDetectorRef);
+
+ public selectAll(): void {
+ this.selectedRowIds.set(
+ (this.dataForSimpleGridWithMultiselect ?? []).map(
+ (item) => item.myId as string,
+ ),
+ );
+ this.#cdr.markForCheck();
+ }
+
+ public clearAll(): void {
+ this.selectedRowIds.set([]);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ public cancelRowDelete(_cancelArgs: SkyAgGridRowDeleteCancelArgs): void {
+ // noop
+ }
+
+ public deleteItem(id: string): void {
+ this.removeRowIds = [id, ...this.removeRowIds];
+ }
+
+ public finishRowDelete(confirmArgs: SkyAgGridRowDeleteConfirmArgs): void {
+ this.dataForRowDeleteGrid = (this.dataForRowDeleteGrid ?? []).filter(
+ (data: RowModel) => data.id !== confirmArgs.id,
+ );
+ }
+
+ public selectRow(): void {
+ this.selectedRowIds.set(['101', '103', '105']);
+ }
+}
diff --git a/libs/components/ag-grid/testing/src/public-api.ts b/libs/components/ag-grid/testing/src/public-api.ts
new file mode 100644
index 0000000000..59d806d6bd
--- /dev/null
+++ b/libs/components/ag-grid/testing/src/public-api.ts
@@ -0,0 +1 @@
+export const greeting = 'Hello World!';
diff --git a/libs/components/ag-grid/testing/tsconfig.spec.json b/libs/components/ag-grid/testing/tsconfig.spec.json
new file mode 100644
index 0000000000..aad2a9f430
--- /dev/null
+++ b/libs/components/ag-grid/testing/tsconfig.spec.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../tsconfig.spec.json",
+ "compilerOptions": {
+ "outDir": "../../../../out-tsc/spec",
+ "types": ["jasmine", "node"]
+ },
+ "include": ["**/*.spec.ts", "**/*.d.ts"]
+}
diff --git a/libs/components/code-examples/src/index.ts b/libs/components/code-examples/src/index.ts
index 78af68d8e0..3803096f10 100644
--- a/libs/components/code-examples/src/index.ts
+++ b/libs/components/code-examples/src/index.ts
@@ -2,6 +2,7 @@ export { ActionBarsSummaryActionBarBasicExampleComponent } from './lib/modules/a
export { ActionBarsSummaryActionBarErrorExampleComponent } from './lib/modules/action-bars/summary-action-bar/error/example.component';
export { ActionBarsSummaryActionBarModalExampleComponent } from './lib/modules/action-bars/summary-action-bar/modal/example.component';
export { ActionBarsSummaryActionBarTabExampleComponent } from './lib/modules/action-bars/summary-action-bar/tab/example.component';
+export { AgGridBasicExampleComponent } from './lib/modules/ag-grid/grid/basic/example.component';
export { AgGridDataEntryGridBasicExampleComponent } from './lib/modules/ag-grid/data-entry-grid/basic/example.component';
export { AgGridDataEntryGridDataManagerAddedExampleComponent } from './lib/modules/ag-grid/data-entry-grid/data-manager-added/example.component';
export { AgGridDataEntryGridFocusExampleComponent } from './lib/modules/ag-grid/data-entry-grid/focus/example.component';
diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.html
new file mode 100644
index 0000000000..1250f12398
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.html
@@ -0,0 +1,31 @@
+
+
+
+
+ Delete
+
+
+
+
+ Mark inactive
+
+
+
+
+ More info
+
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.ts
new file mode 100644
index 0000000000..5dde61d98e
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/context-menu.component.ts
@@ -0,0 +1,36 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ input,
+} from '@angular/core';
+import { SkyDropdownModule } from '@skyux/popovers';
+
+import { AgGridDemoRow } from './data';
+
+@Component({
+ selector: 'app-context-menu',
+ templateUrl: './context-menu.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [SkyDropdownModule],
+})
+export class ContextMenuComponent {
+ public readonly row = input();
+
+ protected readonly contextMenuAriaLabel = computed(
+ () => `Context menu for ${this.row()?.name}`,
+ );
+ protected readonly deleteAriaLabel = computed(
+ () => `Delete ${this.row()?.name}`,
+ );
+ protected readonly markInactiveAriaLabel = computed(
+ () => `Mark ${this.row()?.name} inactive`,
+ );
+ protected readonly moreInfoAriaLabel = computed(
+ () => `More info for ${this.row()?.name}`,
+ );
+
+ protected actionClicked(action: string): void {
+ alert(`${action} clicked for ${this.row()?.name}`);
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/data.ts b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/data.ts
new file mode 100644
index 0000000000..6e74f8d51a
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/data.ts
@@ -0,0 +1,157 @@
+export interface AutocompleteOption {
+ id: string;
+ name: string;
+}
+
+export const DEPARTMENTS = [
+ {
+ id: '1',
+ name: 'Marketing',
+ },
+ {
+ id: '2',
+ name: 'Sales',
+ },
+ {
+ id: '3',
+ name: 'Engineering',
+ },
+ {
+ id: '4',
+ name: 'Customer Support',
+ },
+];
+
+export const JOB_TITLES: Record = {
+ Marketing: [
+ {
+ id: '1',
+ name: 'Social Media Coordinator',
+ },
+ {
+ id: '2',
+ name: 'Blog Manager',
+ },
+ {
+ id: '3',
+ name: 'Events Manager',
+ },
+ ],
+ Sales: [
+ {
+ id: '4',
+ name: 'Business Development Representative',
+ },
+ {
+ id: '5',
+ name: 'Account Executive',
+ },
+ ],
+ Engineering: [
+ {
+ id: '6',
+ name: 'Software Engineer',
+ },
+ {
+ id: '7',
+ name: 'Senior Software Engineer',
+ },
+ {
+ id: '8',
+ name: 'Principal Software Engineer',
+ },
+ {
+ id: '9',
+ name: 'UX Designer',
+ },
+ {
+ id: '10',
+ name: 'Product Manager',
+ },
+ ],
+ 'Customer Support': [
+ {
+ id: '11',
+ name: 'Customer Support Representative',
+ },
+ {
+ id: '12',
+ name: 'Account Manager',
+ },
+ {
+ id: '13',
+ name: 'Customer Support Specialist',
+ },
+ ],
+};
+
+export interface AgGridDemoRow {
+ id: string;
+ selected?: boolean;
+ name: string;
+ age: number;
+ startDate: Date;
+ endDate?: Date;
+ department: AutocompleteOption;
+ jobTitle?: AutocompleteOption;
+}
+
+export const AG_GRID_DEMO_DATA = [
+ {
+ id: '1',
+ name: 'Billy Bob',
+ age: 55,
+ startDate: new Date('12/1/1994'),
+ department: DEPARTMENTS[3],
+ jobTitle: JOB_TITLES['Customer Support'][1],
+ },
+ {
+ id: '2',
+ name: 'Jane Deere',
+ age: 33,
+ startDate: new Date('7/15/2009'),
+ department: DEPARTMENTS[2],
+ jobTitle: JOB_TITLES['Engineering'][2],
+ },
+ {
+ id: '3',
+ name: 'John Doe',
+ age: 38,
+ startDate: new Date('9/1/2017'),
+ endDate: new Date('9/30/2017'),
+ department: DEPARTMENTS[1],
+ },
+ {
+ id: '4',
+ name: 'David Smith',
+ age: 51,
+ startDate: new Date('1/1/2012'),
+ endDate: new Date('6/15/2018'),
+ department: DEPARTMENTS[2],
+ jobTitle: JOB_TITLES['Engineering'][4],
+ },
+ {
+ id: '5',
+ name: 'Emily Johnson',
+ age: 41,
+ startDate: new Date('1/15/2014'),
+ department: DEPARTMENTS[0],
+ jobTitle: JOB_TITLES['Marketing'][2],
+ },
+ {
+ id: '6',
+ name: 'Nicole Davidson',
+ age: 22,
+ startDate: new Date('11/1/2019'),
+ department: DEPARTMENTS[2],
+ jobTitle: JOB_TITLES['Engineering'][0],
+ },
+ {
+ id: '7',
+ name: 'Carl Roberts',
+ age: 23,
+ startDate: new Date('11/1/2019'),
+ department: DEPARTMENTS[2],
+ jobTitle: JOB_TITLES['Engineering'][3],
+ },
+];
diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.html b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.html
new file mode 100644
index 0000000000..c4eed9302c
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+ {{ row.department?.name }}
+
+
+ {{ row.jobTitle?.name }}
+
+
diff --git a/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.ts
new file mode 100644
index 0000000000..e83eda4115
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/ag-grid/grid/basic/example.component.ts
@@ -0,0 +1,18 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { SkyAgGridColumnComponent, SkyAgGridComponent } from '@skyux/ag-grid';
+
+import { ContextMenuComponent } from './context-menu.component';
+import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data';
+
+/**
+ * @title Easy grid™️
+ */
+@Component({
+ selector: 'app-ag-grid-basic-example',
+ templateUrl: './example.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [ContextMenuComponent, SkyAgGridComponent, SkyAgGridColumnComponent],
+})
+export class AgGridBasicExampleComponent {
+ protected gridData: AgGridDemoRow[] = AG_GRID_DEMO_DATA;
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index d382b60fcd..5e433ddf0b 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -45,6 +45,9 @@
"libs/components/action-bars/testing/src/public-api.ts"
],
"@skyux/ag-grid": ["libs/components/ag-grid/src/index.ts"],
+ "@skyux/ag-grid/testing": [
+ "libs/components/ag-grid/testing/src/public-api.ts"
+ ],
"@skyux/angular-tree-component": [
"libs/components/angular-tree-component/src/index.ts"
],
From c62a986bf9da2ce56cd278e2ebbdfc6d0c8a497b Mon Sep 17 00:00:00 2001
From: John White <750350+johnhwhite@users.noreply.github.com>
Date: Fri, 31 Oct 2025 15:31:18 -0400
Subject: [PATCH 02/54] Add tests
---
libs/components/ag-grid/package.json | 11 +--
.../fixtures/ag-grid-test.component.html | 4 ++
.../fixtures/ag-grid-test.component.ts | 1 +
.../sky-ag-grid/sky-ag-grid.component.spec.ts | 48 +++++++++++++
.../sky-ag-grid/sky-ag-grid.component.ts | 72 ++++++++++++-------
.../fixtures/ag-grid-test.component.html | 10 ++-
.../fixtures/ag-grid-test.component.ts | 9 +--
7 files changed, 120 insertions(+), 35 deletions(-)
diff --git a/libs/components/ag-grid/package.json b/libs/components/ag-grid/package.json
index 88e7a6a6d0..fc78cba79f 100644
--- a/libs/components/ag-grid/package.json
+++ b/libs/components/ag-grid/package.json
@@ -21,26 +21,29 @@
}
},
"peerDependencies": {
+ "@angular/cdk": "^20.2.3",
"@angular/common": "^20.3.0",
"@angular/core": "^20.3.0",
"@angular/forms": "^20.3.0",
+ "@angular/router": "^20.3.0",
"@skyux/autonumeric": "0.0.0-PLACEHOLDER",
"@skyux/core": "0.0.0-PLACEHOLDER",
"@skyux/data-manager": "0.0.0-PLACEHOLDER",
"@skyux/datetime": "0.0.0-PLACEHOLDER",
"@skyux/forms": "0.0.0-PLACEHOLDER",
+ "@skyux/help-inline": "0.0.0-PLACEHOLDER",
"@skyux/i18n": "0.0.0-PLACEHOLDER",
"@skyux/icon": "0.0.0-PLACEHOLDER",
"@skyux/indicators": "0.0.0-PLACEHOLDER",
"@skyux/layout": "0.0.0-PLACEHOLDER",
+ "@skyux/lists": "0.0.0-PLACEHOLDER",
"@skyux/lookup": "0.0.0-PLACEHOLDER",
"@skyux/popovers": "0.0.0-PLACEHOLDER",
"@skyux/theme": "0.0.0-PLACEHOLDER",
"ag-grid-angular": "^34.1.2",
- "ag-grid-community": "^34.1.2"
- },
- "dependencies": {
- "tslib": "^2.8.1"
+ "ag-grid-community": "^34.1.2",
+ "rxjs": "7.8.2"
},
+ "dependencies": {},
"sideEffects": false
}
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html
index 9333e34bc3..7512c52397 100644
--- a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/fixtures/ag-grid-test.component.html
@@ -4,6 +4,7 @@
height="200"
width="600"
[data]="dataForSimpleGrid"
+ [displayedColumns]="displayedColumns()"
[page]="page"
[pageQueryParam]="pageQueryParam"
[pageSize]="pageSize"
@@ -44,6 +45,7 @@ Grid with multiselect enabled
fit="scroll"
multiselectRowId="myId"
[data]="dataForSimpleGridWithMultiselect"
+ [displayedColumns]="displayedColumns()"
[(selectedRowIds)]="selectedRowIds"
>
@if (showAllColumns()) {
@@ -68,6 +70,7 @@ Grid with row delete
@@ -125,6 +128,7 @@ Grid w/ aligned columns and inline help
data-sky-id="inline-help-grid"
fit="scroll"
[data]="dataForSimpleGrid"
+ [displayedColumns]="displayedColumns()"
>
@if (showAllColumns()) {
([]);
public readonly removeRowIds = model([]);
public readonly rowHighlightedId = model();
public readonly selectedRowIds = model([]);
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts
index 51586cfcea..8e1a351459 100644
--- a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.spec.ts
@@ -95,6 +95,54 @@ describe('SkyAgGridComponent', () => {
).toEqual(0);
});
+ it('should respond to displayedColumns input', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component).toBeTruthy();
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent))
+ .length,
+ ).toEqual(4 * 3 + 2); // 4 grids with 3 columns each, plus 2 extra headers for the multi-select and row delete grid
+
+ fixture.componentRef.setInput('displayedColumns', ['column1', 'column2']);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent))
+ .length,
+ ).toEqual(4 * 2); // 4 grids with 2 columns each
+ expect(component.visibleColumnIds()).toEqual(['column1', 'column2']);
+
+ fixture.componentRef.setInput('displayedColumns', [
+ 'column1',
+ 'column2',
+ 'column3',
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent))
+ .length,
+ ).toEqual(4 * 3); // 4 grids with 3 columns each
+ expect(component.visibleColumnIds()).toEqual([
+ 'column1',
+ 'column2',
+ 'column3',
+ ]);
+
+ fixture.componentRef.setInput('showAllColumns', false);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridHeaderComponent))
+ .length,
+ ).toEqual(0);
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyAgGridWrapperComponent))
+ .length,
+ ).toEqual(0);
+ });
+
it('should handle empty data', async () => {
component.dataForSimpleGrid = undefined;
fixture.detectChanges();
diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts
index fcca4cf17f..dee0f9e2d6 100644
--- a/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/sky-ag-grid/sky-ag-grid.component.ts
@@ -1,7 +1,12 @@
+import {
+ coerceArray,
+ coerceBooleanProperty,
+ coerceNumberProperty,
+ coerceStringArray,
+} from '@angular/cdk/coercion';
import {
ChangeDetectionStrategy,
Component,
- booleanAttribute,
computed,
contentChildren,
effect,
@@ -9,7 +14,6 @@ import {
input,
linkedSignal,
model,
- numberAttribute,
output,
signal,
untracked,
@@ -28,6 +32,7 @@ import {
GridApi,
GridOptions,
GridPreDestroyedEvent,
+ IRowNode,
ModuleRegistry,
SelectionChangedEvent,
} from 'ag-grid-community';
@@ -93,7 +98,9 @@ export class SkyAgGridComponent<
/**
* The columns to display by default based on the ID or field of the item.
*/
- public readonly displayedColumns = input([]);
+ public readonly displayedColumns = input([], {
+ transform: coerceStringArray,
+ });
/**
* Fires when columns change. This includes changes to the displayed columns and changes
@@ -105,7 +112,9 @@ export class SkyAgGridComponent<
/**
* The columns to hide by default based on the ID or field of the item.
*/
- public readonly hiddenColumns = input([]);
+ public readonly hiddenColumns = input([], {
+ transform: coerceStringArray,
+ });
/**
* Whether to enable the multiselect feature to display a column of
@@ -115,7 +124,7 @@ export class SkyAgGridComponent<
* @default false
*/
public readonly enableMultiselect = input(false, {
- transform: booleanAttribute,
+ transform: coerceBooleanProperty,
});
/**
@@ -131,7 +140,7 @@ export class SkyAgGridComponent<
* The height of the grid.
*/
public readonly height = input(0, {
- transform: numberAttribute,
+ transform: coerceNumberProperty,
});
/**
@@ -145,7 +154,7 @@ export class SkyAgGridComponent<
* @default 1
*/
public readonly page = input(1, {
- transform: numberAttribute,
+ transform: coerceNumberProperty,
});
/**
@@ -153,7 +162,7 @@ export class SkyAgGridComponent<
* @default 0
*/
public readonly pageSize = input(0, {
- transform: numberAttribute,
+ transform: coerceNumberProperty,
});
/**
@@ -165,7 +174,7 @@ export class SkyAgGridComponent<
* The width of the grid in pixels.
*/
public readonly width = input(0, {
- transform: numberAttribute,
+ transform: coerceNumberProperty,
});
/**
@@ -225,11 +234,9 @@ export class SkyAgGridComponent<
fromEvent(api, 'selectionChanged').pipe(
takeUntil(this.#gridDestroyed),
map((selection) =>
- (
- (selection.selectedNodes ?? [])
- .map((node) => node.id as string)
- .filter(Boolean) as string[]
- ).sort((a, b) => a.localeCompare(b)),
+ this.#getRowIds(selection.selectedNodes).sort((a, b) =>
+ a.localeCompare(b),
+ ),
),
distinctUntilChanged(arrayIsEqual),
),
@@ -263,11 +270,11 @@ export class SkyAgGridComponent<
helpPopoverTitle: col.helpPopoverTitle(),
helpPopoverContent: col.helpPopoverContent() || col.description(),
},
- initialHide:
+ hide:
col.hidden() ||
(displayed.length > 0 &&
- !displayed.includes(col.id() || col.field() || '')) ||
- hidden.includes(col.id() || col.field() || ''),
+ !displayed.includes(this.#getColumnIdOrField(col))) ||
+ hidden.includes(this.#getColumnIdOrField(col)),
sortable: col.isSortable(),
lockPosition: col.locked(),
suppressMovable: col.locked(),
@@ -278,17 +285,21 @@ export class SkyAgGridComponent<
} else if (col.id()) {
colDef.colId = col.id();
}
- if (col.cellTemplate()) {
- (colDef.type as string[]).push(SkyCellType.Template);
- colDef.cellRendererParams = { template: col.cellTemplate() };
- } else if (col.type() === 'date') {
+ if (col.type() === 'date') {
(colDef.type as string[]).push(SkyCellType.Date);
+ colDef.cellDataType = 'dateString';
} else if (col.type() === 'number') {
(colDef.type as string[]).push(SkyCellType.Number);
+ colDef.cellDataType = 'number';
} else if (col.type() === 'boolean') {
colDef.cellDataType = 'boolean';
} else {
(colDef.type as string[]).push(SkyCellType.Text);
+ colDef.cellDataType = 'text';
+ }
+ if (col.cellTemplate()) {
+ (colDef.type as string[]).push(SkyCellType.Template);
+ colDef.cellRendererParams = { template: col.cellTemplate() };
}
if (col.width() > 0) {
colDef.initialWidth = col.width();
@@ -297,6 +308,7 @@ export class SkyAgGridComponent<
return colDef;
});
});
+
readonly #activatedRoute = inject(ActivatedRoute, { optional: true });
readonly #router = inject(Router, { optional: true });
@@ -365,8 +377,7 @@ export class SkyAgGridComponent<
const api = this.gridApi();
const selectedRowIds = this.selectedRowIds();
this.data();
- const currentSelectedRowIds =
- api?.getSelectedNodes().map((node) => node.id as string) ?? [];
+ const currentSelectedRowIds = this.#getRowIds(api?.getSelectedNodes());
if (!arrayIsEqual(selectedRowIds, currentSelectedRowIds)) {
api?.deselectAll();
selectedRowIds.forEach((rowId) =>
@@ -410,7 +421,7 @@ export class SkyAgGridComponent<
protected pageChange(page: number): void {
const pageQueryParam = this.pageQueryParam();
- const pageNumber = numberAttribute(page, 1);
+ const pageNumber = coerceNumberProperty(page, 1);
if (
pageQueryParam &&
this.#activatedRoute &&
@@ -429,4 +440,17 @@ export class SkyAgGridComponent<
this.pageNumber.set(page);
}
}
+
+ #getColumnIdOrField(col: SkyAgGridColumnComponent): string {
+ const id = col.id();
+ /* istanbul ignore next */
+ const field = col.field() || '';
+ return id || field;
+ }
+
+ #getRowIds(rows: (IRowNode | undefined)[] | null | undefined): string[] {
+ return coerceArray(rows)
+ .map((node) => node?.id as string)
+ .filter(Boolean) as string[];
+ }
}
diff --git a/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html
index 21fc61885e..7512c52397 100644
--- a/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html
+++ b/libs/components/ag-grid/testing/src/modules/ag-grid/fixtures/ag-grid-test.component.html
@@ -4,11 +4,12 @@
height="200"
width="600"
[data]="dataForSimpleGrid"
+ [displayedColumns]="displayedColumns()"
[page]="page"
[pageQueryParam]="pageQueryParam"
[pageSize]="pageSize"
- [rowHighlightedId]="rowHighlightedId"
- (selectedColumnIdsChange)="visibleColumnIds = $event"
+ [rowHighlightedId]="rowHighlightedId()"
+ (selectedColumnIdsChange)="visibleColumnIds.set($event)"
>
@if (showAllColumns()) {
@@ -44,6 +45,7 @@ Grid with multiselect enabled
fit="scroll"
multiselectRowId="myId"
[data]="dataForSimpleGridWithMultiselect"
+ [displayedColumns]="displayedColumns()"
[(selectedRowIds)]="selectedRowIds"
>
@if (showAllColumns()) {
@@ -60,7 +62,7 @@ Grid with multiselect enabled
}
}
-Selected rows: {{ selectedRowIds | json }}
+Selected rows: {{ selectedRowIds() | json }}
Grid with row delete
@@ -68,6 +70,7 @@ Grid with row delete
@@ -125,6 +128,7 @@ Grid w/ aligned columns and inline help
data-sky-id="inline-help-grid"
fit="scroll"
[data]="dataForSimpleGrid"
+ [displayedColumns]="displayedColumns()"
>
@if (showAllColumns()) {
([]);
+ public readonly removeRowIds = model([]);
+ public readonly rowHighlightedId = model();
public readonly selectedRowIds = model([]);
- public visibleColumnIds: string[] = [];
+ public readonly visibleColumnIds = model([]);
public page = 1;
public pageSize = 0;
@@ -99,7 +100,7 @@ export class AgGridTestComponent {
}
public deleteItem(id: string): void {
- this.removeRowIds = [id, ...this.removeRowIds];
+ this.removeRowIds.update((removeRowIds) => [id, ...removeRowIds]);
}
public finishRowDelete(confirmArgs: SkyAgGridRowDeleteConfirmArgs): void {
From 19a1b98b424768146ae7f7f7dce3a913c38892d6 Mon Sep 17 00:00:00 2001
From: John White <750350+johnhwhite@users.noreply.github.com>
Date: Sat, 1 Nov 2025 01:29:41 -0400
Subject: [PATCH 03/54] Fix test coverage, linting
---
.../data-grid/basic/grid.component.html | 2 +-
.../grids/basic/grid.component.html | 20 +++----
.../components/grids/basic/grid.component.ts | 3 +-
.../list-view-grid.component.html | 54 +++++++------------
.../list-view-grid.component.ts | 2 -
.../fixtures/ag-grid-test.component.html | 2 +-
.../sky-ag-grid-column.component.ts | 2 +-
.../sky-ag-grid/sky-ag-grid.component.spec.ts | 34 ++++++++++++
.../sky-ag-grid/sky-ag-grid.component.ts | 11 ++--
.../fixtures/ag-grid-test.component.html | 2 +-
.../ag-grid/grid/basic/example.component.html | 2 +-
11 files changed, 75 insertions(+), 59 deletions(-)
diff --git a/apps/playground/src/app/components/data-grid/basic/grid.component.html b/apps/playground/src/app/components/data-grid/basic/grid.component.html
index 6228918dc1..8bd3e3d58c 100644
--- a/apps/playground/src/app/components/data-grid/basic/grid.component.html
+++ b/apps/playground/src/app/components/data-grid/basic/grid.component.html
@@ -103,7 +103,7 @@ Grid with row delete
(rowDeleteConfirm)="finishRowDelete($event)"
>