diff --git a/apps/code-examples/src/app/app.component.scss b/apps/code-examples/src/app/app.component.scss
index 6e01270223..05fb5cdabf 100644
--- a/apps/code-examples/src/app/app.component.scss
+++ b/apps/code-examples/src/app/app.component.scss
@@ -1,3 +1,10 @@
+:host {
+ display: block;
+ width: calc(100vw - var(--sky-viewport-left) - var(--sky-viewport-right));
+ height: calc(100vh - var(--sky-viewport-top) - var(--sky-viewport-bottom));
+ position: relative;
+}
+
#home-btn {
display: block;
margin: 5px;
@@ -6,6 +13,9 @@
#content {
// Value set in the app component.
margin-top: var(--sky-viewport-top);
+ overflow-y: auto;
+ position: absolute;
+ inset: 0;
}
#controls {
diff --git a/apps/code-examples/src/styles.scss b/apps/code-examples/src/styles.scss
index 55d6e36fc5..6785b97ab8 100644
--- a/apps/code-examples/src/styles.scss
+++ b/apps/code-examples/src/styles.scss
@@ -1,6 +1,5 @@
html,
body {
- min-height: 100%;
scroll-behavior: smooth;
}
diff --git a/apps/e2e/data-grid-storybook-e2e/cypress.config.ts b/apps/e2e/data-grid-storybook-e2e/cypress.config.ts
new file mode 100644
index 0000000000..79623eb561
--- /dev/null
+++ b/apps/e2e/data-grid-storybook-e2e/cypress.config.ts
@@ -0,0 +1,14 @@
+import { sendCypressScreenshotsToPercy } from '@skyux-sdk/e2e-schematics';
+import { skyE2ePreset } from '@skyux-sdk/e2e-schematics/cypress-preset';
+
+import { defineConfig } from 'cypress';
+
+export default defineConfig({
+ e2e: {
+ ...skyE2ePreset(__dirname, {
+ setupNodeEvents: (on, config) => {
+ sendCypressScreenshotsToPercy(on, config);
+ },
+ }),
+ },
+});
diff --git a/apps/e2e/data-grid-storybook-e2e/eslint.config.js b/apps/e2e/data-grid-storybook-e2e/eslint.config.js
new file mode 100644
index 0000000000..f5e63bf056
--- /dev/null
+++ b/apps/e2e/data-grid-storybook-e2e/eslint.config.js
@@ -0,0 +1,3 @@
+const config = require('../../../eslint-e2e.config');
+
+module.exports = config;
diff --git a/apps/e2e/data-grid-storybook-e2e/project.json b/apps/e2e/data-grid-storybook-e2e/project.json
new file mode 100644
index 0000000000..4e7d1a525c
--- /dev/null
+++ b/apps/e2e/data-grid-storybook-e2e/project.json
@@ -0,0 +1,37 @@
+{
+ "name": "data-grid-storybook-e2e",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "apps/e2e/data-grid-storybook-e2e/src",
+ "projectType": "application",
+ "targets": {
+ "e2e": {
+ "executor": "@nx/cypress:cypress",
+ "options": {
+ "cypressConfig": "apps/e2e/data-grid-storybook-e2e/cypress.config.ts",
+ "devServerTarget": "data-grid-storybook:storybook",
+ "baseUrl": "http://localhost:4400",
+ "browser": "chrome",
+ "testingType": "e2e"
+ },
+ "configurations": {
+ "ci": {
+ "baseUrl": "http://localhost:4200",
+ "browser": "chrome",
+ "devServerTarget": "data-grid-storybook:static-storybook:ci"
+ },
+ "prebuilt": {
+ "devServerTarget": "data-grid-storybook:static-storybook:prebuilt",
+ "baseUrl": ""
+ }
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint",
+ "options": {
+ "lintFilePatterns": ["{projectRoot}/**/*.{js,ts}"]
+ }
+ }
+ },
+ "tags": [],
+ "implicitDependencies": ["data-grid-storybook"]
+}
diff --git a/apps/e2e/data-grid-storybook-e2e/src/e2e/.gitkeep b/apps/e2e/data-grid-storybook-e2e/src/e2e/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/e2e/data-grid-storybook-e2e/src/e2e/data-grid/data-grid.component.cy.ts b/apps/e2e/data-grid-storybook-e2e/src/e2e/data-grid/data-grid.component.cy.ts
new file mode 100644
index 0000000000..09570b10b6
--- /dev/null
+++ b/apps/e2e/data-grid-storybook-e2e/src/e2e/data-grid/data-grid.component.cy.ts
@@ -0,0 +1,29 @@
+import { E2eVariations } from '@skyux-sdk/e2e-schematics';
+
+describe('data-grid', () => {
+ E2eVariations.forEachTheme((theme) => {
+ describe(`in ${theme} theme`, () => {
+ beforeEach(() =>
+ cy.visit(
+ `/iframe.html?globals=theme:${theme}&id=data-gridcomponent--data-grid`,
+ ),
+ );
+
+ it('should render the component', () => {
+ cy.skyReady();
+ cy.get('app-data-grid').should('exist').should('be.visible');
+ cy.get(
+ '[row-index="6"] [col-id="jobTitle"] sky-ag-grid-cell-renderer-template',
+ )
+ .should('exist')
+ .should('be.visible')
+ .should('have.text', 'UX Designer');
+ cy.get('[row-index="6"] [col-id="context"] button.sky-dropdown-button')
+ .should('exist')
+ .should('be.visible')
+ .should('be.enabled');
+ cy.skyVisualTest(`data-grid-${theme}`);
+ });
+ });
+ });
+});
diff --git a/apps/e2e/data-grid-storybook-e2e/src/support/commands.ts b/apps/e2e/data-grid-storybook-e2e/src/support/commands.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/e2e/data-grid-storybook-e2e/src/support/e2e.ts b/apps/e2e/data-grid-storybook-e2e/src/support/e2e.ts
new file mode 100644
index 0000000000..939a57c1a2
--- /dev/null
+++ b/apps/e2e/data-grid-storybook-e2e/src/support/e2e.ts
@@ -0,0 +1,19 @@
+// ***********************************************************
+// This example support/e2e.ts is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+// Import commands.ts using ES2015 syntax:
+import '@percy/cypress';
+import '@skyux-sdk/cypress-commands';
+
+import './commands';
diff --git a/apps/e2e/data-grid-storybook-e2e/tsconfig.json b/apps/e2e/data-grid-storybook-e2e/tsconfig.json
new file mode 100644
index 0000000000..479152932e
--- /dev/null
+++ b/apps/e2e/data-grid-storybook-e2e/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "sourceMap": false,
+ "outDir": "../../../dist/out-tsc",
+ "allowJs": true,
+ "types": ["cypress", "node"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.js", "cypress.config.ts"]
+}
diff --git a/apps/e2e/data-grid-storybook/.storybook/main.ts b/apps/e2e/data-grid-storybook/.storybook/main.ts
new file mode 100644
index 0000000000..24ca0ad7a2
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/.storybook/main.ts
@@ -0,0 +1,10 @@
+import type { StorybookConfig } from 'storybook/internal/types';
+
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import { rootMain } from '../../../../.storybook/main';
+
+const config: StorybookConfig = {
+ ...rootMain,
+ stories: ['../src/app/**/*.stories.@(js|ts)'],
+};
+export default config;
diff --git a/apps/e2e/data-grid-storybook/.storybook/manager.ts b/apps/e2e/data-grid-storybook/.storybook/manager.ts
new file mode 100644
index 0000000000..7db9e198db
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/.storybook/manager.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line @nx/enforce-module-boundaries, no-restricted-syntax
+export * from '../../../../.storybook/manager';
diff --git a/apps/e2e/data-grid-storybook/.storybook/preview.ts b/apps/e2e/data-grid-storybook/.storybook/preview.ts
new file mode 100644
index 0000000000..10230a8523
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/.storybook/preview.ts
@@ -0,0 +1,21 @@
+import {
+ previewWrapperDecorators,
+ previewWrapperGlobalTypes,
+ previewWrapperParameters,
+} from '@skyux/storybook';
+import { moduleMetadata } from '@storybook/angular';
+
+export const parameters = {
+ ...previewWrapperParameters,
+};
+
+export const globalTypes = {
+ ...previewWrapperGlobalTypes,
+};
+
+export const decorators = [
+ ...previewWrapperDecorators,
+ moduleMetadata({
+ imports: [],
+ }),
+];
diff --git a/apps/e2e/data-grid-storybook/.storybook/tsconfig.json b/apps/e2e/data-grid-storybook/.storybook/tsconfig.json
new file mode 100644
index 0000000000..f64fa3a2a2
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/.storybook/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "emitDecoratorMetadata": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true
+ },
+ "exclude": ["../**/*.spec.ts"],
+ "include": [
+ "../src/**/*.stories.ts",
+ "../src/**/*.stories.js",
+ "../src/**/*.stories.jsx",
+ "../src/**/*.stories.tsx",
+
+ "*.ts",
+ "*.js",
+ "./*"
+ ]
+}
diff --git a/apps/e2e/data-grid-storybook/eslint.config.js b/apps/e2e/data-grid-storybook/eslint.config.js
new file mode 100644
index 0000000000..c9958e222d
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/eslint.config.js
@@ -0,0 +1,3 @@
+const config = require('../../../eslint-storybook.config');
+
+module.exports = config;
diff --git a/apps/e2e/data-grid-storybook/project.json b/apps/e2e/data-grid-storybook/project.json
new file mode 100644
index 0000000000..5b3a5c1238
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/project.json
@@ -0,0 +1,146 @@
+{
+ "name": "data-grid-storybook",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "application",
+ "sourceRoot": "apps/e2e/data-grid-storybook/src",
+ "prefix": "app",
+ "tags": ["component-e2e"],
+ "targets": {
+ "build": {
+ "executor": "@angular/build:application",
+ "outputs": ["{options.outputPath.base}"],
+ "options": {
+ "outputPath": {
+ "base": "dist/apps/e2e/data-grid-storybook"
+ },
+ "index": "apps/e2e/data-grid-storybook/src/index.html",
+ "browser": "apps/e2e/data-grid-storybook/src/main.ts",
+ "polyfills": ["zone.js", "libs/components/packages/src/polyfills.js"],
+ "tsConfig": "apps/e2e/data-grid-storybook/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "stylePreprocessorOptions": {
+ "includePaths": ["{workspaceRoot}"]
+ },
+ "styles": [
+ "apps/e2e/data-grid-storybook/src/styles.scss",
+ "libs/components/theme/src/lib/styles/sky.scss",
+ "libs/components/theme/src/lib/styles/themes/modern/styles.scss",
+ "libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss"
+ ]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "2mb",
+ "maximumError": "5mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "6kb",
+ "maximumError": "10kb"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "continuous": true,
+ "executor": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "data-grid-storybook:build:production"
+ },
+ "development": {
+ "buildTarget": "data-grid-storybook:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "executor": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "buildTarget": "data-grid-storybook:build"
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint",
+ "options": {
+ "lintFilePatterns": ["{projectRoot}/**/*.ts", "{projectRoot}/**/*.html"]
+ }
+ },
+ "storybook": {
+ "executor": "@storybook/angular:start-storybook",
+ "options": {
+ "port": 4400,
+ "configDir": "apps/e2e/data-grid-storybook/.storybook",
+ "browserTarget": "data-grid-storybook:build",
+ "compodoc": false,
+ "styles": [
+ "apps/e2e/data-grid-storybook/src/styles.scss",
+ "libs/components/theme/src/lib/styles/sky.scss",
+ "libs/components/theme/src/lib/styles/themes/modern/styles.scss",
+ "libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss"
+ ]
+ },
+ "configurations": {
+ "ci": {
+ "quiet": true,
+ "ci": true
+ }
+ }
+ },
+ "build-storybook": {
+ "executor": "@storybook/angular:build-storybook",
+ "outputs": ["{options.outputDir}"],
+ "options": {
+ "outputDir": "dist/storybook/data-grid-storybook",
+ "configDir": "apps/e2e/data-grid-storybook/.storybook",
+ "browserTarget": "data-grid-storybook:build",
+ "compodoc": false,
+ "styles": [
+ "apps/e2e/data-grid-storybook/src/styles.scss",
+ "libs/components/theme/src/lib/styles/sky.scss",
+ "libs/components/theme/src/lib/styles/themes/modern/styles.scss",
+ "libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss"
+ ]
+ },
+ "configurations": {
+ "ci": {
+ "quiet": true
+ }
+ }
+ },
+ "static-storybook": {
+ "executor": "@nx/web:file-server",
+ "dependsOn": ["build-storybook"],
+ "options": {
+ "buildTarget": "data-grid-storybook:build-storybook",
+ "staticFilePath": "dist/storybook/data-grid-storybook",
+ "spa": true
+ },
+ "configurations": {
+ "ci": {
+ "buildTarget": "data-grid-storybook:build-storybook:ci"
+ },
+ "prebuilt": {
+ "buildTarget": "data-grid-storybook:noop"
+ }
+ }
+ },
+ "noop": {
+ "executor": "nx:noop"
+ }
+ }
+}
diff --git a/apps/e2e/data-grid-storybook/src/app/app.component.ts b/apps/e2e/data-grid-storybook/src/app/app.component.ts
new file mode 100644
index 0000000000..c4687cab76
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/src/app/app.component.ts
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ selector: 'app-root',
+ template: ` `,
+ imports: [RouterOutlet],
+})
+export class AppComponent {}
diff --git a/apps/e2e/data-grid-storybook/src/app/app.routes.ts b/apps/e2e/data-grid-storybook/src/app/app.routes.ts
new file mode 100644
index 0000000000..b71e35cb43
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/src/app/app.routes.ts
@@ -0,0 +1,13 @@
+import { Route } from '@angular/router';
+
+export const routes: Route[] = [
+ {
+ path: '',
+ pathMatch: 'full',
+ redirectTo: 'data-grid',
+ },
+ {
+ path: 'data-grid',
+ loadComponent: () => import('./data-grid/data-grid.component'),
+ },
+];
diff --git a/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.html b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.html
new file mode 100644
index 0000000000..67e54343e1
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+ {{ row.department?.name }}
+
+
+ {{ row.jobTitle?.name }}
+
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+ Mark inactive
+
+
+
+
+ More info
+
+
+
+
+
diff --git a/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.stories.ts b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.stories.ts
new file mode 100644
index 0000000000..e2e586ecc1
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.stories.ts
@@ -0,0 +1,12 @@
+import type { Meta, StoryObj } from '@storybook/angular';
+
+import { DataGridComponent } from './data-grid.component';
+
+export default {
+ id: 'data-gridcomponent',
+ title: 'Components/Data Grid',
+ component: DataGridComponent,
+} as Meta;
+type Story = StoryObj;
+export const DataGrid: Story = {};
+DataGrid.args = {};
diff --git a/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.ts b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.ts
new file mode 100644
index 0000000000..d1b09b9bb2
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/src/app/data-grid/data-grid.component.ts
@@ -0,0 +1,23 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { SkyDataGridModule } from '@skyux/data-grid';
+import { SkyDropdownModule } from '@skyux/popovers';
+
+import { DATA_GRID_DEMO_DATA, DataGridDemoRow } from './data';
+
+/**
+ * @title Basic data grid
+ */
+@Component({
+ selector: 'app-data-grid',
+ templateUrl: './data-grid.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [SkyDataGridModule, SkyDropdownModule],
+})
+export class DataGridComponent {
+ protected gridData: DataGridDemoRow[] = DATA_GRID_DEMO_DATA;
+
+ public actionClicked(row: DataGridDemoRow, action: string): void {
+ alert(`${action} clicked for ${row.name}`);
+ }
+}
+export default DataGridComponent;
diff --git a/apps/e2e/data-grid-storybook/src/app/data-grid/data.ts b/apps/e2e/data-grid-storybook/src/app/data-grid/data.ts
new file mode 100644
index 0000000000..25ebd17408
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/src/app/data-grid/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 DataGridDemoRow {
+ id: string;
+ selected?: boolean;
+ name: string;
+ age: number;
+ startDate: Date;
+ endDate?: Date;
+ department: AutocompleteOption;
+ jobTitle?: AutocompleteOption;
+}
+
+export const DATA_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/apps/e2e/data-grid-storybook/src/index.html b/apps/e2e/data-grid-storybook/src/index.html
new file mode 100644
index 0000000000..35c642541d
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/src/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ data-grid-storybook
+
+
+
+
+
+
+
diff --git a/apps/e2e/data-grid-storybook/src/main.ts b/apps/e2e/data-grid-storybook/src/main.ts
new file mode 100644
index 0000000000..3a19f1ac8d
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/src/main.ts
@@ -0,0 +1,18 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
+import {
+ provideRouter,
+ withEnabledBlockingInitialNavigation,
+} from '@angular/router';
+import { provideInitialTheme } from '@skyux/theme';
+
+import { AppComponent } from './app/app.component';
+import { routes } from './app/app.routes';
+
+bootstrapApplication(AppComponent, {
+ providers: [
+ provideInitialTheme('modern'),
+ provideAnimationsAsync(),
+ provideRouter(routes, withEnabledBlockingInitialNavigation()),
+ ],
+}).catch((err) => console.error(err));
diff --git a/apps/e2e/data-grid-storybook/src/styles.scss b/apps/e2e/data-grid-storybook/src/styles.scss
new file mode 100644
index 0000000000..90d4ee0072
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/src/styles.scss
@@ -0,0 +1 @@
+/* You can add global styles to this file, and also import other style files */
diff --git a/apps/e2e/data-grid-storybook/tsconfig.app.json b/apps/e2e/data-grid-storybook/tsconfig.app.json
new file mode 100644
index 0000000000..ea3e05c0c5
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/tsconfig.app.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": [],
+ "esModuleInterop": true,
+ "resolveJsonModule": true
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": [
+ "**/*.test.ts",
+ "**/*.spec.ts",
+ "**/*.stories.ts",
+ "**/*.stories.js"
+ ]
+}
diff --git a/apps/e2e/data-grid-storybook/tsconfig.json b/apps/e2e/data-grid-storybook/tsconfig.json
new file mode 100644
index 0000000000..9b0b81e919
--- /dev/null
+++ b/apps/e2e/data-grid-storybook/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "compilerOptions": {
+ "resolveJsonModule": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./.storybook/tsconfig.json"
+ }
+ ]
+}
diff --git a/apps/playground/src/app/components/components.module.ts b/apps/playground/src/app/components/components.module.ts
index 0e542c804f..7171592020 100644
--- a/apps/playground/src/app/components/components.module.ts
+++ b/apps/playground/src/app/components/components.module.ts
@@ -75,6 +75,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: () =>
@@ -92,6 +102,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..23a7dd2519
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/basic/grid.component.html
@@ -0,0 +1,191 @@
+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 (row.id) {
+
+
+
+
+ Delete item
+
+
+
+
+ }
+
+
+
+
+ @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..d1bdf5893c
--- /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 {
+ SkyAgGridRowDeleteCancelArgs,
+ SkyAgGridRowDeleteConfirmArgs,
+} from '@skyux/ag-grid';
+import {
+ SkyDataGridColumnComponent,
+ SkyDataGridComponent,
+} from '@skyux/data-grid';
+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: [
+ SkyDataGridComponent,
+ SkyDataGridColumnComponent,
+ SkyPopoverModule,
+ SkyDropdownModule,
+ 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..55b09df48e
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-grid.module.ts
@@ -0,0 +1,67 @@
+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',
+ icon: 'table',
+ library: 'data-grid',
+ },
+ },
+ {
+ path: 'data-manager',
+ loadComponent: () =>
+ import('./data-manager-large/data-manager-large.component'),
+ data: {
+ name: 'Data Grid w/ data-manager',
+ icon: 'table',
+ library: 'data-grid',
+ },
+ },
+ {
+ path: 'data-manager-in-modal',
+ loadComponent: () =>
+ import('./data-manager-large/data-manager-large-in-modal.component'),
+ data: {
+ name: 'Data Grid w/ data-manager, in a modal',
+ icon: 'table',
+ library: 'data-grid',
+ },
+ },
+ {
+ path: 'paging',
+ loadComponent: () => import('./paging/grid-paging.component'),
+ data: {
+ name: 'Data Grid Paging',
+ icon: 'table',
+ library: 'data-grid',
+ },
+ },
+ {
+ path: 'filtered',
+ loadComponent: () => import('./filtered/grid.component'),
+ data: {
+ name: 'Data Grid Filtered',
+ icon: 'filter',
+ library: 'data-grid',
+ },
+ },
+];
+
+@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/data-manager-large/custom-link/custom-link.component.html b/apps/playground/src/app/components/data-grid/data-manager-large/custom-link/custom-link.component.html
new file mode 100644
index 0000000000..35a49a8efc
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/custom-link/custom-link.component.html
@@ -0,0 +1,3 @@
+@if (link) {
+ {{ link() }}
+}
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/custom-link/custom-link.component.ts b/apps/playground/src/app/components/data-grid/data-manager-large/custom-link/custom-link.component.ts
new file mode 100644
index 0000000000..9d129cfd5b
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/custom-link/custom-link.component.ts
@@ -0,0 +1,21 @@
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+
+@Component({
+ selector: 'app-custom-link',
+ templateUrl: './custom-link.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ styles: [
+ `
+ :host {
+ display: block;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+ `,
+ ],
+})
+export class CustomLinkComponent {
+ public readonly link = input();
+}
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.html b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.html
new file mode 100644
index 0000000000..c94895bb57
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.html
@@ -0,0 +1,31 @@
+
+
+ Modal
+
+
+
+
+
+
+ Cancel
+
+
+ Open non-grid modal
+
+
+ Open grid modal
+
+
+
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.ts b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.ts
new file mode 100644
index 0000000000..255c172566
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-in-modal.component.ts
@@ -0,0 +1,67 @@
+import { Component, OnInit, inject } from '@angular/core';
+import {
+ SkyModalInstance,
+ SkyModalModule,
+ SkyModalService,
+} from '@skyux/modals';
+
+import { DataManagerLargeComponent } from './data-manager-large.component';
+
+@Component({
+ selector: 'app-data-manager-large-in-modal-modal',
+ templateUrl: './data-manager-large-in-modal.component.html',
+ imports: [SkyModalModule],
+})
+export class DataManagerLargeInModalModalComponent {
+ protected readonly modal = inject(SkyModalInstance);
+ readonly #modalService = inject(SkyModalService);
+
+ public openGridModal(): void {
+ this.#modalService.open(DataManagerLargeInModalModalGridComponent, {
+ size: 'large',
+ });
+ }
+
+ public openNotGridModal(): void {
+ this.#modalService.open(DataManagerLargeInModalModalNotGridComponent, {
+ size: 'large',
+ });
+ }
+}
+
+@Component({
+ selector: 'app-data-manager-large-in-modal-modal-grid',
+ template: `
+
+
+
+ `,
+ imports: [DataManagerLargeComponent, DataManagerLargeInModalModalComponent],
+})
+export class DataManagerLargeInModalModalGridComponent {}
+
+@Component({
+ selector: 'app-data-manager-large-in-modal-modal-not-grid',
+ template: `
+
+ Not a grid.
+
+ `,
+ imports: [DataManagerLargeInModalModalComponent],
+})
+export class DataManagerLargeInModalModalNotGridComponent {}
+
+@Component({
+ selector: 'app-data-manager-large-in-modal',
+ template: '',
+})
+export class DataManagerLargeInModalComponent implements OnInit {
+ readonly #modalService = inject(SkyModalService);
+
+ public ngOnInit(): void {
+ this.#modalService.open(DataManagerLargeInModalModalGridComponent, {
+ size: 'large',
+ });
+ }
+}
+export default DataManagerLargeInModalComponent;
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-routes.ts b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-routes.ts
new file mode 100644
index 0000000000..ed78baccfd
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large-routes.ts
@@ -0,0 +1,28 @@
+import { ComponentRouteInfo } from '../../../shared/component-info/component-route-info';
+
+import { DataManagerLargeInModalComponent } from './data-manager-large-in-modal.component';
+import { DataManagerLargeComponent } from './data-manager-large.component';
+
+const routes: ComponentRouteInfo[] = [
+ {
+ path: '',
+ pathMatch: 'full',
+ component: DataManagerLargeComponent,
+ data: {
+ name: 'Data Grid (data manager large)',
+ library: 'data-grid',
+ icon: 'table',
+ },
+ },
+ {
+ path: 'in-modal',
+ component: DataManagerLargeInModalComponent,
+ data: {
+ name: 'Data Grid (data manager large, in modal)',
+ library: 'data-grid',
+ icon: 'table',
+ },
+ },
+];
+
+export default routes;
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.html b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.html
new file mode 100644
index 0000000000..561a7f7202
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.html
@@ -0,0 +1,273 @@
+
+
+
+
+
+
+
+
+
+ @if (ready()) {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+
+
+
+
+ Delete
+
+
+
+
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.scss b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.scss
new file mode 100644
index 0000000000..f259ee1110
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.scss
@@ -0,0 +1,27 @@
+app-data-manager-large.use-normal-dom-layout {
+ position: relative;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: calc(100vh - 80px);
+ display: block;
+
+ sky-data-manager,
+ sky-data-view {
+ display: block;
+ height: 100%;
+ width: 100%;
+ }
+
+ sky-data-manager > .sky-data-manager {
+ display: flex;
+ flex-direction: column;
+ flex-basis: fit-content;
+ height: 100%;
+ width: 100%;
+ }
+}
+
+form > * {
+ display: inline-block;
+}
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.ts b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.ts
new file mode 100644
index 0000000000..92c19bd9fa
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-manager-large.component.ts
@@ -0,0 +1,298 @@
+import {
+ Component,
+ Signal,
+ ViewEncapsulation,
+ computed,
+ effect,
+ inject,
+ linkedSignal,
+ model,
+ signal,
+ untracked,
+} from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import {
+ FormBuilder,
+ FormControl,
+ FormGroup,
+ ReactiveFormsModule,
+} from '@angular/forms';
+import { SkyAgGridModule, SkyAgGridRowDeleteConfirmArgs } from '@skyux/ag-grid';
+import { SkyLogService, SkyUIConfigService } from '@skyux/core';
+import { SkyDataGridFilterValue, SkyDataGridModule } from '@skyux/data-grid';
+import {
+ SkyDataManagerConfig,
+ SkyDataManagerModule,
+ SkyDataManagerService,
+ SkyDataManagerState,
+} from '@skyux/data-manager';
+import {
+ SkyFilterBarFilterItem,
+ SkyFilterItemLookupSearchAsyncArgs,
+} from '@skyux/filter-bar';
+import { SkyFilterBarModule } from '@skyux/filter-bar';
+import { SkyCheckboxModule, SkyRadioModule } from '@skyux/forms';
+import { SkyHelpInlineModule } from '@skyux/help-inline';
+import { SkyIconModule } from '@skyux/icon';
+import { SkyDropdownModule } from '@skyux/popovers';
+
+import { BehaviorSubject, of } from 'rxjs';
+import { take } from 'rxjs/operators';
+
+import { CustomLinkComponent } from './custom-link/custom-link.component';
+import { data } from './data-set-large';
+import { LocalStorageConfigService } from './local-storage-config.service';
+
+interface GridSettingsType {
+ enableTopScroll: FormControl;
+ domLayout: FormControl<'normal' | 'autoHeight' | 'print'>;
+ compact: FormControl;
+ showSelect: FormControl;
+ showDelete: FormControl;
+ wrapText: FormControl;
+}
+
+@Component({
+ selector: 'app-data-manager-large',
+ templateUrl: './data-manager-large.component.html',
+ styleUrls: ['./data-manager-large.component.scss'],
+ encapsulation: ViewEncapsulation.None,
+ imports: [
+ SkyAgGridModule,
+ SkyDataGridModule,
+ SkyDataManagerModule,
+ SkyDropdownModule,
+ SkyCheckboxModule,
+ SkyIconModule,
+ SkyRadioModule,
+ ReactiveFormsModule,
+ SkyHelpInlineModule,
+ CustomLinkComponent,
+ SkyFilterBarModule,
+ ],
+ providers: [
+ SkyDataManagerService,
+ {
+ provide: SkyUIConfigService,
+ useClass: LocalStorageConfigService,
+ },
+ ],
+})
+export class DataManagerLargeComponent {
+ public dataManagerConfig: SkyDataManagerConfig = {};
+
+ public defaultDataState = new SkyDataManagerState({
+ additionalData: {
+ compact: false,
+ domLayout: 'autoHeight',
+ enableTopScroll: true,
+ showDelete: true,
+ showSelect: true,
+ version: 2,
+ },
+ filterData: {
+ filtersApplied: false,
+ filters: {},
+ },
+ views: [
+ {
+ viewId: 'gridView',
+ displayedColumnIds: [
+ 'credit_line',
+ 'object_date',
+ 'title',
+ 'culture',
+ 'artist_display_name',
+ /* spell-checker:disable-next-line */
+ 'artist_display_bio',
+ 'accessionyear',
+ 'object_wikidata_url',
+ 'link_resource',
+ 'country',
+ 'department',
+ 'dimensions',
+ 'gallery_number',
+ 'geography_type',
+ 'medium',
+ 'menu',
+ 'object_name',
+ 'object_begin_date',
+ 'object_date_1',
+ 'object_end_date',
+ 'object_number',
+ ],
+ },
+ ],
+ });
+
+ public readonly viewId = 'gridView';
+
+ public dataState: SkyDataManagerState | undefined;
+ public readonly items = signal(
+ data.map((item) => ({
+ id: item.object_id,
+ ...item,
+ })),
+ );
+ public readonly settingsKey = 'data-grid-large-test';
+ public readonly isActive$ = new BehaviorSubject(true);
+ public readonly gridSettings: FormGroup;
+ public readonly gridSettingsChanges: Signal;
+ public readonly enableTopScroll: Signal;
+ public readonly dataManagerStateUpdates: Signal;
+ public readonly dataManagerStateGridSettings: Signal<
+ typeof this.gridSettings.value
+ >;
+ public readonly dataManagerStateShowDelete: Signal;
+
+ protected readonly appliedFilters =
+ model[]>();
+ protected readonly height = computed(() =>
+ this.gridSettingsChanges()?.domLayout === 'normal' ? 500 : undefined,
+ );
+ protected readonly ready = signal(true);
+ protected readonly rowDeleteIds = model([]);
+
+ readonly #dataManagerService = inject(SkyDataManagerService);
+ readonly #logger = inject(SkyLogService);
+
+ constructor() {
+ const formBuilder = inject(FormBuilder);
+ this.gridSettings = formBuilder.group({
+ enableTopScroll: formBuilder.nonNullable.control(true),
+ showSelect: formBuilder.nonNullable.control(true),
+ showDelete: formBuilder.nonNullable.control(true),
+ domLayout: formBuilder.nonNullable.control('autoHeight'),
+ compact: formBuilder.nonNullable.control(false),
+ wrapText: formBuilder.nonNullable.control(false),
+ });
+ this.gridSettingsChanges = toSignal(this.gridSettings.valueChanges, {
+ initialValue: this.gridSettings.value,
+ });
+ this.enableTopScroll = computed(
+ () => this.gridSettingsChanges().enableTopScroll,
+ );
+ effect(() => {
+ this.enableTopScroll();
+ untracked(() => {
+ this.ready.set(false);
+ setTimeout(() => this.ready.set(true), 10);
+ });
+ });
+
+ this.#dataManagerService.initDataManager({
+ activeViewId: 'gridView',
+ dataManagerConfig: this.dataManagerConfig,
+ defaultDataState: this.defaultDataState,
+ settingsKey: this.settingsKey,
+ });
+
+ this.#dataManagerService.initDataView({
+ id: this.viewId,
+ name: 'Grid View',
+ iconName: 'table',
+ searchEnabled: true,
+ columnPickerEnabled: true,
+ });
+
+ this.dataManagerStateUpdates = toSignal(
+ this.#dataManagerService.getDataStateUpdates('gridSettings'),
+ );
+ this.dataManagerStateGridSettings = linkedSignal<
+ SkyDataManagerState,
+ typeof this.gridSettings.value
+ >({
+ source: () => this.dataManagerStateUpdates(),
+ computation: (state) =>
+ state.additionalData ?? {
+ compact: false,
+ domLayout: 'autoHeight',
+ enableTopScroll: true,
+ showDelete: true,
+ showSelect: true,
+ wrapText: false,
+ },
+ equal: (a, b) => JSON.stringify(a) === JSON.stringify(b),
+ });
+ this.dataManagerStateShowDelete = computed(() =>
+ this.dataManagerStateUpdates().views[0].displayedColumnIds.includes(
+ 'menu',
+ ),
+ );
+ effect(() => {
+ const gridSettings = this.gridSettingsChanges();
+ const state = new SkyDataManagerState(
+ untracked(this.dataManagerStateUpdates),
+ );
+ if (gridSettings.showDelete) {
+ state.views[0].displayedColumnIds = [
+ ...new Set(['menu', ...state.views[0].displayedColumnIds]),
+ ];
+ } else {
+ state.views[0].displayedColumnIds =
+ state.views[0].displayedColumnIds.filter((col) => col !== 'menu');
+ }
+ state.additionalData = {
+ ...state.additionalData,
+ ...gridSettings,
+ };
+ this.#dataManagerService.updateDataState(state, 'gridSettings');
+ });
+ effect(() => {
+ const stateGridSettings = this.dataManagerStateGridSettings();
+ this.gridSettings.setValue({
+ compact: stateGridSettings.compact,
+ domLayout: stateGridSettings.domLayout,
+ enableTopScroll: stateGridSettings.enableTopScroll,
+ showDelete: stateGridSettings.showDelete,
+ showSelect: stateGridSettings.showSelect,
+ wrapText: stateGridSettings.wrapText,
+ });
+ });
+ effect(() => {
+ const showDelete = this.dataManagerStateShowDelete();
+ this.gridSettings.controls.showDelete.setValue(showDelete);
+ });
+
+ this.#dataManagerService
+ .getDataStateUpdates('version-update')
+ .pipe(take(1))
+ .subscribe((state) => {
+ if (state.additionalData?.version !== 2) {
+ const update = new SkyDataManagerState(state);
+ update.additionalData ??= {};
+ update.additionalData.version =
+ this.defaultDataState.additionalData.version;
+ const gridViewDefault = this.defaultDataState.views[0];
+ const gridView = state.views[0];
+ gridView.displayedColumnIds = gridViewDefault.displayedColumnIds;
+ this.#dataManagerService.updateDataState(update, 'version-update');
+ }
+ });
+ }
+
+ public markForDelete(rowId: string): void {
+ this.rowDeleteIds.update((rowIds) => {
+ return [...new Set([...rowIds, rowId])];
+ });
+ }
+
+ protected deleteConfirm($event: SkyAgGridRowDeleteConfirmArgs): void {
+ this.items.update((items) => items.filter((item) => item.id !== $event.id));
+ }
+
+ protected searchCulture(search: SkyFilterItemLookupSearchAsyncArgs): void {
+ const options = [...new Set(this.items().map((item) => item.culture))]
+ .filter(Boolean)
+ .sort((a, b) => a.localeCompare(b));
+ search.result = of({
+ items: options.map((culture) => Object.assign(`${culture}`, { culture })),
+ totalCount: options.length,
+ });
+ }
+
+ protected selectionChange($event: string[]): void {
+ this.#logger.info(`selectionChange:`, [$event]);
+ }
+}
+export default DataManagerLargeComponent;
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-set-large.ts b/apps/playground/src/app/components/data-grid/data-manager-large/data-set-large.ts
new file mode 100644
index 0000000000..9926dd9542
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-set-large.ts
@@ -0,0 +1,5913 @@
+// Source: https://github.com/metmuseum/openaccess
+import { SkyCellType } from '@skyux/ag-grid';
+
+import { ColDef } from 'ag-grid-community';
+
+/* spell-checker:disable */
+export const columnDefinitions: ColDef[] = [
+ {
+ field: 'object_number',
+ headerName: 'Object Number',
+ type: SkyCellType.Text,
+ sortable: false,
+ wrapText: true,
+ autoHeight: true,
+ },
+ {
+ field: 'is_highlight',
+ headerName: 'Is Highlight',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'is_timeline_work',
+ headerName: 'Is Timeline Work',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'is_public_domain',
+ headerName: 'Is Public Domain',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'object_id',
+ headerName: 'Object ID',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'gallery_number',
+ headerName: 'Gallery Number',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'department',
+ headerName: 'Department',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'accessionyear',
+ headerName: 'AccessionYear',
+ type: [],
+ sortable: false,
+ },
+ {
+ field: 'object_name',
+ headerName: 'Object Name',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'title',
+ headerName: 'Title',
+ type: [],
+ sortable: false,
+ },
+ {
+ field: 'culture',
+ headerName: 'Culture',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'period',
+ headerName: 'Period',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'dynasty',
+ headerName: 'Dynasty',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'reign',
+ headerName: 'Reign',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'portfolio',
+ headerName: 'Portfolio',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'constituent_id',
+ headerName: 'Constituent ID',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_role',
+ headerName: 'Artist Role',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_prefix',
+ headerName: 'Artist Prefix',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_display_name',
+ headerName: 'Artist Display Name',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_display_bio',
+ headerName: 'Artist Display Bio',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_suffix',
+ headerName: 'Artist Suffix',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_alpha_sort',
+ headerName: 'Artist Alpha Sort',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_nationality',
+ headerName: 'Artist Nationality',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_begin_date',
+ headerName: 'Artist Begin Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_end_date',
+ headerName: 'Artist End Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_gender',
+ headerName: 'Artist Gender',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_ulan_url',
+ headerName: 'Artist ULAN URL',
+ type: ['custom_link'],
+ sortable: true,
+ },
+ {
+ field: 'artist_wikidata_url',
+ headerName: 'Artist Wikidata URL',
+ type: ['custom_link'],
+ sortable: true,
+ },
+ {
+ field: 'object_date',
+ headerName: 'Object Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'object_begin_date',
+ headerName: 'Object Begin Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'object_end_date',
+ headerName: 'Object End Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'medium',
+ headerName: 'Medium',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'dimensions',
+ headerName: 'Dimensions',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'credit_line',
+ headerName: 'Credit Line',
+ type: SkyCellType.Text,
+ sortable: true,
+ },
+ {
+ field: 'geography_type',
+ headerName: 'Geography Type',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'city',
+ headerName: 'City',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'state',
+ headerName: 'State',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'county',
+ headerName: 'County',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'country',
+ headerName: 'Country',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'region',
+ headerName: 'Region',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'subregion',
+ headerName: 'Subregion',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'locale',
+ headerName: 'Locale',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'locus',
+ headerName: 'Locus',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'excavation',
+ headerName: 'Excavation',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'river',
+ headerName: 'River',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'classification',
+ headerName: 'Classification',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'rights_and_reproduction',
+ headerName: 'Rights and Reproduction',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'link_resource',
+ headerName: 'Link Resource',
+ type: ['custom_link'],
+ sortable: true,
+ },
+ {
+ field: 'object_wikidata_url',
+ headerName: 'Object Wikidata URL',
+ type: ['custom_link'],
+ sortable: true,
+ },
+ {
+ field: 'metadata_date',
+ headerName: 'Metadata Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'repository',
+ headerName: 'Repository',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'tags',
+ headerName: 'Tags',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'tags_aat_url',
+ headerName: 'Tags AAT URL',
+ type: ['custom_link'],
+ sortable: true,
+ },
+ {
+ field: 'tags_wikidata_url',
+ headerName: 'Tags Wikidata URL',
+ type: ['custom_link'],
+ sortable: true,
+ },
+];
+
+export const data = [
+ {
+ object_number: '24.109.38a\u2013c',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1153',
+ gallery_number: '704',
+ department: 'The American Wing',
+ accessionyear: '1924',
+ object_name: 'Candlestick',
+ title: 'Candle Holder',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '130',
+ artist_role: 'Maker',
+ artist_prefix: ' ',
+ artist_display_name: 'Joseph Lownes',
+ artist_display_bio: '1758\u20131820',
+ artist_suffix: ' ',
+ artist_alpha_sort: 'Lownes, Joseph',
+ artist_nationality: ' ',
+ artist_begin_date: '1758 ',
+ artist_end_date: '1820 ',
+ artist_gender: '',
+ artist_ulan_url: 'http://vocab.getty.edu/page/ulan/500330248',
+ artist_wikidata_url: '',
+ object_date: '1790\u20131810',
+ object_begin_date: '1790',
+ object_end_date: '1810',
+ medium: 'Silver, steel',
+ dimensions:
+ 'Overall: 5 1/16 x 9 5/8 x 4 3/4 in. (12.9 x 24.4 x 12.1 cm); 17 oz. 17 dwt. (556.5 g)\r\n44.12.13,False,False,True,1170,,The American Wing,1944,Candlestick,Standing candlestick,,,,,,,,,,,,,,,,,,,1730\u201350,1730,1750,Brass and iron,H. 62 in. (157.5 cm),Bequest of Sarah Williams',
+ credit_line: ' 1944"',
+ geography_type: 'Possibly made in',
+ city: 'Boston',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1170',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '11.87.78',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1183',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1911',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'Mexican',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1800',
+ object_begin_date: '1797',
+ object_end_date: '1800',
+ medium: 'Tin-glazed earthenware',
+ dimensions: 'H. 15 1/2 in. (39.4 cm)',
+ credit_line: 'Gift of Mrs. Robert W. de Forest, 1911',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'Mexico',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1183',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dogs',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300265714',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q144',
+ },
+ {
+ object_number: '18.95.4',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1184',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1918',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1800\u20131830',
+ object_begin_date: '1800',
+ object_end_date: '1830',
+ medium: 'Earthenware',
+ dimensions: 'H. 4 in. (10.2 cm); Diam. 5 in. (12.7 cm)',
+ credit_line: 'Rogers Fund, 1918',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1184',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '20.14.7',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1185',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1920',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1700\u20131800',
+ object_begin_date: '1700',
+ object_end_date: '1800',
+ medium: 'Free-blown lead aquamarine glass',
+ dimensions: 'H. 10 1/4 in. (26 cm)',
+ credit_line: 'Rogers Fund, 1920',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1185',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '35.43.1',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1186',
+ gallery_number: '',
+ department: 'The American Wing',
+ accessionyear: '1935',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1650',
+ object_begin_date: '1647',
+ object_end_date: '1650',
+ medium: 'Brass',
+ dimensions: 'H. 8 1/4 in. (21 cm)',
+ credit_line: 'Rogers Fund, 1935',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1186',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '35.43.2',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1187',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1935',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1650',
+ object_begin_date: '1647',
+ object_end_date: '1650',
+ medium: 'Brass',
+ dimensions: 'H. 7 in. (17.8 cm)',
+ credit_line: 'Rogers Fund, 1935',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1187',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '35.43.3',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1188',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1935',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1650',
+ object_begin_date: '1647',
+ object_end_date: '1650',
+ medium: 'Brass',
+ dimensions: 'H. 5 3/4 in. (14.6 cm)',
+ credit_line: 'Rogers Fund, 1935',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1188',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '35.43.4',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1189',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1935',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1650',
+ object_begin_date: '1647',
+ object_end_date: '1650',
+ medium: 'Brass',
+ dimensions: 'H. 5 1/4 in. (13.3 cm)',
+ credit_line: 'Rogers Fund, 1935',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1189',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '36.164',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1190',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1936',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1700\u20131800',
+ object_begin_date: '1700',
+ object_end_date: '1800',
+ medium: 'Steel, brass',
+ dimensions: '10 x 5 x 5 in. (25.4 x 12.7 x 12.7 cm)',
+ credit_line: 'Rogers Fund, 1936',
+ geography_type: '',
+ city: '',
+ state: '',
+ county: '',
+ country: '',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1190',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '37.134.8',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1191',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1937',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1800\u20131900',
+ object_begin_date: '1800',
+ object_end_date: '1900',
+ medium: 'Earthenware',
+ dimensions: 'H. 1 3/4 in. (4.4 cm); Diam. 2 5/8 in. (6.7 cm)',
+ credit_line: 'Gift of Mrs. J. Insley Blair, 1937',
+ geography_type: 'Made in',
+ city: 'Strasburg',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1191',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '41.160.389',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1192',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1770\u20131800',
+ object_begin_date: '1770',
+ object_end_date: '1800',
+ medium: 'Brass',
+ dimensions: 'H. 6 1/2 in. (16.5 cm)',
+ credit_line: 'Bequest of W. Gedney Beatty, 1941',
+ geography_type: '',
+ city: '',
+ state: '',
+ county: '',
+ country: '',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1192',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '41.160.397',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1193',
+ gallery_number: '712',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1660\u201380',
+ object_begin_date: '1660',
+ object_end_date: '1680',
+ medium: 'Brass',
+ dimensions: 'H. 10 in. (25.4 cm)',
+ credit_line: 'Bequest of W. Gedney Beatty, 1941',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1193',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '41.160.398',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1194',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1600\u20131700',
+ object_begin_date: '1600',
+ object_end_date: '1700',
+ medium: 'Brass',
+ dimensions: 'H. 8 in. (20.3 cm)',
+ credit_line: 'Bequest of W. Gedney Beatty, 1941',
+ geography_type: 'Possibly made in|Possibly made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England|Netherlands',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1194',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '41.160.399',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1195',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1600\u20131700',
+ object_begin_date: '1600',
+ object_end_date: '1700',
+ medium: 'Brass',
+ dimensions: 'H. 8 in. (20.3 cm)',
+ credit_line: 'Bequest of W. Gedney Beatty, 1941',
+ geography_type: 'Possibly made in|Possibly made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England|Netherlands',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1195',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '41.160.400',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1196',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1600\u20131700',
+ object_begin_date: '1600',
+ object_end_date: '1700',
+ medium: 'Brass',
+ dimensions: 'H. 7 3/4 in. (19.7 cm)',
+ credit_line: 'Bequest of W. Gedney Beatty, 1941',
+ geography_type: '',
+ city: '',
+ state: '',
+ county: '',
+ country: '',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1196',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '41.160.401',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1197',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1600\u20131700',
+ object_begin_date: '1600',
+ object_end_date: '1700',
+ medium: 'Brass',
+ dimensions: 'H. 8 3/8 in. (21.3 cm)',
+ credit_line: 'Bequest of W. Gedney Beatty, 1941',
+ geography_type: 'Possibly made in|Possibly made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England|Netherlands',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1197',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '41.160.403',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1198',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American or British',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1700\u20131800',
+ object_begin_date: '1700',
+ object_end_date: '1800',
+ medium: 'Brass',
+ dimensions: 'H. 7 in. (17.8 cm)',
+ credit_line: 'Bequest of W. Gedney Beatty, 1941',
+ geography_type: 'Possibly made in|Possibly made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States|England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1198',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '41.160.404',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1199',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American or British',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1700\u20131800',
+ object_begin_date: '1700',
+ object_end_date: '1800',
+ medium: 'Brass',
+ dimensions: 'H. 7 in. (17.8 cm)',
+ credit_line: 'Bequest of W. Gedney Beatty, 1941',
+ geography_type: 'Possibly made in|Possibly made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States|England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1199',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.54',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1200',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1825\u201350',
+ object_begin_date: '1825',
+ object_end_date: '1850',
+ medium: 'Lacy pressed glass',
+ dimensions: 'H. 8 1/2 in. (21.6 cm)',
+ credit_line: 'Rogers Fund, 1946',
+ geography_type: 'Made in',
+ city: 'Pittsburgh',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1200',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.297',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1201',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1860\u201370',
+ object_begin_date: '1860',
+ object_end_date: '1870',
+ medium: 'Pressed amethyst glass',
+ dimensions: 'H. 7 5/8 in. (19.4 cm)',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1201',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.312',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1202',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1875\u20131900',
+ object_begin_date: '1875',
+ object_end_date: '1900',
+ medium: 'Pressed yellow glass',
+ dimensions: 'H. 6 5/8 in. (16.8 cm)',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1202',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dolphins',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369',
+ },
+ {
+ object_number: '46.140.358',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1203',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1840\u201350',
+ object_begin_date: '1840',
+ object_end_date: '1850',
+ medium: 'Lacy pressed glass, pewter',
+ dimensions: 'H. 7 3/8 in. (18.7 cm)',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Made in',
+ city: 'Pittsburgh',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1203',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.359',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1204',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1850\u201370',
+ object_begin_date: '1850',
+ object_end_date: '1870',
+ medium: 'Pressed gray glass',
+ dimensions: 'H. 9 13/16 in. (24.9 cm)',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1204',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.780',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1206',
+ gallery_number: '706',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1850\u201360',
+ object_begin_date: '1850',
+ object_end_date: '1860',
+ medium: 'Lacy pressed glass',
+ dimensions:
+ '11 3/8 x 4 x 4 in. (28.9 x 10.2 x 10.2 cm)\r\n48.135.61,False,False,True,1207,774,The American Wing,1948,Candlestick,Candlestick,,,,,,,,,,,,,,,,,,,ca. 1740,1737,1740,Brass,H. 7 3/8 in. (18.7 cm),Bequest of Adeline R. Brown',
+ credit_line: ' 1947"',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1207',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.62',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1208',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1740\u201370',
+ object_begin_date: '1740',
+ object_end_date: '1770',
+ medium: 'Brass',
+ dimensions: 'H. 7 3/4 in. (19.7 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1208',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.73',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1209',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1910',
+ object_begin_date: '1907',
+ object_end_date: '1910',
+ medium: 'Pewter',
+ dimensions: 'H. 6 in. (15.2 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1209',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.74a\u2013c',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1210',
+ gallery_number: '',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'British or Irish',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1790',
+ object_begin_date: '1787',
+ object_end_date: '1790',
+ medium: 'Blown glass',
+ dimensions: 'H. 10 7/8 in. (27.6 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Possibly made in|Possibly made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England|Ireland',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1210',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.75a\u2013c',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1211',
+ gallery_number: '',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'British or Irish',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1790',
+ object_begin_date: '1787',
+ object_end_date: '1790',
+ medium: 'Blown glass',
+ dimensions: 'H. 10 7/8 in. (27.6 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Possibly made in|Possibly made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England|Ireland',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1211',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.76a\u2013c',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1212',
+ gallery_number: '',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'British',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1790',
+ object_begin_date: '1787',
+ object_end_date: '1790',
+ medium: 'Cut glass',
+ dimensions: 'H. 10 3/8 in. (26.4 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1212',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '51.171.24',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1213',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1951',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1830\u201335',
+ object_begin_date: '1830',
+ object_end_date: '1835',
+ medium: 'Lacy pressed glass',
+ dimensions: 'H. 6 15/16 in. (17.6 cm)',
+ credit_line:
+ 'Gift of Mrs. Charles W. Green, in memory of Dr. Charles W. Green, 1951',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: 'New England ',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1213',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '51.171.32',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1214',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1951',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1830\u201335',
+ object_begin_date: '1830',
+ object_end_date: '1835',
+ medium: 'Lacy pressed glass',
+ dimensions: 'H. 8 15/16 in. (22.7 cm)',
+ credit_line:
+ 'Gift of Mrs. Charles W. Green, in memory of Dr. Charles W. Green, 1951',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: 'New England ',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1214',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '51.171.94',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1215',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1951',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1830\u20131900',
+ object_begin_date: '1830',
+ object_end_date: '1900',
+ medium: 'Pressed blue glass',
+ dimensions: 'H. 7 1/16 in. (17.9 cm)',
+ credit_line:
+ 'Gift of Mrs. Charles W. Green, in memory of Dr. Charles W. Green, 1951',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1215',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '66.10.33',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1216',
+ gallery_number: '734',
+ department: 'The American Wing',
+ accessionyear: '1966',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American, Shaker',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '8737',
+ artist_role: 'Maker',
+ artist_prefix: ' ',
+ artist_display_name:
+ 'United Society of Believers in Christ\u2019s Second Appearing (\u201cShakers\u201d)',
+ artist_display_bio: 'American, active ca. 1750\u2013present',
+ artist_suffix: ' ',
+ artist_alpha_sort:
+ 'United Society of Believers in Christ\u2019s Second Appearing',
+ artist_nationality: ' ',
+ artist_begin_date: '1750 ',
+ artist_end_date: '9999 ',
+ artist_gender: '',
+ artist_ulan_url: '(not assigned)',
+ artist_wikidata_url: 'https://www.wikidata.org/wiki/Q1370167',
+ object_date: '1820\u201360',
+ object_begin_date: '1820',
+ object_end_date: '1860',
+ medium: 'Tin',
+ dimensions: 'H. 6 in. (15.2 cm); Diam. 3 5/8 in. (9.2 cm)',
+ credit_line: 'Friends of the American Wing Fund, 1966',
+ geography_type: 'Made in',
+ city: 'New Lebanon',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1216',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '1971.180.119',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1217',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1971',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'Chinese',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1800\u20131900',
+ object_begin_date: '1800',
+ object_end_date: '1900',
+ medium: 'Porcelain',
+ dimensions: '5 3/16 x 6 1/16 in. (13.2 x 15.4 cm)',
+ credit_line: 'Bequest of Flora E. Whiting, 1971',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'China',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1217',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Elephants|Flowers',
+ tags_aat_url:
+ 'http://vocab.getty.edu/page/aat/300250160|http://vocab.getty.edu/page/aat/300132399',
+ tags_wikidata_url:
+ 'https://www.wikidata.org/wiki/Q7378|https://www.wikidata.org/wiki/Q506',
+ },
+ {
+ object_number: '1984.133',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1218',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1984',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1845\u201360',
+ object_begin_date: '1845',
+ object_end_date: '1860',
+ medium: 'Porcelain',
+ dimensions: 'H. 9 7/8 in. (25.1 cm); Diam. 3 15/16 in. (10 cm)',
+ credit_line: 'Friends of the American Wing Fund, 1984',
+ geography_type: 'Made in',
+ city: 'Trenton',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1218',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.296',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1219',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '113',
+ artist_role: 'Maker',
+ artist_prefix: ' ',
+ artist_display_name: 'Boston & Sandwich Glass Company',
+ artist_display_bio: 'American, 1825\u20131888, Sandwich, Massachusetts',
+ artist_suffix: ' ',
+ artist_alpha_sort: 'Boston and Sandwich Glass Company ',
+ artist_nationality: ' ',
+ artist_begin_date: '1825 ',
+ artist_end_date: '1888 ',
+ artist_gender: '',
+ artist_ulan_url: 'http://vocab.getty.edu/page/ulan/500334773',
+ artist_wikidata_url: 'https://www.wikidata.org/wiki/Q4948224',
+ object_date: '1835\u201340',
+ object_begin_date: '1835',
+ object_end_date: '1840',
+ medium: 'Pressed amethyst glass',
+ dimensions: 'H. 6 15/16 in. (17.6 cm)',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Made in',
+ city: 'Sandwich',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1219',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.360',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1220',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '113',
+ artist_role: 'Maker',
+ artist_prefix: ' ',
+ artist_display_name: 'Boston & Sandwich Glass Company',
+ artist_display_bio: 'American, 1825\u20131888, Sandwich, Massachusetts',
+ artist_suffix: ' ',
+ artist_alpha_sort: 'Boston and Sandwich Glass Company ',
+ artist_nationality: ' ',
+ artist_begin_date: '1825 ',
+ artist_end_date: '1888 ',
+ artist_gender: '',
+ artist_ulan_url: 'http://vocab.getty.edu/page/ulan/500334773',
+ artist_wikidata_url: 'https://www.wikidata.org/wiki/Q4948224',
+ object_date: 'ca. 1840',
+ object_begin_date: '1837',
+ object_end_date: '1840',
+ medium: 'Pressed glass',
+ dimensions: 'H. 8 1/2 in. (21.6 cm)',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Made in',
+ city: 'Sandwich',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1220',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dolphins',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369',
+ },
+ {
+ object_number: '41.99.4',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1221',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '130',
+ artist_role: 'Maker',
+ artist_prefix: ' ',
+ artist_display_name: 'E. Durnall',
+ artist_display_bio: ' ',
+ artist_suffix: ' ',
+ artist_alpha_sort: 'Durnall, E.',
+ artist_nationality: ' ',
+ artist_begin_date: ' ',
+ artist_end_date: '9999 ',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1750\u201360',
+ object_begin_date: '1750',
+ object_end_date: '1760',
+ medium: 'Brass',
+ dimensions: 'H. 7 5/8 in. (19.4 cm)',
+ credit_line:
+ 'Gift of James De Lancey Verplanck and John Bayard Rodgers Verplanck, 1941',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1221',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '41.160.36',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1222',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'Mexican',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '131',
+ artist_role: 'Maker',
+ artist_prefix: ' ',
+ artist_display_name: 'Diego Gonzales de la Cueva',
+ artist_display_bio: ' ',
+ artist_suffix: ' ',
+ artist_alpha_sort: 'Gonzales, de la Cueva Diego',
+ artist_nationality: ' ',
+ artist_begin_date: '1733 ',
+ artist_end_date: '1778 ',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1700\u20131800',
+ object_begin_date: '1700',
+ object_end_date: '1800',
+ medium: 'Silver',
+ dimensions:
+ 'Overall: 7 3/16 in. (18.3 cm); 19 oz. 7 dwt. (601.9 g)\r\n57.153a',
+ credit_line: ' b"',
+ geography_type: 'False',
+ city: 'False',
+ state: 'True',
+ county: '1223',
+ country: '750',
+ region: 'The American Wing',
+ subregion: '1957',
+ locale: 'Candlestick',
+ locus: 'Candlestick',
+ excavation: 'American',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: '',
+ object_wikidata_url: '124',
+ metadata_date: 'Maker',
+ repository: ' ',
+ tags: 'Cornelius Kierstede',
+ tags_aat_url: '1674\u2013ca. 1757',
+ tags_wikidata_url: ' ',
+ },
+ {
+ object_number: '46.140.320',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1230',
+ gallery_number: '706',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '1314108',
+ artist_role: 'Designer|Manufacturer',
+ artist_prefix: 'Designed by|Manufactured by',
+ artist_display_name: 'Henry Whitney|New England Glass Company',
+ artist_display_bio:
+ ' |American, East Cambridge, Massachusetts, 1818\u20131888',
+ artist_suffix: ' | ',
+ artist_alpha_sort: 'Whitney, Henry|New England Glass Company ',
+ artist_nationality: ' | ',
+ artist_begin_date: ' |1818 ',
+ artist_end_date: '9999 |1888 ',
+ artist_gender: '|',
+ artist_ulan_url: '|http://vocab.getty.edu/page/ulan/500356007',
+ artist_wikidata_url: '|https://www.wikidata.org/wiki/Q17083157',
+ object_date: '1870\u201375',
+ object_begin_date: '1870',
+ object_end_date: '1875',
+ medium: 'Pressed glass',
+ dimensions: 'H. 9 5/8 in. (24.4 cm)',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Made in',
+ city: 'East Cambridge',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1230',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Caryatids',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300001583',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q208120',
+ },
+ {
+ object_number: '61.231.1',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1231',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1961',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '130',
+ artist_role: 'Maker',
+ artist_prefix: ' ',
+ artist_display_name: 'Cornelius and Company',
+ artist_display_bio: '1838\u20131851',
+ artist_suffix: ' ',
+ artist_alpha_sort: 'Cornelius and Company ',
+ artist_nationality: ' ',
+ artist_begin_date: '1838 ',
+ artist_end_date: '1851 ',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1840\u201350',
+ object_begin_date: '1840',
+ object_end_date: '1850',
+ medium: 'Gilt metal, marble, crystal',
+ dimensions: '16 7/8 x 4 3/4 x 3 3/8 in. (42.9 x 12.1 x 8.6 cm)',
+ credit_line: 'Gift of Mary E. Steers, 1961',
+ geography_type: 'Made in',
+ city: 'Philadelphia',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1231',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '61.231.2',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1232',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1961',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '130',
+ artist_role: 'Maker',
+ artist_prefix: ' ',
+ artist_display_name: 'Cornelius and Company',
+ artist_display_bio: '1838\u20131851',
+ artist_suffix: ' ',
+ artist_alpha_sort: 'Cornelius and Company ',
+ artist_nationality: ' ',
+ artist_begin_date: '1838 ',
+ artist_end_date: '1851 ',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1840\u201350',
+ object_begin_date: '1840',
+ object_end_date: '1850',
+ medium: 'Gilt metal, marble, crystal',
+ dimensions: '16 7/8 x 4 3/4 x 3 3/8 in. (42.9 x 12.1 x 8.6 cm)',
+ credit_line: 'Gift of Mary E. Steers, 1961',
+ geography_type: 'Made in',
+ city: 'Philadelphia',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1232',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '24.116.6',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1235',
+ gallery_number: '',
+ department: 'The American Wing',
+ accessionyear: '1924',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'British',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1800\u20131830',
+ object_begin_date: '1800',
+ object_end_date: '1830',
+ medium: 'Metal, earthenware, glass',
+ dimensions: 'H. 12 in. (30.5 cm); Diam. 4 7/16 in. (11.3 cm)',
+ credit_line: 'Bequest of Miss Emilie Ogden, 1924',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1235',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '24.116.7',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1236',
+ gallery_number: '',
+ department: 'The American Wing',
+ accessionyear: '1924',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'British',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1800\u20131830',
+ object_begin_date: '1800',
+ object_end_date: '1830',
+ medium: 'Metal, earthenware, glass',
+ dimensions: 'H. 12 in. (30.5 cm); Diam. 4 7/16 in. (11.3 cm)',
+ credit_line: 'Bequest of Miss Emilie Ogden, 1924',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1236',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Human Figures',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300404114',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q5937779',
+ },
+ {
+ object_number: '35.124.1',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1241',
+ gallery_number: '704',
+ department: 'The American Wing',
+ accessionyear: '1935',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1830\u201355',
+ object_begin_date: '1830',
+ object_end_date: '1855',
+ medium: 'Blown glass',
+ dimensions: 'H. 9 1/8 in. (23.2 cm)',
+ credit_line: 'Rogers Fund, 1935',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1241',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '35.124.2',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1242',
+ gallery_number: '704',
+ department: 'The American Wing',
+ accessionyear: '1935',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1830\u201355',
+ object_begin_date: '1830',
+ object_end_date: '1855',
+ medium: 'Blown glass',
+ dimensions: 'H. 9 1/8 in. (23.2 cm)',
+ credit_line: 'Rogers Fund, 1935',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1242',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '36.148.1',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1243',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1936',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1840\u201360',
+ object_begin_date: '1840',
+ object_end_date: '1860',
+ medium: 'Yellow glass',
+ dimensions: 'H. 10 3/8 in. (26.4 cm)',
+ credit_line: 'Rogers Fund, 1936',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1243',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dolphins',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369',
+ },
+ {
+ object_number: '36.148.2',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1244',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1936',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1840\u201360',
+ object_begin_date: '1840',
+ object_end_date: '1860',
+ medium: 'Yellow glass',
+ dimensions: 'H. 10 3/8 in. (26.4 cm)',
+ credit_line: 'Rogers Fund, 1936',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1244',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dolphins',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369',
+ },
+ {
+ object_number: '38.138.1',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1245',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1938',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1720',
+ object_begin_date: '1717',
+ object_end_date: '1720',
+ medium: 'Brass',
+ dimensions: 'H. 6 in. (15.2 cm)',
+ credit_line: 'Gift of Allan B. A. Bradley, 1938',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1245',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '38.138.2',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1246',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1938',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1720',
+ object_begin_date: '1717',
+ object_end_date: '1720',
+ medium: 'Brass',
+ dimensions: 'H. 6 in. (15.2 cm)',
+ credit_line: 'Gift of Allan B. A. Bradley, 1938',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1246',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '41.160.387',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1247',
+ gallery_number: '729',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1700\u20131800',
+ object_begin_date: '1700',
+ object_end_date: '1800',
+ medium: 'Brass',
+ dimensions: 'H. 6 1/2 in. (16.5 cm)',
+ credit_line: 'Bequest of W. Gedney Beatty, 1941',
+ geography_type: '',
+ city: '',
+ state: '',
+ county: '',
+ country: '',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1247',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '41.160.388',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1248',
+ gallery_number: '729',
+ department: 'The American Wing',
+ accessionyear: '1941',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1700\u20131800',
+ object_begin_date: '1700',
+ object_end_date: '1800',
+ medium: 'Brass',
+ dimensions: 'H. 6 1/2 in. (16.5 cm)',
+ credit_line: 'Bequest of W. Gedney Beatty, 1941',
+ geography_type: '',
+ city: '',
+ state: '',
+ county: '',
+ country: '',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1248',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.67.3',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1249',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'British',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1800\u20131830',
+ object_begin_date: '1800',
+ object_end_date: '1830',
+ medium: 'Earthenware, lusterware',
+ dimensions: 'H. 6 1/4 in. (15.9 cm)',
+ credit_line:
+ 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1249',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.67.4',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1250',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'British',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1800\u20131830',
+ object_begin_date: '1800',
+ object_end_date: '1830',
+ medium: 'Earthenware, lusterware',
+ dimensions: 'H. 6 1/4 in. (15.9 cm)',
+ credit_line:
+ 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1250',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.67.61a, b',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1251',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1800\u20131900',
+ object_begin_date: '1800',
+ object_end_date: '1900',
+ medium: 'Brass',
+ dimensions: 'H. 9 5/8 in. (24.4 cm)',
+ credit_line:
+ 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'France',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1251',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.67.62a, b',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1252',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1800\u20131900',
+ object_begin_date: '1800',
+ object_end_date: '1900',
+ medium: 'Brass',
+ dimensions: 'H. 9 5/8 in. (24.4 cm)',
+ credit_line:
+ 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'France',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1252',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.67.63a, b',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1253',
+ gallery_number: '',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1770\u20131800',
+ object_begin_date: '1770',
+ object_end_date: '1800',
+ medium: 'Brass',
+ dimensions: 'H. 11 in. (27.9 cm)',
+ credit_line:
+ 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1253',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.67.64a, b',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1254',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1770\u20131800',
+ object_begin_date: '1770',
+ object_end_date: '1800',
+ medium: 'Brass',
+ dimensions: 'H. 11 in. (27.9 cm)',
+ credit_line:
+ 'Gift of the Members of the Committee of the Bertha King Benkard Memorial Fund, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1254',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.331',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1255',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1840\u201360',
+ object_begin_date: '1840',
+ object_end_date: '1860',
+ medium: 'Pressed glass',
+ dimensions: 'H. 5 1/16 in. (12.9 cm)',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: 'New England ',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1255',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.332',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1256',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1840\u201360',
+ object_begin_date: '1840',
+ object_end_date: '1860',
+ medium: 'Pressed glass',
+ dimensions: 'H. 5 1/8 in. (13 cm)',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: 'New England ',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1256',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.734',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1257',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1880\u201390',
+ object_begin_date: '1880',
+ object_end_date: '1890',
+ medium: 'Pressed purple marble glass',
+ dimensions: 'Dimensions unavailable',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1257',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.735',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1258',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1880\u201390',
+ object_begin_date: '1880',
+ object_end_date: '1890',
+ medium: 'Pressed purple marble glass',
+ dimensions: 'Dimensions unavailable',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1258',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.736',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1259',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1880\u201390',
+ object_begin_date: '1880',
+ object_end_date: '1890',
+ medium: 'Pressed purple marble glass',
+ dimensions: 'Dimensions unavailable',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1259',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '46.140.737',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1260',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1946',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1880\u201390',
+ object_begin_date: '1880',
+ object_end_date: '1890',
+ medium: 'Pressed purple marble glass',
+ dimensions: 'Dimensions unavailable',
+ credit_line: 'Gift of Mrs. Emily Winthrop Miles, 1946',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1260',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.37',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1261',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1710\u201320',
+ object_begin_date: '1710',
+ object_end_date: '1720',
+ medium: 'Brass',
+ dimensions: 'H. 6 1/4 in. (15.9 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1261',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.38',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1262',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1710\u201320',
+ object_begin_date: '1710',
+ object_end_date: '1720',
+ medium: 'Brass',
+ dimensions: 'H. 6 1/4 in. (15.9 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1262',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.45',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1263',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1760',
+ object_begin_date: '1757',
+ object_end_date: '1760',
+ medium: 'Brass',
+ dimensions: 'H. 5 1/4 in. (13.3 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1263',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.46',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1264',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1760',
+ object_begin_date: '1757',
+ object_end_date: '1760',
+ medium: 'Brass',
+ dimensions: 'H. 5 1/4 in. (13.3 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1264',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.47',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1265',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1750',
+ object_begin_date: '1747',
+ object_end_date: '1750',
+ medium: 'Brass',
+ dimensions: 'H. 8 3/8 in. (21.3 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1265',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.48',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1266',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1750',
+ object_begin_date: '1747',
+ object_end_date: '1750',
+ medium: 'Brass',
+ dimensions: 'H. 8 3/8 in. (21.3 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1266',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.49',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1267',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1725\u201340',
+ object_begin_date: '1725',
+ object_end_date: '1740',
+ medium: 'Brass',
+ dimensions: 'H. 8 3/8 in. (21.3 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1267',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.50',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1268',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1725\u201340',
+ object_begin_date: '1725',
+ object_end_date: '1740',
+ medium: 'Brass',
+ dimensions: 'H. 8 3/8 in. (21.3 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1268',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.51',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1269',
+ gallery_number: '',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American or British',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1750',
+ object_begin_date: '1747',
+ object_end_date: '1750',
+ medium: 'Brass',
+ dimensions: 'H. 8 5/8 in. (21.9 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Possibly made in|Possibly made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States|England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1269',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.53',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1270',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1750\u201360',
+ object_begin_date: '1750',
+ object_end_date: '1760',
+ medium: 'Brass',
+ dimensions: 'H. 8 3/4 in. (22.2 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1270',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.54',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1271',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1750\u201360',
+ object_begin_date: '1750',
+ object_end_date: '1760',
+ medium: 'Brass',
+ dimensions: 'H. 8 3/4 in. (22.2 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1271',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.55',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1272',
+ gallery_number: '',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1750\u201370',
+ object_begin_date: '1750',
+ object_end_date: '1770',
+ medium: 'Brass',
+ dimensions: 'H. 7 3/4 in. (19.7 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1272',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.59',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1273',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1750\u201360',
+ object_begin_date: '1750',
+ object_end_date: '1760',
+ medium: 'Brass',
+ dimensions: 'H. 7 7/8 in. (20 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1273',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.60',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1274',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1750\u201360',
+ object_begin_date: '1750',
+ object_end_date: '1760',
+ medium: 'Brass',
+ dimensions: 'H. 7 7/8 in. (20 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1274',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.63',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1275',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1760',
+ object_begin_date: '1757',
+ object_end_date: '1760',
+ medium: 'Brass',
+ dimensions: 'H. 10 in. (25.4 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1275',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.64',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1276',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1760',
+ object_begin_date: '1757',
+ object_end_date: '1760',
+ medium: 'Brass',
+ dimensions: 'H. 10 in. (25.4 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1276',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.71',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1277',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1820',
+ object_begin_date: '1817',
+ object_end_date: '1820',
+ medium: 'Pewter',
+ dimensions: 'H. 7 3/4 in. (19.7 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: '',
+ city: '',
+ state: '',
+ county: '',
+ country: '',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1277',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.135.72',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1278',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1820',
+ object_begin_date: '1817',
+ object_end_date: '1820',
+ medium: 'Pewter',
+ dimensions: 'H. 7 3/4 in. (19.7 cm)',
+ credit_line: 'Bequest of Adeline R. Brown, 1947',
+ geography_type: '',
+ city: '',
+ state: '',
+ county: '',
+ country: '',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1278',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.145.1',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1279',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1790',
+ object_begin_date: '1787',
+ object_end_date: '1790',
+ medium: 'Sheffield silver plate',
+ dimensions: 'H. 11 1/2 in. (29.2 cm)',
+ credit_line: 'Bequest of Fannie Randolph Gaunt, 1948',
+ geography_type: 'Probably made in',
+ city: 'Sheffield',
+ state: 'South Yorkshire',
+ county: 'South Yorkshire',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1279',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '48.145.2',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1280',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1948',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1790',
+ object_begin_date: '1787',
+ object_end_date: '1790',
+ medium: 'Silver plate on copper',
+ dimensions: 'H. 11 1/2 in. (29.2 cm)',
+ credit_line: 'Bequest of Fannie Randolph Gaunt, 1948',
+ geography_type: 'Probably made in',
+ city: 'Sheffield',
+ state: 'South Yorkshire',
+ county: 'South Yorkshire',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1280',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '57.131.1',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1281',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1957',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1840\u201350',
+ object_begin_date: '1840',
+ object_end_date: '1850',
+ medium: 'Pressed glass',
+ dimensions: 'H. 10 3/8 in. (26.4 cm)',
+ credit_line:
+ 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: 'New England ',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1281',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dolphins',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369',
+ },
+ {
+ object_number: '57.131.2',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1282',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1957',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1840\u201350',
+ object_begin_date: '1840',
+ object_end_date: '1850',
+ medium: 'Pressed glass',
+ dimensions: 'H. 10 1/4 in. (26 cm)',
+ credit_line:
+ 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: 'New England ',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1282',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dolphins',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369',
+ },
+ {
+ object_number: '57.131.5',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1283',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1957',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1920s',
+ object_begin_date: '1917',
+ object_end_date: '1920',
+ medium: 'Pressed opaque green and white glass',
+ dimensions: 'H. 10 3/4 in. (27.3 cm)',
+ credit_line:
+ 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'Czech Republic',
+ region: 'Bohemia',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1283',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dolphins',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369',
+ },
+ {
+ object_number: '57.131.6',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1284',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1957',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1920s',
+ object_begin_date: '1917',
+ object_end_date: '1920',
+ medium: 'Pressed opaque white and green glass',
+ dimensions: 'H. 10 3/4 in. (27.3 cm)',
+ credit_line:
+ 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'Czech Republic',
+ region: 'Bohemia',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1284',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dolphins',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369',
+ },
+ {
+ object_number: '57.131.7',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1285',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1957',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1845\u201370',
+ object_begin_date: '1845',
+ object_end_date: '1870',
+ medium: 'Pressed opaque blue glass',
+ dimensions: 'H. 9 3/4 in. (24.8 cm)',
+ credit_line:
+ 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1285',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dolphins',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369',
+ },
+ {
+ object_number: '57.131.8',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1286',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1957',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1845\u201370',
+ object_begin_date: '1845',
+ object_end_date: '1870',
+ medium: 'Pressed opaque blue glass',
+ dimensions: 'H. 9 3/4 in. (24.8 cm)',
+ credit_line:
+ 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1286',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dolphins',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300250159',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q7369',
+ },
+ {
+ object_number: '57.131.9',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1287',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1957',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1840\u201370',
+ object_begin_date: '1840',
+ object_end_date: '1870',
+ medium: 'Pressed opaque blue and white glass',
+ dimensions: 'H. 10 7/8 in. (27.6 cm)',
+ credit_line:
+ 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1287',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '57.131.10',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1288',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1957',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1840\u201370',
+ object_begin_date: '1840',
+ object_end_date: '1870',
+ medium: 'Pressed opaque blue and white glass',
+ dimensions: 'H. 10 15/16 in. (27.8 cm)',
+ credit_line:
+ 'Bequest of Anna G. W. Green, in memory of her husband, Dr. Charles W. Green, 1957',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1288',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '66.10.30',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1289',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1966',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American, Shaker',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '8737',
+ artist_role: 'Maker',
+ artist_prefix: ' ',
+ artist_display_name:
+ 'United Society of Believers in Christ\u2019s Second Appearing (\u201cShakers\u201d)',
+ artist_display_bio: 'American, active ca. 1750\u2013present',
+ artist_suffix: ' ',
+ artist_alpha_sort:
+ 'United Society of Believers in Christ\u2019s Second Appearing',
+ artist_nationality: ' ',
+ artist_begin_date: '1750 ',
+ artist_end_date: '9999 ',
+ artist_gender: '',
+ artist_ulan_url: '(not assigned)',
+ artist_wikidata_url: 'https://www.wikidata.org/wiki/Q1370167',
+ object_date: '1820\u201360',
+ object_begin_date: '1820',
+ object_end_date: '1860',
+ medium: 'Tin',
+ dimensions: 'H. 6 in. (15.2 cm); Diam. 3 5/8 in. (9.2 cm)',
+ credit_line: 'Friends of the American Wing Fund, 1966',
+ geography_type: 'Made in',
+ city: 'New Lebanon',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1289',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '66.10.31',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1290',
+ gallery_number: '',
+ department: 'The American Wing',
+ accessionyear: '1966',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American, Shaker',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '8737',
+ artist_role: 'Maker',
+ artist_prefix: ' ',
+ artist_display_name:
+ 'United Society of Believers in Christ\u2019s Second Appearing (\u201cShakers\u201d)',
+ artist_display_bio: 'American, active ca. 1750\u2013present',
+ artist_suffix: ' ',
+ artist_alpha_sort:
+ 'United Society of Believers in Christ\u2019s Second Appearing',
+ artist_nationality: ' ',
+ artist_begin_date: '1750 ',
+ artist_end_date: '9999 ',
+ artist_gender: '',
+ artist_ulan_url: '(not assigned)',
+ artist_wikidata_url: 'https://www.wikidata.org/wiki/Q1370167',
+ object_date: '1820\u201360',
+ object_begin_date: '1820',
+ object_end_date: '1860',
+ medium: 'Tin',
+ dimensions: 'H. 6 in. (15.2 cm); Diam. 3 5/8 in. (9.2 cm)',
+ credit_line: 'Friends of the American Wing Fund, 1966',
+ geography_type: 'Made in',
+ city: 'New Lebanon',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1290',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '1970.126.6',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1291',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1970',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1810',
+ object_begin_date: '1807',
+ object_end_date: '1810',
+ medium: 'Silver plate on copper',
+ dimensions: '12 1/16 x 6 1/8 x 5 in. (30.6 x 15.6 x 12.7 cm)',
+ credit_line:
+ 'Gift of Mrs. B. Langdon Tyler and Mrs. William Floyd Nichols, 1970',
+ geography_type: 'Made in',
+ city: 'Sheffield',
+ state: 'South Yorkshire',
+ county: 'South Yorkshire',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1291',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '1970.126.7',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1292',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1970',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1810',
+ object_begin_date: '1807',
+ object_end_date: '1810',
+ medium: 'Silver plate on copper',
+ dimensions: '12 1/16 x 6 1/8 x 5 in. (30.6 x 15.6 x 12.7 cm)',
+ credit_line:
+ 'Gift of Mrs. B. Langdon Tyler and Mrs. William Floyd Nichols, 1970',
+ geography_type: 'Made in',
+ city: 'Sheffield',
+ state: 'South Yorkshire',
+ county: 'South Yorkshire',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1292',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '1971.180.69',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1293',
+ gallery_number: '710',
+ department: 'The American Wing',
+ accessionyear: '1971',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1670\u20131700',
+ object_begin_date: '1670',
+ object_end_date: '1700',
+ medium: 'Brass',
+ dimensions: 'H. 8 1/2 in. (21.6 cm); Diam. 6 1/8 in. (15.6 cm)',
+ credit_line: 'Bequest of Flora E. Whiting, 1971',
+ geography_type: '',
+ city: '',
+ state: '',
+ county: '',
+ country: '',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1293',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '1971.180.70',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1294',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1971',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1670\u20131700',
+ object_begin_date: '1670',
+ object_end_date: '1700',
+ medium: 'Brass',
+ dimensions: 'H. 8 1/2 in. (21.6 cm); Diam. 6 1/8 in. (15.6 cm)',
+ credit_line: 'Bequest of Flora E. Whiting, 1971',
+ geography_type: '',
+ city: '',
+ state: '',
+ county: '',
+ country: '',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1294',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '1971.180.78a, b',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1295',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1971',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1700\u20131800',
+ object_begin_date: '1700',
+ object_end_date: '1800',
+ medium: 'Copper, enamel, brass',
+ dimensions: '8 5/8 x 4 13/16 in. (21.9 x 12.2 cm)',
+ credit_line: 'Bequest of Flora E. Whiting, 1971',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1295',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+];
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/data-set-small.ts b/apps/playground/src/app/components/data-grid/data-manager-large/data-set-small.ts
new file mode 100644
index 0000000000..57e94f38c9
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/data-set-small.ts
@@ -0,0 +1,789 @@
+// Source: https://github.com/metmuseum/openaccess
+import { SkyCellType } from '@skyux/ag-grid';
+
+import { ColDef } from 'ag-grid-community';
+
+/* spell-checker:disable */
+export const columnDefinitions: ColDef[] = [
+ {
+ field: 'select',
+ headerName: ' ',
+ type: [SkyCellType.RowSelector],
+ },
+ {
+ field: 'object_number',
+ headerName: 'Object Number',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'is_highlight',
+ headerName: 'Is Highlight',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'is_timeline_work',
+ headerName: 'Is Timeline Work',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'is_public_domain',
+ headerName: 'Is Public Domain',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'object_id',
+ headerName: 'Object ID',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'gallery_number',
+ headerName: 'Gallery Number',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'department',
+ headerName: 'Department',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'accessionyear',
+ headerName: 'AccessionYear',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'object_name',
+ headerName: 'Object Name',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'title',
+ headerName: 'Title',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'culture',
+ headerName: 'Culture',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'period',
+ headerName: 'Period',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'dynasty',
+ headerName: 'Dynasty',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'reign',
+ headerName: 'Reign',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'portfolio',
+ headerName: 'Portfolio',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'constituent_id',
+ headerName: 'Constituent ID',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_role',
+ headerName: 'Artist Role',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_prefix',
+ headerName: 'Artist Prefix',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_display_name',
+ headerName: 'Artist Display Name',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_display_bio',
+ headerName: 'Artist Display Bio',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_suffix',
+ headerName: 'Artist Suffix',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_alpha_sort',
+ headerName: 'Artist Alpha Sort',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_nationality',
+ headerName: 'Artist Nationality',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_begin_date',
+ headerName: 'Artist Begin Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_end_date',
+ headerName: 'Artist End Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_gender',
+ headerName: 'Artist Gender',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'artist_ulan_url',
+ headerName: 'Artist ULAN URL',
+ type: ['custom_link'],
+ sortable: true,
+ },
+ {
+ field: 'artist_wikidata_url',
+ headerName: 'Artist Wikidata URL',
+ type: ['custom_link'],
+ sortable: true,
+ },
+ {
+ field: 'object_date',
+ headerName: 'Object Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'object_begin_date',
+ headerName: 'Object Begin Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'object_end_date',
+ headerName: 'Object End Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'medium',
+ headerName: 'Medium',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'dimensions',
+ headerName: 'Dimensions',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'credit_line',
+ headerName: 'Credit Line',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'geography_type',
+ headerName: 'Geography Type',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'city',
+ headerName: 'City',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'state',
+ headerName: 'State',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'county',
+ headerName: 'County',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'country',
+ headerName: 'Country',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'region',
+ headerName: 'Region',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'subregion',
+ headerName: 'Subregion',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'locale',
+ headerName: 'Locale',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'locus',
+ headerName: 'Locus',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'excavation',
+ headerName: 'Excavation',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'river',
+ headerName: 'River',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'classification',
+ headerName: 'Classification',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'rights_and_reproduction',
+ headerName: 'Rights and Reproduction',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'link_resource',
+ headerName: 'Link Resource',
+ type: ['custom_link'],
+ sortable: true,
+ },
+ {
+ field: 'object_wikidata_url',
+ headerName: 'Object Wikidata URL',
+ type: ['custom_link'],
+ sortable: true,
+ },
+ {
+ field: 'metadata_date',
+ headerName: 'Metadata Date',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'repository',
+ headerName: 'Repository',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'tags',
+ headerName: 'Tags',
+ type: [],
+ sortable: true,
+ },
+ {
+ field: 'tags_aat_url',
+ headerName: 'Tags AAT URL',
+ type: ['custom_link'],
+ sortable: true,
+ },
+ {
+ field: 'tags_wikidata_url',
+ headerName: 'Tags Wikidata URL',
+ type: ['custom_link'],
+ sortable: true,
+ },
+];
+
+export const data = [
+ {
+ object_number: '24.109.38a\u2013c',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1153',
+ gallery_number: '704',
+ department: 'The American Wing',
+ accessionyear: '1924',
+ object_name: 'Candlestick',
+ title: 'Candle Holder',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '130',
+ artist_role: 'Maker',
+ artist_prefix: ' ',
+ artist_display_name: 'Joseph Lownes',
+ artist_display_bio: '1758\u20131820',
+ artist_suffix: ' ',
+ artist_alpha_sort: 'Lownes, Joseph',
+ artist_nationality: ' ',
+ artist_begin_date: '1758 ',
+ artist_end_date: '1820 ',
+ artist_gender: '',
+ artist_ulan_url: 'http://vocab.getty.edu/page/ulan/500330248',
+ artist_wikidata_url: '',
+ object_date: '1790\u20131810',
+ object_begin_date: '1790',
+ object_end_date: '1810',
+ medium: 'Silver, steel',
+ dimensions:
+ 'Overall: 5 1/16 x 9 5/8 x 4 3/4 in. (12.9 x 24.4 x 12.1 cm); 17 oz. 17 dwt. (556.5 g)\r\n44.12.13,False,False,True,1170,,The American Wing,1944,Candlestick,Standing candlestick,,,,,,,,,,,,,,,,,,,1730\u201350,1730,1750,Brass and iron,H. 62 in. (157.5 cm),Bequest of Sarah Williams',
+ credit_line: ' 1944"',
+ geography_type: 'Possibly made in',
+ city: 'Boston',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1170',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '11.87.78',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1183',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1911',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'Mexican',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1800',
+ object_begin_date: '1797',
+ object_end_date: '1800',
+ medium: 'Tin-glazed earthenware',
+ dimensions: 'H. 15 1/2 in. (39.4 cm)',
+ credit_line: 'Gift of Mrs. Robert W. de Forest, 1911',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'Mexico',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1183',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: 'Dogs',
+ tags_aat_url: 'http://vocab.getty.edu/page/aat/300265714',
+ tags_wikidata_url: 'https://www.wikidata.org/wiki/Q144',
+ },
+ {
+ object_number: '18.95.4',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1184',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1918',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1800\u20131830',
+ object_begin_date: '1800',
+ object_end_date: '1830',
+ medium: 'Earthenware',
+ dimensions: 'H. 4 in. (10.2 cm); Diam. 5 in. (12.7 cm)',
+ credit_line: 'Rogers Fund, 1918',
+ geography_type: 'Probably made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1184',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '20.14.7',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'True',
+ object_id: '1185',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1920',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: 'American',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: '1700\u20131800',
+ object_begin_date: '1700',
+ object_end_date: '1800',
+ medium: 'Free-blown lead aquamarine glass',
+ dimensions: 'H. 10 1/4 in. (26 cm)',
+ credit_line: 'Rogers Fund, 1920',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'United States',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1185',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '35.43.1',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1186',
+ gallery_number: '',
+ department: 'The American Wing',
+ accessionyear: '1935',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1650',
+ object_begin_date: '1647',
+ object_end_date: '1650',
+ medium: 'Brass',
+ dimensions: 'H. 8 1/4 in. (21 cm)',
+ credit_line: 'Rogers Fund, 1935',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1186',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '35.43.2',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1187',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1935',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1650',
+ object_begin_date: '1647',
+ object_end_date: '1650',
+ medium: 'Brass',
+ dimensions: 'H. 7 in. (17.8 cm)',
+ credit_line: 'Rogers Fund, 1935',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1187',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '35.43.3',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1188',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1935',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1650',
+ object_begin_date: '1647',
+ object_end_date: '1650',
+ medium: 'Brass',
+ dimensions: 'H. 5 3/4 in. (14.6 cm)',
+ credit_line: 'Rogers Fund, 1935',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1188',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+ {
+ object_number: '35.43.4',
+ is_highlight: 'False',
+ is_timeline_work: 'False',
+ is_public_domain: 'False',
+ object_id: '1189',
+ gallery_number: '774',
+ department: 'The American Wing',
+ accessionyear: '1935',
+ object_name: 'Candlestick',
+ title: 'Candlestick',
+ culture: '',
+ period: '',
+ dynasty: '',
+ reign: '',
+ portfolio: '',
+ constituent_id: '',
+ artist_role: '',
+ artist_prefix: '',
+ artist_display_name: '',
+ artist_display_bio: '',
+ artist_suffix: '',
+ artist_alpha_sort: '',
+ artist_nationality: '',
+ artist_begin_date: '',
+ artist_end_date: '',
+ artist_gender: '',
+ artist_ulan_url: '',
+ artist_wikidata_url: '',
+ object_date: 'ca. 1650',
+ object_begin_date: '1647',
+ object_end_date: '1650',
+ medium: 'Brass',
+ dimensions: 'H. 5 1/4 in. (13.3 cm)',
+ credit_line: 'Rogers Fund, 1935',
+ geography_type: 'Made in',
+ city: '',
+ state: '',
+ county: '',
+ country: 'England',
+ region: '',
+ subregion: '',
+ locale: '',
+ locus: '',
+ excavation: '',
+ river: '',
+ classification: '',
+ rights_and_reproduction: '',
+ link_resource: 'http://www.metmuseum.org/art/collection/search/1189',
+ object_wikidata_url: '',
+ metadata_date: '',
+ repository: 'Metropolitan Museum of Art, New York, NY',
+ tags: '',
+ tags_aat_url: '',
+ tags_wikidata_url: '',
+ },
+];
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.html b/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.html
new file mode 100644
index 0000000000..3f1e5cbb39
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.html
@@ -0,0 +1,7 @@
+
+
+
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.ts b/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.ts
new file mode 100644
index 0000000000..a5a8e27e9a
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/delete-button/delete-button.component.ts
@@ -0,0 +1,28 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ inject,
+ input,
+} from '@angular/core';
+import { SkyIconModule } from '@skyux/icon';
+
+import { DataManagerLargeComponent } from '../data-manager-large.component';
+
+@Component({
+ selector: 'app-delete-button',
+ imports: [SkyIconModule],
+ templateUrl: './delete-button.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DeleteButtonComponent {
+ public readonly rowId = input();
+
+ readonly #component = inject(DataManagerLargeComponent);
+
+ protected delete(): void {
+ const id = this.rowId();
+ if (id) {
+ this.#component.markForDelete(id);
+ }
+ }
+}
diff --git a/apps/playground/src/app/components/data-grid/data-manager-large/local-storage-config.service.ts b/apps/playground/src/app/components/data-grid/data-manager-large/local-storage-config.service.ts
new file mode 100644
index 0000000000..30484ef330
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/data-manager-large/local-storage-config.service.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@angular/core';
+import { SkyUIConfigService } from '@skyux/core';
+
+import { Observable, of } from 'rxjs';
+
+const SETTINGS_KEY_PREFIX = 'data-grid-test-';
+
+@Injectable()
+export class LocalStorageConfigService extends SkyUIConfigService {
+ public override getConfig(key: string, defaultConfig?: any): Observable {
+ const settingsJSON = localStorage.getItem(`${SETTINGS_KEY_PREFIX}${key}`);
+ if (settingsJSON) {
+ return of(JSON.parse(settingsJSON));
+ }
+ return of(defaultConfig);
+ }
+
+ public override setConfig(key: string, value: any): Observable {
+ localStorage.setItem(`${SETTINGS_KEY_PREFIX}${key}`, JSON.stringify(value));
+
+ return of();
+ }
+}
diff --git a/apps/playground/src/app/components/data-grid/filtered/grid.component.html b/apps/playground/src/app/components/data-grid/filtered/grid.component.html
new file mode 100644
index 0000000000..709d824270
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/filtered/grid.component.html
@@ -0,0 +1,98 @@
+Filtered Data Grid
+
+
+ This example demonstrates integrating sky-filter-bar with
+ sky-ag-grid using typed filters. All filters (text, number, date,
+ and boolean) are applied using AG Grid's external filtering mechanism via
+ isExternalFilterPresent and doesExternalFilterPass.
+
+
+Filter Bar + AG Grid Integration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Current Filter State
+
+
+
Applied Filters:
+
{{ appliedFilters() | json }}
+
+
Typed Filter Access:
+
+
+ Name filter (string):
+ {{ activeNameFilter() ?? 'not set' }}
+
+
+ Salary filter (range):
+ {{ activeSalaryFilter() | json }}
+
+
+ Start date filter (date range):
+ {{ activeStartDateFilter() | json }}
+
+
+
diff --git a/apps/playground/src/app/components/data-grid/filtered/grid.component.scss b/apps/playground/src/app/components/data-grid/filtered/grid.component.scss
new file mode 100644
index 0000000000..e994b038c3
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/filtered/grid.component.scss
@@ -0,0 +1,20 @@
+.filter-debug {
+ background-color: #f5f5f5;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 16px;
+ margin-top: 16px;
+
+ pre {
+ background-color: #fff;
+ border: 1px solid #ddd;
+ padding: 8px;
+ overflow-x: auto;
+ }
+
+ code {
+ background-color: #e8e8e8;
+ padding: 2px 6px;
+ border-radius: 3px;
+ }
+}
diff --git a/apps/playground/src/app/components/data-grid/filtered/grid.component.ts b/apps/playground/src/app/components/data-grid/filtered/grid.component.ts
new file mode 100644
index 0000000000..0f88d9f5fb
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/filtered/grid.component.ts
@@ -0,0 +1,152 @@
+import { JsonPipe } from '@angular/common';
+import { Component, computed, signal } from '@angular/core';
+import {
+ SkyDataGridColumnComponent,
+ SkyDataGridComponent,
+ SkyDataGridFilterValue,
+} from '@skyux/data-grid';
+import { SkyFilterBarModule } from '@skyux/filter-bar';
+import { SkyFilterStateFilterItem } from '@skyux/lists';
+
+import { HideInactiveFilterModalComponent } from './hide-inactive-filter-modal.component';
+import { NameFilterModalComponent } from './name-filter-modal.component';
+import { SalaryFilterModalComponent } from './salary-filter-modal.component';
+import { StartDateFilterModalComponent } from './start-date-filter-modal.component';
+
+interface Employee {
+ id: string;
+ name: string;
+ department: string;
+ salary: number;
+ startDate: string;
+ active: boolean;
+}
+
+@Component({
+ selector: 'app-filtered-grid',
+ imports: [
+ JsonPipe,
+ SkyDataGridComponent,
+ SkyDataGridColumnComponent,
+ SkyFilterBarModule,
+ ],
+ templateUrl: './grid.component.html',
+ styleUrl: './grid.component.scss',
+})
+export default class FilteredGridComponent {
+ protected readonly nameFilterModal = NameFilterModalComponent;
+ protected readonly salaryFilterModal = SalaryFilterModalComponent;
+ protected readonly hideInactiveModal = HideInactiveFilterModalComponent;
+ protected readonly startDateFilterModal = StartDateFilterModalComponent;
+
+ protected readonly appliedFilters = signal<
+ SkyFilterStateFilterItem[]
+ >([]);
+
+ /**
+ * Raw employee data before filtering.
+ */
+ protected readonly allEmployees: Employee[] = [
+ {
+ id: '1',
+ name: 'Alice Johnson',
+ department: 'Engineering',
+ salary: 95000,
+ startDate: '2020-03-15',
+ active: true,
+ },
+ {
+ id: '2',
+ name: 'Bob Smith',
+ department: 'Sales',
+ salary: 72000,
+ startDate: '2019-07-22',
+ active: true,
+ },
+ {
+ id: '3',
+ name: 'Carol Williams',
+ department: 'Engineering',
+ salary: 110000,
+ startDate: '2018-01-10',
+ active: true,
+ },
+ {
+ id: '4',
+ name: 'David Brown',
+ department: 'Marketing',
+ salary: 65000,
+ startDate: '2021-09-01',
+ active: false,
+ },
+ {
+ id: '5',
+ name: 'Eva Martinez',
+ department: 'Engineering',
+ salary: 88000,
+ startDate: '2020-11-30',
+ active: true,
+ },
+ {
+ id: '6',
+ name: 'Frank Garcia',
+ department: 'Sales',
+ salary: 78000,
+ startDate: '2017-04-18',
+ active: true,
+ },
+ {
+ id: '7',
+ name: 'Grace Lee',
+ department: 'Marketing',
+ salary: 71000,
+ startDate: '2022-02-14',
+ active: true,
+ },
+ {
+ id: '8',
+ name: 'Henry Wilson',
+ department: 'Engineering',
+ salary: 125000,
+ startDate: '2016-08-05',
+ active: false,
+ },
+ {
+ id: '9',
+ name: 'Ivy Chen',
+ department: 'Sales',
+ salary: 82000,
+ startDate: '2019-12-20',
+ active: true,
+ },
+ {
+ id: '10',
+ name: 'Jack Taylor',
+ department: 'Marketing',
+ salary: 58000,
+ startDate: '2023-01-09',
+ active: true,
+ },
+ ];
+
+ /**
+ * Example of typed filter value access for display purposes.
+ */
+ protected readonly activeNameFilter = computed(
+ () =>
+ this.appliedFilters()?.find((f) => f.filterId === 'name')?.filterValue
+ ?.value as string | undefined,
+ );
+
+ protected readonly activeSalaryFilter = computed(
+ () =>
+ this.appliedFilters()?.find((f) => f.filterId === 'salary')?.filterValue
+ ?.displayValue as string | undefined,
+ );
+
+ protected readonly activeStartDateFilter = computed(
+ () =>
+ this.appliedFilters()?.find((f) => f.filterId === 'startDate')
+ ?.filterValue?.displayValue as string | undefined,
+ );
+}
diff --git a/apps/playground/src/app/components/data-grid/filtered/hide-inactive-filter-modal.component.ts b/apps/playground/src/app/components/data-grid/filtered/hide-inactive-filter-modal.component.ts
new file mode 100644
index 0000000000..fc5ac9242e
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/filtered/hide-inactive-filter-modal.component.ts
@@ -0,0 +1,50 @@
+import { Component, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ SkyFilterBarFilterValue,
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyCheckboxModule } from '@skyux/forms';
+import { SkyModalModule } from '@skyux/modals';
+
+@Component({
+ selector: 'app-hide-inactive-filter-modal',
+ imports: [FormsModule, SkyCheckboxModule, SkyModalModule],
+ template: `
+
+
+
+
+
+
+ Apply
+
+
+ Cancel
+
+
+
+ `,
+})
+export class HideInactiveFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+
+ public filterLabelText = this.#context.filterLabelText;
+ public hideInactive = !!this.#context.filterValue?.value;
+
+ public apply(): void {
+ const filterValue: SkyFilterBarFilterValue | undefined = this.hideInactive
+ ? { value: true, displayValue: 'Showing active only' }
+ : undefined;
+ this.modalInstance.save({ filterValue });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+}
diff --git a/apps/playground/src/app/components/data-grid/filtered/name-filter-modal.component.ts b/apps/playground/src/app/components/data-grid/filtered/name-filter-modal.component.ts
new file mode 100644
index 0000000000..57dcb25918
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/filtered/name-filter-modal.component.ts
@@ -0,0 +1,52 @@
+import { Component, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ SkyFilterBarFilterValue,
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyInputBoxModule } from '@skyux/forms';
+import { SkyModalModule } from '@skyux/modals';
+
+@Component({
+ selector: 'app-name-filter-modal',
+ imports: [FormsModule, SkyInputBoxModule, SkyModalModule],
+ template: `
+
+
+
+
+
+
+
+
+ Apply
+
+
+ Cancel
+
+
+
+ `,
+})
+export class NameFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+
+ public filterLabelText = this.#context.filterLabelText;
+ public nameFilter = (this.#context.filterValue?.value as string) ?? '';
+
+ public apply(): void {
+ const filterValue: SkyFilterBarFilterValue | undefined = this.nameFilter
+ ? {
+ value: this.nameFilter,
+ displayValue: `Name contains "${this.nameFilter}"`,
+ }
+ : undefined;
+ this.modalInstance.save({ filterValue });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+}
diff --git a/apps/playground/src/app/components/data-grid/filtered/salary-filter-modal.component.ts b/apps/playground/src/app/components/data-grid/filtered/salary-filter-modal.component.ts
new file mode 100644
index 0000000000..3675432045
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/filtered/salary-filter-modal.component.ts
@@ -0,0 +1,146 @@
+import { Component, inject } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
+import {
+ SkyAutonumericModule,
+ SkyAutonumericOptions,
+} from '@skyux/autonumeric';
+import { SkyNumericService } from '@skyux/core';
+import {
+ SkyDataGridNumberRangeFilterFormGroup,
+ SkyDataGridNumberRangeFilterValue,
+} from '@skyux/data-grid';
+import {
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyInputBoxModule } from '@skyux/forms';
+import { SkyModalError, SkyModalModule } from '@skyux/modals';
+
+import { map } from 'rxjs/operators';
+
+@Component({
+ selector: 'app-salary-filter-modal',
+ imports: [
+ ReactiveFormsModule,
+ SkyInputBoxModule,
+ SkyModalModule,
+ SkyAutonumericModule,
+ ],
+ template: `
+
+ `,
+})
+export class SalaryFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+ readonly #fb = inject(FormBuilder);
+ readonly #existingValue = this.#context.filterValue?.value as
+ | SkyDataGridNumberRangeFilterValue
+ | undefined;
+ readonly #numeric = inject(SkyNumericService);
+
+ public filterLabelText = this.#context.filterLabelText;
+
+ protected readonly form = this.#fb.group(
+ {
+ from: this.#fb.control(this.#existingValue?.from ?? null),
+ to: this.#fb.control(this.#existingValue?.to ?? null),
+ },
+ {
+ validators: (formGroup: SkyDataGridNumberRangeFilterFormGroup) => {
+ const min = formGroup.controls.from.value;
+ const max = formGroup.controls.to.value;
+ // At least one value must be provided.
+ if (min === null && max === null) {
+ return {
+ salaryRangeRequired: { message: 'Salary Range required' },
+ };
+ }
+ // If both values are provided, min must be less than or equal to max.
+ if (min !== null && max !== null && min >= max) {
+ return { salaryRangeInvalid: { message: 'Salary Range Invalid' } };
+ }
+ return null;
+ },
+ updateOn: 'change',
+ },
+ ) as SkyDataGridNumberRangeFilterFormGroup;
+
+ protected readonly formErrors = toSignal(
+ this.form.statusChanges.pipe(
+ map(() => Object.values(this.form.errors ?? {}) as SkyModalError[]),
+ ),
+ );
+ protected autonumericOptions: SkyAutonumericOptions = 'dollarPos' as const;
+
+ public apply(): void {
+ if (this.form.valid) {
+ const value = {
+ from: this.form.controls.from.value,
+ to: this.form.controls.to.value,
+ } as SkyDataGridNumberRangeFilterValue;
+ let displayValue = '';
+ if (value.from && value.to) {
+ displayValue = `${this.#format(value.from)} - ${this.#format(value.to)}`;
+ } else if (value.from) {
+ displayValue = `From ${this.#format(value.from)}`;
+ } else if (value.to) {
+ displayValue = `Up to ${this.#format(value.to)}`;
+ }
+ this.modalInstance.save({ filterValue: { value, displayValue } });
+ } else {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+ }
+
+ public clear(): void {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+
+ #format(value: number): string {
+ return this.#numeric.formatNumber(value, { format: 'currency' });
+ }
+}
diff --git a/apps/playground/src/app/components/data-grid/filtered/start-date-filter-modal.component.ts b/apps/playground/src/app/components/data-grid/filtered/start-date-filter-modal.component.ts
new file mode 100644
index 0000000000..156fdcbf52
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/filtered/start-date-filter-modal.component.ts
@@ -0,0 +1,91 @@
+import { Component, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ SkyDateRangeCalculation,
+ SkyDateRangeCalculatorId,
+ SkyDateRangePickerModule,
+} from '@skyux/datetime';
+import {
+ SkyFilterBarFilterValue,
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyModalModule } from '@skyux/modals';
+
+@Component({
+ selector: 'app-start-date-filter-modal',
+ imports: [FormsModule, SkyDateRangePickerModule, SkyModalModule],
+ template: `
+
+
+
+
+
+
+ Apply
+
+
+ Clear
+
+
+ Cancel
+
+
+
+ `,
+})
+export class StartDateFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+
+ public filterLabelText = this.#context.filterLabelText;
+ public dateRange: SkyDateRangeCalculation | undefined;
+
+ constructor() {
+ const existingValue = this.#context.filterValue?.value as
+ | SkyDateRangeCalculation
+ | undefined;
+ if (existingValue) {
+ this.dateRange = existingValue;
+ }
+ }
+
+ public apply(): void {
+ if (
+ this.dateRange &&
+ this.dateRange.calculatorId !== SkyDateRangeCalculatorId.AnyTime
+ ) {
+ const filterValue: SkyFilterBarFilterValue = {
+ value: this.dateRange,
+ displayValue: this.#formatDateRange(this.dateRange),
+ };
+ this.modalInstance.save({ filterValue });
+ } else {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+ }
+
+ public clear(): void {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+
+ #formatDateRange(range: SkyDateRangeCalculation): string {
+ if (range.startDate && range.endDate) {
+ const start = new Date(range.startDate).toLocaleDateString();
+ const end = new Date(range.endDate).toLocaleDateString();
+ return `${start} - ${end}`;
+ } else if (range.startDate) {
+ return `After ${new Date(range.startDate).toLocaleDateString()}`;
+ } else if (range.endDate) {
+ return `Before ${new Date(range.endDate).toLocaleDateString()}`;
+ }
+ return 'Date range applied';
+ }
+}
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..756e160132
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.html
@@ -0,0 +1,53 @@
+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..a12a3cc6dc
--- /dev/null
+++ b/apps/playground/src/app/components/data-grid/paging/grid-paging.component.ts
@@ -0,0 +1,18 @@
+import { Component, input } from '@angular/core';
+import {
+ SkyDataGridColumnComponent,
+ SkyDataGridComponent,
+} from '@skyux/data-grid';
+
+import { AG_GRID_DEMO_DATA } from '../../../shared/data-manager/data-manager-data';
+
+@Component({
+ selector: 'app-data-grid-paging',
+ imports: [SkyDataGridComponent, SkyDataGridColumnComponent],
+ 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..be6f4789a7
--- /dev/null
+++ b/apps/playground/src/app/components/grids/basic/grid.component.html
@@ -0,0 +1,242 @@
+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
+
+
+
+
+
+
+ @if (row.id) {
+
+
+
+
+ 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..96e523acc3
--- /dev/null
+++ b/apps/playground/src/app/components/grids/basic/grid.component.ts
@@ -0,0 +1,216 @@
+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: [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..ef4c99d089
--- /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: '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..5424aca70b
--- /dev/null
+++ b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.html
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Toggle row highlight
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+List-view-grid with inline delete
+
+
+
+
+
+
+
+
+
+
+
+ @if (row.id) {
+
+ 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..0c24293e7c
--- /dev/null
+++ b/apps/playground/src/app/components/list-builder/list-view-grid/list-view-grid.component.ts
@@ -0,0 +1,108 @@
+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: [
+ 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/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts
index 2734f19abf..f75d6a8c27 100644
--- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts
+++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-data-manager-adapter.directive.ts
@@ -46,6 +46,8 @@ function toColumnWidthName(breakpoint: SkyBreakpoint): 'xs' | 'sm' {
return breakpoint === 'xs' ? 'xs' : 'sm';
}
+const RESERVED_COLUMNS = ['ag-Grid-SelectionColumn'];
+
/**
* Connects `SkyAgGridWrapperComponent` with a `SkyDataViewComponent` to control the grid using a `SkyDataManagerService` instance.
*/
@@ -403,7 +405,11 @@ export class SkyAgGridDataManagerAdapterDirective implements OnDestroy {
const hideColumns = agGrid.api
.getColumnState()
.map((col) => col.colId)
- .filter((colId) => !displayedColumnIds.includes(colId));
+ .filter(
+ (colId) =>
+ !displayedColumnIds.includes(colId) &&
+ !RESERVED_COLUMNS.includes(colId),
+ );
agGrid.api.setColumnsVisible(hideColumns, false);
agGrid.api.setColumnsVisible(displayedColumnIds, true);
diff --git a/libs/components/ag-grid/src/lib/styles/ag-grid-theme.ts b/libs/components/ag-grid/src/lib/styles/ag-grid-theme.ts
index fdf35a95cb..863176cc27 100644
--- a/libs/components/ag-grid/src/lib/styles/ag-grid-theme.ts
+++ b/libs/components/ag-grid/src/lib/styles/ag-grid-theme.ts
@@ -24,6 +24,10 @@ const defaultsForAllThemes: Partial = {
'var(--sky-override-switch-checked-color, var(--sky-color-icon-inverse, var(--sky-text-color-default)))',
checkboxIndeterminateBackgroundColor:
'var(--sky-override-switch-checked-color, var(--sky-color-icon-inverse, transparent))',
+ checkboxIndeterminateBorderColor: `var(
+ --sky-override-ag-grid-checkbox-unchecked-border-color,
+ var(--sky-color-border-switch-base)
+ )`,
checkboxUncheckedBackgroundColor:
'var(--sky-override-ag-grid-checkbox-unchecked-background-color, var(--sky-color-background-input-base))',
checkboxUncheckedBorderColor: `var(
diff --git a/libs/components/code-examples/package.json b/libs/components/code-examples/package.json
index fd6d28626c..25b02fe3ec 100644
--- a/libs/components/code-examples/package.json
+++ b/libs/components/code-examples/package.json
@@ -30,6 +30,7 @@
"@skyux/avatar": "0.0.0-PLACEHOLDER",
"@skyux/colorpicker": "0.0.0-PLACEHOLDER",
"@skyux/core": "0.0.0-PLACEHOLDER",
+ "@skyux/data-grid": "0.0.0-PLACEHOLDER",
"@skyux/data-manager": "0.0.0-PLACEHOLDER",
"@skyux/datetime": "0.0.0-PLACEHOLDER",
"@skyux/errors": "0.0.0-PLACEHOLDER",
diff --git a/libs/components/code-examples/project.json b/libs/components/code-examples/project.json
index 9903ca20af..58ed832e87 100644
--- a/libs/components/code-examples/project.json
+++ b/libs/components/code-examples/project.json
@@ -40,6 +40,7 @@
"tsConfig": "libs/components/code-examples/tsconfig.spec.json",
"karmaConfig": "libs/components/code-examples/karma.conf.js",
"styles": [
+ "libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss",
"libs/components/theme/src/lib/styles/sky.scss",
"libs/components/theme/src/lib/styles/themes/modern/styles.scss"
],
diff --git a/libs/components/code-examples/routes/src/index.ts b/libs/components/code-examples/routes/src/index.ts
index afc6a12efd..1d41e5e10b 100644
--- a/libs/components/code-examples/routes/src/index.ts
+++ b/libs/components/code-examples/routes/src/index.ts
@@ -225,6 +225,34 @@ export const routes: Routes = [
({ CoreNumericBasicExampleComponent: c }) => c,
),
},
+ {
+ path: 'DataGridAsynchronousDataComponent',
+ loadComponent: () =>
+ import('@skyux/code-examples').then(
+ ({ DataGridAsynchronousDataComponent: c }) => c,
+ ),
+ },
+ {
+ path: 'DataGridBasicExampleComponent',
+ loadComponent: () =>
+ import('@skyux/code-examples').then(
+ ({ DataGridBasicExampleComponent: c }) => c,
+ ),
+ },
+ {
+ path: 'DataGridDataManagerExampleComponent',
+ loadComponent: () =>
+ import('@skyux/code-examples').then(
+ ({ DataGridDataManagerExampleComponent: c }) => c,
+ ),
+ },
+ {
+ path: 'DataGridPagingComponent',
+ loadComponent: () =>
+ import('@skyux/code-examples').then(
+ ({ DataGridPagingComponent: c }) => c,
+ ),
+ },
{
path: 'DataManagerBasicExampleComponent',
loadComponent: () =>
@@ -323,6 +351,13 @@ export const routes: Routes = [
({ FilterBarSelectableExampleComponent: c }) => c,
),
},
+ {
+ path: 'FilteredDataGridComponent',
+ loadComponent: () =>
+ import('@skyux/code-examples').then(
+ ({ FilteredDataGridComponent: c }) => c,
+ ),
+ },
{
path: 'FlyoutBasicExampleComponent',
loadComponent: () =>
diff --git a/libs/components/code-examples/src/index.ts b/libs/components/code-examples/src/index.ts
index 78af68d8e0..c639aa936a 100644
--- a/libs/components/code-examples/src/index.ts
+++ b/libs/components/code-examples/src/index.ts
@@ -29,6 +29,11 @@ export { CoreIdExampleComponent } from './lib/modules/core/id/example.component'
export { CoreMediaQueryBasicExampleComponent } from './lib/modules/core/media-query/basic/example.component';
export { CoreMediaQueryResponsiveHostExampleComponent } from './lib/modules/core/media-query/responsive-host/example.component';
export { CoreNumericBasicExampleComponent } from './lib/modules/core/numeric/basic/example.component';
+export { DataGridBasicExampleComponent } from './lib/modules/data-grid/basic/example.component';
+export { FilteredDataGridComponent } from './lib/modules/data-grid/filtered/example.component';
+export { DataGridPagingComponent } from './lib/modules/data-grid/paging/example.component';
+export { DataGridDataManagerExampleComponent } from './lib/modules/data-grid/data-manager/example.component';
+export { DataGridAsynchronousDataComponent } from './lib/modules/data-grid/asynchronous-data/example.component';
export { DataManagerBasicExampleComponent } from './lib/modules/data-manager/data-manager/basic/example.component';
export { DatetimeDatePipeBasicExampleComponent } from './lib/modules/datetime/date-pipe/basic/example.component';
export { DatetimeDateRangePickerBasicExampleComponent } from './lib/modules/datetime/date-range-picker/basic/example.component';
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data-sort-and-filter.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data-sort-and-filter.ts
new file mode 100644
index 0000000000..f2f007d1e3
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data-sort-and-filter.ts
@@ -0,0 +1,138 @@
+import { SkyDataGridNumberRangeFilterValue } from '@skyux/data-grid';
+import { SkyDataManagerSortOption } from '@skyux/data-manager';
+import { SkyDateRange } from '@skyux/datetime';
+import { SkyFilterStateFilterItem } from '@skyux/lists';
+
+import { Employee } from './data';
+
+export function dataSortAndFilter(
+ allEmployees: Employee[],
+ appliedFilters: SkyFilterStateFilterItem<
+ string | SkyDataGridNumberRangeFilterValue | SkyDateRange | boolean
+ >[],
+ sort: SkyDataManagerSortOption | undefined,
+ searchText: string | undefined,
+): Employee[] {
+ const normalSearch = searchText?.normalize().toLowerCase();
+ const records = allEmployees.filter((employee: Employee) => {
+ let includeEmployee = true;
+ if (normalSearch) {
+ includeEmployee &&= Object.values(employee).some((value) =>
+ String(value ?? '')
+ .normalize()
+ .toLowerCase()
+ .includes(normalSearch),
+ );
+ }
+ for (const filterItem of appliedFilters) {
+ switch (filterItem.filterId) {
+ case 'name':
+ includeEmployee &&= filterByName(
+ filterItem as SkyFilterStateFilterItem,
+ employee,
+ );
+ break;
+ case 'salary':
+ includeEmployee &&= filterBySalary(
+ filterItem as SkyFilterStateFilterItem,
+ employee,
+ );
+ break;
+ case 'startDate':
+ includeEmployee &&= filterByDate(
+ filterItem as SkyFilterStateFilterItem,
+ employee,
+ );
+ break;
+ case 'hideInactive':
+ includeEmployee &&= filterByActive(
+ filterItem as SkyFilterStateFilterItem,
+ employee,
+ );
+ break;
+ default:
+ }
+ }
+ return includeEmployee;
+ });
+ if (sort) {
+ switch (sort.propertyName) {
+ case 'name':
+ records.sort(
+ (a, b) => a.name.localeCompare(b.name) * (sort.descending ? -1 : 1),
+ );
+ break;
+ case 'salary':
+ records.sort(
+ (a, b) => (a.salary - b.salary) * (sort.descending ? -1 : 1),
+ );
+ break;
+ case 'startDate':
+ records.sort(
+ (a, b) =>
+ (new Date(a.startDate).getTime() -
+ new Date(b.startDate).getTime()) *
+ (sort.descending ? -1 : 1),
+ );
+ break;
+ case 'active':
+ records.sort(
+ (a, b) =>
+ (Number(a.active) - Number(b.active)) * (sort.descending ? -1 : 1),
+ );
+ break;
+ default:
+ }
+ }
+ return records;
+}
+
+function filterByName(
+ filterItem: SkyFilterStateFilterItem,
+ employee: Employee,
+): boolean {
+ return (
+ !filterItem.filterValue?.value ||
+ employee.name
+ .normalize()
+ .toLowerCase()
+ .includes(filterItem.filterValue.value.normalize().toLowerCase())
+ );
+}
+
+function filterBySalary(
+ filterItem: SkyFilterStateFilterItem,
+ employee: Employee,
+): boolean {
+ const filterValue: SkyDataGridNumberRangeFilterValue | undefined =
+ filterItem.filterValue?.value;
+ return (
+ !filterValue ||
+ ((Number.isNaN(Number(filterValue.from ?? undefined)) ||
+ employee.salary >= Number(filterValue.from)) &&
+ (Number.isNaN(Number(filterValue.to ?? undefined)) ||
+ employee.salary <= Number(filterValue.to)))
+ );
+}
+
+function filterByDate(
+ filterItem: SkyFilterStateFilterItem,
+ employee: Employee,
+): boolean {
+ const filterValue: SkyDateRange | undefined = filterItem.filterValue?.value;
+ // This is simplistic and does not account for time zones.
+ return (
+ !filterValue ||
+ ((!filterValue.startDate ||
+ new Date(employee.startDate) >= filterValue.startDate) &&
+ (!filterValue.endDate ||
+ new Date(employee.startDate) <= filterValue.endDate))
+ );
+}
+
+function filterByActive(
+ filterItem: SkyFilterStateFilterItem,
+ employee: Employee,
+): boolean {
+ return !filterItem.filterValue?.value || employee.active;
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data.ts
new file mode 100644
index 0000000000..169d6792a7
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/data.ts
@@ -0,0 +1,130 @@
+import {
+ SkyDataGridNumberRangeFilterValue,
+ SkyDataGridPageRequest,
+} from '@skyux/data-grid';
+import { SkyDataManagerState } from '@skyux/data-manager';
+import { SkyDateRange } from '@skyux/datetime';
+import { SkyFilterState, SkyFilterStateFilterItem } from '@skyux/lists';
+
+import { Observable, delay, of } from 'rxjs';
+
+import { dataSortAndFilter } from './data-sort-and-filter';
+
+export interface Employee {
+ id: string;
+ name: string;
+ department: string;
+ salary: number;
+ startDate: string;
+ active: boolean;
+}
+
+const employees = [
+ {
+ id: '1',
+ name: 'Alice Johnson',
+ department: 'Engineering',
+ salary: 95000,
+ startDate: '2020-03-15',
+ active: true,
+ },
+ {
+ id: '2',
+ name: 'Bob Smith',
+ department: 'Sales',
+ salary: 72000,
+ startDate: '2019-07-22',
+ active: true,
+ },
+ {
+ id: '3',
+ name: 'Carol Williams',
+ department: 'Engineering',
+ salary: 110000,
+ startDate: '2018-01-10',
+ active: true,
+ },
+ {
+ id: '4',
+ name: 'David Brown',
+ department: 'Marketing',
+ salary: 65000,
+ startDate: '2021-09-01',
+ active: false,
+ },
+ {
+ id: '5',
+ name: 'Eva Martinez',
+ department: 'Engineering',
+ salary: 88000,
+ startDate: '2020-11-30',
+ active: true,
+ },
+ {
+ id: '6',
+ name: 'Frank Garcia',
+ department: 'Sales',
+ salary: 78000,
+ startDate: '2017-04-18',
+ active: true,
+ },
+ {
+ id: '7',
+ name: 'Grace Lee',
+ department: 'Marketing',
+ salary: 71000,
+ startDate: '2022-02-14',
+ active: true,
+ },
+ {
+ id: '8',
+ name: 'Henry Wilson',
+ department: 'Engineering',
+ salary: 125000,
+ startDate: '2016-08-05',
+ active: false,
+ },
+ {
+ id: '9',
+ name: 'Ivy Chen',
+ department: 'Sales',
+ salary: 82000,
+ startDate: '2019-12-20',
+ active: true,
+ },
+ {
+ id: '10',
+ name: 'Jack Taylor',
+ department: 'Marketing',
+ salary: 58000,
+ startDate: '2023-01-09',
+ active: true,
+ },
+];
+
+export function remoteService(params: {
+ dataManagerUpdates: SkyDataManagerState | undefined;
+ pageRequest: SkyDataGridPageRequest | undefined;
+}): Observable<{ data: Employee[] | null; count: number } | null> {
+ if (params.pageRequest?.pageSize) {
+ const pageNumber = params.pageRequest.pageNumber;
+ const pageSize = params.pageRequest.pageSize;
+ const data = dataSortAndFilter(
+ employees,
+ ((
+ params.dataManagerUpdates?.filterData?.filters as
+ | SkyFilterState
+ | undefined
+ )?.appliedFilters ?? []) as SkyFilterStateFilterItem<
+ string | SkyDataGridNumberRangeFilterValue | SkyDateRange | boolean
+ >[],
+ params.dataManagerUpdates?.activeSortOption,
+ params.dataManagerUpdates?.searchText ?? '',
+ );
+ return of({
+ data: data.slice((pageNumber - 1) * pageSize, pageNumber * pageSize),
+ count: data.length,
+ }).pipe(delay(800));
+ }
+ return of(null).pipe(delay(800));
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.html b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.html
new file mode 100644
index 0000000000..0aff6322ed
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.html
@@ -0,0 +1,87 @@
+@let records = recordsToShow | async;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ value
+ | skyNumeric: { format: 'currency', iso: 'USD', truncate: false }
+ }}
+
+
+ {{
+ value | skyDate: 'mediumDate'
+ }}
+
+
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.ts
new file mode 100644
index 0000000000..c0c2af5077
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/example.component.ts
@@ -0,0 +1,104 @@
+import { AsyncPipe } from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ inject,
+ signal,
+} from '@angular/core';
+import { toObservable, toSignal } from '@angular/core/rxjs-interop';
+import { SkyNumericPipe } from '@skyux/core';
+import { SkyDataGridModule, SkyDataGridPageRequest } from '@skyux/data-grid';
+import {
+ SkyDataManagerModule,
+ SkyDataManagerService,
+ SkyDataManagerState,
+} from '@skyux/data-manager';
+import { SkyDatePipe } from '@skyux/datetime';
+import { SkyFilterBarModule } from '@skyux/filter-bar';
+import { SkyListSummaryModule } from '@skyux/lists';
+
+import { switchMap } from 'rxjs';
+
+import { remoteService } from './data';
+import { HideInactiveFilterModalComponent } from './hide-inactive-filter-modal.component';
+import { NameFilterModalComponent } from './name-filter-modal.component';
+import { SalaryFilterModalComponent } from './salary-filter-modal.component';
+import { StartDateFilterModalComponent } from './start-date-filter-modal.component';
+
+/**
+ * @title Asynchronous data loading
+ */
+@Component({
+ selector: 'app-data-grid-asynchronous-data',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [SkyDataManagerService],
+ imports: [
+ SkyDataGridModule,
+ SkyDataManagerModule,
+ SkyDatePipe,
+ SkyFilterBarModule,
+ SkyListSummaryModule,
+ SkyNumericPipe,
+ AsyncPipe,
+ ],
+ templateUrl: './example.component.html',
+})
+export class DataGridAsynchronousDataComponent {
+ protected readonly nameFilterModal = NameFilterModalComponent;
+ protected readonly salaryFilterModal = SalaryFilterModalComponent;
+ protected readonly hideInactiveModal = HideInactiveFilterModalComponent;
+ protected readonly startDateFilterModal = StartDateFilterModalComponent;
+ protected readonly pageRequest = signal(
+ undefined,
+ );
+
+ protected readonly viewId = 'dataGridWithCustomFilters' as const;
+
+ // Computed client side in this example, but could be an HTTP resource where parameters are sent to the server for determining data to show.
+ readonly #requestParameters = toObservable(
+ computed(() => ({
+ dataManagerUpdates: this.#dataManagerUpdates(),
+ pageRequest: this.pageRequest(),
+ })),
+ );
+ protected readonly recordsToShow = this.#requestParameters.pipe(
+ switchMap(remoteService),
+ );
+
+ readonly #dataManagerSvc = inject(SkyDataManagerService);
+ readonly #dataManagerUpdates = toSignal(
+ this.#dataManagerSvc.getDataStateUpdates('dataManagerUpdates'),
+ );
+
+ constructor() {
+ this.#dataManagerSvc.initDataManager({
+ activeViewId: this.viewId,
+ dataManagerConfig: {},
+ defaultDataState: new SkyDataManagerState({
+ filterData: {
+ filters: {},
+ },
+ views: [
+ {
+ viewId: this.viewId,
+ displayedColumnIds: [
+ 'name',
+ 'department',
+ 'salary',
+ 'startDate',
+ 'active',
+ ],
+ },
+ ],
+ }),
+ });
+ this.#dataManagerSvc.initDataView({
+ id: this.viewId,
+ name: 'Data Grid View',
+ iconName: 'table',
+ searchEnabled: true,
+ columnPickerEnabled: true,
+ });
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/hide-inactive-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/hide-inactive-filter-modal.component.ts
new file mode 100644
index 0000000000..fc5ac9242e
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/hide-inactive-filter-modal.component.ts
@@ -0,0 +1,50 @@
+import { Component, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ SkyFilterBarFilterValue,
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyCheckboxModule } from '@skyux/forms';
+import { SkyModalModule } from '@skyux/modals';
+
+@Component({
+ selector: 'app-hide-inactive-filter-modal',
+ imports: [FormsModule, SkyCheckboxModule, SkyModalModule],
+ template: `
+
+
+
+
+
+
+ Apply
+
+
+ Cancel
+
+
+
+ `,
+})
+export class HideInactiveFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+
+ public filterLabelText = this.#context.filterLabelText;
+ public hideInactive = !!this.#context.filterValue?.value;
+
+ public apply(): void {
+ const filterValue: SkyFilterBarFilterValue | undefined = this.hideInactive
+ ? { value: true, displayValue: 'Showing active only' }
+ : undefined;
+ this.modalInstance.save({ filterValue });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/name-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/name-filter-modal.component.ts
new file mode 100644
index 0000000000..57dcb25918
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/name-filter-modal.component.ts
@@ -0,0 +1,52 @@
+import { Component, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ SkyFilterBarFilterValue,
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyInputBoxModule } from '@skyux/forms';
+import { SkyModalModule } from '@skyux/modals';
+
+@Component({
+ selector: 'app-name-filter-modal',
+ imports: [FormsModule, SkyInputBoxModule, SkyModalModule],
+ template: `
+
+
+
+
+
+
+
+
+ Apply
+
+
+ Cancel
+
+
+
+ `,
+})
+export class NameFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+
+ public filterLabelText = this.#context.filterLabelText;
+ public nameFilter = (this.#context.filterValue?.value as string) ?? '';
+
+ public apply(): void {
+ const filterValue: SkyFilterBarFilterValue | undefined = this.nameFilter
+ ? {
+ value: this.nameFilter,
+ displayValue: `Name contains "${this.nameFilter}"`,
+ }
+ : undefined;
+ this.modalInstance.save({ filterValue });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/salary-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/salary-filter-modal.component.ts
new file mode 100644
index 0000000000..3675432045
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/salary-filter-modal.component.ts
@@ -0,0 +1,146 @@
+import { Component, inject } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
+import {
+ SkyAutonumericModule,
+ SkyAutonumericOptions,
+} from '@skyux/autonumeric';
+import { SkyNumericService } from '@skyux/core';
+import {
+ SkyDataGridNumberRangeFilterFormGroup,
+ SkyDataGridNumberRangeFilterValue,
+} from '@skyux/data-grid';
+import {
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyInputBoxModule } from '@skyux/forms';
+import { SkyModalError, SkyModalModule } from '@skyux/modals';
+
+import { map } from 'rxjs/operators';
+
+@Component({
+ selector: 'app-salary-filter-modal',
+ imports: [
+ ReactiveFormsModule,
+ SkyInputBoxModule,
+ SkyModalModule,
+ SkyAutonumericModule,
+ ],
+ template: `
+
+ `,
+})
+export class SalaryFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+ readonly #fb = inject(FormBuilder);
+ readonly #existingValue = this.#context.filterValue?.value as
+ | SkyDataGridNumberRangeFilterValue
+ | undefined;
+ readonly #numeric = inject(SkyNumericService);
+
+ public filterLabelText = this.#context.filterLabelText;
+
+ protected readonly form = this.#fb.group(
+ {
+ from: this.#fb.control(this.#existingValue?.from ?? null),
+ to: this.#fb.control(this.#existingValue?.to ?? null),
+ },
+ {
+ validators: (formGroup: SkyDataGridNumberRangeFilterFormGroup) => {
+ const min = formGroup.controls.from.value;
+ const max = formGroup.controls.to.value;
+ // At least one value must be provided.
+ if (min === null && max === null) {
+ return {
+ salaryRangeRequired: { message: 'Salary Range required' },
+ };
+ }
+ // If both values are provided, min must be less than or equal to max.
+ if (min !== null && max !== null && min >= max) {
+ return { salaryRangeInvalid: { message: 'Salary Range Invalid' } };
+ }
+ return null;
+ },
+ updateOn: 'change',
+ },
+ ) as SkyDataGridNumberRangeFilterFormGroup;
+
+ protected readonly formErrors = toSignal(
+ this.form.statusChanges.pipe(
+ map(() => Object.values(this.form.errors ?? {}) as SkyModalError[]),
+ ),
+ );
+ protected autonumericOptions: SkyAutonumericOptions = 'dollarPos' as const;
+
+ public apply(): void {
+ if (this.form.valid) {
+ const value = {
+ from: this.form.controls.from.value,
+ to: this.form.controls.to.value,
+ } as SkyDataGridNumberRangeFilterValue;
+ let displayValue = '';
+ if (value.from && value.to) {
+ displayValue = `${this.#format(value.from)} - ${this.#format(value.to)}`;
+ } else if (value.from) {
+ displayValue = `From ${this.#format(value.from)}`;
+ } else if (value.to) {
+ displayValue = `Up to ${this.#format(value.to)}`;
+ }
+ this.modalInstance.save({ filterValue: { value, displayValue } });
+ } else {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+ }
+
+ public clear(): void {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+
+ #format(value: number): string {
+ return this.#numeric.formatNumber(value, { format: 'currency' });
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/start-date-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/start-date-filter-modal.component.ts
new file mode 100644
index 0000000000..156fdcbf52
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/asynchronous-data/start-date-filter-modal.component.ts
@@ -0,0 +1,91 @@
+import { Component, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ SkyDateRangeCalculation,
+ SkyDateRangeCalculatorId,
+ SkyDateRangePickerModule,
+} from '@skyux/datetime';
+import {
+ SkyFilterBarFilterValue,
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyModalModule } from '@skyux/modals';
+
+@Component({
+ selector: 'app-start-date-filter-modal',
+ imports: [FormsModule, SkyDateRangePickerModule, SkyModalModule],
+ template: `
+
+
+
+
+
+
+ Apply
+
+
+ Clear
+
+
+ Cancel
+
+
+
+ `,
+})
+export class StartDateFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+
+ public filterLabelText = this.#context.filterLabelText;
+ public dateRange: SkyDateRangeCalculation | undefined;
+
+ constructor() {
+ const existingValue = this.#context.filterValue?.value as
+ | SkyDateRangeCalculation
+ | undefined;
+ if (existingValue) {
+ this.dateRange = existingValue;
+ }
+ }
+
+ public apply(): void {
+ if (
+ this.dateRange &&
+ this.dateRange.calculatorId !== SkyDateRangeCalculatorId.AnyTime
+ ) {
+ const filterValue: SkyFilterBarFilterValue = {
+ value: this.dateRange,
+ displayValue: this.#formatDateRange(this.dateRange),
+ };
+ this.modalInstance.save({ filterValue });
+ } else {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+ }
+
+ public clear(): void {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+
+ #formatDateRange(range: SkyDateRangeCalculation): string {
+ if (range.startDate && range.endDate) {
+ const start = new Date(range.startDate).toLocaleDateString();
+ const end = new Date(range.endDate).toLocaleDateString();
+ return `${start} - ${end}`;
+ } else if (range.startDate) {
+ return `After ${new Date(range.startDate).toLocaleDateString()}`;
+ } else if (range.endDate) {
+ return `Before ${new Date(range.endDate).toLocaleDateString()}`;
+ }
+ return 'Date range applied';
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/basic/data.ts b/libs/components/code-examples/src/lib/modules/data-grid/basic/data.ts
new file mode 100644
index 0000000000..25ebd17408
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-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 DataGridDemoRow {
+ id: string;
+ selected?: boolean;
+ name: string;
+ age: number;
+ startDate: Date;
+ endDate?: Date;
+ department: AutocompleteOption;
+ jobTitle?: AutocompleteOption;
+}
+
+export const DATA_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/data-grid/basic/example.component.html b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.html
new file mode 100644
index 0000000000..a7ffb82ffb
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.html
@@ -0,0 +1,91 @@
+@let names = selectedNames();
+
+
+
+ Selected rows: {{ names }}
+ @if (!names) {
+ no rows selected
+ }
+
+
+
+
+
+
+
+
+
+
+ {{ row.department?.name }}
+
+
+ {{ row.jobTitle?.name }}
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+ Mark inactive
+
+
+
+
+ More info
+
+
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.spec.ts b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.spec.ts
new file mode 100644
index 0000000000..da013e7833
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.spec.ts
@@ -0,0 +1,78 @@
+import { HarnessLoader } from '@angular/cdk/testing';
+import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { SkyDataGridHarness } from '@skyux/data-grid/testing';
+import {
+ SkyDropdownHarness,
+ SkyDropdownMenuHarness,
+} from '@skyux/popovers/testing';
+
+import { DataGridBasicExampleComponent } from './example.component';
+
+describe('Basic data grid example', () => {
+ let fixture: ComponentFixture;
+ let loader: HarnessLoader;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [DataGridBasicExampleComponent],
+ }).compileComponents();
+ fixture = TestBed.createComponent(DataGridBasicExampleComponent);
+ loader = TestbedHarnessEnvironment.loader(fixture);
+ fixture.detectChanges();
+ });
+
+ it('should create the component and show data', async () => {
+ expect(fixture.componentInstance).toBeDefined();
+ const gridHarness = await loader.getHarness(
+ SkyDataGridHarness.with({
+ dataSkyId: 'example-data-grid',
+ }),
+ );
+ expect(await gridHarness.getDisplayedColumnIds()).toEqual([
+ 'ag-Grid-SelectionColumn',
+ 'context',
+ 'name',
+ 'age',
+ 'startDate',
+ 'endDate',
+ 'department',
+ 'jobTitle',
+ ]);
+ expect(await gridHarness.getDisplayedColumnHeaderNames()).toEqual([
+ '',
+ 'Context menu',
+ 'Name',
+ 'Age',
+ 'Start date',
+ 'End date',
+ 'Department',
+ 'Title',
+ ]);
+ });
+
+ it('should show context menu and handle item click', async () => {
+ expect(fixture.componentInstance).toBeDefined();
+ const gridHarness = await loader.getHarness(
+ SkyDataGridHarness.with({
+ dataSkyId: 'example-data-grid',
+ }),
+ );
+ const menuButtonHarness = await gridHarness.queryHarness(
+ SkyDropdownHarness.with({
+ dataSkyId: 'context-menu-2',
+ }),
+ );
+ await menuButtonHarness.clickDropdownButton();
+ const menuHarness = await TestbedHarnessEnvironment.documentRootLoader(
+ fixture,
+ ).getHarness(SkyDropdownMenuHarness);
+ const deleteButton = await menuHarness.querySelector(
+ 'button[aria-label="More info for Jane Deere"]',
+ );
+ expect(deleteButton).toBeTruthy();
+ const actionSpy = spyOn(window, 'alert').and.stub();
+ await deleteButton?.click();
+ expect(actionSpy).toHaveBeenCalledWith('More info clicked for Jane Deere');
+ });
+});
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.ts
new file mode 100644
index 0000000000..273b06832a
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/basic/example.component.ts
@@ -0,0 +1,65 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ model,
+ signal,
+} from '@angular/core';
+import {
+ SkyDataGridModule,
+ SkyDataGridRowDeleteCancelArgs,
+ SkyDataGridRowDeleteConfirmArgs,
+} from '@skyux/data-grid';
+import { SkyBoxModule } from '@skyux/layout';
+import { SkyDropdownModule } from '@skyux/popovers';
+
+import { DATA_GRID_DEMO_DATA, DataGridDemoRow } from './data';
+
+/**
+ * @title Basic data grid
+ */
+@Component({
+ selector: 'app-data-grid-basic-example',
+ templateUrl: './example.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [SkyBoxModule, SkyDataGridModule, SkyDropdownModule],
+})
+export class DataGridBasicExampleComponent {
+ protected readonly gridData = signal(DATA_GRID_DEMO_DATA);
+ protected readonly rowDeleteIds = model([]);
+ protected readonly selectedRowIds = model([]);
+
+ protected readonly selectedNames = computed(() => {
+ const selectedRowIds = this.selectedRowIds();
+ return this.gridData()
+ .filter((row) => selectedRowIds.includes(row.id))
+ .map((row: DataGridDemoRow) => row.name)
+ .sort((a, b) => a.localeCompare(b))
+ .join(', ');
+ });
+
+ public actionClicked(row: DataGridDemoRow, action: string): void {
+ if (action === 'Delete') {
+ this.rowDeleteIds.update((rowDeleteIds) => [
+ ...new Set([...rowDeleteIds, row.id]),
+ ]);
+ } else {
+ alert(`${action} clicked for ${row.name}`);
+ }
+ }
+
+ protected rowDeleteConfirm($event: SkyDataGridRowDeleteConfirmArgs): void {
+ this.gridData.update((gridData) =>
+ gridData.filter(({ id }) => id !== $event.id),
+ );
+ this.rowDeleteIds.update((rowDeleteIds) =>
+ rowDeleteIds.filter((id) => id !== $event.id),
+ );
+ }
+
+ protected rowDeleteCancel($event: SkyDataGridRowDeleteCancelArgs): void {
+ this.rowDeleteIds.update((rowDeleteIds) =>
+ rowDeleteIds.filter((id) => id !== $event.id),
+ );
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/data.ts b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/data.ts
new file mode 100644
index 0000000000..69feb368ec
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/data.ts
@@ -0,0 +1,159 @@
+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 DataGridDemoRow {
+ id: string;
+ selected?: boolean;
+ name: string;
+ age: number;
+ startDate: Date;
+ endDate?: Date;
+ department: string;
+ jobTitle?: string;
+}
+
+export const DATA_GRID_DEMO_DATA: DataGridDemoRow[] = [
+ {
+ id: '4b7f07b6-d8d3-41cd-84ad-f3ed51cee5c0',
+ name: 'Billy Bob',
+ age: 55,
+ startDate: new Date('12/1/1994'),
+ department: DEPARTMENTS[3].name,
+ jobTitle: JOB_TITLES['Customer Support'][1].name,
+ },
+ {
+ id: 'aea50a38-aa1e-44e0-94b5-52d3f577767f',
+ name: 'Jane Deere',
+ age: 33,
+ startDate: new Date('7/15/2009'),
+ department: DEPARTMENTS[2].name,
+ jobTitle: JOB_TITLES['Engineering'][2].name,
+ },
+ {
+ id: 'e74afbe4-5016-4a20-9803-30a301835c4f',
+ name: 'John Doe',
+ age: 38,
+ startDate: new Date('9/1/2017'),
+ endDate: new Date('9/30/2017'),
+ department: DEPARTMENTS[1].name,
+ jobTitle: JOB_TITLES['Sales'][1].name,
+ },
+ {
+ id: '0274faf9-388e-497d-bced-f2bef3eafcfd',
+ name: 'David Smith',
+ age: 51,
+ startDate: new Date('1/1/2012'),
+ endDate: new Date('6/15/2018'),
+ department: DEPARTMENTS[2].name,
+ jobTitle: JOB_TITLES['Engineering'][4].name,
+ },
+ {
+ id: '09b7da69-0272-4fe0-ace3-658a6d8f175c',
+ selected: true,
+ name: 'Emily Johnson',
+ age: 41,
+ startDate: new Date('1/15/2014'),
+ department: DEPARTMENTS[0].name,
+ jobTitle: JOB_TITLES['Marketing'][2].name,
+ },
+ {
+ id: '3accf076-fff1-4229-bad3-7d2d42d2c42a',
+ name: 'Nicole Davidson',
+ age: 22,
+ startDate: new Date('11/1/2019'),
+ department: DEPARTMENTS[2].name,
+ jobTitle: JOB_TITLES['Engineering'][0].name,
+ },
+ {
+ id: 'a8456cf4-4f8d-40ee-a91a-ece9c2327fe4',
+ name: 'Carl Roberts',
+ age: 23,
+ startDate: new Date('11/1/2019'),
+ department: DEPARTMENTS[2].name,
+ jobTitle: JOB_TITLES['Engineering'][3].name,
+ },
+];
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.html b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.html
new file mode 100644
index 0000000000..5cda4e6d63
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+ Mark inactive
+
+
+
+
+ More info
+
+
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.ts
new file mode 100644
index 0000000000..6e7e2fc4a5
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.component.ts
@@ -0,0 +1,129 @@
+import { I18nPluralPipe } from '@angular/common';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ inject,
+ model,
+ signal,
+} from '@angular/core';
+import {
+ SkyDataGridFilterValue,
+ SkyDataGridModule,
+ SkyDataGridRowDeleteCancelArgs,
+ SkyDataGridRowDeleteConfirmArgs,
+} from '@skyux/data-grid';
+import {
+ SkyDataManagerModule,
+ SkyDataManagerService,
+ SkyDataManagerState,
+} from '@skyux/data-manager';
+import {
+ SkyFilterBarFilterItem,
+ SkyFilterBarModule,
+ SkyFilterItemLookupSearchAsyncArgs,
+} from '@skyux/filter-bar';
+import { SkyListSummaryModule } from '@skyux/lists';
+import { SkyDropdownModule } from '@skyux/popovers';
+
+import { DATA_GRID_DEMO_DATA, DataGridDemoRow } from './data';
+import { ExampleService } from './example.service';
+import { SalesModalComponent } from './sales-modal.component';
+
+/**
+ * @title Data grid with data manager
+ */
+@Component({
+ selector: 'app-data-grid-data-manager-example',
+ templateUrl: './example.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [SkyDataManagerService],
+ imports: [
+ I18nPluralPipe,
+ SkyDataGridModule,
+ SkyDataManagerModule,
+ SkyDropdownModule,
+ SkyFilterBarModule,
+ SkyListSummaryModule,
+ ],
+})
+export class DataGridDataManagerExampleComponent {
+ protected readonly appliedFilters = model<
+ SkyFilterBarFilterItem[]
+ >([]);
+ protected readonly gridData = signal(DATA_GRID_DEMO_DATA);
+ protected readonly recordCount = model(DATA_GRID_DEMO_DATA.length);
+ protected readonly rowDeleteIds = model([]);
+ protected readonly salesModal = SalesModalComponent;
+ protected readonly viewId = 'dataGridWithDataManagerView' as const;
+
+ readonly #dataManagerSvc = inject(SkyDataManagerService);
+ readonly #exampleSvc = inject(ExampleService);
+
+ constructor() {
+ this.#dataManagerSvc.initDataManager({
+ activeViewId: this.viewId,
+ dataManagerConfig: {},
+ defaultDataState: new SkyDataManagerState({
+ filterData: {
+ filters: {},
+ },
+ views: [
+ {
+ viewId: this.viewId,
+ displayedColumnIds: [
+ 'context',
+ 'name',
+ 'age',
+ 'startDate',
+ 'endDate',
+ 'department',
+ 'jobTitle',
+ ],
+ },
+ ],
+ }),
+ });
+ this.#dataManagerSvc.initDataView({
+ id: this.viewId,
+ name: 'Data Grid View',
+ iconName: 'table',
+ searchEnabled: true,
+ columnPickerEnabled: true,
+ });
+ }
+
+ protected onJobTitleSearchAsync(
+ args: SkyFilterItemLookupSearchAsyncArgs,
+ ): void {
+ // In a real-world application the search service might return an Observable
+ // created by calling HttpClient.get(). Assigning that Observable to the result
+ // allows the lookup component to cancel the web request if it does not complete
+ // before the user searches again.
+ args.result = this.#exampleSvc.search(args.searchText);
+ }
+
+ public actionClicked(row: DataGridDemoRow, action: string): void {
+ if (action === 'Delete') {
+ this.rowDeleteIds.update((rowDeleteIds) => [
+ ...new Set([...rowDeleteIds, row.id]),
+ ]);
+ } else {
+ alert(`${action} clicked for ${row.name}`);
+ }
+ }
+
+ protected rowDeleteConfirm($event: SkyDataGridRowDeleteConfirmArgs): void {
+ this.gridData.update((gridData) =>
+ gridData.filter(({ id }) => id !== $event.id),
+ );
+ this.rowDeleteIds.update((rowDeleteIds) =>
+ rowDeleteIds.filter((id) => id !== $event.id),
+ );
+ }
+
+ protected rowDeleteCancel($event: SkyDataGridRowDeleteCancelArgs): void {
+ this.rowDeleteIds.update((rowDeleteIds) =>
+ rowDeleteIds.filter((id) => id !== $event.id),
+ );
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.service.ts b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.service.ts
new file mode 100644
index 0000000000..f091746772
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/example.service.ts
@@ -0,0 +1,32 @@
+import { Injectable } from '@angular/core';
+import { SkyFilterItemLookupSearchAsyncResult } from '@skyux/filter-bar';
+
+import { Observable, delay, of } from 'rxjs';
+
+import { JOB_TITLES } from './data';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ExampleService {
+ #jobs = Object.values(JOB_TITLES).flat();
+
+ public search(
+ searchText: string,
+ ): Observable {
+ searchText = searchText.toUpperCase();
+
+ const matchingJobs = this.#jobs
+ .filter((job) => job.name?.toUpperCase().includes(searchText))
+ .map((job) => Object.assign(`${job.name}`, job));
+
+ // Simulate a network call with latency. A real-world application might
+ // use Angular's HttpClient to create an Observable from a call to a
+ // web service.
+ return of({
+ hasMore: false,
+ items: matchingJobs,
+ totalCount: matchingJobs.length,
+ }).pipe(delay(800));
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.html b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.html
new file mode 100644
index 0000000000..8603b6d8f3
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ Apply
+
+
+ Cancel
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.ts
new file mode 100644
index 0000000000..4b859f66ea
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/data-manager/sales-modal.component.ts
@@ -0,0 +1,57 @@
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ inject,
+} from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ SkyFilterBarFilterValue,
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyCheckboxModule } from '@skyux/forms';
+import { SkyModalModule } from '@skyux/modals';
+
+@Component({
+ selector: 'app-sales-modal',
+ templateUrl: './sales-modal.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [FormsModule, SkyCheckboxModule, SkyModalModule],
+})
+export class SalesModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+
+ protected hideSales = false;
+ protected modalLabel: string;
+
+ readonly #changeDetectorRef = inject(ChangeDetectorRef);
+ readonly #context = this.modalInstance.context;
+
+ constructor() {
+ this.modalLabel = this.#context.filterLabelText;
+
+ if (this.#context.filterValue) {
+ this.hideSales = !!this.#context.filterValue.value;
+ }
+
+ this.#changeDetectorRef.markForCheck();
+ }
+
+ protected applyFilters(): void {
+ let result: SkyFilterBarFilterValue | undefined;
+
+ if (this.hideSales) {
+ result = {
+ value: 'Sales',
+ displayValue: 'True',
+ };
+ }
+
+ this.modalInstance.save({ filterValue: result });
+ }
+
+ protected cancel(): void {
+ this.modalInstance.cancel();
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/data.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/data.ts
new file mode 100644
index 0000000000..5640173be0
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/data.ts
@@ -0,0 +1,91 @@
+export interface Employee {
+ id: string;
+ name: string;
+ department: string;
+ salary: number;
+ startDate: string;
+ active: boolean;
+}
+
+export const employees = [
+ {
+ id: '1',
+ name: 'Alice Johnson',
+ department: 'Engineering',
+ salary: 95000,
+ startDate: '2020-03-15',
+ active: true,
+ },
+ {
+ id: '2',
+ name: 'Bob Smith',
+ department: 'Sales',
+ salary: 72000,
+ startDate: '2019-07-22',
+ active: true,
+ },
+ {
+ id: '3',
+ name: 'Carol Williams',
+ department: 'Engineering',
+ salary: 110000,
+ startDate: '2018-01-10',
+ active: true,
+ },
+ {
+ id: '4',
+ name: 'David Brown',
+ department: 'Marketing',
+ salary: 65000,
+ startDate: '2021-09-01',
+ active: false,
+ },
+ {
+ id: '5',
+ name: 'Eva Martinez',
+ department: 'Engineering',
+ salary: 88000,
+ startDate: '2020-11-30',
+ active: true,
+ },
+ {
+ id: '6',
+ name: 'Frank Garcia',
+ department: 'Sales',
+ salary: 78000,
+ startDate: '2017-04-18',
+ active: true,
+ },
+ {
+ id: '7',
+ name: 'Grace Lee',
+ department: 'Marketing',
+ salary: 71000,
+ startDate: '2022-02-14',
+ active: true,
+ },
+ {
+ id: '8',
+ name: 'Henry Wilson',
+ department: 'Engineering',
+ salary: 125000,
+ startDate: '2016-08-05',
+ active: false,
+ },
+ {
+ id: '9',
+ name: 'Ivy Chen',
+ department: 'Sales',
+ salary: 82000,
+ startDate: '2019-12-20',
+ active: true,
+ },
+ {
+ id: '10',
+ name: 'Jack Taylor',
+ department: 'Marketing',
+ salary: 58000,
+ startDate: '2023-01-09',
+ active: true,
+ },
+];
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.html b/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.html
new file mode 100644
index 0000000000..1e559ec631
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ value | skyNumeric: { format: 'currency', iso: 'USD', truncate: false }
+ }}
+
+
+ {{
+ value | skyDate: 'mediumDate'
+ }}
+
+
+
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.ts
new file mode 100644
index 0000000000..7b6ff7430d
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/example.component.ts
@@ -0,0 +1,34 @@
+import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
+import { SkyNumericPipe } from '@skyux/core';
+import { SkyDataGridFilterValue, SkyDataGridModule } from '@skyux/data-grid';
+import { SkyDatePipe } from '@skyux/datetime';
+import { SkyFilterBarModule } from '@skyux/filter-bar';
+import { SkyFilterStateFilterItem } from '@skyux/lists';
+
+import { Employee, employees } from './data';
+import { HideInactiveFilterModalComponent } from './hide-inactive-filter-modal.component';
+import { NameFilterModalComponent } from './name-filter-modal.component';
+import { SalaryFilterModalComponent } from './salary-filter-modal.component';
+import { StartDateFilterModalComponent } from './start-date-filter-modal.component';
+
+/**
+ * @title Filtered data grid
+ */
+@Component({
+ selector: 'app-filtered-data-grid',
+ imports: [SkyDataGridModule, SkyFilterBarModule, SkyNumericPipe, SkyDatePipe],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ templateUrl: './example.component.html',
+})
+export class FilteredDataGridComponent {
+ protected readonly nameFilterModal = NameFilterModalComponent;
+ protected readonly salaryFilterModal = SalaryFilterModalComponent;
+ protected readonly hideInactiveModal = HideInactiveFilterModalComponent;
+ protected readonly startDateFilterModal = StartDateFilterModalComponent;
+
+ protected readonly appliedFilters = signal<
+ SkyFilterStateFilterItem[]
+ >([]);
+
+ protected readonly allEmployees: Employee[] = employees;
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/hide-inactive-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/hide-inactive-filter-modal.component.ts
new file mode 100644
index 0000000000..fc5ac9242e
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/hide-inactive-filter-modal.component.ts
@@ -0,0 +1,50 @@
+import { Component, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ SkyFilterBarFilterValue,
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyCheckboxModule } from '@skyux/forms';
+import { SkyModalModule } from '@skyux/modals';
+
+@Component({
+ selector: 'app-hide-inactive-filter-modal',
+ imports: [FormsModule, SkyCheckboxModule, SkyModalModule],
+ template: `
+
+
+
+
+
+
+ Apply
+
+
+ Cancel
+
+
+
+ `,
+})
+export class HideInactiveFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+
+ public filterLabelText = this.#context.filterLabelText;
+ public hideInactive = !!this.#context.filterValue?.value;
+
+ public apply(): void {
+ const filterValue: SkyFilterBarFilterValue | undefined = this.hideInactive
+ ? { value: true, displayValue: 'Showing active only' }
+ : undefined;
+ this.modalInstance.save({ filterValue });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/name-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/name-filter-modal.component.ts
new file mode 100644
index 0000000000..57dcb25918
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/name-filter-modal.component.ts
@@ -0,0 +1,52 @@
+import { Component, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ SkyFilterBarFilterValue,
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyInputBoxModule } from '@skyux/forms';
+import { SkyModalModule } from '@skyux/modals';
+
+@Component({
+ selector: 'app-name-filter-modal',
+ imports: [FormsModule, SkyInputBoxModule, SkyModalModule],
+ template: `
+
+
+
+
+
+
+
+
+ Apply
+
+
+ Cancel
+
+
+
+ `,
+})
+export class NameFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+
+ public filterLabelText = this.#context.filterLabelText;
+ public nameFilter = (this.#context.filterValue?.value as string) ?? '';
+
+ public apply(): void {
+ const filterValue: SkyFilterBarFilterValue | undefined = this.nameFilter
+ ? {
+ value: this.nameFilter,
+ displayValue: `Name contains "${this.nameFilter}"`,
+ }
+ : undefined;
+ this.modalInstance.save({ filterValue });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/salary-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/salary-filter-modal.component.ts
new file mode 100644
index 0000000000..3675432045
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/salary-filter-modal.component.ts
@@ -0,0 +1,146 @@
+import { Component, inject } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
+import {
+ SkyAutonumericModule,
+ SkyAutonumericOptions,
+} from '@skyux/autonumeric';
+import { SkyNumericService } from '@skyux/core';
+import {
+ SkyDataGridNumberRangeFilterFormGroup,
+ SkyDataGridNumberRangeFilterValue,
+} from '@skyux/data-grid';
+import {
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyInputBoxModule } from '@skyux/forms';
+import { SkyModalError, SkyModalModule } from '@skyux/modals';
+
+import { map } from 'rxjs/operators';
+
+@Component({
+ selector: 'app-salary-filter-modal',
+ imports: [
+ ReactiveFormsModule,
+ SkyInputBoxModule,
+ SkyModalModule,
+ SkyAutonumericModule,
+ ],
+ template: `
+
+ `,
+})
+export class SalaryFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+ readonly #fb = inject(FormBuilder);
+ readonly #existingValue = this.#context.filterValue?.value as
+ | SkyDataGridNumberRangeFilterValue
+ | undefined;
+ readonly #numeric = inject(SkyNumericService);
+
+ public filterLabelText = this.#context.filterLabelText;
+
+ protected readonly form = this.#fb.group(
+ {
+ from: this.#fb.control(this.#existingValue?.from ?? null),
+ to: this.#fb.control(this.#existingValue?.to ?? null),
+ },
+ {
+ validators: (formGroup: SkyDataGridNumberRangeFilterFormGroup) => {
+ const min = formGroup.controls.from.value;
+ const max = formGroup.controls.to.value;
+ // At least one value must be provided.
+ if (min === null && max === null) {
+ return {
+ salaryRangeRequired: { message: 'Salary Range required' },
+ };
+ }
+ // If both values are provided, min must be less than or equal to max.
+ if (min !== null && max !== null && min >= max) {
+ return { salaryRangeInvalid: { message: 'Salary Range Invalid' } };
+ }
+ return null;
+ },
+ updateOn: 'change',
+ },
+ ) as SkyDataGridNumberRangeFilterFormGroup;
+
+ protected readonly formErrors = toSignal(
+ this.form.statusChanges.pipe(
+ map(() => Object.values(this.form.errors ?? {}) as SkyModalError[]),
+ ),
+ );
+ protected autonumericOptions: SkyAutonumericOptions = 'dollarPos' as const;
+
+ public apply(): void {
+ if (this.form.valid) {
+ const value = {
+ from: this.form.controls.from.value,
+ to: this.form.controls.to.value,
+ } as SkyDataGridNumberRangeFilterValue;
+ let displayValue = '';
+ if (value.from && value.to) {
+ displayValue = `${this.#format(value.from)} - ${this.#format(value.to)}`;
+ } else if (value.from) {
+ displayValue = `From ${this.#format(value.from)}`;
+ } else if (value.to) {
+ displayValue = `Up to ${this.#format(value.to)}`;
+ }
+ this.modalInstance.save({ filterValue: { value, displayValue } });
+ } else {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+ }
+
+ public clear(): void {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+
+ #format(value: number): string {
+ return this.#numeric.formatNumber(value, { format: 'currency' });
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/filtered/start-date-filter-modal.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/filtered/start-date-filter-modal.component.ts
new file mode 100644
index 0000000000..156fdcbf52
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/filtered/start-date-filter-modal.component.ts
@@ -0,0 +1,91 @@
+import { Component, inject } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ SkyDateRangeCalculation,
+ SkyDateRangeCalculatorId,
+ SkyDateRangePickerModule,
+} from '@skyux/datetime';
+import {
+ SkyFilterBarFilterValue,
+ SkyFilterItemModal,
+ SkyFilterItemModalInstance,
+} from '@skyux/filter-bar';
+import { SkyModalModule } from '@skyux/modals';
+
+@Component({
+ selector: 'app-start-date-filter-modal',
+ imports: [FormsModule, SkyDateRangePickerModule, SkyModalModule],
+ template: `
+
+
+
+
+
+
+ Apply
+
+
+ Clear
+
+
+ Cancel
+
+
+
+ `,
+})
+export class StartDateFilterModalComponent implements SkyFilterItemModal {
+ public readonly modalInstance = inject(SkyFilterItemModalInstance);
+ readonly #context = this.modalInstance.context;
+
+ public filterLabelText = this.#context.filterLabelText;
+ public dateRange: SkyDateRangeCalculation | undefined;
+
+ constructor() {
+ const existingValue = this.#context.filterValue?.value as
+ | SkyDateRangeCalculation
+ | undefined;
+ if (existingValue) {
+ this.dateRange = existingValue;
+ }
+ }
+
+ public apply(): void {
+ if (
+ this.dateRange &&
+ this.dateRange.calculatorId !== SkyDateRangeCalculatorId.AnyTime
+ ) {
+ const filterValue: SkyFilterBarFilterValue = {
+ value: this.dateRange,
+ displayValue: this.#formatDateRange(this.dateRange),
+ };
+ this.modalInstance.save({ filterValue });
+ } else {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+ }
+
+ public clear(): void {
+ this.modalInstance.save({ filterValue: undefined });
+ }
+
+ public cancel(): void {
+ this.modalInstance.cancel();
+ }
+
+ #formatDateRange(range: SkyDateRangeCalculation): string {
+ if (range.startDate && range.endDate) {
+ const start = new Date(range.startDate).toLocaleDateString();
+ const end = new Date(range.endDate).toLocaleDateString();
+ return `${start} - ${end}`;
+ } else if (range.startDate) {
+ return `After ${new Date(range.startDate).toLocaleDateString()}`;
+ } else if (range.endDate) {
+ return `Before ${new Date(range.endDate).toLocaleDateString()}`;
+ }
+ return 'Date range applied';
+ }
+}
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/paging/data.ts b/libs/components/code-examples/src/lib/modules/data-grid/paging/data.ts
new file mode 100644
index 0000000000..62f4f7152e
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/paging/data.ts
@@ -0,0 +1,146 @@
+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 const DATA_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/data-grid/paging/example.component.html b/libs/components/code-examples/src/lib/modules/data-grid/paging/example.component.html
new file mode 100644
index 0000000000..10f69c7d8c
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/paging/example.component.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ {{ value?.name ?? '' }}
+
+
+ {{ value?.name ?? '' }}
+
+
diff --git a/libs/components/code-examples/src/lib/modules/data-grid/paging/example.component.ts b/libs/components/code-examples/src/lib/modules/data-grid/paging/example.component.ts
new file mode 100644
index 0000000000..414309a23c
--- /dev/null
+++ b/libs/components/code-examples/src/lib/modules/data-grid/paging/example.component.ts
@@ -0,0 +1,22 @@
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { SkyDataGridModule } from '@skyux/data-grid';
+
+import { DATA_GRID_DEMO_DATA } from './data';
+
+/**
+ * @title Data grid with paging using router query parameters
+ */
+@Component({
+ selector: 'app-data-grid-paging',
+ imports: [SkyDataGridModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ templateUrl: './example.component.html',
+})
+export class DataGridPagingComponent {
+ protected readonly data = DATA_GRID_DEMO_DATA;
+
+ // For demo purposes, only use the query string if we're running the demo in its own SPA as a route and not on the documentation site.
+ protected readonly pageQueryParam =
+ inject(ActivatedRoute).component === DataGridPagingComponent ? 'page' : '';
+}
diff --git a/libs/components/data-grid/README.md b/libs/components/data-grid/README.md
new file mode 100644
index 0000000000..8a47d7d2eb
--- /dev/null
+++ b/libs/components/data-grid/README.md
@@ -0,0 +1,3 @@
+# data-grid
+
+This library was generated with [Nx](https://nx.dev).
diff --git a/libs/components/data-grid/documentation.json b/libs/components/data-grid/documentation.json
new file mode 100644
index 0000000000..a12edb2958
--- /dev/null
+++ b/libs/components/data-grid/documentation.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "../manifest-generator/documentation-schema.json",
+ "groups": {
+ "data-grid-package": {
+ "development": {
+ "docsIds": [
+ "SkyDataGridModule",
+ "SkyDataGridComponent",
+ "SkyDataGridColumnComponent",
+ "SkyDataGridFilterOperator",
+ "SkyDataGridFilterValue",
+ "SkyDataGridNumberRangeFilterValue",
+ "SkyDataGridPageRequest",
+ "SkyDataGridRowDeleteCancelArgs",
+ "SkyDataGridRowDeleteConfirmArgs",
+ "SkyDataGridSort"
+ ],
+ "primaryDocsId": "SkyDataGridModule"
+ },
+ "testing": {
+ "docsIds": ["SkyDataGridHarness", "SkyDataGridHarnessFilters"]
+ },
+ "codeExamples": {
+ "docsIds": [
+ "DataGridBasicExampleComponent",
+ "FilteredDataGridComponent",
+ "DataGridPagingComponent",
+ "DataGridDataManagerExampleComponent",
+ "DataGridAsynchronousDataComponent"
+ ]
+ }
+ }
+ }
+}
diff --git a/libs/components/data-grid/eslint.config.js b/libs/components/data-grid/eslint.config.js
new file mode 100644
index 0000000000..9aa0870e02
--- /dev/null
+++ b/libs/components/data-grid/eslint.config.js
@@ -0,0 +1,3 @@
+const config = require('../../../eslint-libs.config');
+
+module.exports = config;
diff --git a/libs/components/data-grid/karma.conf.js b/libs/components/data-grid/karma.conf.js
new file mode 100644
index 0000000000..b4b6c9cbde
--- /dev/null
+++ b/libs/components/data-grid/karma.conf.js
@@ -0,0 +1,16 @@
+// 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/data-grid'),
+ },
+ });
+};
diff --git a/libs/components/data-grid/ng-package.json b/libs/components/data-grid/ng-package.json
new file mode 100644
index 0000000000..5960c389b8
--- /dev/null
+++ b/libs/components/data-grid/ng-package.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
+ "dest": "../../../dist/libs/components/data-grid",
+ "lib": {
+ "entryFile": "src/index.ts",
+ "styleIncludePaths": ["../../.."]
+ },
+ "inlineStyleLanguage": "scss",
+ "allowedNonPeerDependencies": ["@skyux/icon"]
+}
diff --git a/libs/components/data-grid/package.json b/libs/components/data-grid/package.json
new file mode 100644
index 0000000000..0e45f3e227
--- /dev/null
+++ b/libs/components/data-grid/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@skyux/data-grid",
+ "version": "0.0.0-PLACEHOLDER",
+ "author": "Blackbaud, Inc.",
+ "keywords": [
+ "blackbaud",
+ "skyux"
+ ],
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/blackbaud/skyux.git"
+ },
+ "bugs": {
+ "url": "https://github.com/blackbaud/skyux/issues"
+ },
+ "homepage": "https://github.com/blackbaud/skyux#readme",
+ "peerDependencies": {
+ "@angular/cdk": "^20.2.14",
+ "@angular/common": "^20.3.15",
+ "@angular/core": "^20.3.15",
+ "@angular/forms": "^20.3.15",
+ "@angular/router": "^20.3.15",
+ "@skyux/ag-grid": "^0.0.0-PLACEHOLDER",
+ "@skyux/core": "^0.0.0-PLACEHOLDER",
+ "@skyux/data-manager": "^0.0.0-PLACEHOLDER",
+ "@skyux/datetime": "^0.0.0-PLACEHOLDER",
+ "@skyux/help-inline": "^0.0.0-PLACEHOLDER",
+ "@skyux/indicators": "^0.0.0-PLACEHOLDER",
+ "@skyux/lists": "^0.0.0-PLACEHOLDER",
+ "ag-grid-angular": "^34.3.1",
+ "ag-grid-community": "^34.3.1"
+ },
+ "sideEffects": false
+}
diff --git a/libs/components/data-grid/project.json b/libs/components/data-grid/project.json
new file mode 100644
index 0000000000..a151de2498
--- /dev/null
+++ b/libs/components/data-grid/project.json
@@ -0,0 +1,81 @@
+{
+ "name": "data-grid",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "library",
+ "sourceRoot": "libs/components/data-grid/src",
+ "prefix": "sky",
+ "tags": ["component", "npm"],
+ "targets": {
+ "build": {
+ "executor": "@nx/angular:package",
+ "outputs": ["{workspaceRoot}/dist/{projectRoot}"],
+ "options": {
+ "project": "libs/components/data-grid/ng-package.json"
+ },
+ "configurations": {
+ "production": {
+ "tsConfig": "libs/components/data-grid/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "libs/components/data-grid/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production",
+ "dependsOn": [
+ "^build",
+ {
+ "projects": ["ag-grid", "core"],
+ "target": "build"
+ }
+ ],
+ "inputs": [
+ "buildInputs",
+ "^buildInputs",
+ "{workspaceRoot}/libs/components/data-grid/testing/src/**/*",
+ "!{workspaceRoot}/libs/components/data-grid/testing/src/**/*.spec.ts",
+ "!{workspaceRoot}/libs/components/data-grid/testing/src/**/fixtures/**/*"
+ ]
+ },
+ "test": {
+ "executor": "@angular-devkit/build-angular:karma",
+ "options": {
+ "tsConfig": "libs/components/data-grid/tsconfig.spec.json",
+ "karmaConfig": "libs/components/data-grid/karma.conf.js",
+ "styles": [
+ "libs/components/theme/src/lib/styles/sky.scss",
+ "libs/components/theme/src/lib/styles/themes/modern/styles.scss",
+ "libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss"
+ ],
+ "codeCoverage": true,
+ "codeCoverageExclude": ["**/fixtures/**"],
+ "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",
+ "{projectRoot}/src/**/*.html"
+ ]
+ }
+ }
+ }
+}
diff --git a/libs/components/data-grid/src/index.ts b/libs/components/data-grid/src/index.ts
new file mode 100644
index 0000000000..10048390c4
--- /dev/null
+++ b/libs/components/data-grid/src/index.ts
@@ -0,0 +1,13 @@
+export { SkyDataGridComponent } from './lib/modules/data-grid/data-grid.component';
+export { SkyDataGridColumnComponent } from './lib/modules/data-grid/data-grid-column.component';
+export { SkyDataGridModule } from './lib/modules/data-grid/data-grid.module';
+export { SkyDataGridFilterOperator } from './lib/modules/types/data-grid-filter-operator';
+export { SkyDataGridFilterValue } from './lib/modules/types/data-grid-filter-value';
+export {
+ SkyDataGridNumberRangeFilterValue,
+ SkyDataGridNumberRangeFilterFormGroup,
+} from './lib/modules/types/data-grid-number-range-filter-value';
+export { SkyDataGridSort } from './lib/modules/types/data-grid-sort';
+export { SkyDataGridPageRequest } from './lib/modules/types/data-grid-page-request';
+export { SkyDataGridRowDeleteCancelArgs } from './lib/modules/types/data-grid-row-delete-cancel-args';
+export { SkyDataGridRowDeleteConfirmArgs } from './lib/modules/types/data-grid-row-delete-confirm-args';
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column-inline-help.component.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column-inline-help.component.ts
new file mode 100644
index 0000000000..a327fc0095
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column-inline-help.component.ts
@@ -0,0 +1,36 @@
+import { Component, computed, inject } from '@angular/core';
+import { SkyAgGridHeaderInfo } from '@skyux/ag-grid';
+import { SkyHelpInlineModule } from '@skyux/help-inline';
+
+/**
+ * @internal
+ */
+@Component({
+ selector: 'sky-data-grid-column-inline-help',
+ imports: [SkyHelpInlineModule],
+ template: `@if (showHelpInline()) {
+
+ }`,
+})
+export class SkyDataGridColumnInlineHelpComponent {
+ protected readonly info = inject(SkyAgGridHeaderInfo);
+
+ protected readonly showHelpInline = computed(
+ () => !!this.helpPopoverContent(),
+ );
+
+ protected readonly helpPopoverTitle = computed(
+ () => this.#headerComponentParams().helpPopoverTitle,
+ );
+
+ protected readonly helpPopoverContent = computed(
+ () => this.#headerComponentParams().helpPopoverContent,
+ );
+
+ readonly #headerComponentParams = computed(
+ () => this.info.column?.getColDef().headerComponentParams,
+ );
+}
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column.component.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column.component.ts
new file mode 100644
index 0000000000..b93087cf75
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-column.component.ts
@@ -0,0 +1,189 @@
+import {
+ Component,
+ TemplateRef,
+ booleanAttribute,
+ computed,
+ contentChild,
+ effect,
+ inject,
+ input,
+ numberAttribute,
+} from '@angular/core';
+import { SkyLogService } from '@skyux/core';
+
+import { SkyDataGridFilterOperator } from '../types/data-grid-filter-operator';
+
+/**
+ * Specifies the column information.
+ * @preview
+ */
+@Component({
+ selector: 'sky-data-grid-column',
+ template: '',
+})
+export class SkyDataGridColumnComponent {
+ /**
+ * The unique ID for the column. You must provide either the `columnId` or `field` property
+ * for every column, but do not provide both. Use `columnId` when the column does not map directly to a field
+ * in the data set.
+ */
+ public readonly columnId = input();
+
+ /**
+ * The data type of the column used for filtering, sorting, and rendering when a template is not provided.
+ * @default 'text'
+ */
+ public readonly dataType = input<'text' | 'number' | 'date' | 'boolean'>(
+ 'text',
+ );
+
+ /**
+ * The description for the column.
+ */
+ public readonly description = input();
+
+ /**
+ * The property to retrieve cell information from an entry on the grid `data` array.
+ * You must provide either the `columnId` or `field` property for every column,
+ * but do not provide both. If `columnId` does not exist on a column, then `field` is the entry
+ * for the grid `selectedColumnIds` array.
+ */
+ public readonly field = input();
+
+ /**
+ * When set, `flexWidth` overrides `width` and works like
+ * [CSS flex-grow](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/flex-grow), where a column
+ * with `flexWidth="2"` is twice the width of a column with `flexWidth="1"`, and `flexWidth="0"` does not auto-expand.
+ */
+ public readonly flexWidth = input(-1, {
+ transform: (value) => numberAttribute(value, -1),
+ });
+
+ /**
+ * Text to display in the column header.
+ */
+ public readonly headingText = input.required();
+
+ /**
+ * Whether to prevent `heading` text from being visibly displayed.
+ * @default false
+ */
+ public readonly headingHidden = input(false, {
+ transform: booleanAttribute,
+ });
+
+ /**
+ * 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>();
+
+ /**
+ * Whether the column is initially hidden when grid `selectedColumnIds` are not provided.
+ * @default false
+ */
+ public readonly hidden = input(false, {
+ transform: booleanAttribute,
+ });
+
+ /**
+ * Whether the column can be resized by dragging the column header border.
+ * @default true
+ */
+ public readonly resizable = input(true);
+
+ /**
+ * Whether the column sorts the grid when users click the column header.
+ * @default true
+ */
+ public readonly sortable = input(true);
+
+ /**
+ * Whether the column is locked. The intent is to display locked columns first
+ * on the left side of the grid. If set to `true`, then users cannot drag the column
+ * to another position and or drag other columns before the locked column.
+ * @default false
+ */
+ public readonly locked = input(false, {
+ transform: booleanAttribute,
+ });
+
+ /**
+ * The template for a column. This can be assigned as a reference
+ * to the `template` attribute, or it can be assigned as a child of the `template` element
+ * inside the `sky-grid-column` component. The template has access to the `value` variable,
+ * which contains the value passed to the column, and the `row` variable, which contains
+ * the entire row data.
+ */
+ public readonly template = input>();
+
+ /**
+ * The width of the column in pixels. When no width is set, the column width is evenly distributed.
+ */
+ public readonly width = input(0, {
+ transform: (value) => numberAttribute(value, 0),
+ });
+
+ /**
+ * Whether text in this column should wrap to multiple lines. For best performance, large grids should set a `height`
+ * and not enable `wrapText` on any column so that rows can be virtually drawn as needed. Not setting a `height` or
+ * enabling `wrapText` forces the grid to draw every row in order to determine the scroll height.
+ * @default false
+ */
+ public readonly wrapText = input(false, {
+ transform: booleanAttribute,
+ });
+
+ /**
+ * The filter ID that maps this column to a filter bar item.
+ * When set, changes to the corresponding filter will apply to this column.
+ * Defaults to the column's `field` value.
+ */
+ public readonly filterId = input();
+
+ /**
+ * The filter operator to use when applying the filter.
+ *
+ * Defaults based on column type:
+ *
+ * - text: 'contains'
+ * - number: 'equals'
+ * - date: 'equals'
+ */
+ public readonly filterOperator = input<
+ SkyDataGridFilterOperator | undefined
+ >();
+
+ protected readonly templateChild = contentChild(TemplateRef);
+
+ constructor() {
+ const logger = inject(SkyLogService);
+ effect(() => {
+ const columnId = this.columnId();
+ const field = this.field();
+ if (columnId && field) {
+ logger.warn(
+ `A should have either a columnId or a field, but not both.`,
+ );
+ }
+ });
+ }
+
+ /**
+ * @internal
+ */
+ public readonly cellTemplate = computed | undefined>(
+ () => {
+ const template = this.template();
+ const templateChild = this.templateChild();
+ return template || templateChild;
+ },
+ );
+}
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.spec.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.spec.ts
new file mode 100644
index 0000000000..65c9203784
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.spec.ts
@@ -0,0 +1,787 @@
+import { TestBed } from '@angular/core/testing';
+import { SkyLogService } from '@skyux/core';
+import { SkyDateRange } from '@skyux/datetime';
+import { SkyFilterStateFilterItem } from '@skyux/lists';
+
+import { SkyDataGridFilterOperator } from '../types/data-grid-filter-operator';
+import { SkyDataGridFilterValue } from '../types/data-grid-filter-value';
+import { SkyDataGridNumberRangeFilterValue } from '../types/data-grid-number-range-filter-value';
+
+import { doesFilterPass } from './data-grid-filter';
+
+interface TestDataRow {
+ id: string;
+ test?: string;
+ text?: string;
+ num?: number;
+ date?: Date | string;
+ bool?: boolean;
+}
+
+interface TestColumnFilter {
+ filterId: string | undefined;
+ field: keyof TestDataRow | undefined;
+ filterOperator: SkyDataGridFilterOperator | undefined;
+ type: 'text' | 'number' | 'date' | 'boolean';
+}
+
+/**
+ * Helper function to call doesFilterPass with consistent typing.
+ */
+function testFilterPass(
+ filters: SkyFilterStateFilterItem[],
+ data: TestDataRow | undefined,
+ columns: TestColumnFilter[],
+ logger: SkyLogService,
+): boolean {
+ return doesFilterPass(
+ filters,
+ data as TestDataRow,
+ columns,
+ logger,
+ );
+}
+
+describe('data-grid-filter', () => {
+ let logService: SkyLogService;
+ let logSpy: jasmine.Spy;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ logService = TestBed.inject(SkyLogService);
+ logSpy = spyOn(logService, 'warn');
+ });
+
+ it('should apply filters for data grid', () => {
+ expect(
+ testFilterPass(
+ [{ filterId: 'test', filterValue: { value: 'example' } }],
+ { id: '1', test: 'example' },
+ [
+ {
+ filterId: undefined,
+ field: 'test',
+ filterOperator: 'contains',
+ type: 'text',
+ },
+ ],
+ logService,
+ ),
+ ).toBeTrue();
+ expect(logSpy).not.toHaveBeenCalled();
+ });
+
+ it('should pass if column is not found', () => {
+ expect(
+ testFilterPass(
+ [{ filterId: 'missing', filterValue: { value: 'example' } }],
+ { id: '1', test: 'example' },
+ [
+ {
+ filterId: 'test',
+ field: 'test',
+ filterOperator: 'contains',
+ type: 'text',
+ },
+ ],
+ logService,
+ ),
+ ).toBeTrue();
+ });
+
+ it('should pass if filter value is undefined', () => {
+ expect(
+ testFilterPass(
+ [
+ {
+ filterId: 'test',
+ filterValue: { value: undefined as unknown as string },
+ },
+ ],
+ { id: '1', test: 'example' },
+ [
+ {
+ filterId: 'test',
+ field: 'test',
+ filterOperator: 'contains',
+ type: 'text',
+ },
+ ],
+ logService,
+ ),
+ ).toBeTrue();
+ });
+
+ it('should not pass if data is undefined', () => {
+ expect(
+ testFilterPass(
+ [{ filterId: 'test', filterValue: { value: 'example' } }],
+ undefined,
+ [
+ {
+ filterId: 'test',
+ field: 'test',
+ filterOperator: 'contains',
+ type: 'text',
+ },
+ ],
+ logService,
+ ),
+ ).toBeFalse();
+ });
+
+ it('should not pass if data field is undefined', () => {
+ expect(
+ testFilterPass(
+ [{ filterId: 'test', filterValue: { value: 'example' } }],
+ { id: '1', test: undefined },
+ [
+ {
+ filterId: 'test',
+ field: 'test',
+ filterOperator: 'contains',
+ type: 'text',
+ },
+ ],
+ logService,
+ ),
+ ).toBeFalse();
+ });
+
+ describe('text filters', () => {
+ function createTextColumn(
+ operator: SkyDataGridFilterOperator | undefined,
+ ): TestColumnFilter[] {
+ return [
+ {
+ filterId: 'text',
+ field: 'text',
+ filterOperator: operator,
+ type: 'text',
+ },
+ ];
+ }
+
+ it('should filter with "equals"', () => {
+ const cols = createTextColumn('equals');
+ // Match
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'abc' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ // No match
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'abc' } }],
+ { id: '1', text: 'def' },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ // Case insensitive
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'ABC' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ });
+
+ it('should filter with "notEqual"', () => {
+ const cols = createTextColumn('notEqual');
+ // Match (fail)
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'abc' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ // No match (pass)
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'abc' } }],
+ { id: '1', text: 'def' },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ });
+
+ it('should filter with "contains"', () => {
+ const cols = createTextColumn('contains');
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'bc' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'de' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ });
+
+ it('should filter with "notContains"', () => {
+ const cols = createTextColumn('notContains');
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'bc' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'de' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ });
+
+ it('should filter with "startsWith"', () => {
+ const cols = createTextColumn('startsWith');
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'ab' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'bc' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ });
+
+ it('should filter with "endsWith"', () => {
+ const cols = createTextColumn('endsWith');
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'bc' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'ab' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ });
+
+ it('should handle undefined/null values gracefully', () => {
+ const cols = createTextColumn('contains');
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'null' } }],
+ { id: '1', text: 'null' },
+ cols,
+ logService,
+ ),
+ ).toBeTrue(); // 'null' string matches null
+ });
+
+ it('should warn and pass for unsupported operator', () => {
+ const cols = createTextColumn(
+ 'unknown' as unknown as SkyDataGridFilterOperator,
+ );
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: 'abc' } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(logSpy).toHaveBeenCalledWith(
+ 'Unsupported text filter operator: unknown',
+ );
+ });
+
+ it('should filter with array values (OR logic)', () => {
+ const cols = createTextColumn('equals');
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: ['abc', 'xyz'] } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+
+ expect(
+ testFilterPass(
+ [{ filterId: 'text', filterValue: { value: ['xyz', 'uvw'] } }],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ });
+
+ it('should support "val" property for filter value', () => {
+ const cols = createTextColumn('equals');
+ expect(
+ testFilterPass(
+ [
+ { filterId: 'text', filterValue: { val: 'abc' } },
+ ] as unknown as SkyFilterStateFilterItem[],
+ { id: '1', text: 'abc' },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ });
+ });
+
+ describe('number filters', () => {
+ function createNumColumn(
+ operator: SkyDataGridFilterOperator | undefined,
+ ): TestColumnFilter[] {
+ return [
+ {
+ filterId: 'num',
+ field: 'num',
+ filterOperator: operator,
+ type: 'number',
+ },
+ ];
+ }
+
+ it('should filter single values with basic operators', () => {
+ // equals
+ let cols = createNumColumn('equals');
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 10 },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 11 },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+
+ // notEqual
+ cols = createNumColumn('notEqual');
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 11 },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 10 },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+
+ // lessThan
+ cols = createNumColumn('lessThan');
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 9 },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 10 },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+
+ // lessThanOrEqual
+ cols = createNumColumn('lessThanOrEqual');
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 10 },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 11 },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+
+ // greaterThan
+ cols = createNumColumn('greaterThan');
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 11 },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 10 },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+
+ // greaterThanOrEqual
+ cols = createNumColumn('greaterThanOrEqual');
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 10 },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 9 },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ });
+
+ it('should filter number ranges', () => {
+ const cols = createNumColumn(undefined);
+ const rangeFilter = (
+ from: number | null,
+ to: number | null,
+ ): { value: SkyDataGridNumberRangeFilterValue } => ({
+ value: { from, to } as SkyDataGridNumberRangeFilterValue,
+ });
+ // In range
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: rangeFilter(5, 15) }],
+ { id: '1', num: 10 },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ // Out range (low)
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: rangeFilter(5, 15) }],
+ { id: '1', num: 4 },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ // Out range (high)
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: rangeFilter(5, 15) }],
+ { id: '1', num: 16 },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ // Open ended (from)
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: rangeFilter(5, null) }],
+ { id: '1', num: 6 },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: rangeFilter(5, null) }],
+ { id: '1', num: 4 },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ // Open ended (to)
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: rangeFilter(null, 15) }],
+ { id: '1', num: 14 },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: rangeFilter(null, 15) }],
+ { id: '1', num: 16 },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ });
+
+ it('should warn and pass for unsupported operator', () => {
+ const cols = createNumColumn(
+ 'unknown' as unknown as SkyDataGridFilterOperator,
+ );
+ expect(
+ testFilterPass(
+ [{ filterId: 'num', filterValue: { value: 10 } }],
+ { id: '1', num: 10 },
+ cols,
+ logService,
+ ),
+ ).toBeTrue(); // defaults to false in numericFilter but !false is true here?
+ // Wait, numericFilter default returns false. !false is true. Correct.
+ expect(logSpy).toHaveBeenCalledWith(
+ 'Unsupported number or date filter operator: unknown',
+ );
+ });
+ });
+
+ describe('date filters', () => {
+ function createDateColumn(
+ operator: SkyDataGridFilterOperator | undefined,
+ ): TestColumnFilter[] {
+ return [
+ {
+ filterId: 'date',
+ field: 'date',
+ filterOperator: operator,
+ type: 'date',
+ },
+ ];
+ }
+
+ function dateRangeFilter(
+ startDate: Date | undefined,
+ endDate?: Date,
+ ): { value: SkyDateRange } {
+ return { value: { startDate, endDate } as SkyDateRange };
+ }
+
+ it('should filter single dates', () => {
+ const date = new Date(2022, 1, 1);
+ const sameDate = new Date(2022, 1, 1, 12, 0); // same day, different time
+ const nextDate = new Date(2022, 1, 2);
+
+ // equals
+ const cols = createDateColumn('equals');
+ expect(
+ testFilterPass(
+ [{ filterId: 'date', filterValue: { value: date } }],
+ { id: '1', date: sameDate },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'date', filterValue: { value: date } }],
+ { id: '1', date: nextDate },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+
+ // date string handling
+ expect(
+ testFilterPass(
+ [{ filterId: 'date', filterValue: { value: '2022-02-01' } }], // Note: Timezone sensitive if not careful, but zeroHour uses local parts
+ { id: '1', date: date }, // Date in local time
+ cols,
+ logService,
+ ),
+ ).toBeTrue(); // assuming '2022-02-01' parses to same local day or constructed similarly
+ });
+
+ it('should filter date ranges', () => {
+ const cols = createDateColumn(undefined);
+ const start = new Date(2022, 1, 1);
+ const end = new Date(2022, 1, 10);
+ const mid = new Date(2022, 1, 5);
+ const midDay = new Date(2022, 1, 5, 12, 0, 0);
+ const endDay = new Date(2022, 1, 10, 23, 59, 59);
+
+ const before = new Date(2022, 0, 1);
+ const after = new Date(2022, 2, 1);
+
+ // In range
+ expect(
+ testFilterPass(
+ [{ filterId: 'date', filterValue: dateRangeFilter(start, end) }],
+ { id: '1', date: mid },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+
+ // Mid day should pass
+ expect(
+ testFilterPass(
+ [{ filterId: 'date', filterValue: dateRangeFilter(start, end) }],
+ { id: '1', date: midDay },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+
+ // End day (end of day) should pass
+ expect(
+ testFilterPass(
+ [{ filterId: 'date', filterValue: dateRangeFilter(start, end) }],
+ { id: '1', date: endDay },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+
+ // Out range
+ expect(
+ testFilterPass(
+ [{ filterId: 'date', filterValue: dateRangeFilter(start, end) }],
+ { id: '1', date: before },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ expect(
+ testFilterPass(
+ [{ filterId: 'date', filterValue: dateRangeFilter(start, end) }],
+ { id: '1', date: after },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+
+ // Open ended
+ expect(
+ testFilterPass(
+ [{ filterId: 'date', filterValue: dateRangeFilter(start) }],
+ { id: '1', date: mid },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'date', filterValue: dateRangeFilter(start) }],
+ { id: '1', date: before },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ });
+ });
+
+ describe('boolean filters', () => {
+ function createBoolColumn(
+ operator: SkyDataGridFilterOperator | undefined,
+ ): TestColumnFilter[] {
+ return [
+ {
+ filterId: 'bool',
+ field: 'bool',
+ filterOperator: operator,
+ type: 'boolean',
+ },
+ ];
+ }
+
+ it('should filter boolean values', () => {
+ // equals
+ let cols = createBoolColumn('equals');
+ expect(
+ testFilterPass(
+ [{ filterId: 'bool', filterValue: { value: true } }],
+ { id: '1', bool: true },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'bool', filterValue: { value: true } }],
+ { id: '1', bool: false },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+
+ // notEqual
+ cols = createBoolColumn('notEqual');
+ expect(
+ testFilterPass(
+ [{ filterId: 'bool', filterValue: { value: true } }],
+ { id: '1', bool: false },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(
+ testFilterPass(
+ [{ filterId: 'bool', filterValue: { value: true } }],
+ { id: '1', bool: true },
+ cols,
+ logService,
+ ),
+ ).toBeFalse();
+ });
+
+ it('should warn for unsupported boolean operator', () => {
+ const cols = createBoolColumn('contains');
+ expect(
+ testFilterPass(
+ [{ filterId: 'bool', filterValue: { value: true } }],
+ { id: '1', bool: true },
+ cols,
+ logService,
+ ),
+ ).toBeTrue();
+ expect(logSpy).toHaveBeenCalledWith(
+ 'Unsupported boolean filter operator: contains',
+ );
+ });
+ });
+});
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.ts
new file mode 100644
index 0000000000..06a647fe96
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid-filter.ts
@@ -0,0 +1,287 @@
+import { SkyLogService } from '@skyux/core';
+import { SkyDateRange } from '@skyux/datetime';
+import { SkyFilterStateFilterItem } from '@skyux/lists';
+
+import { SkyDataGridFilterOperator } from '../types/data-grid-filter-operator';
+import { SkyDataGridFilterValue } from '../types/data-grid-filter-value';
+import { SkyDataGridNumberRangeFilterValue } from '../types/data-grid-number-range-filter-value';
+
+interface SkyDataGridColumnFilter<
+ T extends Record<'id', string> = Record<'id', string> &
+ Record,
+> {
+ filterId: string | undefined;
+ field: keyof T | undefined;
+ filterOperator: SkyDataGridFilterOperator | undefined;
+ type: 'text' | 'number' | 'date' | 'boolean';
+}
+
+/**
+ * @internal
+ */
+export function doesFilterPass<
+ T extends Record<'id', string> = Record<'id', string> &
+ Record,
+>(
+ filterItems: SkyFilterStateFilterItem[],
+ data: Record<'id', string> & Partial,
+ columns: readonly SkyDataGridColumnFilter[],
+ logger: SkyLogService,
+): boolean {
+ return filterItems.every((filterItem) =>
+ doesSingleFilterPass(filterItem, data, columns, logger),
+ );
+}
+
+function doesSingleFilterPass<
+ T extends Record<'id', string> = Record<'id', string> &
+ Record,
+>(
+ filter: SkyFilterStateFilterItem,
+ data: Record<'id', string> & Partial,
+ columns: readonly SkyDataGridColumnFilter[],
+ logger: SkyLogService,
+): boolean {
+ // Find column with matching filterId
+ let column = columns.find((col) => col.filterId === filter.filterId);
+ column ??= columns.find((col) => col.field === filter.filterId);
+ if (!column || filter.filterValue?.value === undefined) {
+ return true;
+ }
+
+ const rowValue =
+ column?.field && data && column?.field in data
+ ? data[column?.field as keyof T]
+ : undefined;
+ const filterValue = filter.filterValue.value as
+ | SkyDataGridFilterValue
+ | undefined;
+ const filterOperator = column?.filterOperator as
+ | SkyDataGridFilterOperator
+ | undefined;
+
+ switch (column?.type) {
+ case 'text':
+ return doesTextFilterPass(
+ filterOperator ?? 'contains',
+ filterValue,
+ String(rowValue ?? ''),
+ logger,
+ );
+ case 'number':
+ return doesNumericFilterPass(
+ filterValue as string | number | SkyDataGridNumberRangeFilterValue,
+ rowValue as string | number,
+ filterOperator,
+ logger,
+ );
+ case 'date':
+ return doesDateFilterPass(
+ filterValue as SkyDateRange | Date | string,
+ rowValue as Date | string,
+ filterOperator,
+ logger,
+ );
+ case 'boolean':
+ return doesBooleanFilterPass(
+ filterValue,
+ rowValue,
+ filterOperator,
+ logger,
+ );
+ }
+}
+
+function doesBooleanFilterPass(
+ filterValue: unknown,
+ rowValue: unknown,
+ filterOperator: SkyDataGridFilterOperator | undefined,
+ logger: SkyLogService,
+): boolean {
+ if (
+ filterOperator === 'notEqual' &&
+ Boolean(rowValue) === Boolean(filterValue)
+ ) {
+ return false;
+ } else if (
+ (filterOperator ?? 'equals') === 'equals' &&
+ Boolean(rowValue) !== Boolean(filterValue)
+ ) {
+ return false;
+ } else if (
+ filterOperator &&
+ !['equals', 'notEqual'].includes(filterOperator)
+ ) {
+ logger.warn(`Unsupported boolean filter operator: ${filterOperator}`);
+ }
+ return true;
+}
+
+function doesDateFilterPass(
+ filterValue: SkyDateRange | Date | string,
+ rowValue: Date | string,
+ filterOperator: SkyDataGridFilterOperator | undefined,
+ logger: SkyLogService,
+): boolean {
+ const rowDate = asDate(rowValue);
+ if (isDateRangeFilter(filterValue)) {
+ const range = filterValue as SkyDateRange;
+ return !(
+ (range.startDate && zeroHour(rowDate) < zeroHour(range.startDate)) ||
+ (range.endDate && zeroHour(rowDate) > zeroHour(range.endDate))
+ );
+ } else {
+ const filterDate = asDate(filterValue);
+ return numericFilter(
+ filterOperator ?? 'equals',
+ zeroHour(filterDate),
+ zeroHour(rowDate),
+ logger,
+ );
+ }
+}
+
+/**
+ * Date object at UTC midnight.
+ */
+function asDate(value: Date | string): Date {
+ const date =
+ value instanceof Date ? new Date(value) : new Date(value as string);
+ date.setHours(
+ date.getHours() + Math.floor(date.getTimezoneOffset() / 60),
+ date.getMinutes() + (date.getTimezoneOffset() % 60),
+ );
+ date.setTime(zeroHour(date));
+ return date;
+}
+
+/**
+ * Timestamp at UTC midnight.
+ */
+function zeroHour(value: Date): number {
+ return Date.UTC(value.getFullYear(), value.getMonth(), value.getDate());
+}
+
+function doesNumericFilterPass(
+ filterValue: SkyDataGridNumberRangeFilterValue | string | number,
+ rowValue: string | number,
+ filterOperator: SkyDataGridFilterOperator | undefined,
+ logger: SkyLogService,
+): boolean {
+ if (isNumberRangeFilter(filterValue)) {
+ if (
+ typeof (filterValue.from ?? undefined) !== 'undefined' &&
+ Number(rowValue) < Number(filterValue.from)
+ ) {
+ // If the low end is defined and the value is less than the low end.
+ return false;
+ }
+ if (
+ typeof (filterValue.to ?? undefined) !== 'undefined' &&
+ Number(rowValue) > Number(filterValue.to)
+ ) {
+ // If the high end is defined and the value is greater than the high end.
+ return false;
+ }
+ } else {
+ return numericFilter(
+ filterOperator ?? 'equals',
+ Number(filterValue),
+ Number(rowValue),
+ logger,
+ );
+ }
+ return true;
+}
+
+/**
+ * Ensures the provided value matches the number range filter shape. It must have a `from` and `to` property,
+ * with at least one being a number (both can be numbers).
+ */
+function isNumberRangeFilter(
+ value: unknown,
+): value is SkyDataGridNumberRangeFilterValue {
+ return (
+ typeof value === 'object' &&
+ value !== null &&
+ 'from' in value &&
+ 'to' in value &&
+ (typeof (value as SkyDataGridNumberRangeFilterValue).from === 'number' ||
+ typeof (value as SkyDataGridNumberRangeFilterValue).to === 'number') &&
+ (typeof (value as SkyDataGridNumberRangeFilterValue).from === 'number' ||
+ value.from === null) &&
+ (typeof (value as SkyDataGridNumberRangeFilterValue).to === 'number' ||
+ value.to === null)
+ );
+}
+
+/**
+ * Ensures the provided value matches the date range filter shape. It must have a `startDate` and/or `endDate` property,
+ * with at least one being a Date object.
+ */
+function isDateRangeFilter(value: unknown): value is SkyDateRange {
+ return (
+ typeof value === 'object' &&
+ value !== null &&
+ ('startDate' in value || 'endDate' in value) &&
+ ((value as SkyDateRange).startDate instanceof Date ||
+ (value as SkyDateRange).endDate instanceof Date)
+ );
+}
+
+function doesTextFilterPass(
+ filterOperator: SkyDataGridFilterOperator,
+ filterValue: unknown,
+ rowValue: string,
+ logger: SkyLogService,
+): boolean {
+ const rowString = rowValue.normalize().toLocaleLowerCase();
+ const filterArray: string[] = (
+ Array.isArray(filterValue) ? filterValue : [filterValue]
+ ).map((value) => String(value).normalize().toLocaleLowerCase());
+
+ switch (filterOperator) {
+ case 'equals':
+ return filterArray.some((value) => value === rowString);
+ case 'notEqual':
+ return filterArray.every((value) => value !== rowString);
+ case 'contains':
+ return filterArray.some((value) => rowString.includes(value));
+ case 'notContains':
+ return filterArray.every((value) => !rowString.includes(value));
+ case 'startsWith':
+ return filterArray.some((value) => rowString.startsWith(value));
+ case 'endsWith':
+ return filterArray.some((value) => rowString.endsWith(value));
+ default:
+ logger.warn(`Unsupported text filter operator: ${filterOperator}`);
+ return true;
+ }
+}
+
+function numericFilter(
+ filterOperator: SkyDataGridFilterOperator,
+ filterValue: number,
+ rowValue: number,
+ logger: SkyLogService,
+): boolean {
+ switch (filterOperator) {
+ case 'equals':
+ return rowValue === filterValue;
+ case 'notEqual':
+ return rowValue !== filterValue;
+ case 'lessThan':
+ return rowValue < filterValue;
+ case 'lessThanOrEqual':
+ return rowValue <= filterValue;
+ case 'greaterThan':
+ return rowValue > filterValue;
+ case 'greaterThanOrEqual':
+ return rowValue >= filterValue;
+ default:
+ logger.warn(
+ `Unsupported number or date filter operator: ${filterOperator}`,
+ );
+ return true;
+ }
+}
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.css b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.css
new file mode 100644
index 0000000000..a06b21df93
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.css
@@ -0,0 +1,4 @@
+:host {
+ display: block;
+ max-width: 100%;
+}
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.html b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.html
new file mode 100644
index 0000000000..5850e8e557
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.html
@@ -0,0 +1,49 @@
+
+@if (gridOptions(); as gridOptions) {
+ @if (useDataManager) {
+
+
+
+ } @else {
+
+
+
+ }
+
+ @if (pageCount(); as pageCount) {
+
+ }
+}
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.spec.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.spec.ts
new file mode 100644
index 0000000000..4c0d86118f
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.spec.ts
@@ -0,0 +1,1502 @@
+import { HarnessLoader } from '@angular/cdk/testing';
+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 { provideNoopAnimations } from '@angular/platform-browser/animations';
+import { ActivatedRoute, Router, provideRouter } from '@angular/router';
+import { SkyAppTestUtility } from '@skyux-sdk/testing';
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import { SkyAgGridWrapperHarness } from '@skyux/ag-grid/testing';
+import { SkyLogService } from '@skyux/core';
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import { SkyDataManagerHarness } from '@skyux/data-manager/testing';
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import { SkyWaitHarness } from '@skyux/indicators/testing';
+// eslint-disable-next-line @nx/enforce-module-boundaries
+import { SkyPagingHarness } from '@skyux/lists/testing';
+import { SkySearchHarness } from '@skyux/lookup/testing';
+
+import { getGridApi } from 'ag-grid-community';
+
+import { SkyDataGridComponent } from './data-grid.component';
+import { DataGridTestComponent } from './fixtures/data-grid-test.component';
+import { DataGridWDataManagerTestComponent } from './fixtures/data-grid-w-data-manager-test.component';
+
+describe('SkyDataGridComponent', () => {
+ describe('without data manager', () => {
+ let fixture: ComponentFixture;
+ let component: DataGridTestComponent;
+ let loggerSpy: jasmine.Spy;
+ let loader: HarnessLoader;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [provideRouter([]), provideLocationMocks()],
+ });
+ fixture = TestBed.createComponent(DataGridTestComponent);
+ loader = TestbedHarnessEnvironment.loader(fixture);
+ component = fixture.componentInstance;
+ const logger = TestBed.inject(SkyLogService);
+ loggerSpy = spyOn(logger, 'warn').and.returnValue(undefined);
+ });
+
+ 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(SkyDataGridComponent)),
+ ).toEqual([]);
+
+ fixture.componentRef.setInput('showAllGrids', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ fixture.debugElement.queryAll(By.directive(SkyDataGridComponent))
+ .length,
+ ).toEqual(4);
+ });
+
+ it('should destroy and recreate columns', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component).toBeTruthy();
+ const grids = await loader.getAllHarnesses(SkyAgGridWrapperHarness);
+ expect(
+ await Promise.all(
+ grids.map((grid) => grid.getDisplayedColumnIds()),
+ ).then((cols) =>
+ cols.map((id) => id.length).reduce((a, b) => a + b, 0),
+ ),
+ ).toEqual(4 * 3 + 3); // 4 grids with 3 columns each, plus 3 extra headers for the multi-select and row delete grid
+
+ fixture.componentRef.setInput('showCol3', false);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ await Promise.all(
+ grids.map((grid) => grid.getDisplayedColumnIds()),
+ ).then((cols) =>
+ cols.map((id) => id.length).reduce((a, b) => a + b, 0),
+ ),
+ ).toEqual(4 * 2 + 4); // 4 grids with 2 columns each, plus 4 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(
+ await Promise.all(
+ grids.map((grid) => grid.getDisplayedColumnIds()),
+ ).then((cols) =>
+ cols.map((id) => id.length).reduce((a, b) => a + b, 0),
+ ),
+ ).toEqual(4 * 3 + 3); // 4 grids with 3 columns each, plus 3 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.nativeElement.querySelectorAll('sky-ag-grid-wrapper'),
+ ).toHaveSize(0);
+ });
+
+ it('should respond to displayedColumnIds input', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component).toBeTruthy();
+ const grids = await loader.getAllHarnesses(SkyAgGridWrapperHarness);
+ expect(
+ await Promise.all(
+ grids.map((grid) => grid.getDisplayedColumnIds()),
+ ).then((cols) => cols.flatMap((id) => id).length),
+ ).toEqual(4 * 3 + 3); // 4 grids with 3 columns each, plus 3 extra headers for the multi-select and row delete grid
+
+ fixture.componentRef.setInput('displayedColumnIds', [
+ 'column1',
+ 'column2',
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ await Promise.all(
+ grids.map((grid) => grid.getDisplayedColumnIds()),
+ ).then((cols) => cols.flatMap((id) => id).length),
+ ).toEqual(4 * 2 + 1); // 4 grids with 2 columns each, plus 1 extra header for multi-select
+ expect(component.visibleColumnIds()).toEqual(['column1', 'column2']);
+
+ fixture.componentRef.setInput('displayedColumnIds', [
+ 'column1',
+ 'column2',
+ 'column3',
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ await Promise.all(
+ grids.map((grid) => grid.getDisplayedColumnIds()),
+ ).then((cols) => cols.flatMap((id) => id).length),
+ ).toEqual(4 * 3 + 1); // 4 grids with 3 columns each, plus 1 extra header for multi-select
+ expect(component.visibleColumnIds()).toEqual([
+ 'column1',
+ 'column2',
+ 'column3',
+ ]);
+
+ fixture.componentRef.setInput('showAllColumns', false);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ fixture.nativeElement.querySelectorAll('sky-ag-grid-wrapper'),
+ ).toHaveSize(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 handle data changing from populated to undefined', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ component.dataForSimpleGrid = undefined;
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(0);
+ });
+
+ it('should handle data changing from undefined to populated', async () => {
+ component.dataForSimpleGrid = undefined;
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(0);
+
+ component.dataForSimpleGrid = [
+ { id: '1', column1: '1', column2: 'Apple', column3: true },
+ { id: '2', column1: '01', column2: 'Banana', column3: false },
+ ];
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(2);
+ });
+
+ it('should handle data sort', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const gridElement = fixture.nativeElement.querySelector(
+ '[data-sky-id="grid"] ag-grid-angular',
+ );
+ const api = getGridApi(gridElement);
+ expect(api).toBeTruthy();
+ expect(api?.getState()?.sort?.sortModel).toBeUndefined();
+
+ fixture.componentRef.setInput('sortField', {
+ fieldSelector: 'column1',
+ descending: false,
+ });
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getState()?.sort?.sortModel).toEqual([
+ {
+ colId: 'column1',
+ sort: 'asc',
+ },
+ ]);
+
+ fixture.componentRef.setInput('sortField', {
+ fieldSelector: 'column1',
+ descending: true,
+ });
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getState()?.sort?.sortModel).toEqual([
+ {
+ colId: 'column1',
+ sort: 'desc',
+ },
+ ]);
+
+ const column2SortButton = gridElement.querySelector(
+ '.ag-header-cell.ag-header-cell-sortable[col-id="column2"] button.ag-header-cell-label-sortable',
+ ) as HTMLButtonElement;
+ expect(column2SortButton).toBeTruthy();
+ SkyAppTestUtility.fireDomEvent(column2SortButton, 'click');
+ await fixture.whenStable();
+ SkyAppTestUtility.fireDomEvent(column2SortButton, 'click');
+ await fixture.whenStable();
+
+ expect(fixture.componentInstance.sortField()).toEqual({
+ fieldSelector: 'column2',
+ descending: true,
+ });
+ expect(api?.getState()?.sort?.sortModel).toEqual([
+ {
+ colId: 'column2',
+ sort: 'desc',
+ },
+ ]);
+
+ fixture.componentRef.setInput('sortField', undefined);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getState()?.sort?.sortModel).toBeUndefined();
+ });
+
+ it('should update grid options when pageSize changes', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getGridOption('pagination')).toBeFalsy();
+
+ fixture.componentRef.setInput('pageSize', 2);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getGridOption('pagination')).toBeTruthy();
+ expect(api?.getGridOption('paginationPageSize')).toBe(2);
+ });
+
+ it('should constrict grid page size when using externalRowCount', async () => {
+ fixture.componentRef.setInput('pageSize', 2);
+ fixture.componentRef.setInput('externalRowCount', 123);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getGridOption('pagination')).toBeFalsy();
+ expect(api?.getDisplayedRowCount()).toBe(2);
+ });
+
+ it('should reset page number when it is no longer valid', async () => {
+ fixture.componentRef.setInput('pageSize', 2);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const grid = await loader.getHarness(SkyAgGridWrapperHarness);
+ expect(await grid.isGridReady()).toBeTrue();
+ expect(component.page()).toBe(1);
+ component.page.set(2);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const gridApi = getGridApi(
+ fixture.nativeElement.querySelector('ag-grid-angular'),
+ );
+ expect(gridApi?.paginationGetCurrentPage()).toBe(1); // zero-based
+ fixture.componentRef.setInput('pageSize', 10);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component.page()).toBe(1);
+ });
+
+ it('should update grid options when height changes', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getGridOption('domLayout')).toBe('autoHeight');
+
+ fixture.componentRef.setInput('height', 200);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getGridOption('domLayout')).toBe('normal');
+
+ fixture.componentRef.setInput('height', 0);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getGridOption('domLayout')).toBe('autoHeight');
+ });
+
+ it('should update grid options when multiselect changes', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getGridOption('rowSelection')).toEqual(
+ jasmine.objectContaining({
+ mode: 'singleRow',
+ enableClickSelection: false,
+ enableSelectionWithoutKeys: true,
+ checkboxes: false,
+ }),
+ );
+
+ fixture.componentRef.setInput('multiselect', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getGridOption('rowSelection')).toEqual({
+ mode: 'multiRow',
+ checkboxes: true,
+ headerCheckbox: true,
+ checkboxLocation: 'selectionColumn',
+ });
+ });
+
+ 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 update selectedRowIds when data changes to remove IDs no longer in data', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Select some rows
+ component.selectedRowIds.set(['101', '102', '103', '104', '105']);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component.selectedRowIds()).toEqual([
+ '101',
+ '102',
+ '103',
+ '104',
+ '105',
+ ]);
+
+ // Remove some items from the data (remove myId 102, 104)
+ component.dataForSimpleGridWithMultiselect = [
+ { id: '1', column1: '1', column2: 'Apple', column3: true, myId: '101' },
+ {
+ id: '3',
+ column1: '11',
+ column2: 'Banana',
+ column3: true,
+ myId: '103',
+ },
+ {
+ id: '5',
+ column1: '13',
+ column2: 'Edamame',
+ column3: true,
+ myId: '105',
+ },
+ ];
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // selectedRowIds should be updated to only include IDs still in the data
+ expect(component.selectedRowIds()).toEqual(['101', '103', '105']);
+ });
+
+ it('should update selectedRowIds when data changes from populated to fewer items', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Select all rows
+ component.selectedRowIds.set([
+ '101',
+ '102',
+ '103',
+ '104',
+ '105',
+ '106',
+ '107',
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component.selectedRowIds()).toHaveSize(7);
+
+ // Reduce data to just 2 items
+ component.dataForSimpleGridWithMultiselect = [
+ { id: '1', column1: '1', column2: 'Apple', column3: true, myId: '101' },
+ {
+ id: '2',
+ column1: '01',
+ column2: 'Banana',
+ column3: false,
+ myId: '102',
+ },
+ ];
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // selectedRowIds should only include IDs that are still in the data
+ expect(component.selectedRowIds()).toEqual(['101', '102']);
+ });
+
+ it('should clear selectedRowIds when data changes to empty', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Select some rows
+ component.selectedRowIds.set(['101', '102', '103']);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component.selectedRowIds()).toEqual(['101', '102', '103']);
+
+ // Clear the data
+ component.dataForSimpleGridWithMultiselect = [];
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // selectedRowIds should be empty
+ expect(component.selectedRowIds()).toEqual([]);
+ });
+
+ it('should update selectedRowIds when filters are applied to remove IDs of filtered-out rows', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredMultiselectGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-multiselect-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Select some rows (ids 1, 2, 3, 4, 5)
+ component.selectedRowIds.set(['1', '2', '3', '4', '5']);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component.selectedRowIds()).toEqual(['1', '2', '3', '4', '5']);
+
+ // Apply a filter that only shows rows where column2 starts with 'B' (Banana)
+ // This should only include rows with ids 2 and 3
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column2Filter',
+ filterValue: { value: 'B', displayValue: 'Starts with B' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should have 2 rows displayed
+ expect(api?.getDisplayedRowCount()).toBe(2);
+
+ // selectedRowIds should only include IDs of visible rows (2 and 3)
+ expect(component.selectedRowIds()).toEqual(['2', '3']);
+ });
+
+ it('should restore selectedRowIds when filters are cleared', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredMultiselectGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-multiselect-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+
+ // Select rows 2 and 3 (Banana rows)
+ component.selectedRowIds.set(['2', '3']);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Apply filter that shows Banana rows
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column2Filter',
+ filterValue: { value: 'B', displayValue: 'Starts with B' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(2);
+ expect(component.selectedRowIds()).toEqual(['2', '3']);
+
+ // Clear filters - all rows should be visible again
+ fixture.componentRef.setInput('appliedFilters', []);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // selectedRowIds should still be ['2', '3'] since those IDs are still valid
+ expect(component.selectedRowIds()).toEqual(['2', '3']);
+ });
+
+ it('should update selectedRowIds when filter changes to a more restrictive filter', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredMultiselectGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-multiselect-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+
+ // Select all rows
+ component.selectedRowIds.set(['1', '2', '3', '4', '5', '6', '7']);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(component.selectedRowIds()).toHaveSize(7);
+
+ // Apply a filter that shows rows where column1 contains '1'
+ // Rows: 1 (column1='1'), 2 (column1='01'), 3 (column1='11'), 4 (column1='12'), 5 (column1='13'), 7 (column1='21')
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column1Filter',
+ filterValue: { value: '1', displayValue: 'Contains 1' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(6);
+ expect(component.selectedRowIds()).toEqual([
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '7',
+ ]);
+
+ // Apply a more restrictive filter that shows only rows starting with 'B'
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column1Filter',
+ filterValue: { value: '1', displayValue: 'Contains 1' },
+ },
+ {
+ filterId: 'column2Filter',
+ filterValue: { value: 'B', displayValue: 'Starts with B' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Only rows 2 and 3 match both filters
+ expect(api?.getDisplayedRowCount()).toBe(2);
+ expect(component.selectedRowIds()).toEqual(['2', '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 () => {
+ fixture.componentRef.setInput('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 () => {
+ fixture.componentRef.setInput('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.set(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.set(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.set(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.set(0);
+ fixture.detectChanges();
+ expect(navSpy).not.toHaveBeenCalledWith([], {
+ relativeTo: jasmine.any(ActivatedRoute),
+ queryParams: { page: 0 },
+ queryParamsHandling: 'merge',
+ });
+
+ component.page.set(Number.POSITIVE_INFINITY);
+ fixture.detectChanges();
+ expect(navSpy).not.toHaveBeenCalledWith([], {
+ relativeTo: jasmine.any(ActivatedRoute),
+ queryParams: { page: Number.POSITIVE_INFINITY },
+ queryParamsHandling: 'merge',
+ });
+ });
+
+ describe('apply filters', () => {
+ it('should not apply filter when externalRowCount is set', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.componentRef.setInput('externalRowCount', 123);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply a text filter
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column1Filter',
+ filterValue: { value: ['1'], displayValue: 'Contains "1"' },
+ },
+ {
+ filterId: 'column2Filter',
+ filterValue: { value: 'Ban', displayValue: 'Starts with Ban' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should not filter rows because externalRowCount is set and data has not been updated.
+ expect(api?.getDisplayedRowCount()).toBe(7);
+ });
+
+ it('should apply text filter to grid', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply a text filter
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column1Filter',
+ filterValue: { value: ['1'], displayValue: 'Contains "1"' },
+ },
+ {
+ filterId: 'column2Filter',
+ filterValue: { value: 'Ban', displayValue: 'Starts with Ban' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should filter to rows where column2 starts with 'Ban' (Banana)
+ expect(api?.getDisplayedRowCount()).toBe(2);
+ });
+
+ it('should apply multiple filters simultaneously', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply multiple filters
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column1Filter',
+ filterValue: { value: '1', displayValue: 'Contains 1' },
+ },
+ {
+ filterId: 'column2Filter',
+ filterValue: { value: 'B', displayValue: 'Starts with B' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should filter to rows where column1 contains '1' AND column2 starts with 'B'
+ // column1='1' has column2='Apple' (no match)
+ // column1='01' has column2='Banana' (match)
+ // column1='11' has column2='Banana' (match)
+ // column1='12' has column2='Daikon' (no match)
+ // column1='13' has column2='Edamame' (no match)
+ expect(api?.getDisplayedRowCount()).toBe(2);
+ });
+
+ it('should clear filters when appliedFilters is set to empty', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply filter first
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column2Filter',
+ filterValue: { value: 'Ban', displayValue: 'Starts with Ban' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(2);
+
+ // Clear filters
+ fixture.componentRef.setInput('appliedFilters', []);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(api?.getDisplayedRowCount()).toBe(7);
+ });
+
+ it('should clear filters when appliedFilters is set to undefined', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply filter first
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column2Filter',
+ filterValue: { value: 'Ban', displayValue: 'Starts with Ban' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(2);
+
+ // Clear filters by setting undefined
+ fixture.componentRef.setInput('appliedFilters', undefined);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(api?.getDisplayedRowCount()).toBe(7);
+ });
+
+ it('should ignore filters without matching column filterId', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply a filter with non-existent filterId
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'nonExistentFilter',
+ filterValue: { value: 'test', displayValue: 'Test' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should not filter any rows since the filterId doesn't match any column
+ expect(api?.getDisplayedRowCount()).toBe(7);
+ });
+
+ it('should ignore filters with undefined value', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply a filter with undefined value
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column2Filter',
+ filterValue: undefined,
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should not filter any rows since the value is undefined
+ expect(api?.getDisplayedRowCount()).toBe(7);
+ });
+
+ it('should update filter when appliedFilters changes', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+
+ // Apply initial filter
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column2Filter',
+ filterValue: { value: 'A', displayValue: 'Starts with A' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should filter to Apple only
+ expect(api?.getDisplayedRowCount()).toBe(1);
+
+ // Update filter
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column2Filter',
+ filterValue: { value: 'B', displayValue: 'Starts with B' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should now filter to Banana rows
+ expect(api?.getDisplayedRowCount()).toBe(2);
+ });
+
+ it('should apply text filter with operators', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply a text filter using a column without filterOperator (should default to 'contains')
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column2VarOperatorFilter',
+ filterValue: { value: 'ana', displayValue: 'Contains ana' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should filter to rows where column2 contains 'ana' (Banana)
+ expect(api?.getDisplayedRowCount()).toBe(2);
+
+ fixture.componentRef.setInput('textFilterOperator', 'startsWith');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(0);
+
+ fixture.componentRef.setInput('textFilterOperator', 'endsWith');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(2);
+
+ fixture.componentRef.setInput('textFilterOperator', 'notContains');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(5);
+
+ fixture.componentRef.setInput('textFilterOperator', 'equals');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(0);
+
+ fixture.componentRef.setInput('textFilterOperator', 'notEqual');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Does not apply to text filters, but verify no errors occur.
+ fixture.componentRef.setInput('textFilterOperator', 'lessThanOrEqual');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+ expect(loggerSpy).toHaveBeenCalledWith(
+ `Unsupported text filter operator: lessThanOrEqual`,
+ );
+
+ // Update filter to use 'startsWith' operator
+ fixture.componentRef.setInput('textFilterOperator', 'startsWith');
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column2VarOperatorFilter',
+ filterValue: { value: 'Ban', displayValue: 'Starts with Ban' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should still filter to rows where column2 starts with 'Ban' (Banana)
+ expect(api?.getDisplayedRowCount()).toBe(2);
+
+ component.dataForFilteredGrid = [
+ ...component.dataForFilteredGrid.map((item) => ({
+ ...item,
+ column2: null,
+ })),
+ ];
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(0);
+ });
+
+ it('should apply number filter to grid', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply a number filter (equals)
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'numericFilter',
+ filterValue: { value: 200, displayValue: 'Equals 200' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should filter to rows where numericColumn equals 200
+ expect(api?.getDisplayedRowCount()).toBe(1);
+
+ fixture.componentRef.setInput('numberFilterOperator', 'notEqual');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(6);
+
+ fixture.componentRef.setInput(
+ 'numberFilterOperator',
+ 'lessThanOrEqual',
+ );
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(5);
+
+ fixture.componentRef.setInput('numberFilterOperator', 'lessThan');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(4);
+
+ fixture.componentRef.setInput(
+ 'numberFilterOperator',
+ 'greaterThanOrEqual',
+ );
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(3);
+
+ fixture.componentRef.setInput('numberFilterOperator', 'greaterThan');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(2);
+
+ // Does not apply to number filters, but verify no errors occur.
+ fixture.componentRef.setInput('numberFilterOperator', 'contains');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+ expect(loggerSpy).toHaveBeenCalledWith(
+ `Unsupported number or date filter operator: contains`,
+ );
+ });
+
+ it('should apply number range filter to grid', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply a number range filter
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'numericFilter',
+ filterValue: {
+ value: { from: 150, to: 250 },
+ displayValue: 'Between 150 and 250',
+ },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should filter to rows where numericColumn is between 150 and 250 (inclusive)
+ // Values: 100, 200, 150, 250, 175, 300, 125 -> 200, 150, 250, 175 = 4 rows
+ expect(api?.getDisplayedRowCount()).toBe(4);
+
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'numericFilter',
+ filterValue: {
+ value: { from: null, to: 250 },
+ displayValue: 'Up to 250',
+ },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(6);
+
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'numericFilter',
+ filterValue: {
+ value: { from: 150, to: null },
+ displayValue: 'Up from 150',
+ },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(5);
+ });
+
+ it('should apply date filter to grid', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply a date filter (equals)
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'dateFilter',
+ filterValue: {
+ value: new Date('2024-03-10T00:00:00.000Z'),
+ displayValue: 'March 10, 2024',
+ },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should filter to rows where dateColumn equals 2024-03-10
+ expect(api?.getDisplayedRowCount()).toBe(1);
+ });
+
+ it('should apply date range filter to grid', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply a date range filter
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'dateFilter',
+ filterValue: {
+ value: {
+ startDate: new Date('2024-02-01T00:00:00.000Z'),
+ endDate: new Date('2024-05-01T00:00:00.000Z'),
+ },
+ displayValue: 'Feb 1 to May 1, 2024',
+ },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should filter to rows where dateColumn is between Feb 1 and May 1, 2024
+ // Dates: Jan 15, Feb 20, Mar 10, Apr 5, May 25, Jun 30, Jul 12
+ // In range: Feb 20, Mar 10, Apr 5 = 3 rows
+ expect(api?.getDisplayedRowCount()).toBe(3);
+
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'dateFilter',
+ filterValue: {
+ value: {
+ endDate: new Date('2024-05-01T00:00:00.000Z'),
+ },
+ displayValue: 'Before May 1, 2024',
+ },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(4);
+ });
+
+ it('should apply boolean filter', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply a boolean filter
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'column3Filter',
+ filterValue: { value: true, displayValue: 'True' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Boolean filters are not supported by AG Grid, so no filtering should occur
+ expect(api?.getDisplayedRowCount()).toBe(4);
+
+ fixture.componentRef.setInput('booleanFilterOperator', 'notEqual');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(3);
+
+ // Does not apply to boolean filters, but verify no errors occur.
+ fixture.componentRef.setInput('booleanFilterOperator', 'contains');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+ expect(loggerSpy).toHaveBeenCalledWith(
+ `Unsupported boolean filter operator: contains`,
+ );
+ });
+
+ it('should ignore filter when column with filterId does not exist in grid', async () => {
+ fixture.componentRef.setInput('showAllGrids', false);
+ fixture.componentRef.setInput('showFilteredGrid', true);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const api = getGridApi(
+ fixture.nativeElement.querySelector(
+ '[data-sky-id="filtered-grid"] ag-grid-angular',
+ ),
+ );
+ expect(api).toBeTruthy();
+ expect(api?.getDisplayedRowCount()).toBe(7);
+
+ // Apply a filter referencing a non-existent column
+ fixture.componentRef.setInput('appliedFilters', [
+ {
+ filterId: 'nonExistentColumnFilter',
+ filterValue: { value: 'test', displayValue: 'Test' },
+ },
+ ]);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Should not filter any rows since the column doesn't exist
+ expect(api?.getDisplayedRowCount()).toBe(7);
+ });
+ });
+ });
+
+ describe('with data manager', () => {
+ let fixture: ComponentFixture;
+ let loader: HarnessLoader;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ provideRouter([]),
+ provideLocationMocks(),
+ provideNoopAnimations(),
+ ],
+ });
+ fixture = TestBed.createComponent(DataGridWDataManagerTestComponent);
+ loader = TestbedHarnessEnvironment.loader(fixture);
+ const logger = TestBed.inject(SkyLogService);
+ spyOn(logger, 'warn');
+ });
+
+ it('should pick up displayed columns from data manager', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const dm = await loader.getHarness(
+ SkyDataManagerHarness.with({ dataSkyId: 'my-data-manager' }),
+ );
+ const columnChooser = await dm
+ .getToolbar()
+ .then(async (toolbar) => await toolbar.openColumnPicker());
+ await columnChooser.clearAll();
+ await columnChooser.selectColumns({ titleText: 'Name' });
+ await columnChooser.saveAndClose();
+ const gridElement =
+ fixture.nativeElement.querySelector('ag-grid-angular');
+ expect(gridElement).toBeTruthy();
+ const gridApi = getGridApi(gridElement);
+ expect(gridApi?.getAllDisplayedColumns()).toHaveSize(1);
+ });
+
+ it('should handle displayed columns from data manager when not specified', async () => {
+ fixture.componentInstance.displayedColumnIds = undefined;
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const grid = await loader.getHarness(SkyAgGridWrapperHarness);
+ expect(await grid.isGridReady()).toBeTrue();
+ const gridElement =
+ fixture.nativeElement.querySelector('ag-grid-angular');
+ expect(gridElement).toBeTruthy();
+ const gridApi = getGridApi(gridElement);
+ expect(gridApi?.getColumns()).toHaveSize(2);
+ expect(await grid.getDisplayedColumnIds()).toHaveSize(2);
+ });
+
+ it('should pick up search text from data manager', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ const gridElement =
+ fixture.nativeElement.querySelector('ag-grid-angular');
+ expect(gridElement).toBeTruthy();
+ const gridApi = getGridApi(gridElement);
+ expect(gridApi?.isDestroyed()).toBeFalse();
+ const dm = await loader.getHarness(
+ SkyDataManagerHarness.with({ dataSkyId: 'my-data-manager' }),
+ );
+ const toolbar = await dm.getToolbar();
+ const search = (await toolbar.getSearch()) as SkySearchHarness;
+ expect(search).toBeTruthy();
+ if (await search.isCollapsed()) {
+ await search.clickOpenSearchButton();
+ }
+ await search.enterText('fruit');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(gridApi?.getGridOption('quickFilterText')).toBe('fruit');
+ });
+
+ it('should send sort field update through data manager', async () => {
+ fixture.componentRef.setInput('sortField', {
+ fieldSelector: 'category',
+ descending: false,
+ });
+ fixture.detectChanges();
+ await fixture.whenStable();
+ expect(
+ fixture.componentInstance.dataManagerState().activeSortOption,
+ ).toEqual({
+ id: 'category',
+ propertyName: 'category',
+ label: 'Category',
+ descending: false,
+ });
+ });
+ });
+});
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.ts
new file mode 100644
index 0000000000..fa4de5a9cd
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.component.ts
@@ -0,0 +1,897 @@
+import {
+ coerceArray,
+ coerceBooleanProperty,
+ coerceNumberProperty,
+ coerceStringArray,
+} from '@angular/cdk/coercion';
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ contentChildren,
+ effect,
+ inject,
+ input,
+ model,
+ output,
+ signal,
+ untracked,
+} from '@angular/core';
+import {
+ takeUntilDestroyed,
+ toObservable,
+ toSignal,
+} from '@angular/core/rxjs-interop';
+import { ActivatedRoute, Router } from '@angular/router';
+import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid';
+import { SkyLogService, SkyViewkeeperModule } from '@skyux/core';
+import { SkyDataManagerService } from '@skyux/data-manager';
+import { SkyWaitModule } from '@skyux/indicators';
+import {
+ SkyDataHost,
+ SkyDataHostService,
+ SkyFilterStateFilterItem,
+ SkyPagingModule,
+} from '@skyux/lists';
+
+import { AgGridAngular } from 'ag-grid-angular';
+import {
+ AllCommunityModule,
+ AutoSizeStrategy,
+ ColDef,
+ ColumnMovedEvent,
+ DisplayedColumnsChangedEvent,
+ GetRowIdParams,
+ GridApi,
+ GridOptions,
+ GridPreDestroyedEvent,
+ IRowNode,
+ ModuleRegistry,
+ RowSelectionOptions,
+ SelectionChangedEvent,
+ SortChangedEvent,
+ SortDirection,
+} from 'ag-grid-community';
+import {
+ EMPTY,
+ ObservableInput,
+ distinctUntilChanged,
+ filter,
+ fromEvent,
+ fromEventPattern,
+ map,
+ merge,
+ startWith,
+ switchMap,
+ takeUntil,
+} from 'rxjs';
+
+import { SkyDataGridFilterValue } from '../types/data-grid-filter-value';
+import { SkyDataGridPageRequest } from '../types/data-grid-page-request';
+import { SkyDataGridRowDeleteCancelArgs } from '../types/data-grid-row-delete-cancel-args';
+import { SkyDataGridRowDeleteConfirmArgs } from '../types/data-grid-row-delete-confirm-args';
+import { SkyDataGridSort } from '../types/data-grid-sort';
+
+import { SkyDataGridColumnInlineHelpComponent } from './data-grid-column-inline-help.component';
+import { SkyDataGridColumnComponent } from './data-grid-column.component';
+import { doesFilterPass } from './data-grid-filter';
+
+ModuleRegistry.registerModules([AllCommunityModule]);
+
+function arraySorted(arr: string[]): string[] {
+ return arr.slice().sort((a, b) => a.localeCompare(b));
+}
+
+function arrayIsEqual(
+ a: string[] | undefined,
+ b: string[] | undefined,
+): boolean {
+ if (!Array.isArray(a) || !Array.isArray(b) || a?.length !== b?.length) {
+ return false;
+ }
+ const bSorted = arraySorted(b);
+ return arraySorted(a).every((v, i) => v === bSorted[i]);
+}
+
+/**
+ * @preview
+ */
+@Component({
+ selector: 'sky-data-grid',
+ imports: [
+ AgGridAngular,
+ SkyAgGridModule,
+ SkyPagingModule,
+ SkyViewkeeperModule,
+ SkyWaitModule,
+ ],
+ templateUrl: './data-grid.component.html',
+ styleUrl: './data-grid.component.css',
+ host: {
+ '[class.sky-margin-stacked-lg]': 'stacked()',
+ '[style.width.px]': 'width() || undefined',
+ },
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SkyDataGridComponent<
+ T extends Record<'id', string> = Record<'id', string> &
+ Record,
+> {
+ /**
+ * The filter state from a
+ * [`SkyFilterBarComponent`](https://developer.blackbaud.com/skyux/components/filter-bar?docs-active-tab=development#class_sky-filter-bar-component).
+ * When provided, filters are automatically applied to columns that have matching `filterId` values using the
+ * respective `SkyDataGridColumnComponent`'s `filterOperator` as the comparator. To use the built-in filters, the
+ * filter values are:
+ *
+ * - For a boolean column, use a `boolean` with `'equals'` or `'notEqual'` as the operator.
+ * - For a date column, use a [`SkyDateRange`](https://developer.blackbaud.com/skyux/components/date-range-picker?docs-active-tab=development#interface_sky-date-range) or `Date`.
+ * - For a number column, use a `SkyDataGridNumberRangeFilterValue` or a `number`.
+ * - For a text column, use `string` or `string[]` as the filter value to match one or more text values.
+ *
+ * To provide custom filtering functions, use the `externalRowCount` input and update the `data` input when filters change.
+ */
+ public readonly appliedFilters = input<
+ SkyFilterStateFilterItem[]
+ >([]);
+
+ /**
+ * Enable a compact layout for the grid when using modern theme. Compact layout uses
+ * a smaller font size and row height to display more data in a smaller space.
+ * @default false
+ */
+ public readonly compact = input(false, {
+ transform: coerceBooleanProperty,
+ });
+
+ /**
+ * The data for the grid. Each item requires an `id`, and other properties should map to a `field` of the grid columns.
+ * When `data` is `null` or `undefined`, the grid will show a loading indicator, and when `data` is an empty array,
+ * the grid will show a "no rows" message.
+ */
+ public readonly data = input();
+
+ /**
+ * The number of records when using a remote data source.
+ * When `externalRowCount` is set, it is expected that `data` will be updated whenever `pageRequest` emits a new value.
+ * When both `externalRowCount` and `pageSize` are set, the number of pages is assumed to be `Math.ceil(externalRowCount / pageSize)`.
+ * If `externalRowCount` is not set, the data grid will page, sort, filter, and apply SKY UX data manager search text to the
+ * `data` provided, and the row count is assumed to be `data.length`.
+ */
+ public readonly externalRowCount = input(undefined);
+
+ /**
+ * 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 in CSS pixels. For best performance, large grids should set a `height` value and not enable
+ * `wrapText` on any column so that rows can be virtually drawn as needed. When `wrapText` is enabled on any column,
+ * or when `height` is not set, the grid needs to build every row in order to determine the scroll height, creating
+ * hundreds or thousands of invisible DOM elements and slowing down the browser.
+ */
+ public readonly height = input(0, {
+ transform: (val: unknown) => coerceNumberProperty(val, 0),
+ });
+
+ /**
+ * The column IDs or fields for columns to hide. Should not be combined with `displayedColumnIds`.
+ */
+ public readonly hiddenColumnIds = input([], {
+ transform: coerceStringArray,
+ });
+
+ /**
+ * 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 multiselect = input(false, {
+ transform: coerceBooleanProperty,
+ });
+
+ /**
+ * The unique ID that matches a property on the `data` object.
+ * @default 'id'
+ */
+ public readonly multiselectRowId = input('id');
+
+ /**
+ * The number of items to display per page. Setting this value enables pagination.
+ */
+ public readonly pageSize = input(0, {
+ transform: (value: unknown) => coerceNumberProperty(value, 0),
+ });
+
+ /**
+ * The query parameter name that stores the current page number.
+ * When set, the grid syncs page changes to the URL for deep linking, and there should only be one grid on the page.
+ */
+ public readonly pageQueryParam = input();
+
+ /**
+ * 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();
+
+ /**
+ * Whether the data grid is stacked with another element below it. When specified, the appropriate
+ * vertical spacing is automatically added to the data grid.
+ * @default false
+ */
+ public readonly stacked = input(false, {
+ transform: coerceBooleanProperty,
+ });
+
+ /**
+ * Move the horizontal scrollbar to just below the header row.
+ * @default false
+ */
+ public readonly topScrollEnabled = input(false, {
+ transform: coerceBooleanProperty,
+ });
+
+ /**
+ * The width of the grid in CSS pixels. When no width is set, the grid will use the width of its container.
+ */
+ public readonly width = input(0, {
+ transform: (val: unknown) => coerceNumberProperty(val, 0),
+ });
+
+ /**
+ * The column IDs or fields for columns to show. Should not be combined with `hiddenColumnIds`.
+ */
+ public readonly displayedColumnIds = input([], {
+ transform: coerceStringArray,
+ });
+
+ /**
+ * 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 displayedColumnIdsChange = output();
+
+ /**
+ * The current page number of the grid when `pageSize` has been set.
+ */
+ public readonly page = model(1);
+
+ /**
+ * The set of IDs for the rows to prompt for delete confirmation.
+ * The IDs match the `multiselectRowId` properties of the `data` objects.
+ */
+ public readonly rowDeleteIds = model([]);
+
+ /**
+ * 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 = model([]);
+
+ /**
+ * The sort setting for the grid.
+ */
+ public readonly sortField = model(undefined);
+
+ /**
+ * Fires when sorting or page number changes.
+ */
+ public readonly pageRequest = output();
+
+ /**
+ * Emits a row count after filters are updated. Not used when `externalRowCount` is set.
+ */
+ public readonly rowCountChange = output();
+
+ /**
+ * 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(SkyDataGridColumnComponent);
+ protected readonly gridApi = signal | undefined>(undefined);
+ protected readonly gridOptions = computed(() => {
+ const columnDefs = this.#columnDefs();
+ if (columnDefs.length === 0) {
+ return undefined;
+ }
+ const { pagination, paginationPageSize } = untracked(() =>
+ this.#paginationOptions(),
+ );
+ const rowData = untracked(() => this.rowData());
+ return this.#gridService.getGridOptions({
+ gridOptions: {
+ columnDefs,
+ context: {
+ enableTopScroll: untracked(() => this.topScrollEnabled()),
+ },
+ domLayout: untracked(() => this.height()) ? 'normal' : 'autoHeight',
+ onGridReady: (args) => {
+ this.gridApi.set(args.api);
+ this.gridReady.set(true);
+ },
+ pagination,
+ paginationPageSize,
+ suppressPaginationPanel: true,
+ rowData: rowData.length ? rowData : null,
+ getRowId: (params: GetRowIdParams) =>
+ params.data[
+ untracked(() => this.multiselectRowId()) as keyof T
+ ] as string,
+ rowSelection: untracked(() => this.#getRowSelection()),
+ autoSizeStrategy: untracked(() => this.#getAutoSizeStrategy()),
+ },
+ }) as GridOptions;
+ });
+
+ protected readonly gridReady = signal(false);
+ protected readonly rowData = computed(() => {
+ const pageSize = this.pageSize();
+ const useInternalFilters = this.#useInternalFilters();
+ let data = this.data() ?? [];
+ if (pageSize > 0 && !useInternalFilters) {
+ data = data.slice(0, pageSize);
+ }
+ return data;
+ });
+
+ protected readonly isExternalFilterPresent = computed(() => {
+ const hasFilters = (this.appliedFilters() ?? []).length > 0;
+ const useInternalFilters = this.#useInternalFilters();
+ return hasFilters && useInternalFilters;
+ });
+
+ protected readonly doesExternalFilterPass = computed(
+ (): ((node: Pick, 'data'>) => boolean) => {
+ const appliedFilters = this.appliedFilters();
+ const columns = this.columns().map((col) => ({
+ filterId: col.filterId(),
+ field: col.field() as keyof T | undefined,
+ filterOperator: col.filterOperator(),
+ type: col.dataType(),
+ }));
+ return (node: Pick, 'data'>): boolean =>
+ doesFilterPass(
+ appliedFilters,
+ node.data as Record<'id', string> & Partial,
+ columns,
+ this.#logger,
+ );
+ },
+ );
+
+ protected readonly pageCount = computed(() => {
+ const dataLength = this.rowData().length;
+ const externalRowCount = this.externalRowCount();
+ const pageSize = this.pageSize();
+ const gridReady = this.gridReady();
+ if (!gridReady || pageSize === 0) {
+ return 0;
+ }
+ return Math.ceil((externalRowCount ?? dataLength) / pageSize);
+ });
+
+ protected readonly useDataManager = !!inject(SkyDataManagerService, {
+ optional: true,
+ });
+ protected readonly viewId = computed(
+ () => this.#dataHostService?.hostId() ?? '',
+ );
+ protected readonly skyViewkeeper = computed(() => {
+ // Only used when not using SkyDataManagerService because data manager handles SkyViewkeeper.
+ const classes = ['.ag-header'];
+ if (this.topScrollEnabled()) {
+ classes.push('.ag-body-horizontal-scroll');
+ }
+ return classes;
+ });
+
+ readonly #activatedRoute = inject(ActivatedRoute, { optional: true });
+ readonly #dataHostService = inject(SkyDataHostService, { optional: true });
+ readonly #dataHostServiceUpdates = toSignal(
+ this.#dataHostService?.getDataHostUpdates('SkyDataGridComponent') ?? EMPTY,
+ );
+ readonly #dataHostDisplayedColumnIds = computed(
+ () => this.#dataHostServiceUpdates()?.displayedColumnIds ?? [],
+ );
+ readonly #dataHostSearchText = computed(
+ () => this.#dataHostServiceUpdates()?.searchText ?? '',
+ );
+ readonly #gridService = inject(SkyAgGridService);
+ readonly #logger = inject(SkyLogService);
+ readonly #router = inject(Router, { optional: true });
+
+ readonly #columnDefs = computed[]>(() => {
+ const columns = this.columns();
+ const sort = this.sortField();
+ const displayed = this.#displayedColumnIds();
+ const hidden = this.hiddenColumnIds().filter(Boolean);
+ return columns.map((col) =>
+ this.#createColDef(col, displayed, hidden, sort),
+ );
+ });
+ readonly #displayedColumnIds = computed(() => {
+ const displayedColumnIds = this.displayedColumnIds().filter(Boolean);
+ const dataHostDisplayedColumnIds = this.#dataHostDisplayedColumnIds();
+ const notHidden = this.columns()
+ .filter((col) => !col.hidden())
+ .map((col) => this.#getColumnIdOrField(col));
+ if (dataHostDisplayedColumnIds.length > 0) {
+ return dataHostDisplayedColumnIds;
+ }
+ if (displayedColumnIds.length > 0) {
+ return displayedColumnIds;
+ }
+ return notHidden;
+ });
+
+ readonly #dataHost = computed(() => {
+ const sortField = this.sortField();
+ const id = this.viewId();
+ const dataHost = untracked(() => this.#dataHostServiceUpdates());
+ if (dataHost) {
+ return {
+ activeSortOption: sortField
+ ? {
+ propertyName: sortField.fieldSelector,
+ descending: !!sortField.descending,
+ }
+ : undefined,
+ displayedColumnIds: this.#displayedColumnIds(),
+ id,
+ page: this.page(),
+ searchText: dataHost.searchText,
+ selectedIds: this.selectedRowIds(),
+ };
+ }
+ return undefined;
+ });
+
+ readonly #gridDestroyed = toObservable(this.gridApi).pipe(
+ filter(Boolean),
+ switchMap((api) =>
+ fromEventPattern((handler) =>
+ api.addEventListener('gridPreDestroyed', handler),
+ ),
+ ),
+ );
+ readonly #gridSelectedRowIds = toObservable(this.gridApi).pipe(
+ filter(Boolean),
+ switchMap((api) =>
+ fromEvent(api, 'selectionChanged').pipe(
+ takeUntil(this.#gridDestroyed),
+ map((selection) =>
+ arraySorted(this.#getRowIds(selection.selectedNodes)),
+ ),
+ distinctUntilChanged(arrayIsEqual),
+ ),
+ ),
+ );
+ readonly #gridDisplayedColumnIds = toObservable(this.gridApi).pipe(
+ filter(Boolean),
+ switchMap((api) =>
+ merge(
+ fromEvent(api, 'columnMoved').pipe(
+ takeUntil(this.#gridDestroyed),
+ map((columnsEvent) =>
+ columnsEvent.api
+ .getAllDisplayedColumns()
+ .map((col) => col.getColId()),
+ ),
+ ),
+ fromEvent(
+ api,
+ 'displayedColumnsChanged',
+ ).pipe(
+ takeUntil(this.#gridDestroyed),
+ map((columnsEvent) =>
+ columnsEvent.api
+ .getAllDisplayedColumns()
+ .map((col) => col.getColId()),
+ ),
+ distinctUntilChanged(arrayIsEqual),
+ ),
+ ),
+ ),
+ );
+ readonly #gridSortChange = toObservable(this.gridApi).pipe(
+ filter(Boolean),
+ switchMap((api) =>
+ fromEvent(api, 'sortChanged').pipe(
+ takeUntil(this.#gridDestroyed),
+ map((sortEvent): SkyDataGridSort | undefined => {
+ const sortColumn = sortEvent?.columns?.find((col) => !!col.getSort());
+ if (sortColumn) {
+ return {
+ descending: sortColumn.getSort() === 'desc',
+ fieldSelector: sortColumn.getColId(),
+ };
+ }
+ return undefined;
+ }),
+ ),
+ ),
+ );
+ readonly #paginationOptions = computed(() => {
+ const pageSize = this.pageSize();
+ const hasPageSize = pageSize > 0;
+ const useInternalFilters = this.#useInternalFilters();
+ const pagination = hasPageSize && useInternalFilters;
+ const paginationPageSize = (pagination && pageSize) || undefined;
+ return {
+ pagination,
+ paginationPageSize,
+ };
+ });
+ readonly #useInternalFilters = computed(() => {
+ const externalRowCount = this.externalRowCount();
+ return typeof externalRowCount === 'undefined';
+ });
+ readonly #queryParamPage = toSignal(
+ toObservable(this.pageQueryParam).pipe(
+ switchMap(
+ (pageQueryParam): ObservableInput =>
+ pageQueryParam && this.#activatedRoute
+ ? this.#activatedRoute.queryParamMap.pipe(
+ startWith(this.#activatedRoute.snapshot.queryParamMap),
+ map((params) =>
+ coerceNumberProperty(params.get(pageQueryParam), 1),
+ ),
+ )
+ : [],
+ ),
+ ),
+ { initialValue: 1 },
+ );
+
+ constructor() {
+ // Update specific grid options after the grid has been loaded.
+ effect(() => {
+ const api = untracked(() => this.gridApi());
+ const columnDefs = this.#columnDefs();
+ api?.setGridOption('columnDefs', columnDefs);
+ });
+ effect(() => {
+ const api = untracked(() => this.gridApi());
+ const height = this.height();
+ api?.setGridOption('domLayout', height ? 'normal' : 'autoHeight');
+ });
+ effect(() => {
+ const api = untracked(() => this.gridApi());
+ const loading = (this.data() ?? 'loading') === 'loading';
+ api?.setGridOption('loading', loading);
+ });
+ effect(() => {
+ const api = untracked(() => this.gridApi());
+ const { pagination, paginationPageSize } = this.#paginationOptions();
+ api?.setGridOption('pagination', pagination);
+ api?.setGridOption('paginationPageSize', paginationPageSize);
+ });
+ effect(() => {
+ const api = untracked(() => this.gridApi());
+ const rowData = this.rowData();
+ api?.setGridOption('rowData', rowData);
+ });
+ effect(() => {
+ const api = untracked(() => this.gridApi());
+ const rowSelection = this.#getRowSelection();
+ api?.setGridOption('rowSelection', rowSelection);
+ });
+
+ // Apply inputs once the grid is loaded and on subsequent changes.
+ effect(() => {
+ const api = this.gridApi();
+ const multiselectRowId = this.multiselectRowId();
+ const validRowIds = this.rowData().map((row): string =>
+ String(row[multiselectRowId as keyof T]),
+ );
+ const selectedRowIds = coerceStringArray(this.selectedRowIds());
+ const validSelectedRowIds = selectedRowIds.filter((id) =>
+ validRowIds.includes(id),
+ );
+ if (!arrayIsEqual(validSelectedRowIds, selectedRowIds)) {
+ this.selectedRowIds.set(validSelectedRowIds);
+ }
+ const currentSelectedRowIds = this.#getRowIds(api?.getSelectedNodes());
+ if (!arrayIsEqual(validSelectedRowIds, currentSelectedRowIds)) {
+ api?.deselectAll();
+ validSelectedRowIds.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 page = this.page();
+ const pageCount = this.pageCount();
+ if (!pageCount || !api) {
+ return;
+ }
+ if (page < 1 || page > pageCount) {
+ this.page.set(1);
+ } else {
+ api.paginationGoToPage(page - 1);
+ }
+ });
+
+ // Sync page from URL query parameter.
+ effect(() => {
+ const queryParamPage = this.#queryParamPage();
+ this.page.set(queryParamPage);
+ });
+
+ this.#gridDestroyed.pipe(takeUntilDestroyed()).subscribe(() => {
+ this.gridApi.set(undefined);
+ this.gridReady.set(false);
+ });
+
+ // Emit updates from the grid.
+ this.#gridSelectedRowIds
+ .pipe(
+ takeUntilDestroyed(),
+ map((ids) => coerceStringArray(ids)),
+ filter((rowIds) => !arrayIsEqual(this.selectedRowIds(), rowIds)),
+ )
+ .subscribe((rowIds) => {
+ this.selectedRowIds.set(rowIds);
+ });
+ this.#gridDisplayedColumnIds
+ .pipe(
+ takeUntilDestroyed(),
+ map((ids) => coerceStringArray(ids)),
+ )
+ .subscribe((columnIds) => {
+ this.displayedColumnIdsChange.emit(columnIds);
+ });
+ this.#gridSortChange.pipe(takeUntilDestroyed()).subscribe((sortChange) => {
+ if (sortChange) {
+ this.sortField.update((sort) => {
+ if (
+ !!sort !== !!sortChange ||
+ !!sort?.descending !== !!sortChange?.descending ||
+ sort?.fieldSelector !== sortChange?.fieldSelector
+ ) {
+ return sortChange;
+ }
+ return sort;
+ });
+ }
+ });
+ effect(() => {
+ const isExternalFilterPresent = this.isExternalFilterPresent();
+ const doesExternalFilterPass = this.doesExternalFilterPass();
+ let rowData = [...this.rowData()];
+ const searchText = this.#dataHostSearchText().normalize().toLowerCase();
+ const useInternalFilters = this.#useInternalFilters();
+ const multiselectRowId = this.multiselectRowId();
+ if (useInternalFilters) {
+ if (isExternalFilterPresent) {
+ rowData = rowData.filter((data) => doesExternalFilterPass({ data }));
+ }
+ if (searchText) {
+ rowData = rowData.filter((data) =>
+ Object.values(data).some((value) =>
+ String(value ?? '')
+ .normalize()
+ .toLowerCase()
+ .includes(searchText),
+ ),
+ );
+ }
+ const validRowIds = rowData.map((row): string =>
+ String(row[multiselectRowId as keyof T]),
+ );
+ const selectedRowIds = coerceStringArray(this.selectedRowIds());
+ const validSelectedRowIds = selectedRowIds.filter((id) =>
+ validRowIds.includes(id),
+ );
+ if (!arrayIsEqual(validSelectedRowIds, selectedRowIds)) {
+ this.selectedRowIds.set(validSelectedRowIds);
+ }
+ this.rowCountChange.emit(rowData.length);
+ }
+ });
+
+ effect(() => {
+ const searchText = this.#dataHostSearchText();
+ const api = this.gridApi();
+ const useInternalFilters = this.#useInternalFilters();
+ if (useInternalFilters) {
+ api?.setGridOption('quickFilterText', searchText);
+ }
+ });
+
+ effect(() => {
+ const dataHost = this.#dataHost();
+ if (dataHost) {
+ this.#dataHostService?.updateDataHost(dataHost, 'SkyDataGridComponent');
+ }
+ });
+
+ effect(() => {
+ this.pageRequest.emit({
+ pageNumber: this.page(),
+ pageSize: this.pageSize() || undefined,
+ sortField: this.sortField(),
+ });
+ }, {});
+ }
+
+ protected currentPageChange(page: number): void {
+ if (page && page !== this.page()) {
+ const pageQueryParam = this.pageQueryParam();
+ if (pageQueryParam) {
+ // When using a query parameter, send the change through the router.
+ void this.#router?.navigate([], {
+ relativeTo: this.#activatedRoute,
+ queryParams: {
+ [pageQueryParam]: page === 1 ? null : page,
+ },
+ queryParamsHandling: 'merge',
+ });
+ } else {
+ this.page.set(page);
+ }
+ }
+ }
+
+ #createColDef(
+ col: SkyDataGridColumnComponent,
+ displayed: string[],
+ hidden: string[],
+ sort: SkyDataGridSort | undefined,
+ ): ColDef {
+ const field = col.field();
+ const colDef: ColDef = {
+ colId: col.columnId(),
+ field,
+ headerName: col.headingText(),
+ headerComponentParams: this.#getHeaderComponentParams(col),
+ hide: this.#hideColumn(col, displayed, hidden),
+ resizable: col.resizable(),
+ sortable: col.sortable(),
+ lockPosition: col.locked(),
+ suppressMovable: col.locked(),
+ type: [],
+ autoHeight: col.wrapText(),
+ wrapText: col.wrapText(),
+ sort: this.#getSort(sort, col),
+ };
+ if (col.dataType() === 'date') {
+ (colDef.type as string[]).push(SkyCellType.Date);
+ colDef.cellDataType = 'dateString';
+ } else if (field && col.dataType() === 'number') {
+ (colDef.type as string[]).push(SkyCellType.Number);
+ colDef.cellDataType = 'number';
+ colDef.valueGetter = (params): number => Number(params.data[field]);
+ } else if (col.dataType() === '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 };
+ }
+ this.#applyColumnWidthSettings(col, colDef);
+ return colDef;
+ }
+
+ #applyColumnWidthSettings(
+ col: SkyDataGridColumnComponent,
+ colDef: ColDef,
+ ): void {
+ if (col.flexWidth() > -1) {
+ colDef.initialFlex = col.flexWidth();
+ } else if (col.width() > 0) {
+ colDef.initialWidth = col.width();
+ }
+ if (col.width() > 0) {
+ colDef.minWidth = col.width();
+ colDef.suppressSizeToFit = true;
+ }
+ if (!col.resizable() || col.flexWidth() === 0) {
+ colDef.suppressSizeToFit = true;
+ colDef.suppressAutoSize = true;
+ }
+ }
+
+ #getSort(
+ sort: SkyDataGridSort | undefined,
+ col: SkyDataGridColumnComponent,
+ ): SortDirection {
+ return sort?.fieldSelector === col.field()
+ ? sort?.descending
+ ? 'desc'
+ : 'asc'
+ : null;
+ }
+
+ #getHeaderComponentParams(col: SkyDataGridColumnComponent): object {
+ return {
+ headerHidden: col.headingHidden(),
+ helpPopoverTitle: col.helpPopoverTitle(),
+ helpPopoverContent: col.helpPopoverContent() || col.description(),
+ inlineHelpComponent: SkyDataGridColumnInlineHelpComponent,
+ };
+ }
+
+ #getColumnIdOrField(col: SkyDataGridColumnComponent): string {
+ const id = col.columnId();
+ const field = col.field() || '';
+ return id || field;
+ }
+
+ #hideColumn(
+ col: SkyDataGridColumnComponent,
+ displayed: string[],
+ hidden: string[],
+ ): boolean {
+ return (
+ col.hidden() ||
+ (displayed.length > 0 &&
+ !displayed.includes(this.#getColumnIdOrField(col))) ||
+ hidden.includes(this.#getColumnIdOrField(col))
+ );
+ }
+
+ #getRowIds(rows: (IRowNode | undefined)[] | null | undefined): string[] {
+ return coerceArray(rows)
+ .map((node) => node?.id as string)
+ .filter(Boolean) as string[];
+ }
+
+ #getAutoSizeStrategy(): AutoSizeStrategy {
+ const width = this.width();
+ return this.fit() === 'width' || width
+ ? {
+ type: 'fitGridWidth',
+ }
+ : {
+ type: 'fitCellContents',
+ };
+ }
+
+ #getRowSelection(): RowSelectionOptions {
+ return this.multiselect()
+ ? {
+ checkboxes: true,
+ checkboxLocation: 'selectionColumn',
+ headerCheckbox: true,
+ mode: 'multiRow',
+ }
+ : {
+ checkboxes: false,
+ mode: 'singleRow',
+ };
+ }
+}
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/data-grid.module.ts b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.module.ts
new file mode 100644
index 0000000000..bb2bcee5f5
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/data-grid.module.ts
@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+
+import { SkyDataGridColumnComponent } from './data-grid-column.component';
+import { SkyDataGridComponent } from './data-grid.component';
+
+/**
+ * @preview
+ */
+@NgModule({
+ exports: [SkyDataGridComponent, SkyDataGridColumnComponent],
+ imports: [SkyDataGridComponent, SkyDataGridColumnComponent],
+})
+export class SkyDataGridModule {}
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.html b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.html
new file mode 100644
index 0000000000..8291d4c8a6
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.html
@@ -0,0 +1,251 @@
+@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()) {
+
+ }
+ }
+
+}
+
+@if (showFilteredMultiselectGrid()) {
+ Grid with filters and multiselect
+
+
+
+
+}
+
+@if (showFilteredGrid()) {
+ Grid with filters
+
+
+
+
+
+
+
+
+}
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.ts b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.ts
new file mode 100644
index 0000000000..def8e8aa79
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-test.component.ts
@@ -0,0 +1,200 @@
+import { JsonPipe } from '@angular/common';
+import {
+ ChangeDetectorRef,
+ Component,
+ inject,
+ input,
+ model,
+} from '@angular/core';
+import { SkyFilterBarFilterItem } from '@skyux/filter-bar';
+import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers';
+
+import { SkyDataGridFilterOperator } from '../../types/data-grid-filter-operator';
+import { SkyDataGridFilterValue } from '../../types/data-grid-filter-value';
+import { SkyDataGridRowDeleteCancelArgs } from '../../types/data-grid-row-delete-cancel-args';
+import { SkyDataGridRowDeleteConfirmArgs } from '../../types/data-grid-row-delete-confirm-args';
+import { SkyDataGridSort } from '../../types/data-grid-sort';
+import { SkyDataGridColumnComponent } from '../data-grid-column.component';
+import { SkyDataGridComponent } from '../data-grid.component';
+
+interface RowModel {
+ id: string;
+ column1: string;
+ column2: string;
+ column3: boolean;
+ myId?: string;
+}
+
+interface FilteredRowModel {
+ id: string;
+ column1: string;
+ column2: string | null;
+ column3: boolean;
+ numericColumn: number;
+ dateColumn: string;
+}
+
+@Component({
+ selector: 'app-data-grid-test',
+ templateUrl: './data-grid-test.component.html',
+ imports: [
+ SkyDataGridComponent,
+ SkyDataGridColumnComponent,
+ SkyPopoverModule,
+ SkyDropdownModule,
+ JsonPipe,
+ ],
+})
+export class DataGridTestComponent {
+ 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 dataForFilteredGrid: FilteredRowModel[] = [
+ {
+ id: '1',
+ column1: '1',
+ column2: 'Apple',
+ column3: true,
+ numericColumn: 100,
+ dateColumn: new Date('2024-01-15T00:00:00.000Z').toISOString(),
+ },
+ {
+ id: '2',
+ column1: '01',
+ column2: 'Banana',
+ column3: false,
+ numericColumn: 200,
+ dateColumn: new Date('2024-02-20T00:00:00.000Z').toISOString(),
+ },
+ {
+ id: '3',
+ column1: '11',
+ column2: 'Banana',
+ column3: true,
+ numericColumn: 150,
+ dateColumn: new Date('2024-03-10T00:00:00.000Z').toISOString(),
+ },
+ {
+ id: '4',
+ column1: '12',
+ column2: 'Daikon',
+ column3: false,
+ numericColumn: 250,
+ dateColumn: new Date('2024-04-05T00:00:00.000Z').toISOString(),
+ },
+ {
+ id: '5',
+ column1: '13',
+ column2: 'Edamame',
+ column3: true,
+ numericColumn: 175,
+ dateColumn: new Date('2024-05-25T00:00:00.000Z').toISOString(),
+ },
+ {
+ id: '6',
+ column1: '20',
+ column2: 'Fig',
+ column3: false,
+ numericColumn: 300,
+ dateColumn: new Date('2024-06-30T00:00:00.000Z').toISOString(),
+ },
+ {
+ id: '7',
+ column1: '21',
+ column2: 'Grape',
+ column3: true,
+ numericColumn: 125,
+ dateColumn: new Date('2024-07-12T00:00:00.000Z').toISOString(),
+ },
+ ];
+
+ public readonly displayedColumnIds = input([]);
+ public readonly appliedFilters = input<
+ SkyFilterBarFilterItem[]
+ >([]);
+ public readonly removeRowIds = model([]);
+ public readonly rowHighlightedId = model();
+ public readonly selectedRowIds = model([]);
+ public readonly showFilteredGrid = input(false);
+ protected readonly showFilteredMultiselectGrid = input(false);
+ public readonly externalRowCount = input();
+ public readonly visibleColumnIds = model([]);
+ public readonly textFilterOperator = input();
+ public readonly numberFilterOperator = input();
+ public readonly booleanFilterOperator = input();
+
+ public readonly sortField = model(undefined);
+
+ public readonly multiselect = input();
+ public readonly height = input();
+ public readonly pageSize = input();
+
+ public page = model(1);
+ 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: SkyDataGridRowDeleteCancelArgs): void {
+ // noop
+ }
+
+ public deleteItem(id: string): void {
+ this.removeRowIds.update((removeRowIds) => [id, ...removeRowIds]);
+ }
+
+ public finishRowDelete(confirmArgs: SkyDataGridRowDeleteConfirmArgs): 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/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.html b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.html
new file mode 100644
index 0000000000..46d244cb90
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.ts b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.ts
new file mode 100644
index 0000000000..be77a8dde4
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/data-grid/fixtures/data-grid-w-data-manager-test.component.ts
@@ -0,0 +1,90 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ OnInit,
+ inject,
+ model,
+} from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import {
+ SkyDataManagerModule,
+ SkyDataManagerService,
+ SkyDataManagerState,
+} from '@skyux/data-manager';
+
+import { SkyDataGridSort } from '../../types/data-grid-sort';
+import { SkyDataGridModule } from '../data-grid.module';
+
+interface RowModel {
+ id: string;
+ name: string | null;
+ category: string | null;
+}
+
+@Component({
+ selector: 'app-data-grid-w-data-manager-test',
+ templateUrl: './data-grid-w-data-manager-test.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [SkyDataGridModule, SkyDataManagerModule],
+ providers: [SkyDataManagerService],
+})
+export class DataGridWDataManagerTestComponent implements OnInit {
+ public data: RowModel[] = [
+ { id: '1', name: 'Apple', category: 'Fruit' },
+ { id: '2', name: 'Banana', category: 'Fruit' },
+ { id: '3', name: 'Carrot', category: 'Vegetable' },
+ { id: '4', name: null, category: null },
+ ];
+ public viewId = 'my-view';
+ public readonly dataManagerService = inject(SkyDataManagerService);
+
+ public displayedColumnIds: string[] | undefined = ['name', 'category'];
+ public searchEnabled = true;
+ public columnPickerEnabled = true;
+
+ public readonly dataManagerState = toSignal(
+ this.dataManagerService.getDataStateUpdates(
+ 'DataGridWDataManagerTestComponent',
+ ),
+ { initialValue: new SkyDataManagerState({}) },
+ );
+ public sortField = model();
+
+ public ngOnInit(): void {
+ this.dataManagerService.initDataManager({
+ activeViewId: this.viewId,
+ dataManagerConfig: {
+ sortOptions: [
+ {
+ id: 'name',
+ propertyName: 'name',
+ label: 'Name',
+ descending: false,
+ },
+ {
+ id: 'category',
+ propertyName: 'category',
+ label: 'Category',
+ descending: false,
+ },
+ ],
+ },
+ defaultDataState: new SkyDataManagerState({
+ views: [
+ {
+ viewId: this.viewId,
+ displayedColumnIds: this.displayedColumnIds,
+ },
+ ],
+ }),
+ });
+
+ this.dataManagerService.initDataView({
+ id: this.viewId,
+ name: 'Grid View',
+ iconName: 'table',
+ searchEnabled: this.searchEnabled,
+ columnPickerEnabled: this.columnPickerEnabled,
+ });
+ }
+}
diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-filter-operator.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-filter-operator.ts
new file mode 100644
index 0000000000..b6e51e7208
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/types/data-grid-filter-operator.ts
@@ -0,0 +1,17 @@
+/**
+ * Filter operators that can be used when applying filter-bar filters to AG Grid columns.
+ */
+export type SkyDataGridFilterOperator =
+ // Text operators
+ | 'contains'
+ | 'notContains'
+ | 'startsWith'
+ | 'endsWith'
+ // Number/Date operators
+ | 'lessThan'
+ | 'lessThanOrEqual'
+ | 'greaterThan'
+ | 'greaterThanOrEqual'
+ // Common operators
+ | 'equals'
+ | 'notEqual';
diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-filter-value.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-filter-value.ts
new file mode 100644
index 0000000000..eab086c082
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/types/data-grid-filter-value.ts
@@ -0,0 +1,12 @@
+import { SkyDateRange } from '@skyux/datetime';
+
+import { SkyDataGridNumberRangeFilterValue } from './data-grid-number-range-filter-value';
+
+export type SkyDataGridFilterValue =
+ | SkyDataGridNumberRangeFilterValue
+ | SkyDateRange
+ | boolean
+ | Date
+ | number
+ | string
+ | string[];
diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-number-range-filter-value.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-number-range-filter-value.ts
new file mode 100644
index 0000000000..b9e5145565
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/types/data-grid-number-range-filter-value.ts
@@ -0,0 +1,41 @@
+import { FormControl, FormGroup } from '@angular/forms';
+
+/**
+ * Filter value for number range filters.
+ */
+export type SkyDataGridNumberRangeFilterValue =
+ | {
+ /**
+ * The minimum value of the range.
+ */
+ from: number;
+ /**
+ * The maximum value of the range.
+ */
+ to: number;
+ }
+ | {
+ /**
+ * The minimum value of the range. Null indicates no minimum.
+ */
+ from: null | undefined;
+ /**
+ * The maximum value of the range.
+ */
+ to: number;
+ }
+ | {
+ /**
+ * The minimum value of the range.
+ */
+ from: number;
+ /**
+ * The maximum value of the range. Null indicates no maximum.
+ */
+ to: null | undefined;
+ };
+
+export type SkyDataGridNumberRangeFilterFormGroup = FormGroup<{
+ from: FormControl;
+ to: FormControl;
+}>;
diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-page-request.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-page-request.ts
new file mode 100644
index 0000000000..a6f5f6699a
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/types/data-grid-page-request.ts
@@ -0,0 +1,18 @@
+import { SkyDataGridSort } from './data-grid-sort';
+
+export interface SkyDataGridPageRequest {
+ /**
+ * The current page number (1-based).
+ */
+ pageNumber: number;
+
+ /**
+ * The number of items per page. When `undefined`, paging is not applied and all items are returned.
+ */
+ pageSize: number | undefined;
+
+ /**
+ * The data property and direction for sorting. When `undefined`, no sorting is applied.
+ */
+ sortField: SkyDataGridSort | undefined;
+}
diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-cancel-args.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-cancel-args.ts
new file mode 100644
index 0000000000..f90ea64e76
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-cancel-args.ts
@@ -0,0 +1,6 @@
+import { SkyAgGridRowDeleteCancelArgs } from '@skyux/ag-grid';
+
+/**
+ * Information regarding a row whose deletion has been cancelled.
+ */
+export type SkyDataGridRowDeleteCancelArgs = SkyAgGridRowDeleteCancelArgs;
diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-confirm-args.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-confirm-args.ts
new file mode 100644
index 0000000000..f62a160a5d
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/types/data-grid-row-delete-confirm-args.ts
@@ -0,0 +1,6 @@
+import { SkyAgGridRowDeleteConfirmArgs } from '@skyux/ag-grid';
+
+/**
+ * Information regarding a row whose deletion has been confirmed.
+ */
+export type SkyDataGridRowDeleteConfirmArgs = SkyAgGridRowDeleteConfirmArgs;
diff --git a/libs/components/data-grid/src/lib/modules/types/data-grid-sort.ts b/libs/components/data-grid/src/lib/modules/types/data-grid-sort.ts
new file mode 100644
index 0000000000..cc82d62454
--- /dev/null
+++ b/libs/components/data-grid/src/lib/modules/types/data-grid-sort.ts
@@ -0,0 +1,15 @@
+/**
+ * Used for applying sort to a `SkyDataGridComponent` as well as receiving updates when the data grid sorting changes.
+ */
+export interface SkyDataGridSort {
+ /**
+ * Whether to apply the sort in descending order.
+ * @required
+ */
+ descending?: boolean;
+ /**
+ * The data property to sort by.
+ * @required
+ */
+ fieldSelector: string;
+}
diff --git a/libs/components/data-grid/testing/eslint.config.js b/libs/components/data-grid/testing/eslint.config.js
new file mode 100644
index 0000000000..b4c331fb6a
--- /dev/null
+++ b/libs/components/data-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/data-grid/testing/karma.conf.js b/libs/components/data-grid/testing/karma.conf.js
new file mode 100644
index 0000000000..c3ee28e599
--- /dev/null
+++ b/libs/components/data-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/data-grid/testing',
+ ),
+ },
+ });
+};
diff --git a/libs/components/data-grid/testing/ng-package.json b/libs/components/data-grid/testing/ng-package.json
new file mode 100644
index 0000000000..fbafcc4448
--- /dev/null
+++ b/libs/components/data-grid/testing/ng-package.json
@@ -0,0 +1,5 @@
+{
+ "lib": {
+ "entryFile": "src/public-api.ts"
+ }
+}
diff --git a/libs/components/data-grid/testing/project.json b/libs/components/data-grid/testing/project.json
new file mode 100644
index 0000000000..ff334a4d1f
--- /dev/null
+++ b/libs/components/data-grid/testing/project.json
@@ -0,0 +1,47 @@
+{
+ "name": "data-grid-testing",
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "library",
+ "sourceRoot": "libs/components/data-grid/testing/src",
+ "prefix": "sky",
+ "tags": ["testing"],
+ "targets": {
+ "test": {
+ "executor": "@angular-devkit/build-angular:karma",
+ "options": {
+ "tsConfig": "libs/components/data-grid/testing/tsconfig.spec.json",
+ "karmaConfig": "libs/components/data-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/data-grid/testing/src/modules/data-grid/data-grid-harness.filters.ts b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.filters.ts
new file mode 100644
index 0000000000..695d6006c8
--- /dev/null
+++ b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.filters.ts
@@ -0,0 +1,8 @@
+import { SkyHarnessFilters } from '@skyux/core/testing';
+
+/**
+ * A set of criteria that can be used to filter a list of `SkyDataGridHarness` instances.
+ * @preview
+ */
+// eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-empty-object-type
+export interface SkyDataGridHarnessFilters extends SkyHarnessFilters {}
diff --git a/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.spec.ts b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.spec.ts
new file mode 100644
index 0000000000..c59b70f018
--- /dev/null
+++ b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.spec.ts
@@ -0,0 +1,99 @@
+import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SkyDataGridHarness } from './data-grid-harness';
+import { DataGridTestComponent } from './fixtures/data-grid-test.component';
+
+describe('data-grid-harness', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ fixture = TestBed.createComponent(DataGridTestComponent);
+ });
+
+ it('should check if the grid is ready', async () => {
+ fixture.componentRef.setInput('showAllColumns', false);
+ fixture.detectChanges();
+
+ const harness = await TestbedHarnessEnvironment.loader(fixture).getHarness(
+ SkyDataGridHarness.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(
+ SkyDataGridHarness.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(
+ SkyDataGridHarness.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/data-grid/testing/src/modules/data-grid/data-grid-harness.ts b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.ts
new file mode 100644
index 0000000000..a28be985e6
--- /dev/null
+++ b/libs/components/data-grid/testing/src/modules/data-grid/data-grid-harness.ts
@@ -0,0 +1,59 @@
+import { HarnessPredicate } from '@angular/cdk/testing';
+import { SkyAgGridWrapperHarness } from '@skyux/ag-grid/testing';
+import { SkyQueryableComponentHarness } from '@skyux/core/testing';
+
+import { SkyDataGridHarnessFilters } from './data-grid-harness.filters';
+
+/**
+ * Harness for interacting with SKY UX data grid components in tests.
+ * @preview
+ */
+export class SkyDataGridHarness extends SkyQueryableComponentHarness {
+ /**
+ * @internal
+ */
+ public static hostSelector = 'sky-data-grid';
+
+ /**
+ * Gets a `HarnessPredicate` that can be used to search for a
+ * `SkyDataGridHarness` that meets certain criteria
+ */
+ public static with(
+ filters: SkyDataGridHarnessFilters,
+ ): HarnessPredicate {
+ return SkyDataGridHarness.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/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.html b/libs/components/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.html
new file mode 100644
index 0000000000..8a466bffe9
--- /dev/null
+++ b/libs/components/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.html
@@ -0,0 +1,160 @@
+@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/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.ts b/libs/components/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.ts
new file mode 100644
index 0000000000..2a92e0abc2
--- /dev/null
+++ b/libs/components/data-grid/testing/src/modules/data-grid/fixtures/data-grid-test.component.ts
@@ -0,0 +1,115 @@
+import { JsonPipe } from '@angular/common';
+import {
+ ChangeDetectorRef,
+ Component,
+ inject,
+ input,
+ model,
+} from '@angular/core';
+import {
+ SkyDataGridColumnComponent,
+ SkyDataGridComponent,
+ SkyDataGridRowDeleteCancelArgs,
+ SkyDataGridRowDeleteConfirmArgs,
+} from '@skyux/data-grid';
+import { SkyDropdownModule, SkyPopoverModule } from '@skyux/popovers';
+
+interface RowModel {
+ id: string;
+ column1: string;
+ column2: string;
+ column3: boolean;
+ myId?: string;
+}
+
+@Component({
+ selector: 'app-data-grid-test',
+ templateUrl: './data-grid-test.component.html',
+ imports: [
+ SkyDataGridComponent,
+ SkyDataGridColumnComponent,
+ SkyPopoverModule,
+ SkyDropdownModule,
+ JsonPipe,
+ ],
+})
+export class DataGridTestComponent {
+ 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 displayedColumns = input([]);
+ 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: SkyDataGridRowDeleteCancelArgs): void {
+ // noop
+ }
+
+ public deleteItem(id: string): void {
+ this.removeRowIds.update((removeRowIds) => [id, ...removeRowIds]);
+ }
+
+ public finishRowDelete(confirmArgs: SkyDataGridRowDeleteConfirmArgs): 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/data-grid/testing/src/public-api.ts b/libs/components/data-grid/testing/src/public-api.ts
new file mode 100644
index 0000000000..9e0c6d26be
--- /dev/null
+++ b/libs/components/data-grid/testing/src/public-api.ts
@@ -0,0 +1,2 @@
+export { SkyDataGridHarness } from './modules/data-grid/data-grid-harness';
+export { SkyDataGridHarnessFilters } from './modules/data-grid/data-grid-harness.filters';
diff --git a/libs/components/data-grid/testing/tsconfig.spec.json b/libs/components/data-grid/testing/tsconfig.spec.json
new file mode 100644
index 0000000000..aad2a9f430
--- /dev/null
+++ b/libs/components/data-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/data-grid/tsconfig.json b/libs/components/data-grid/tsconfig.json
new file mode 100644
index 0000000000..c63981277b
--- /dev/null
+++ b/libs/components/data-grid/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../../tsconfig.base.json",
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.lib.prod.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ]
+}
diff --git a/libs/components/data-grid/tsconfig.lib.json b/libs/components/data-grid/tsconfig.lib.json
new file mode 100644
index 0000000000..4952dcda5f
--- /dev/null
+++ b/libs/components/data-grid/tsconfig.lib.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true
+ },
+ "exclude": ["src/test.ts", "**/*.spec.ts"],
+ "include": ["**/*.ts"]
+}
diff --git a/libs/components/data-grid/tsconfig.lib.prod.json b/libs/components/data-grid/tsconfig.lib.prod.json
new file mode 100644
index 0000000000..2a2faa884c
--- /dev/null
+++ b/libs/components/data-grid/tsconfig.lib.prod.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.lib.json",
+ "compilerOptions": {
+ "declarationMap": false
+ },
+ "angularCompilerOptions": {
+ "compilationMode": "partial"
+ }
+}
diff --git a/libs/components/data-grid/tsconfig.spec.json b/libs/components/data-grid/tsconfig.spec.json
new file mode 100644
index 0000000000..bb6f4b9ec9
--- /dev/null
+++ b/libs/components/data-grid/tsconfig.spec.json
@@ -0,0 +1,8 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "types": ["jasmine", "node"]
+ },
+ "include": ["**/*.spec.ts", "**/*.d.ts"]
+}
diff --git a/libs/components/data-manager/src/index.ts b/libs/components/data-manager/src/index.ts
index cba2a981a1..92f53159c5 100644
--- a/libs/components/data-manager/src/index.ts
+++ b/libs/components/data-manager/src/index.ts
@@ -22,6 +22,7 @@ export { SkyDataManagerDockType } from './lib/modules/data-manager/types/data-ma
// Obscure names are used to indicate types are not part of public API.
export { SkyDataManagerColumnPickerComponent as λ1 } from './lib/modules/data-manager/data-manager-column-picker/data-manager-column-picker.component';
export { SkyDataManagerFilterControllerDirective as λ9 } from './lib/modules/data-manager/data-manager-filters/data-manager-filter-controller.directive';
+export { SkyDataManagerHostControllerDirective as λ10 } from './lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive';
export { SkyDataManagerToolbarLeftItemComponent as λ3 } from './lib/modules/data-manager/data-manager-toolbar/data-manager-toolbar-left-item.component';
export { SkyDataManagerToolbarPrimaryItemComponent as λ4 } from './lib/modules/data-manager/data-manager-toolbar/data-manager-toolbar-primary-item.component';
export { SkyDataManagerToolbarRightItemComponent as λ5 } from './lib/modules/data-manager/data-manager-toolbar/data-manager-toolbar-right-item.component';
diff --git a/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.spec.ts b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.spec.ts
new file mode 100644
index 0000000000..354c3850ab
--- /dev/null
+++ b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.spec.ts
@@ -0,0 +1,703 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { SkyDataHost } from '@skyux/lists';
+
+import { SkyDataManagerService } from '../data-manager.service';
+import { SkyDataManagerState } from '../models/data-manager-state';
+
+import { SkyDataManagerHostControllerDirective } from './data-manager-host-controller.directive';
+import { SkyDataManagerHostService } from './data-manager-host.service';
+
+@Component({
+ template: `
`,
+ imports: [SkyDataManagerHostControllerDirective],
+})
+class TestHostComponent {
+ public viewId = 'test-view';
+}
+
+describe('SkyDataManagerHostControllerDirective', () => {
+ let fixture: ComponentFixture;
+ let component: TestHostComponent;
+ let dataManagerService: SkyDataManagerService;
+ let adapterService: SkyDataManagerHostService;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [TestHostComponent],
+ providers: [SkyDataManagerService],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(TestHostComponent);
+ component = fixture.componentInstance;
+ dataManagerService = TestBed.inject(SkyDataManagerService);
+ fixture.detectChanges();
+
+ const directiveDebugEl = fixture.debugElement.query(
+ By.directive(SkyDataManagerHostControllerDirective),
+ );
+ adapterService = directiveDebugEl.injector.get(SkyDataManagerHostService);
+ });
+
+ it('should create and provide the adapter service', () => {
+ expect(component).toBeTruthy();
+ expect(adapterService).toBeTruthy();
+ expect(adapterService).toBeInstanceOf(SkyDataManagerHostService);
+ });
+
+ it('should update the adapter when the data manager state includes the view', async () => {
+ const spy = spyOn(adapterService, 'updateDataHost').and.callThrough();
+
+ const newState = new SkyDataManagerState({
+ activeSortOption: undefined,
+ searchText: 'search-text',
+ selectedIds: ['1', '2'],
+ views: [
+ {
+ viewId: component.viewId,
+ displayedColumnIds: ['col-1'],
+ additionalData: {
+ page: 3,
+ },
+ },
+ ],
+ });
+
+ const sourceId = 'host-controller-test';
+ dataManagerService.updateDataState(newState, sourceId);
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [hostState, id] = spy.calls.mostRecent().args;
+ expect(id).toBe('skyDataManagerHostController');
+ expect(hostState).toEqual(
+ jasmine.objectContaining({
+ id: component.viewId,
+ searchText: 'search-text',
+ selectedIds: ['1', '2'],
+ page: 3,
+ displayedColumnIds: ['col-1'],
+ activeSortOption: undefined,
+ }),
+ );
+ });
+
+ it('should propagate adapter changes back into the data manager state', async () => {
+ dataManagerService.updateDataManagerConfig({
+ sortOptions: [
+ {
+ id: 'name-sort',
+ label: 'Name (A-Z)',
+ propertyName: 'name',
+ descending: false,
+ },
+ {
+ id: 'name-sort-za',
+ label: 'Name (Z-A)',
+ propertyName: 'name',
+ descending: true,
+ },
+ ],
+ });
+
+ const initialState = new SkyDataManagerState({
+ activeSortOption: {
+ id: 'name-sort-za',
+ label: 'Name',
+ propertyName: 'name',
+ descending: true,
+ },
+ searchText: 'initial-search',
+ views: [
+ {
+ viewId: component.viewId,
+ },
+ ],
+ });
+
+ dataManagerService.updateDataState(initialState, 'initial');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const spy = spyOn(dataManagerService, 'updateDataState').and.callThrough();
+
+ const hostState: SkyDataHost = {
+ id: component.viewId,
+ displayedColumnIds: ['updated-1', 'updated-2'],
+ page: 4,
+ selectedIds: [],
+ searchText: undefined,
+ activeSortOption: { propertyName: 'name', descending: false },
+ };
+
+ adapterService.updateDataHost(hostState, 'host-list');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy.calls.count()).toBeGreaterThan(0);
+ const [updatedState, source] = spy.calls.mostRecent().args;
+ expect(source).toBe('skyDataManagerHostController--test-view');
+ expect(updatedState.searchText).toBeUndefined();
+ expect(updatedState.activeSortOption?.propertyName).toBe('name');
+ expect(updatedState.activeSortOption?.descending).toBeFalse();
+
+ const viewState = updatedState.getViewStateById(component.viewId);
+ expect(viewState?.displayedColumnIds).toEqual(['updated-1', 'updated-2']);
+ expect(viewState?.additionalData?.page).toBe(4);
+ });
+
+ describe('distinct changes for data host updates', () => {
+ it('should not update adapter when data manager state has no changes', async () => {
+ const initialState = new SkyDataManagerState({
+ searchText: 'search-text',
+ selectedIds: ['1', '2'],
+ views: [
+ {
+ viewId: component.viewId,
+ displayedColumnIds: ['col-1'],
+ additionalData: {
+ page: 1,
+ },
+ },
+ ],
+ });
+
+ dataManagerService.updateDataState(initialState, 'initial');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // Wait for the initial sync to the adapter to complete
+ // The directive will have updated the adapter once with initial values
+ const spy = spyOn(adapterService, 'updateDataHost').and.callThrough();
+
+ // Update with identical state values
+ const identicalState = new SkyDataManagerState({
+ searchText: 'search-text',
+ selectedIds: ['1', '2'],
+ views: [
+ {
+ viewId: component.viewId,
+ displayedColumnIds: ['col-1'],
+ additionalData: {
+ page: 1,
+ },
+ },
+ ],
+ });
+
+ dataManagerService.updateDataState(identicalState, 'duplicate-update');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // The directive should not update the adapter since the computed host state is identical
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('should update adapter when searchText changes', async () => {
+ const initialState = new SkyDataManagerState({
+ searchText: 'initial-search',
+ views: [{ viewId: component.viewId }],
+ });
+
+ dataManagerService.updateDataState(initialState, 'initial');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const spy = spyOn(adapterService, 'updateDataHost').and.callThrough();
+
+ const updatedState = new SkyDataManagerState({
+ searchText: 'updated-search',
+ views: [{ viewId: component.viewId }],
+ });
+
+ dataManagerService.updateDataState(updatedState, 'search-update');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [hostState] = spy.calls.mostRecent().args;
+ expect(hostState.searchText).toBe('updated-search');
+ });
+
+ it('should update adapter when page changes', async () => {
+ const initialState = new SkyDataManagerState({
+ views: [
+ {
+ viewId: component.viewId,
+ additionalData: { page: 1 },
+ },
+ ],
+ });
+
+ dataManagerService.updateDataState(initialState, 'initial');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const spy = spyOn(adapterService, 'updateDataHost').and.callThrough();
+
+ const updatedState = new SkyDataManagerState({
+ views: [
+ {
+ viewId: component.viewId,
+ additionalData: { page: 2 },
+ },
+ ],
+ });
+
+ dataManagerService.updateDataState(updatedState, 'page-update');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [hostState] = spy.calls.mostRecent().args;
+ expect(hostState.page).toBe(2);
+ });
+
+ it('should update adapter when displayedColumnIds changes', async () => {
+ const initialState = new SkyDataManagerState({
+ views: [
+ {
+ viewId: component.viewId,
+ displayedColumnIds: ['col-1', 'col-2'],
+ },
+ ],
+ });
+
+ dataManagerService.updateDataState(initialState, 'initial');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const spy = spyOn(adapterService, 'updateDataHost').and.callThrough();
+
+ const updatedState = new SkyDataManagerState({
+ views: [
+ {
+ viewId: component.viewId,
+ displayedColumnIds: ['col-1', 'col-3'],
+ },
+ ],
+ });
+
+ dataManagerService.updateDataState(updatedState, 'columns-update');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [hostState] = spy.calls.mostRecent().args;
+ expect(hostState.displayedColumnIds).toEqual(['col-1', 'col-3']);
+ });
+
+ it('should update adapter when displayedColumnIds length changes', async () => {
+ const initialState = new SkyDataManagerState({
+ views: [
+ {
+ viewId: component.viewId,
+ displayedColumnIds: ['col-1', 'col-2'],
+ },
+ ],
+ });
+
+ dataManagerService.updateDataState(initialState, 'initial');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const spy = spyOn(adapterService, 'updateDataHost').and.callThrough();
+
+ const updatedState = new SkyDataManagerState({
+ views: [
+ {
+ viewId: component.viewId,
+ displayedColumnIds: ['col-1'],
+ },
+ ],
+ });
+
+ dataManagerService.updateDataState(updatedState, 'columns-length-update');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [hostState] = spy.calls.mostRecent().args;
+ expect(hostState.displayedColumnIds).toEqual(['col-1']);
+ });
+
+ it('should update adapter when selectedIds changes', async () => {
+ const initialState = new SkyDataManagerState({
+ selectedIds: ['1', '2'],
+ views: [{ viewId: component.viewId }],
+ });
+
+ dataManagerService.updateDataState(initialState, 'initial');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const spy = spyOn(adapterService, 'updateDataHost').and.callThrough();
+
+ const updatedState = new SkyDataManagerState({
+ selectedIds: ['1', '3'],
+ views: [{ viewId: component.viewId }],
+ });
+
+ dataManagerService.updateDataState(updatedState, 'selected-update');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [hostState] = spy.calls.mostRecent().args;
+ expect(hostState.selectedIds).toEqual(['1', '3']);
+ });
+
+ it('should update adapter when selectedIds length changes', async () => {
+ const initialState = new SkyDataManagerState({
+ selectedIds: ['1', '2'],
+ views: [{ viewId: component.viewId }],
+ });
+
+ dataManagerService.updateDataState(initialState, 'initial');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const spy = spyOn(adapterService, 'updateDataHost').and.callThrough();
+
+ const updatedState = new SkyDataManagerState({
+ selectedIds: ['1'],
+ views: [{ viewId: component.viewId }],
+ });
+
+ dataManagerService.updateDataState(
+ updatedState,
+ 'selected-length-update',
+ );
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [hostState] = spy.calls.mostRecent().args;
+ expect(hostState.selectedIds).toEqual(['1']);
+ });
+
+ it('should update adapter when activeSortOption propertyName changes', async () => {
+ const initialState = new SkyDataManagerState({
+ activeSortOption: {
+ id: '1',
+ label: 'Name',
+ propertyName: 'name',
+ descending: false,
+ },
+ views: [{ viewId: component.viewId }],
+ });
+
+ dataManagerService.updateDataState(initialState, 'initial');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const spy = spyOn(adapterService, 'updateDataHost').and.callThrough();
+
+ const updatedState = new SkyDataManagerState({
+ activeSortOption: {
+ id: '2',
+ label: 'Date',
+ propertyName: 'date',
+ descending: false,
+ },
+ views: [{ viewId: component.viewId }],
+ });
+
+ dataManagerService.updateDataState(updatedState, 'sort-property-update');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [hostState] = spy.calls.mostRecent().args;
+ expect(hostState.activeSortOption?.propertyName).toBe('date');
+ });
+
+ it('should update adapter when activeSortOption descending changes', async () => {
+ const initialState = new SkyDataManagerState({
+ activeSortOption: {
+ id: '1',
+ label: 'Name',
+ propertyName: 'name',
+ descending: false,
+ },
+ views: [{ viewId: component.viewId }],
+ });
+
+ dataManagerService.updateDataState(initialState, 'initial');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const spy = spyOn(adapterService, 'updateDataHost').and.callThrough();
+
+ const updatedState = new SkyDataManagerState({
+ activeSortOption: {
+ id: '1',
+ label: 'Name',
+ propertyName: 'name',
+ descending: true,
+ },
+ views: [{ viewId: component.viewId }],
+ });
+
+ dataManagerService.updateDataState(
+ updatedState,
+ 'sort-descending-update',
+ );
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [hostState] = spy.calls.mostRecent().args;
+ expect(hostState.activeSortOption?.descending).toBeTrue();
+ });
+ });
+
+ describe('distinct changes for data manager state updates', () => {
+ async function setupInitialState(): Promise {
+ dataManagerService.updateDataManagerConfig({
+ sortOptions: [
+ {
+ id: 'name-sort',
+ label: 'Name',
+ propertyName: 'name',
+ descending: false,
+ },
+ {
+ id: 'name-sort',
+ label: 'Name',
+ propertyName: 'name',
+ descending: true,
+ },
+ {
+ id: 'date-sort',
+ label: 'Date',
+ propertyName: 'date',
+ descending: false,
+ },
+ {
+ id: 'date-sort',
+ label: 'Date',
+ propertyName: 'date',
+ descending: true,
+ },
+ ],
+ });
+
+ const initialState = new SkyDataManagerState({
+ activeSortOption: {
+ id: 'name-sort',
+ label: 'Name',
+ propertyName: 'name',
+ descending: false,
+ },
+ searchText: 'initial-search',
+ views: [
+ {
+ viewId: component.viewId,
+ displayedColumnIds: ['col-1', 'col-2'],
+ additionalData: { page: 1 },
+ },
+ ],
+ });
+
+ dataManagerService.updateDataState(initialState, 'initial');
+ fixture.detectChanges();
+ await fixture.whenStable();
+ }
+
+ it('should not echo data manager state changes back to data manager', async () => {
+ await setupInitialState();
+
+ const spy = spyOn(
+ dataManagerService,
+ 'updateDataState',
+ ).and.callThrough();
+
+ // When data manager state changes, the directive converts it and sends to adapter.
+ // Verify that this converted state doesn't echo back to data manager.
+ const updatedState = new SkyDataManagerState({
+ activeSortOption: {
+ id: 'name-sort',
+ label: 'Name',
+ propertyName: 'name',
+ descending: false,
+ },
+ searchText: 'initial-search',
+ views: [
+ {
+ viewId: component.viewId,
+ displayedColumnIds: ['col-1', 'col-2'],
+ additionalData: { page: 2 },
+ },
+ ],
+ });
+
+ dataManagerService.updateDataState(updatedState, 'external-source');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // The directive should have updated the adapter, but that adapter update
+ // should not cause an echo back to data manager since the values are equivalent
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy).toHaveBeenCalledWith(jasmine.anything(), 'external-source');
+ });
+
+ it('should not echo adapter state changes back to adapter', async () => {
+ await setupInitialState();
+
+ const adapterSpy = spyOn(
+ adapterService,
+ 'updateDataHost',
+ ).and.callThrough();
+
+ // Update adapter with new displayedColumnIds
+ const updatedHostState: SkyDataHost = {
+ id: component.viewId,
+ displayedColumnIds: ['col-1', 'col-3'],
+ page: 1,
+ searchText: 'initial-search',
+ activeSortOption: { propertyName: 'name', descending: false },
+ selectedIds: undefined,
+ };
+
+ adapterService.updateDataHost(updatedHostState, 'external-source');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ // The adapter was called once directly, but the update to data manager
+ // should not cause the directive to echo back to adapter
+ expect(adapterSpy).toHaveBeenCalledTimes(1);
+ expect(adapterSpy).toHaveBeenCalledWith(
+ updatedHostState,
+ 'external-source',
+ );
+ });
+
+ it('should update data manager when displayedColumnIds changes from adapter', async () => {
+ await setupInitialState();
+
+ const spy = spyOn(
+ dataManagerService,
+ 'updateDataState',
+ ).and.callThrough();
+
+ const updatedHostState: SkyDataHost = {
+ id: component.viewId,
+ displayedColumnIds: ['col-1', 'col-3'],
+ page: 1,
+ searchText: 'initial-search',
+ activeSortOption: { propertyName: 'name', descending: false },
+ selectedIds: ['1', '2', '3'],
+ };
+
+ adapterService.updateDataHost(updatedHostState, 'external-source');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [updatedState] = spy.calls.mostRecent().args;
+ expect(updatedState?.selectedIds).toEqual(['1', '2', '3']);
+ const viewState = updatedState.getViewStateById(component.viewId);
+ expect(viewState?.displayedColumnIds).toEqual(['col-1', 'col-3']);
+
+ const updatedHostState2: SkyDataHost = {
+ id: component.viewId,
+ displayedColumnIds: ['col-1', 'col-3'],
+ page: 1,
+ searchText: 'initial-search',
+ activeSortOption: { propertyName: 'name', descending: false },
+ selectedIds: ['3', '4', '5'],
+ };
+
+ adapterService.updateDataHost(updatedHostState2, 'external-source');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [updatedState2] = spy.calls.mostRecent().args;
+ expect(updatedState2?.selectedIds).toEqual(['3', '4', '5']);
+ });
+
+ it('should update data manager when page changes from adapter', async () => {
+ await setupInitialState();
+
+ const spy = spyOn(
+ dataManagerService,
+ 'updateDataState',
+ ).and.callThrough();
+
+ const updatedHostState: SkyDataHost = {
+ id: component.viewId,
+ displayedColumnIds: ['col-1', 'col-2'],
+ page: 5,
+ searchText: 'initial-search',
+ activeSortOption: { propertyName: 'name', descending: false },
+ selectedIds: undefined,
+ };
+
+ adapterService.updateDataHost(updatedHostState, 'external-source');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [updatedState] = spy.calls.mostRecent().args;
+ const viewState = updatedState.getViewStateById(component.viewId);
+ expect(viewState?.additionalData?.page).toBe(5);
+ });
+
+ it('should update data manager when activeSortOption propertyName changes from adapter', async () => {
+ await setupInitialState();
+
+ const spy = spyOn(
+ dataManagerService,
+ 'updateDataState',
+ ).and.callThrough();
+
+ const updatedHostState: SkyDataHost = {
+ id: component.viewId,
+ displayedColumnIds: ['col-1', 'col-2'],
+ page: 1,
+ searchText: 'initial-search',
+ activeSortOption: { propertyName: 'date', descending: false },
+ selectedIds: undefined,
+ };
+
+ adapterService.updateDataHost(updatedHostState, 'external-source');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [updatedState] = spy.calls.mostRecent().args;
+ expect(updatedState.activeSortOption?.propertyName).toBe('date');
+ });
+
+ it('should update data manager when displayedColumnIds length changes from adapter', async () => {
+ await setupInitialState();
+
+ const spy = spyOn(
+ dataManagerService,
+ 'updateDataState',
+ ).and.callThrough();
+
+ const updatedHostState: SkyDataHost = {
+ id: component.viewId,
+ displayedColumnIds: ['col-1'],
+ page: 1,
+ searchText: 'initial-search',
+ activeSortOption: { propertyName: 'name', descending: false },
+ selectedIds: undefined,
+ };
+
+ adapterService.updateDataHost(updatedHostState, 'external-source');
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(spy).toHaveBeenCalled();
+ const [updatedState] = spy.calls.mostRecent().args;
+ const viewState = updatedState.getViewStateById(component.viewId);
+ expect(viewState?.displayedColumnIds).toEqual(['col-1']);
+ });
+ });
+});
diff --git a/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.ts b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.ts
new file mode 100644
index 0000000000..a1074dbef1
--- /dev/null
+++ b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host-controller.directive.ts
@@ -0,0 +1,237 @@
+import {
+ Directive,
+ computed,
+ effect,
+ inject,
+ input,
+ linkedSignal,
+ untracked,
+} from '@angular/core';
+import { toObservable, toSignal } from '@angular/core/rxjs-interop';
+import { SkyDataHost, SkyDataHostService } from '@skyux/lists';
+
+import { filter, map, switchMap } from 'rxjs';
+
+import { SkyDataManagerFilterControllerDirective } from '../data-manager-filters/data-manager-filter-controller.directive';
+import { SkyDataManagerService } from '../data-manager.service';
+import { SkyDataManagerSortOption } from '../models/data-manager-sort-option';
+import { SkyDataManagerState } from '../models/data-manager-state';
+import { SkyDataViewState } from '../models/data-view-state';
+
+import { SkyDataManagerHostService } from './data-manager-host.service';
+
+/**
+ * A directive applied to a data-displaying component (like `sky-data-grid`) that enables integration with a data manager.
+ * This directive synchronizes the component's data state with the data manager's state, including displayed columns,
+ * sort order, current page, and selected rows.
+ */
+@Directive({
+ selector: '[skyDataManagerHostController]',
+ providers: [
+ SkyDataManagerHostService,
+ {
+ provide: SkyDataHostService,
+ useExisting: SkyDataManagerHostService,
+ },
+ ],
+ hostDirectives: [SkyDataManagerFilterControllerDirective],
+})
+export class SkyDataManagerHostControllerDirective {
+ /**
+ * The view ID for the data view in the data manager.
+ * @required
+ */
+ public readonly viewId = input.required();
+
+ readonly #sourceId = 'skyDataManagerHostController';
+ readonly #dataManagerSourceId = computed(
+ () => `${this.#sourceId}--${this.viewId()}`,
+ );
+ readonly #dataManagerService = inject(SkyDataManagerService);
+ readonly #dataManagerSortOptions = toSignal<
+ SkyDataManagerSortOption[],
+ SkyDataManagerSortOption[]
+ >(
+ this.#dataManagerService
+ .getDataManagerConfigUpdates()
+ .pipe(map((options) => options.sortOptions ?? [])),
+ { initialValue: [] },
+ );
+ readonly #dataManagerState = toSignal(
+ toObservable(this.#dataManagerSourceId).pipe(
+ filter(Boolean),
+ switchMap((sourceId) =>
+ this.#dataManagerService.getDataStateUpdates(sourceId),
+ ),
+ ),
+ { initialValue: new SkyDataManagerState({}) },
+ );
+ readonly #adapterService = inject(SkyDataManagerHostService);
+ readonly #dataHostState = toSignal(
+ this.#adapterService.getDataHostUpdates(this.#sourceId),
+ );
+ readonly #lastSentDataHost = linkedSignal(() => this.#dataHostState());
+ readonly #lastSentDataManagerState = linkedSignal(() =>
+ this.#dataManagerState(),
+ );
+
+ constructor() {
+ effect(() => {
+ const dataManagerState = this.#dataManagerState();
+ const lastSentDataHost = untracked(() => this.#lastSentDataHost());
+ const newState = this.#dataHostStateFromDataManagerState(
+ dataManagerState,
+ lastSentDataHost,
+ );
+ if (
+ newState &&
+ (!lastSentDataHost ||
+ this.#dataHostHasChanges(lastSentDataHost, newState))
+ ) {
+ this.#lastSentDataHost.set(newState);
+ this.#adapterService.updateDataHost(newState, this.#sourceId);
+ }
+ });
+ effect(() => {
+ const lastSentDataManagerState = untracked(() =>
+ this.#lastSentDataManagerState(),
+ );
+ const dataHost = this.#dataHostState();
+ if (lastSentDataManagerState && dataHost) {
+ const newDataManagerState = this.#dataManagerStateFromDataHostState(
+ lastSentDataManagerState,
+ dataHost,
+ );
+ if (
+ this.#dataManagerStateHasChanges(
+ lastSentDataManagerState,
+ newDataManagerState,
+ )
+ ) {
+ this.#lastSentDataManagerState.set(newDataManagerState);
+ this.#dataManagerService.updateDataState(
+ newDataManagerState,
+ this.#dataManagerSourceId(),
+ );
+ }
+ }
+ });
+ }
+
+ #dataHostStateFromDataManagerState(
+ dataState: SkyDataManagerState,
+ dataHost: SkyDataHost | undefined,
+ ): SkyDataHost | undefined {
+ const viewId = this.viewId();
+ const viewState = dataState.getViewStateById(viewId);
+
+ if (viewState) {
+ return {
+ activeSortOption: dataState.activeSortOption
+ ? {
+ propertyName: dataState.activeSortOption.propertyName,
+ descending: dataState.activeSortOption.descending,
+ }
+ : undefined,
+ displayedColumnIds:
+ viewState.displayedColumnIds.length > 0
+ ? viewState.displayedColumnIds
+ : (dataHost?.displayedColumnIds ?? []),
+ id: viewId,
+ page: Number(viewState.additionalData?.page ?? dataHost?.page ?? 1),
+ searchText: dataState.searchText,
+ selectedIds: dataState.selectedIds,
+ };
+ }
+ return undefined;
+ }
+
+ #dataManagerStateFromDataHostState(
+ dataState: SkyDataManagerState,
+ dataHost: SkyDataHost,
+ ): SkyDataManagerState {
+ const id = this.viewId();
+ const activeSortOption = this.#dataManagerSortOptions().find(
+ (option) =>
+ option.propertyName === dataHost.activeSortOption?.propertyName &&
+ option.descending === !!dataHost.activeSortOption?.descending,
+ );
+ const viewState = dataState.getViewStateById(id);
+ return new SkyDataManagerState({
+ ...dataState,
+ activeSortOption,
+ searchText: dataHost.searchText,
+ selectedIds: dataHost.selectedIds,
+ views: [
+ ...dataState.views.filter(({ viewId }) => viewId !== id),
+ new SkyDataViewState({
+ viewId: id,
+ columnIds: viewState?.columnIds,
+ columnWidths: viewState?.columnWidths,
+ displayedColumnIds: dataHost.displayedColumnIds,
+ additionalData: {
+ ...(viewState?.additionalData ?? {}),
+ page: dataHost.page,
+ },
+ }),
+ ],
+ });
+ }
+
+ #dataHostHasChanges(
+ oldDataHost: SkyDataHost,
+ newDataHost: SkyDataHost,
+ ): boolean {
+ return (
+ String(oldDataHost.activeSortOption?.propertyName) !==
+ String(newDataHost.activeSortOption?.propertyName) ||
+ !!oldDataHost.activeSortOption?.descending !==
+ !!newDataHost.activeSortOption?.descending ||
+ oldDataHost.id !== newDataHost.id ||
+ Number(oldDataHost.page) !== Number(newDataHost.page) ||
+ String(oldDataHost.searchText) !== String(newDataHost.searchText) ||
+ oldDataHost.displayedColumnIds.length !==
+ newDataHost.displayedColumnIds.length ||
+ oldDataHost.displayedColumnIds.some(
+ (id, idx) => String(id) !== String(newDataHost.displayedColumnIds[idx]),
+ ) ||
+ Number(oldDataHost.selectedIds?.length) !==
+ Number(newDataHost.selectedIds?.length) ||
+ !!oldDataHost.selectedIds?.some(
+ (id, idx) => id !== newDataHost.selectedIds?.[idx],
+ )
+ );
+ }
+
+ #dataManagerStateHasChanges(
+ oldDataManagerState: SkyDataManagerState,
+ newDataManagerState: SkyDataManagerState,
+ ): boolean {
+ const viewId = this.viewId();
+ const oldDataViewState = oldDataManagerState.getViewStateById(viewId);
+ const newDataViewState = newDataManagerState.getViewStateById(viewId);
+ return (
+ String(oldDataManagerState.activeSortOption?.propertyName) !==
+ String(newDataManagerState.activeSortOption?.propertyName) ||
+ !!oldDataManagerState.activeSortOption?.descending !==
+ !!newDataManagerState.activeSortOption?.descending ||
+ !!oldDataViewState?.additionalData !==
+ !!newDataViewState?.additionalData ||
+ Number(oldDataViewState?.additionalData?.page) !==
+ Number(newDataViewState?.additionalData?.page) ||
+ String(oldDataManagerState.searchText) !==
+ String(newDataManagerState.searchText) ||
+ Number(oldDataManagerState.selectedIds?.length ?? 0) !==
+ Number(newDataManagerState.selectedIds?.length ?? 0) ||
+ !!oldDataManagerState.selectedIds?.some(
+ (id, idx) =>
+ String(id) !== String(newDataManagerState.selectedIds?.[idx]),
+ ) ||
+ Number(oldDataViewState?.displayedColumnIds?.length) !==
+ Number(newDataViewState?.displayedColumnIds?.length) ||
+ !!oldDataViewState?.displayedColumnIds?.some(
+ (id, idx) => id !== newDataViewState?.displayedColumnIds?.[idx],
+ )
+ );
+ }
+}
diff --git a/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host.service.ts b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host.service.ts
new file mode 100644
index 0000000000..2fd3212f5c
--- /dev/null
+++ b/libs/components/data-manager/src/lib/modules/data-manager/data-manager-host/data-manager-host.service.ts
@@ -0,0 +1,32 @@
+import { Injectable, signal } from '@angular/core';
+import { SkyDataHost, SkyDataHostService } from '@skyux/lists';
+
+import { Observable, ReplaySubject } from 'rxjs';
+import { filter, map } from 'rxjs/operators';
+
+interface SkyDataHostChange {
+ dataHost: SkyDataHost;
+ sourceId: string;
+}
+
+@Injectable()
+export class SkyDataManagerHostService extends SkyDataHostService {
+ readonly #dataHostChange = new ReplaySubject(1);
+ readonly #dataHostId = signal('');
+
+ public override hostId = this.#dataHostId.asReadonly();
+
+ public getDataHostUpdates(sourceId: string): Observable {
+ return this.#dataHostChange.pipe(
+ filter((c) => c.sourceId !== sourceId),
+ map((c) => c.dataHost),
+ );
+ }
+
+ public updateDataHost(dataHost: SkyDataHost, sourceId: string): void {
+ if (dataHost.id) {
+ this.#dataHostId.set(dataHost.id);
+ }
+ this.#dataHostChange.next({ dataHost, sourceId });
+ }
+}
diff --git a/libs/components/data-manager/src/lib/modules/data-manager/data-manager.module.ts b/libs/components/data-manager/src/lib/modules/data-manager/data-manager.module.ts
index 025f9b2ce1..cf80e3bdc7 100644
--- a/libs/components/data-manager/src/lib/modules/data-manager/data-manager.module.ts
+++ b/libs/components/data-manager/src/lib/modules/data-manager/data-manager.module.ts
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { SKY_DATA_MANAGER_COLUMN_PICKER_PROVIDERS } from './data-manager-column-picker/data-manager-column-picker-providers';
import { SkyDataManagerFilterControllerDirective } from './data-manager-filters/data-manager-filter-controller.directive';
+import { SkyDataManagerHostControllerDirective } from './data-manager-host/data-manager-host-controller.directive';
import { SkyDataManagerToolbarLeftItemComponent } from './data-manager-toolbar/data-manager-toolbar-left-item.component';
import { SkyDataManagerToolbarPrimaryItemComponent } from './data-manager-toolbar/data-manager-toolbar-primary-item.component';
import { SkyDataManagerToolbarRightItemComponent } from './data-manager-toolbar/data-manager-toolbar-right-item.component';
@@ -14,6 +15,7 @@ import { SkyDataViewComponent } from './data-view.component';
imports: [
SkyDataManagerComponent,
SkyDataManagerFilterControllerDirective,
+ SkyDataManagerHostControllerDirective,
SkyDataManagerToolbarComponent,
SkyDataManagerToolbarLeftItemComponent,
SkyDataManagerToolbarPrimaryItemComponent,
@@ -24,6 +26,7 @@ import { SkyDataViewComponent } from './data-view.component';
exports: [
SkyDataManagerComponent,
SkyDataManagerFilterControllerDirective,
+ SkyDataManagerHostControllerDirective,
SkyDataManagerToolbarComponent,
SkyDataManagerToolbarLeftItemComponent,
SkyDataManagerToolbarPrimaryItemComponent,
diff --git a/libs/components/lists/src/index.ts b/libs/components/lists/src/index.ts
index 39dee804b2..1759bc1173 100644
--- a/libs/components/lists/src/index.ts
+++ b/libs/components/lists/src/index.ts
@@ -10,6 +10,9 @@ export { SkyPagingContentChangeArgs } from './lib/modules/paging/types/paging-co
export { SkyRepeaterExpandModeType } from './lib/modules/repeater/repeater-expand-mode-type';
export { SkyRepeaterModule } from './lib/modules/repeater/repeater.module';
+export { SkyDataHost } from './lib/modules/shared/data-host-service/data-host';
+export { SkyDataHostService } from './lib/modules/shared/data-host-service/data-host.service';
+
export { SkyFilterStateService } from './lib/modules/shared/filter-state-service/filter-state.service';
export { SkyFilterState } from './lib/modules/shared/filter-state-service/filter-state';
export { SkyFilterStateFilterItem } from './lib/modules/shared/filter-state-service/filter-state-filter-item';
diff --git a/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.service.ts b/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.service.ts
new file mode 100644
index 0000000000..d8d6ff552b
--- /dev/null
+++ b/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.service.ts
@@ -0,0 +1,21 @@
+import { Signal } from '@angular/core';
+
+import { Observable } from 'rxjs';
+
+import { SkyDataHost } from './data-host';
+
+export abstract class SkyDataHostService {
+ public abstract hostId: Signal;
+
+ /**
+ * Subscribe to data host updates that did not originate from the given source ID.
+ * This mirrors the data manager service pattern to avoid update loops between participants.
+ */
+ public abstract getDataHostUpdates(sourceId: string): Observable;
+
+ /**
+ * Updates the data hosts and broadcasts the change to subscribers.
+ * Implementations should emit a new value to `filterStateChange` with the provided data.
+ */
+ public abstract updateDataHost(host: SkyDataHost, sourceId: string): void;
+}
diff --git a/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.ts b/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.ts
new file mode 100644
index 0000000000..7a3012c4c0
--- /dev/null
+++ b/libs/components/lists/src/lib/modules/shared/data-host-service/data-host.ts
@@ -0,0 +1,50 @@
+/**
+ * Represents data state for components that can integrate with a data manager.
+ * This interface provides a standard way for data-displaying components (like data grids)
+ * to communicate their state with data management systems.
+ * @internal
+ */
+export interface SkyDataHost {
+ /**
+ * The data property and direction for sorting.
+ */
+ activeSortOption:
+ | {
+ /**
+ * The data property to sort by.
+ * @required
+ */
+ propertyName: string;
+ /**
+ * Whether to apply the sort in descending order.
+ * @required
+ */
+ descending: boolean;
+ }
+ | undefined;
+
+ /**
+ * The IDs or fields for columns to display.
+ */
+ displayedColumnIds: string[];
+
+ /**
+ * An identifier for the data host.
+ */
+ id: string;
+
+ /**
+ * The current page number (1-based).
+ */
+ page: number | undefined;
+
+ /**
+ * The search text to apply.
+ */
+ searchText: string | undefined;
+
+ /**
+ * The currently selected rows or objects.
+ */
+ selectedIds: string[] | undefined;
+}
diff --git a/libs/components/manifest/project.json b/libs/components/manifest/project.json
index 298b75f82b..ba4e90ead0 100644
--- a/libs/components/manifest/project.json
+++ b/libs/components/manifest/project.json
@@ -48,6 +48,12 @@
"!{workspaceRoot}/libs/components/core/.storybook/**/*",
"!{workspaceRoot}/libs/components/core/jest.config.ts",
"!{workspaceRoot}/libs/components/core/src/test-setup.ts",
+ "{workspaceRoot}/libs/components/data-grid/**/*.ts",
+ "!{workspaceRoot}/libs/components/data-grid/**/*.@(spec|stories).ts",
+ "!{workspaceRoot}/libs/components/data-grid/**/fixtures/**/*",
+ "!{workspaceRoot}/libs/components/data-grid/.storybook/**/*",
+ "!{workspaceRoot}/libs/components/data-grid/jest.config.ts",
+ "!{workspaceRoot}/libs/components/data-grid/src/test-setup.ts",
"{workspaceRoot}/libs/components/data-manager/**/*.ts",
"!{workspaceRoot}/libs/components/data-manager/**/*.@(spec|stories).ts",
"!{workspaceRoot}/libs/components/data-manager/**/fixtures/**/*",
diff --git a/libs/components/packages/package.json b/libs/components/packages/package.json
index d271ef6dc6..f8e39fd751 100644
--- a/libs/components/packages/package.json
+++ b/libs/components/packages/package.json
@@ -52,6 +52,7 @@
"@skyux/colorpicker": "0.0.0-PLACEHOLDER",
"@skyux/config": "0.0.0-PLACEHOLDER",
"@skyux/core": "0.0.0-PLACEHOLDER",
+ "@skyux/data-grid": "0.0.0-PLACEHOLDER",
"@skyux/data-manager": "0.0.0-PLACEHOLDER",
"@skyux/datetime": "0.0.0-PLACEHOLDER",
"@skyux/docs-tools": "0.0.0-PLACEHOLDER",
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 46106c4e79..dafa9e1e59 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -69,6 +69,10 @@
"@skyux/config": ["libs/components/config/src/index.ts"],
"@skyux/core": ["libs/components/core/src/index.ts"],
"@skyux/core/testing": ["libs/components/core/testing/src/public-api.ts"],
+ "@skyux/data-grid": ["libs/components/data-grid/src/index.ts"],
+ "@skyux/data-grid/testing": [
+ "libs/components/data-grid/testing/src/public-api.ts"
+ ],
"@skyux/data-manager": ["libs/components/data-manager/src/index.ts"],
"@skyux/data-manager/testing": [
"libs/components/data-manager/testing/src/public-api.ts"