diff --git a/LICENSE b/LICENSE index 402787459ca..be067d20653 100644 --- a/LICENSE +++ b/LICENSE @@ -39,6 +39,7 @@ Covered Components, Directives and Services with MIT License: - igx-divider - igx-drop-down - igx-expansion-panel +- igx-grid-lite - igx-icon - igx-input-group - igx-linear-bar @@ -81,7 +82,7 @@ Covered Components, Directives and Services with MIT License: - IgxTextHighlightService - igxToggle -The MIT License applies exclusively to the components (encompassing all related modules and directives), directives, and services listed above and their associated source code. +The MIT License applies exclusively to the components (encompassing all related modules and directives), directives, and services listed above and their associated source code. All other parts of this package remain under the Infragistics Commercial License. The MIT License (MIT) diff --git a/package-lock.json b/package-lock.json index 24bda946397..3dbe2d51ac2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ "hammerjs": "^2.0.8", "ig-typedoc-theme": "^7.0.1", "igniteui-dockmanager": "^1.17.0", + "igniteui-grid-lite": "~0.4.0", "igniteui-i18n-resources": "^1.0.2", "igniteui-sassdoc-theme": "^2.1.0", "igniteui-webcomponents": "^6.5.0", @@ -1038,6 +1039,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -13982,6 +13984,22 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/igniteui-grid-lite": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/igniteui-grid-lite/-/igniteui-grid-lite-0.4.0.tgz", + "integrity": "sha512-QZ12Q9C9FxrJIZIO0beOQMAZ2E9UADEqS0cjL2Qt2YP1DNge7+x+AzXCp8aOOzT5Y5E+O6O0u6u7JPdtc3RhxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lit-labs/virtualizer": "~2.1.0", + "@lit/context": "~1.1.5", + "igniteui-webcomponents": "~6.5.0", + "lit": "^3.3.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/igniteui-i18n-core": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/igniteui-i18n-core/-/igniteui-i18n-core-1.0.2.tgz", @@ -16039,6 +16057,7 @@ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", diff --git a/package.json b/package.json index ce159cbd550..e6c97e183e3 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,7 @@ "hammerjs": "^2.0.8", "ig-typedoc-theme": "^7.0.1", "igniteui-dockmanager": "^1.17.0", + "igniteui-grid-lite": "~0.4.0", "igniteui-i18n-resources": "^1.0.2", "igniteui-sassdoc-theme": "^2.1.0", "igniteui-webcomponents": "^6.5.0", diff --git a/projects/igniteui-angular/grids/lite/README.md b/projects/igniteui-angular/grids/lite/README.md new file mode 100644 index 00000000000..6de9a65c2a5 --- /dev/null +++ b/projects/igniteui-angular/grids/lite/README.md @@ -0,0 +1,290 @@ +# IgxGridLite + +**IgxGridLite** is a lightweight Angular wrapper component for the `igniteui-grid-lite` web component, providing a simple and performant data grid solution with essential features like sorting, filtering, and virtualization. + +A walkthrough of how to get started can be found [here](https://www.infragistics.com/products/ignite-ui-angular/angular/components/grid-lite/overview) + +## Usage + +```html + + +``` + +Or with manual column definitions: + +```html + + + + + + +``` + +## Getting Started + +### Installation +To get started, install Ignite UI for Angular package as well as the Ignite UI for Web Component one that powers the UI: + +```shell +npm install igniteui-grid-lite +``` + +### Dependencies + +The Grid Lite is exported as a standalone component, thus all you need to do in your application is to import the `IgxGridLiteComponent` and `IgxGridLiteColumnComponent` in your component: + +```typescript +import { IgxGridLiteComponent, IgxGridLiteColumnComponent } from 'igniteui-angular/grids/lite'; + +@Component({ + selector: 'app-grid-lite-sample', + templateUrl: './grid-lite-sample.html', + standalone: true, + imports: [IgxGridLiteComponent, IgxGridLiteColumnComponent] +}) +export class GridLiteSampleComponent { + public data = [ + { id: 1, firstName: 'John', lastName: 'Doe', age: 30 }, + { id: 2, firstName: 'Jane', lastName: 'Smith', age: 25 } + ]; +} +``` + +### Basic Configuration + +Define the grid with auto-generated columns: + +```html + + +``` + +Or define columns manually: + +```html + + + + + + +``` + +### Sorting + +Configure sorting mode: + +```typescript +protected sortingOptions: IgxGridLiteSortingOptions = { + mode: 'single' +} +``` + +```html + + +``` + +Set initial sorting expressions: + +```typescript +protected sortingExpressions: IgxGridLiteSortingExpression[] = [ + { + key: 'firstName', + direction: 'ascending' + } +] +``` + +```html + + +``` + +### Filtering + +Set initial filtering expressions: + +```typescript +protected filteringExpressions: IgxGridLiteFilteringExpression[] = [ + { + key: 'age', + condition: 'greaterThan', + searchTerm: 50 + } +] +``` + +```html + + +``` + +### Custom Templates + +Define custom header templates: + +```html + + + +
{{ column.header }} (Custom)
+
+
+
+``` + +Define custom cell templates: + +```html + + + + @if (value === true) { + Yes + } @else { + No + } + + + +``` + +### Events + +Listen to sorting and filtering events: + +```html + + +``` + +```typescript +public onSorting(event: CustomEvent) { + console.log('Sorting initiated:', event.detail); +} + +public onSorted(event: CustomEvent) { + console.log('Sorting completed:', event.detail); +} + +public onFiltering(event: CustomEvent) { + console.log('Filtering initiated:', event.detail); +} + +public onFiltered(event: CustomEvent) { + console.log('Filtering completed:', event.detail); +} +``` + +## API + +### Inputs + +**IgxGridLiteComponent** + +| Name | Type | Description | +|------|------|-------------| +| `data` | `any[]` | The data source for the grid | +| `autoGenerate` | `boolean` | Whether to auto-generate columns from data. Default is `false` | +| `sortingOptions` | `IgxGridLiteSortingOptions` | Configuration for sorting behavior (single/multiple mode) | +| `sortingExpressions` | `IgxGridLiteSortingExpression[]` | Initial sorting state | +| `filteringExpressions` | `IgxGridLiteFilteringExpression[]` | Initial filtering state | +| `dataPipelineConfiguration` | `IgxGridLiteDataPipelineConfiguration` | Configuration for remote data operations | + +**IgxGridLiteColumnComponent** + +| Name | Type | Description | +|------|------|-------------| +| `field` | `string` | The data field to bind to | +| `header` | `string` | The column header text | +| `dataType` | `'string' \| 'number' \| 'boolean' | The data type of the column. Default is `'string'` | +| `width` | `string` | The width of the column | +| `hidden` | `boolean` | Indicates whether the column is hidden. Default is `false` | +| `resizable` | `boolean` | Indicates whether the column is resizable. Default is `false` | +| `sortable` | `boolean` | Indicates whether the column is sortable. Default is `false` | +| `sortingCaseSensitive` | `boolean` | Whether sort operations will be case sensitive. Default is `false` | +| `sortConfiguration` | `IgxGridLiteColumnSortConfiguration` | Sort configuration for the column (e.g., custom comparer) | +| `filterable` | `boolean` | Indicates whether the column is filterable. Default is `false` | +| `filteringCaseSensitive` | `boolean` | Whether filter operations will be case sensitive. Default is `false` | +| `headerTemplate` | `TemplateRef` | Custom template for the header | +| `cellTemplate` | `TemplateRef` | Custom template for cells | + +### Outputs + +| Name | Type | Description | +|------|------|-------------| +| `sorting` | `CustomEvent` | Emitted when sorting is initiated | +| `sorted` | `CustomEvent` | Emitted when sorting completes | +| `filtering` | `CustomEvent` | Emitted when filtering is initiated | +| `filtered` | `CustomEvent` | Emitted when filtering completes | + +### Properties + +| Name | Type | Description | +|------|------|-------------| +| `columns` | `IgxGridLiteColumnConfiguration[]` | Gets the column configuration | +| `rows` | `any[]` | Gets the currently rendered rows | +| `dataView` | `ReadonlyArray` | Gets the data after sort/filter operations | + +### Methods + +| Name | Parameters | Description | +|------|------------|-------------| +| `sort` | `expressions: IgxGridLiteSortingExpression \| IgxGridLiteSortingExpression[]` | Performs a sort operation | +| `clearSort` | `key?: Keys` | Clears sorting for a specific column or all columns | +| `filter` | `config: IgxGridLiteFilteringExpression \| IgxGridLiteFilteringExpression[]` | Performs a filter operation | +| `clearFilter` | `key?: Keys` | Clears filtering for a specific column or all columns | +| `navigateTo` | `row: number, column?: Keys, activate?: boolean` | Navigates to a specific cell | +| `getColumn` | `id: Keys \| number` | Returns column configuration by field or index | + +## Template Directives + +### igxGridLiteHeader + +Context properties: +- `$implicit` - The column configuration object +- `column` - The column configuration object + +```html + +
{{ column.header }}
+
+``` + +### igxGridLiteCell + +Context properties: +- `$implicit` - The cell value +- `value` - The cell value +- `column` - The column configuration +- `rowIndex` - The row index +- `data` - The row data object + +```html + +
{{ value }} - {{ data.otherField }}
+
+``` + +## Related Components + +- [IgxGrid](../grid/README.md) - Full-featured data grid with advanced capabilities +- [IgxTreeGrid](../tree-grid/README.md) - same-schema hierarchical or flat self-referencing data grid +- [IgxHierarchicalGrid](../hierarchical-grid/README.md) - Multi-level hierarchical schema data grid + +## Additional Resources + +- [Official Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/grid-lite/overview) diff --git a/projects/igniteui-angular/grids/lite/index.ts b/projects/igniteui-angular/grids/lite/index.ts new file mode 100644 index 00000000000..decc72d85bc --- /dev/null +++ b/projects/igniteui-angular/grids/lite/index.ts @@ -0,0 +1 @@ +export * from './src/public_api'; diff --git a/projects/igniteui-angular/grids/lite/ng-package.json b/projects/igniteui-angular/grids/lite/ng-package.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/projects/igniteui-angular/grids/lite/ng-package.json @@ -0,0 +1 @@ +{} diff --git a/projects/igniteui-angular/grids/lite/src/grid-lite-column.component.html b/projects/igniteui-angular/grids/lite/src/grid-lite-column.component.html new file mode 100644 index 00000000000..5a0c589cae5 --- /dev/null +++ b/projects/igniteui-angular/grids/lite/src/grid-lite-column.component.html @@ -0,0 +1,15 @@ + diff --git a/projects/igniteui-angular/grids/lite/src/grid-lite-column.component.ts b/projects/igniteui-angular/grids/lite/src/grid-lite-column.component.ts new file mode 100644 index 00000000000..4af3a883104 --- /dev/null +++ b/projects/igniteui-angular/grids/lite/src/grid-lite-column.component.ts @@ -0,0 +1,190 @@ +import { booleanAttribute, ChangeDetectionStrategy, Component, contentChild, CUSTOM_ELEMENTS_SCHEMA, Directive, effect, EmbeddedViewRef, inject, input, TemplateRef, ViewContainerRef } from '@angular/core'; +import { ColumnConfiguration, ColumnSortConfiguration, IgcCellContext, IgcHeaderContext, Keys, DataType } from 'igniteui-grid-lite'; + +/** Configuration object for grid columns. */ +export type IgxGridLiteColumnConfiguration = ColumnConfiguration; + +export type IgxGridLiteColumnSortConfiguration = ColumnSortConfiguration; + + +/** + * Directive providing type information for header template contexts. + * Use this directive on ng-template elements that render header templates. + * + * @example + * ```html + * + *
{{column.header}}
+ *
+ * ``` + */ +@Directive({ selector: '[igxGridLiteHeader]' }) +export class IgxGridLiteHeaderTemplateDirective { + public template = inject>>(TemplateRef); + + public static ngTemplateContextGuard(_: IgxGridLiteHeaderTemplateDirective, ctx: any): ctx is IgxGridLiteHeaderTemplateContext { + return true; + } +} + +/** + * Directive providing type information for cell template contexts. + * Use this directive on ng-template elements that render cell templates. + * + * @example + * ```html + * + *
{{value}}
+ *
+ * ``` + */ +@Directive({ selector: '[igxGridLiteCell]' }) +export class IgxGridLiteCellTemplateDirective { + public template = inject>>(TemplateRef); + + public static ngTemplateContextGuard(_: IgxGridLiteCellTemplateDirective, ctx: unknown): ctx is IgxGridLiteCellTemplateContext { + return true; + } +} + +@Component({ + selector: 'igx-grid-lite-column', + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + templateUrl: './grid-lite-column.component.html' +}) +export class IgxGridLiteColumnComponent { + + //#region Internal state + + private readonly _view = inject(ViewContainerRef); + + /** Reference to the embedded view for the header template and its template function. */ + private headerViewRef?: EmbeddedViewRef>; + protected headerTemplateFunc?: (ctx: IgcHeaderContext) => Node[]; + + /** Reference to the embedded view for the cell template and its template function. */ + private cellViewRefs? = new Map>>(); + protected cellTemplateFunc?: (ctx: IgcCellContext) => Node[]; + + /** Template directives used for inline templating */ + private readonly headerTemplateDirective = contentChild(IgxGridLiteHeaderTemplateDirective); + private readonly cellTemplateDirective = contentChild(IgxGridLiteCellTemplateDirective); + + //#endregion + + //#region Inputs + + /** The field from the data for this column. */ + public readonly field = input>(); + + /** The data type of the column's values. */ + public readonly dataType = input('string'); + + /** The header text of the column. */ + public readonly header = input(); + + /** The width of the column. */ + public readonly width = input(); + + /** Indicates whether the column is hidden. */ + public readonly hidden = input(false, { transform: booleanAttribute }); + + /** Indicates whether the column is resizable. */ + public readonly resizable = input(false, { transform: booleanAttribute }); + + /** Indicates whether the column is sortable. */ + public readonly sortable = input(false, { transform: booleanAttribute }); + + /** Whether sort operations will be case sensitive. */ + public readonly sortingCaseSensitive = input(false, { transform: booleanAttribute }); + + /** Sort configuration for the column (e.g., custom comparer). */ + public readonly sortConfiguration = input>(); + + /** Indicates whether the column is filterable. */ + public readonly filterable = input(false, { transform: booleanAttribute }); + + /** Whether filter operations will be case sensitive. */ + public readonly filteringCaseSensitive = input(false, { transform: booleanAttribute }); + + /** Custom header template for the column. */ + public readonly headerTemplate = input>>(); + + /** Custom cell template for the column. */ + public readonly cellTemplate = input>>(); + + //#endregion + + constructor() { + effect((onCleanup) => { + const directive = this.headerTemplateDirective(); + const template = this.headerTemplate() ?? directive?.template; + if (template) { + this.headerTemplateFunc = (ctx: IgcHeaderContext) => { + if (!this.headerViewRef) { + const angularContext = { + ...ctx, + $implicit: ctx.column + } + this.headerViewRef = this._view.createEmbeddedView(template, angularContext); + } + return this.headerViewRef.rootNodes; + }; + } + onCleanup(() => { + if (this.headerViewRef) { + this.headerViewRef.destroy(); + this.headerViewRef = undefined; + } + }) + }); + + effect((onCleanup) => { + const directive = this.cellTemplateDirective(); + const template = this.cellTemplate() ?? directive?.template; + if (template) { + this.cellTemplateFunc = (ctx: IgcCellContext) => { + const oldViewRef = this.cellViewRefs.get(ctx.row.data); + const angularContext = { + ...ctx, + $implicit: ctx.value, + } as IgxGridLiteCellTemplateContext; + if (!oldViewRef) { + const newViewRef = this._view.createEmbeddedView(template, angularContext); + this.cellViewRefs.set(ctx.row.data, newViewRef); + return newViewRef.rootNodes; + } + Object.assign(oldViewRef.context, angularContext); + return oldViewRef.rootNodes; + }; + } + onCleanup(() => { + this.cellViewRefs.forEach((viewRef) => { + viewRef.destroy(); + }); + this.cellViewRefs?.clear(); + }); + }); + } +} + +/** + * Context provided to the header template. + */ +export type IgxGridLiteHeaderTemplateContext = IgcHeaderContext & { + /** + * The current configuration for the column. + */ + $implicit: IgcHeaderContext['column']; +} + +/** + * Context provided to the header template. + */ +export type IgxGridLiteCellTemplateContext = IgcCellContext & { + /** + * The value from the data source for this cell. + */ + $implicit: IgcCellContext['value']; +}; diff --git a/projects/igniteui-angular/grids/lite/src/grid-lite.component.spec.ts b/projects/igniteui-angular/grids/lite/src/grid-lite.component.spec.ts new file mode 100644 index 00000000000..5ad0312ac75 --- /dev/null +++ b/projects/igniteui-angular/grids/lite/src/grid-lite.component.spec.ts @@ -0,0 +1,307 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { Component, viewChild } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { IgxGridLiteComponent, IgxGridLiteFilteringExpression, IgxGridLiteSortingExpression } from './grid-lite.component'; +import { IgxGridLiteColumnComponent, IgxGridLiteCellTemplateDirective, IgxGridLiteHeaderTemplateDirective, IgxGridLiteColumnConfiguration } from './grid-lite-column.component'; + +describe('IgxGridLiteComponent', () => { + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + IgxGridLiteComponent, + IgxGridLiteColumnComponent + ] + }).compileComponents(); + })); + + describe('Basic', () => { + it('should initialize grid with data and columns', async () => { + const fixture = TestBed.createComponent(BasicGridComponent); + fixture.detectChanges(); + await setUp(fixture); + fixture.detectChanges(); + + const gridElement = fixture.debugElement.query(By.directive(IgxGridLiteComponent)); + expect(gridElement).toBeTruthy(); + + const columnElements = fixture.debugElement.queryAll(By.directive(IgxGridLiteColumnComponent)); + expect(columnElements.length).toBe(3); + }); + + it('should render grid with auto-generate enabled', async () => { + const fixture = TestBed.createComponent(GridComponentAutogenerate); + fixture.detectChanges(); + await setUp(fixture); + fixture.detectChanges(); + + const gridElement = fixture.nativeElement.querySelector('igx-grid-lite'); + expect(gridElement).toBeTruthy(); + expect(gridElement.autoGenerate).toBeTrue(); + expect(gridElement.columns.length).toBe(5); + }); + + it('should update grid data when data input changes', async () => { + const fixture = TestBed.createComponent(BasicGridComponent); + fixture.detectChanges(); + await setUp(fixture); + + const gridComponent = fixture.componentInstance.grid(); + const newData: TestData[] = [ + { id: 99, name: 'Z', active: true, importance: 'high', address: { city: 'Boston', code: 2101 } } + ]; + fixture.componentInstance.data = newData; + fixture.detectChanges(); + await fixture.whenStable(); + + expect(gridComponent.dataView.length).toBe(1); + expect(gridComponent.rows.length).toBe(1); + }); + + it('should update sortingExpressions model and emit sorted output when sorted event fires', async () => { + const fixture = TestBed.createComponent(BasicGridComponent); + fixture.detectChanges(); + await setUp(fixture); + + const gridComponent = fixture.componentInstance.grid(); + const gridElement = fixture.nativeElement.querySelector('igx-grid-lite'); + const sortedSpy = jasmine.createSpy('sorted'); + + gridElement.addEventListener('sorted', sortedSpy); + expect(gridComponent.sortingExpressions()).toEqual([]); + + // Simulate the web component emitting the sorted event + const expressions: IgxGridLiteSortingExpression[] = [ + { key: 'name', direction: 'ascending' } + ]; + gridElement.sortingExpressions = expressions; + gridElement.dispatchEvent(new CustomEvent('sorted', { detail: expressions })); + fixture.detectChanges(); + + expect(sortedSpy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ detail: expressions })); + expect(gridComponent.sortingExpressions().length).toBe(1); + expect(gridComponent.sortingExpressions()[0].key).toBe('name'); + + gridComponent.sortingExpressions.set([]); + fixture.detectChanges(); + expect(gridElement.sortingExpressions).toEqual([]); + }); + + it('should update filteringExpressions model and emit filtered output when filtered event fires', async () => { + const fixture = TestBed.createComponent(BasicGridComponent); + fixture.detectChanges(); + await setUp(fixture); + + const gridComponent = fixture.componentInstance.grid(); + const gridElement = fixture.nativeElement.querySelector('igx-grid-lite'); + const filteredSpy = jasmine.createSpy('filtered'); + + gridElement.addEventListener('filtered', filteredSpy); + expect(gridComponent.filteringExpressions()).toEqual([]); + + // Simulate the web component emitting the filtered event + const expressions: IgxGridLiteFilteringExpression[] = [ + { key: 'active', condition: 'true', searchTerm: true } + ]; + gridElement.filterExpressions = expressions; + gridElement.dispatchEvent(new CustomEvent('filtered', { detail: expressions })); + fixture.detectChanges(); + + expect(filteredSpy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ detail: expressions })); + expect(gridComponent.filteringExpressions().length).toBe(1); + expect(gridComponent.filteringExpressions()[0].key).toBe('active'); + + gridComponent.filteringExpressions.set([]); + fixture.detectChanges(); + expect(gridElement.filterExpressions).toEqual([]); + }); + }); + + describe('Templates', () => { + + function get(element: any, selector: string) { + return element.renderRoot.querySelector(selector); + } + + it('should render custom header and cell templates when set as inputs', async () => { + const fixture = TestBed.createComponent(GridComponentTemplate); + fixture.detectChanges(); + + await setUp(fixture); + fixture.detectChanges(); + const gridComponent: IgxGridLiteComponent = fixture.componentInstance.grid(); + + + // Check header template + const gridElement = fixture.nativeElement.querySelector('igx-grid-lite'); + const headerRow = get(gridElement, 'igc-grid-lite-header-row'); + const headerCell = headerRow.headers.at(0); + const headerText = get(headerCell, '[part~="title"]').innerText; + expect(headerText).toEqual('Name (Custom)'); + + // check cell template + const cell = gridComponent.rows[0].cells[1]; + const content = get(cell, 'span').innerText; + expect(content).toEqual('No'); + }); + + it('should render custom header and cell templates when set as a column child', async () => { + const fixture = TestBed.createComponent(GridComponentTemplate); + fixture.detectChanges(); + await setUp(fixture); + fixture.detectChanges(); + const gridComponent: IgxGridLiteComponent = fixture.componentInstance.grid(); + + + // Check header template + const gridElement = fixture.nativeElement.querySelector('igx-grid-lite'); + const headerRow = get(gridElement, 'igc-grid-lite-header-row'); + const headerCell = headerRow.headers.at(2); + const headerText = get(headerCell, '[part~="title"]').innerText; + expect(headerText).toEqual('Importance (Custom Inline)'); + + // check cell template + const cell = gridComponent.rows[0].cells[3]; + const content = get(cell, 'span').innerText; + expect(content).toEqual('New York, 10001'); + }); + + }); +}); + + +type Importance = 'low' | 'medium' | 'high'; +interface TestData { + id: number; + name: string; + active: boolean; + importance: Importance; + address: { + city: string; + code: number; + }; +} + +@Component({ + template: ` + + @for(column of columns; track column.field) { + + + } + + `, + standalone: true, + imports: [IgxGridLiteComponent, IgxGridLiteColumnComponent] +}) +class BasicGridComponent { + public grid = viewChild>('grid'); + public data: TestData[] = [ + { + id: 1, + name: 'A', + active: false, + importance: 'medium', + address: { city: 'New York', code: 10001 }, + }, + { + id: 2, + name: 'B', + active: false, + importance: 'low', + address: { city: 'Los Angeles', code: 90001 }, + }, + { + id: 3, + name: 'C', + active: true, + importance: 'medium', + address: { city: 'Chicago', code: 60601 }, + }, + { + id: 4, + name: 'D', + active: false, + importance: 'low', + address: { city: 'New York', code: 10002 }, + }, + { id: 5, name: 'a', active: true, importance: 'high', address: { city: 'Chicago', code: 60602 } }, + { + id: 6, + name: 'b', + active: false, + importance: 'medium', + address: { city: 'Los Angeles', code: 90002 }, + }, + { id: 7, name: 'c', active: true, importance: 'low', address: { city: 'New York', code: 10003 } }, + { id: 8, name: 'd', active: true, importance: 'high', address: { city: 'Chicago', code: 60603 } }, + ]; + public columns: IgxGridLiteColumnConfiguration[] = [ + { field: 'id', header: 'ID', dataType: 'number' }, + { field: 'name', header: 'Name', dataType: 'string' }, + { field: 'active', header: 'Active', dataType: 'boolean' } + ] + public shouldAutoGenerate = true; +} + +@Component({ + template: ` + + + `, + standalone: true, + imports: [IgxGridLiteComponent, IgxGridLiteColumnComponent] +}) +class GridComponentAutogenerate extends BasicGridComponent { + public override shouldAutoGenerate = true; + public override columns = []; +} + + +@Component({ + template: ` + + + + + +
{{column.header}} (Custom Inline)
+
+
+ + + {{value.city}}, {{value.code}} + + + + +
{{column.header}} (Custom)
+
+ + + @if (value === true) { + Yes + } @else { + No + } + +
+ `, + standalone: true, + imports: [IgxGridLiteComponent, IgxGridLiteColumnComponent, IgxGridLiteCellTemplateDirective, IgxGridLiteHeaderTemplateDirective] +}) +class GridComponentTemplate extends BasicGridComponent { +} + +async function setUp(fixture: ComponentFixture) { + await customElements.whenDefined('igx-grid-lite'); + + const gridElement = fixture.nativeElement.querySelector('igx-grid-lite'); + const gridBody = gridElement?.renderRoot.querySelector('igc-virtualizer'); + if (gridBody?.updateComplete) { + await gridBody.updateComplete; + } +} diff --git a/projects/igniteui-angular/grids/lite/src/grid-lite.component.ts b/projects/igniteui-angular/grids/lite/src/grid-lite.component.ts new file mode 100644 index 00000000000..aa7e9f68e8e --- /dev/null +++ b/projects/igniteui-angular/grids/lite/src/grid-lite.component.ts @@ -0,0 +1,240 @@ +import { booleanAttribute, ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, effect, ElementRef, inject, input, model, OnInit } from '@angular/core'; +import { DataPipelineConfiguration, FilterExpression, GridLiteSortingOptions, IgcGridLite, Keys, SortingExpression } from 'igniteui-grid-lite'; +import { IgxGridLiteColumnConfiguration } from './grid-lite-column.component'; + +export type IgxGridLiteSortingOptions = GridLiteSortingOptions; +export type IgxGridLiteDataPipelineConfiguration = DataPipelineConfiguration; +export type IgxGridLiteSortingExpression = SortingExpression; +export type IgxGridLiteFilteringExpression = FilterExpression; + + +class IgxGridLite extends IgcGridLite { + public static override get tagName() { + return 'igx-grid-lite' as any; + } + public static override register(): void { + // still call super for child components: + super.register(); + + if (!customElements.get(IgxGridLite.tagName)) { + customElements.define(IgxGridLite.tagName, IgxGridLite); + } + } +} + +/** + * The Grid Lite is a web component for displaying data in a tabular format quick and easy. + * + * Out of the box it provides row virtualization, sort and filter operations (client and server side), + * the ability to template cells and headers and column hiding. + * + * @fires sorting - Emitted when sorting is initiated through the UI. + * @fires sorted - Emitted when a sort operation initiated through the UI has completed. + * @fires filtering - Emitted when filtering is initiated through the UI. + * @fires filtered - Emitted when a filter operation initiated through the UI has completed. + */ +@Component({ + selector: 'igx-grid-lite', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + host: { + '[data]': "data()", + '[autoGenerate]': "autoGenerate()", + '[sortingOptions]': "sortingOptions()", + '[dataPipelineConfiguration]': "dataPipelineConfiguration()", + '(sorted)': "onSorted($any($event))", + '(filtered)': "onFiltered($any($event))" + }, + template: `` +}) + +export class IgxGridLiteComponent implements OnInit { + + //#region Internal state + + private readonly gridRef = inject(ElementRef) as ElementRef>; + + //#endregion + + //#region Inputs + + /** The data source for the grid. */ + public readonly data = input([]); + + /** + * Whether the grid will try to "resolve" its column configuration based on the passed + * data source. + * + * @remarks + * This property is ignored if any existing column configuration already exists in the grid. + */ + public readonly autoGenerate = input(false, { transform: booleanAttribute });; + + /** Sort configuration property for the grid. */ + public readonly sortingOptions = input({ + mode: 'multiple' + }); + + /** + * Configuration object which controls remote data operations for the grid. + */ + public readonly dataPipelineConfiguration = input(); + + /** + * The sort state for the grid. + * + * @remarks + * This is a two-way bindable property. It will be updated when sort operations + * complete through the UI. + */ + public readonly sortingExpressions = model[]>([]); + + /** + * The filter state for the grid. + * + * @remarks + * This is a two-way bindable property. It will be updated when filter operations + * complete through the UI. + */ + public readonly filteringExpressions = model[]>([]); + + //#endregion + + //#region Getters / Setters + + /** + * Get the column configuration of the grid. + */ + public get columns(): IgxGridLiteColumnConfiguration[] { + return this.gridRef.nativeElement.columns ?? []; + } + + /** + * Returns the collection of rendered row elements in the grid. + * + * @remarks + * Since the grid has virtualization, this property returns only the currently rendered + * chunk of elements in the DOM. + */ + public get rows() { + return this.gridRef.nativeElement.rows ?? []; + } + + /** + * Returns the state of the data source after sort/filter operations + * have been applied. + */ + public get dataView(): ReadonlyArray { + return this.gridRef.nativeElement.dataView ?? []; + } + + //#endregion + + constructor() { + // D.P. Temporary guarded assign instead of binding to prevent WC issue with setter logic re-doing sort/filter + effect(() => { + const grid = this.gridRef.nativeElement + if (!grid) return; + const newValue = this.filteringExpressions(); + if (new Set(newValue).symmetricDifference(new Set(grid.filterExpressions)).size !== 0) { + grid.clearFilter(); + grid.filterExpressions = newValue; + } + }); + effect(() => { + const grid = this.gridRef.nativeElement + if (!grid) return; + const newValue = this.sortingExpressions(); + if (new Set(newValue).symmetricDifference(new Set(grid.sortingExpressions)).size !== 0) { + grid.clearSort(); + grid.sortingExpressions = newValue; + } + }); + } + + /** + * @hidden @internal + */ + public ngOnInit(): void { + IgxGridLite.register(); + } + + //#region Public API + + /** + * Performs a filter operation in the grid based on the passed expression(s). + */ + public filter(config: IgxGridLiteFilteringExpression | IgxGridLiteFilteringExpression[]): void { + this.gridRef.nativeElement.filter(config as FilterExpression | FilterExpression[]); + } + + /** + * Performs a sort operation in the grid based on the passed expression(s). + */ + public sort(expressions: IgxGridLiteSortingExpression | IgxGridLiteSortingExpression[]) { + this.gridRef.nativeElement.sort(expressions); + } + + /** + * Resets the current sort state of the control. + */ + public clearSort(key?: Keys): void { + this.gridRef.nativeElement.clearSort(key); + } + + /** + * Resets the current filter state of the control. + */ + public clearFilter(key?: Keys): void { + this.gridRef.nativeElement.clearFilter(key); + } + + /** + * Navigates to a position in the grid based on provided row index and column field. + * @param row The row index to navigate to + * @param column The column field to navigate to, if any + * @param activate Optionally also activate the navigated cell + */ + public async navigateTo(row: number, column?: Keys, activate = false) { + await this.gridRef.nativeElement.navigateTo(row, column, activate); + } + + /** + * Returns a {@link IgxGridLiteColumnConfiguration} for a given column. + */ + public getColumn(id: Keys | number): IgxGridLiteColumnConfiguration | undefined { + return this.gridRef.nativeElement.getColumn(id); + } + + //#endregion + + //#region Event handlers + + protected onSorted(_event: CustomEvent>): void { + this.sortingExpressions.set(this.gridRef.nativeElement.sortingExpressions ?? []); + } + + protected onFiltered(_event: CustomEvent>): void { + this.filteringExpressions.set(this.gridRef.nativeElement.filterExpressions ?? []); + } + + //#endregion + +} + +declare global { + interface HTMLElementTagNameMap { + [IgxGridLite.tagName]: IgxGridLite; + } + + interface HTMLElementEventMap { + 'sorting': CustomEvent>; + 'sorted': CustomEvent>; + 'filtering': CustomEvent>; + 'filtered': CustomEvent>; + } +} + +// see https://github.com/ng-packagr/ng-packagr/issues/3233 +export {}; diff --git a/projects/igniteui-angular/grids/lite/src/public_api.ts b/projects/igniteui-angular/grids/lite/src/public_api.ts new file mode 100644 index 00000000000..265ac3d59c8 --- /dev/null +++ b/projects/igniteui-angular/grids/lite/src/public_api.ts @@ -0,0 +1,2 @@ +export * from './grid-lite.component'; +export * from './grid-lite-column.component' diff --git a/projects/igniteui-angular/package.json b/projects/igniteui-angular/package.json index 03b78f5cc53..0084e4aea5e 100644 --- a/projects/igniteui-angular/package.json +++ b/projects/igniteui-angular/package.json @@ -86,7 +86,8 @@ "@angular/forms": "21", "hammerjs": "^2.0.8", "@types/hammerjs": "^2.0.46", - "igniteui-webcomponents": "^6.5.0" + "igniteui-webcomponents": "^6.5.0", + "igniteui-grid-lite": "~0.4.0" }, "peerDependenciesMeta": { "hammerjs": { @@ -97,6 +98,9 @@ }, "igniteui-webcomponents": { "optional": true + }, + "igniteui-grid-lite": { + "optional": true } }, "igxDevDependencies": { diff --git a/projects/igniteui-angular/schematics/utils/dependency-handler.ts b/projects/igniteui-angular/schematics/utils/dependency-handler.ts index 69d6cd47173..80f86324481 100644 --- a/projects/igniteui-angular/schematics/utils/dependency-handler.ts +++ b/projects/igniteui-angular/schematics/utils/dependency-handler.ts @@ -28,7 +28,6 @@ export const DEPENDENCIES_MAP: PackageEntry[] = [ { name: '@igniteui/material-icons-extended', target: PackageTarget.REGULAR }, { name: 'igniteui-i18n-core', target: PackageTarget.REGULAR }, { name: 'igniteui-theming', target: PackageTarget.NONE }, - { name: 'igniteui-webcomponents', target: PackageTarget.NONE }, // peerDependencies { name: '@angular/forms', target: PackageTarget.NONE }, { name: '@angular/common', target: PackageTarget.NONE }, @@ -36,6 +35,8 @@ export const DEPENDENCIES_MAP: PackageEntry[] = [ { name: '@angular/animations', target: PackageTarget.NONE }, { name: 'hammerjs', target: PackageTarget.REGULAR }, { name: '@types/hammerjs', target: PackageTarget.DEV }, + { name: 'igniteui-webcomponents', target: PackageTarget.NONE }, + { name: 'igniteui-grid-lite', target: PackageTarget.NONE }, // igxDevDependencies { name: '@igniteui/angular-schematics', target: PackageTarget.DEV } ]; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index fdf8a450f40..07c62d4b908 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -212,6 +212,11 @@ export class AppComponent implements OnInit { icon: 'web', name: 'Forms' }, + { + link: '/gridLite', + icon: 'view_column', + name: 'Grid Lite' + }, { link: '/grid', icon: 'view_column', diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 62c98189e5c..209146558d0 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -150,6 +150,7 @@ import { HoundComponent } from './hound/hound.component'; import { LabelSampleComponent } from "./label/label.sample"; import { GridRecreateSampleComponent } from './grid-re-create/grid-re-create.sample'; import { HierarchicalGridAdvancedFilteringSampleComponent } from './hierarchical-grid-advanced-filtering/hierarchical-grid-advanced-filtering.sample'; +import { GridLiteSampleComponent } from './grid-lite/grid-lite.sample'; export const appRoutes: Routes = [ { @@ -229,7 +230,7 @@ export const appRoutes: Routes = [ path: 'circular-progress', component: CircularProgressSampleComponent }, - { + { path: 'divider', component: DividerComponent }, @@ -546,6 +547,10 @@ export const appRoutes: Routes = [ path: 'buttonGroup', component: ButtonGroupSampleComponent }, + { + path: "gridLite", + component: GridLiteSampleComponent + }, { path: 'gridColumnGroups', component: GridColumnGroupsSampleComponent diff --git a/src/app/grid-lite/data.service.ts b/src/app/grid-lite/data.service.ts new file mode 100644 index 00000000000..acb978dfcb9 --- /dev/null +++ b/src/app/grid-lite/data.service.ts @@ -0,0 +1,141 @@ +import { Injectable } from '@angular/core'; + +export interface UserSimple { + id: string; + username: string; + email: string; + subscribed: boolean; +} + +export interface ProductInfo { + id: string; + name: string; + price: number; + sold: number; + rating: number; + total: number; +} + +export interface User { + id: string; + firstName: string; + lastName: string; + age: number; + email: string; + avatar: string; + active: boolean; + priority: 'Low' | 'Standard' | 'High'; + satisfaction: number; + registeredAt: Date; +} + +@Injectable({ + providedIn: 'root' +}) +export class GridLiteDataService { + private counter = 0; + + private namesMen = ['John', 'john', 'Mark', 'Charlie', 'Martin', 'Bill', 'Frank', 'Larry', 'Henry', 'Steve', 'Mike', 'Andrew']; + private namesWomen = ['Jane', 'Alice', 'Diana', 'Eve', 'Grace', 'Katie', 'Irene', 'Liz', 'Fiona', 'Pam', 'Val', 'Mindy']; + private lastNames = ['Smith', 'Johnson', 'Mendoza', 'Brown', 'Spencer', 'Stone', 'Stark', 'Rooney']; + private productNames = ['Widget', 'Gadget', 'Gizmo', 'Device', 'Tool', 'Instrument', 'Machine', 'Equipment']; + private productModels = ['Pro', 'Plus', 'Max', 'Ultra', 'Mini', 'Lite']; + private priorities: ('Low' | 'Standard' | 'High')[] = ['Low', 'Standard', 'High']; + + private randomInt(min: number, max: number): number { + const array = new Uint32Array(1); + window.crypto.getRandomValues(array); + const random01 = array[0] / 2 ** 32; + return Math.floor(random01 * (max - min + 1)) + min; + } + + private randomFloat(min: number, max: number, precision = 2): number { + const array = new Uint32Array(1); + window.crypto.getRandomValues(array); + const random01 = array[0] / 2 ** 32; + return parseFloat((random01 * (max - min) + min).toFixed(precision)); + } + + private randomElement(array: T[]): T { + return array[this.randomInt(0, array.length - 1)]; + } + + private randomBoolean(): boolean { + const array = new Uint8Array(1); + window.crypto.getRandomValues(array); + return (array[0] & 1) === 0; + } + + private generateId(): string { + return `1000-${this.counter++}-${this.randomInt(1000, 9999)}`; + } + + private createProductInfo(): ProductInfo { + const price = this.randomFloat(50, 500, 2); + const sold = this.randomInt(10, 100); + const total = parseFloat((price * sold).toFixed(2)); + const product = this.randomElement(this.productNames) + ' ' + this.randomElement(this.productModels); + + return { + price, + sold, + total, + id: this.generateId(), + name: product, + rating: this.randomFloat(0, 5, 1) + }; + } + + private createUserSimple(): UserSimple { + const firstName = this.randomElement(this.namesMen.concat(this.namesWomen)).toLowerCase(); + const lastName = this.randomElement(this.lastNames).toLowerCase(); + const email = firstName + '.' + lastName + '@example.com'; + const username = firstName + '.' + lastName + this.randomInt(1, 99); + return { + id: this.generateId(), + username: username, + email: email, + subscribed: this.randomBoolean() + }; + } + + private createUser(): User { + let imagePath: string = ""; + let firstName: string = ""; + const gender = this.randomInt(0, 1); + if (gender === 0) { + imagePath = "https://dl.infragistics.com/x/img/people/men/" + this.randomInt(10, 40) + ".png"; + firstName = this.randomElement(this.namesMen); + } else { + imagePath = "https://dl.infragistics.com/x/img/people/women/" + this.randomInt(10, 40) + ".png"; + firstName = this.randomElement(this.namesWomen); + } + const lastName = this.randomElement(this.lastNames); + const email = firstName.toLowerCase() + '.' + lastName.toLowerCase() + '@example.com'; + + return { + id: this.generateId(), + firstName, + lastName, + age: this.randomInt(18, 90), + email, + avatar: imagePath, + active: this.randomBoolean(), + priority: this.randomElement(this.priorities), + satisfaction: this.randomInt(0, 5), + registeredAt: new Date(Date.now() - this.randomInt(0, 365 * 24 * 60 * 60 * 1000)) + }; + } + + public generateUsers(count: number): User[] { + return Array.from({ length: count }, () => this.createUser()); + } + + public generateProducts(count: number): ProductInfo[] { + return Array.from({ length: count }, () => this.createProductInfo()); + } + + public generateSimpleUsers(count: number): UserSimple[] { + return Array.from({ length: count }, () => this.createUserSimple()); + } +} diff --git a/src/app/grid-lite/grid-lite.sample.html b/src/app/grid-lite/grid-lite.sample.html new file mode 100644 index 00000000000..b54523d933e --- /dev/null +++ b/src/app/grid-lite/grid-lite.sample.html @@ -0,0 +1,76 @@ + + + + + +
{{column.header}} Templated inline
+
+
+ + + + +
Cell {{value}}
+
+
+
+ + +
{{column.header}} Templated
+
+ + +
Cell [{{row.index}}, {{column.field}}]: {{value}} {{row.data}}
+
diff --git a/src/app/grid-lite/grid-lite.sample.scss b/src/app/grid-lite/grid-lite.sample.scss new file mode 100644 index 00000000000..549bfa51179 --- /dev/null +++ b/src/app/grid-lite/grid-lite.sample.scss @@ -0,0 +1,4 @@ +.grid { + width: 90%; + justify-self: center; +} diff --git a/src/app/grid-lite/grid-lite.sample.ts b/src/app/grid-lite/grid-lite.sample.ts new file mode 100644 index 00000000000..cc48126acb8 --- /dev/null +++ b/src/app/grid-lite/grid-lite.sample.ts @@ -0,0 +1,42 @@ +import { Component, CUSTOM_ELEMENTS_SCHEMA, inject, viewChild } from '@angular/core'; +import { IgxGridLiteCellTemplateDirective, IgxGridLiteColumnComponent, IgxGridLiteComponent, IgxGridLiteFilteringExpression, IgxGridLiteHeaderTemplateDirective, IgxGridLiteSortingExpression, IgxGridLiteSortingOptions } from "igniteui-angular/grids/lite"; +import { GridLiteDataService } from './data.service'; +@Component({ + selector: 'app-grid-lite-sample', + templateUrl: 'grid-lite.sample.html', + styleUrls: ['grid-lite.sample.scss'], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [IgxGridLiteComponent, IgxGridLiteColumnComponent, IgxGridLiteHeaderTemplateDirective, IgxGridLiteCellTemplateDirective] +}) +export class GridLiteSampleComponent { + protected grid = viewChild>('grid'); + protected data = []; + private dataService = inject(GridLiteDataService); + + protected sortingExpressions: IgxGridLiteSortingExpression[] = [ + { + key: 'firstName', + direction: "ascending" + } + ] + + protected filteringExpressions: IgxGridLiteFilteringExpression[] = [ + { + key: 'age', + condition: 'greaterThan', + searchTerm: 50 + } + ] + + protected sortingOptions: IgxGridLiteSortingOptions = { + mode: 'multiple' + } + + constructor() { + this.data = this.dataService.generateUsers(10); + } + + protected logEvent(name: string, event: any) { + console.log(name, event); + } +}