diff --git a/projects/igniteui-angular/src/lib/grids/common/enums.ts b/projects/igniteui-angular/src/lib/grids/common/enums.ts
index 88d277ccd39..bd66328c97e 100644
--- a/projects/igniteui-angular/src/lib/grids/common/enums.ts
+++ b/projects/igniteui-angular/src/lib/grids/common/enums.ts
@@ -73,6 +73,20 @@ export const GridSelectionMode = {
} as const;
export type GridSelectionMode = (typeof GridSelectionMode)[keyof typeof GridSelectionMode];
+
+/**
+ * Enumeration representing different cell merging modes for the grid elements.
+ * - 'never': Never merge cells.
+ * - 'always': Always merge adjacent cells based on merge strategy.
+ * - 'onSort': Only merge cells in column that are sorted.
+ */
+export const GridCellMergeMode = {
+ never: 'never',
+ always: 'always',
+ onSort: 'onSort'
+} as const;
+export type GridCellMergeMode = (typeof GridCellMergeMode)[keyof typeof GridCellMergeMode];
+
/** Enumeration representing different column display order options. */
export const ColumnDisplayOrder = {
Alphabetical: 'Alphabetical',
diff --git a/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts b/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts
index 3c8d8aa4f38..3b8d8d2e098 100644
--- a/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts
+++ b/projects/igniteui-angular/src/lib/grids/common/grid.interface.ts
@@ -1,4 +1,4 @@
-import { ColumnPinningPosition, FilterMode, GridPagingMode, GridSelectionMode, GridSummaryCalculationMode, GridSummaryPosition, GridValidationTrigger, RowPinningPosition, Size } from './enums';
+import { ColumnPinningPosition, FilterMode, GridCellMergeMode, GridPagingMode, GridSelectionMode, GridSummaryCalculationMode, GridSummaryPosition, GridValidationTrigger, RowPinningPosition, Size } from './enums';
import {
ISearchInfo, IGridCellEventArgs, IRowSelectionEventArgs, IColumnSelectionEventArgs,
IPinColumnCancellableEventArgs, IColumnVisibilityChangedEventArgs, IColumnVisibilityChangingEventArgs,
@@ -690,6 +690,7 @@ export interface GridServiceType {
export interface GridType extends IGridDataBindable {
/** Represents the locale of the grid: `USD`, `EUR`, `GBP`, `CNY`, `JPY`, etc. */
locale: string;
+ cellMergeMode: GridCellMergeMode;
resourceStrings: IGridResourceStrings;
/* blazorSuppress */
/** Represents the native HTML element itself */
@@ -1180,6 +1181,7 @@ export interface GridType extends IGridDataBindable {
getEmptyRecordObjectFor(inRow: RowType): any;
isSummaryRow(rec: any): boolean;
isRecordPinned(rec: any): boolean;
+ isRecordMerged(rec: any): boolean;
getInitialPinnedIndex(rec: any): number;
isRecordPinnedByViewIndex(rowIndex: number): boolean;
isColumnGrouped(fieldName: string): boolean;
diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts
index 5213151e54e..3398421a771 100644
--- a/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts
+++ b/projects/igniteui-angular/src/lib/grids/grid-base.directive.ts
@@ -93,7 +93,8 @@ import {
RowPinningPosition,
GridPagingMode,
GridValidationTrigger,
- Size
+ Size,
+ GridCellMergeMode
} from './common/enums';
import {
IGridCellEventArgs,
@@ -2911,6 +2912,14 @@ export abstract class IgxGridBaseDirective implements GridType,
// }
}
+ /**
+ * Gets/Sets cell merge mode.
+ *
+ */
+ @WatchChanges()
+ @Input()
+ public cellMergeMode: GridCellMergeMode = GridCellMergeMode.never;
+
/**
* Gets/Sets row selection mode
*
@@ -3635,6 +3644,14 @@ export abstract class IgxGridBaseDirective implements GridType,
return this.getInitialPinnedIndex(rec) !== -1;
}
+ /**
+ * @hidden
+ * @internal
+ */
+ public isRecordMerged(rec) {
+ return rec.cellMergeMeta;
+ }
+
/**
* @hidden
* @internal
diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html
index 7289bc16f29..e3b00c36d28 100644
--- a/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html
+++ b/projects/igniteui-angular/src/lib/grids/grid/grid-row.component.html
@@ -31,7 +31,19 @@
}
}
-
+ @if (this.hasMergedCells) {
+
+
+
+ }
+ @else {
+
+ }
+
@if (pinnedColumns.length > 0 && !grid.isPinningToStart) {
@for (col of pinnedColumns | igxNotGrouped; track trackPinnedColumn(col)) {
@@ -158,6 +170,40 @@
+
+
+
+
+
() };
+ for (const col of visibleColumns) {
+ recData.cellMergeMeta.set(col.field, { rowSpan: 1 });
+ //TODO condition can be a strategy or some callback that the user can set.
+ //TODO can also be limited to only sorted columns
+ if ( prev && prev.recordRef[col.field] === rec[col.field]) {
+ // const root = prev.cellMergeMeta.get(col.field)?.root ?? prev;
+ // root.cellMergeMeta.get(col.field).rowSpan += 1;
+ // recData.cellMergeMeta.get(col.field).root = root;
+ recData.cellMergeMeta.get(col.field).prev = prev;
+ let curr = prev;
+ while(curr) {
+ curr.cellMergeMeta.get(col.field).rowSpan += 1;
+ curr = curr.cellMergeMeta.get(col.field).prev;
+ }
+ }
+ }
+ prev = recData;
+ result.push(recData);
+ }
+ return result;
+ }
+}
+
+export interface IMergeByResult {
+ rowSpan: number;
+ root?: any;
+ prev?: any;
+}
+
/**
* @hidden
*/
diff --git a/projects/igniteui-angular/src/lib/grids/row.directive.ts b/projects/igniteui-angular/src/lib/grids/row.directive.ts
index b2118b1f15e..a353b728c6e 100644
--- a/projects/igniteui-angular/src/lib/grids/row.directive.ts
+++ b/projects/igniteui-angular/src/lib/grids/row.directive.ts
@@ -27,6 +27,7 @@ import { mergeWith } from 'lodash-es';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { trackByIdentity } from '../core/utils';
+import { IMergeByResult } from './grid/grid.pipes';
@Directive({
selector: '[igxRowBaseComponent]',
@@ -117,6 +118,10 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy {
return this.grid.isRecordPinned(this.data);
}
+ public get hasMergedCells(): boolean {
+ return this.grid.isRecordMerged(this.data);
+ }
+
/**
* Gets the expanded state of the row.
* ```typescript
@@ -592,6 +597,11 @@ export class IgxRowDirective implements DoCheck, AfterViewInit, OnDestroy {
this.addAnimationEnd.emit(this);
}
+ protected getMergeCellSpan(col: ColumnType){
+ const rowCount = this.data.cellMergeMeta.get(col.field).rowSpan;
+ return `repeat(${rowCount},51px)`;
+ }
+
/**
* @hidden
*/
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index c4637552c6b..c3d2d4d104b 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -211,6 +211,11 @@ export class AppComponent implements OnInit {
icon: 'view_column',
name: 'Grid Cell Editing'
},
+ {
+ link: '/gridCellMerging',
+ icon: 'view_column',
+ name: 'Grid Cell Merging'
+ },
{
link: '/gridClipboard',
icon: 'insert_comment',
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 992516f1574..5234d396dc1 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -51,6 +51,7 @@ import { TimePickerSampleComponent } from './time-picker/time-picker.sample';
import { ToastShowcaseSampleComponent } from './toast-showcase/toast-showcase.sample';
import { VirtualForSampleComponent } from './virtual-for-directive/virtual-for.sample';
import { GridCellEditingComponent } from './grid-cellEditing/grid-cellEditing.component';
+import { GridCellMergingComponent } from './grid-cellMerging/grid-cellMerging.component';
import { GridSampleComponent } from './grid/grid.sample';
import { GridColumnMovingSampleComponent } from './grid-column-moving/grid-column-moving.sample';
import { GridColumnSelectionSampleComponent } from './grid-column-selection/grid-column-selection.sample';
@@ -419,6 +420,10 @@ export const appRoutes: Routes = [
path: 'gridCellEditing',
component: GridCellEditingComponent
},
+ {
+ path: 'gridCellMerging',
+ component: GridCellMergingComponent
+ },
{
path: 'gridConditionalCellStyling',
component: GridCellStylingSampleComponent
diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.html b/src/app/grid-cellMerging/grid-cellMerging.component.html
new file mode 100644
index 00000000000..6b2628ac345
--- /dev/null
+++ b/src/app/grid-cellMerging/grid-cellMerging.component.html
@@ -0,0 +1,15 @@
+Grid with cell merge
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.scss b/src/app/grid-cellMerging/grid-cellMerging.component.scss
new file mode 100644
index 00000000000..3d4037836d0
--- /dev/null
+++ b/src/app/grid-cellMerging/grid-cellMerging.component.scss
@@ -0,0 +1,19 @@
+.sample-actions {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 1rem 0;
+ gap: 0.5rem;
+}
+
+.density-chooser {
+ margin-bottom: 1rem;
+
+ igx-buttongroup {
+ display: block;
+ width: 500px;
+ }
+}
+
+.grid-size {
+ --ig-size: var(--ig-size-small);
+}
diff --git a/src/app/grid-cellMerging/grid-cellMerging.component.ts b/src/app/grid-cellMerging/grid-cellMerging.component.ts
new file mode 100644
index 00000000000..c4184a997ae
--- /dev/null
+++ b/src/app/grid-cellMerging/grid-cellMerging.component.ts
@@ -0,0 +1,357 @@
+import { Component, HostBinding, ViewChild } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import {
+ IgxColumnComponent,
+ IgxGridComponent,
+} from 'igniteui-angular';
+
+import { data, dataWithoutPK } from '../shared/data';
+
+@Component({
+ selector: 'app-grid-cellMerging',
+ templateUrl: 'grid-cellMerging.component.html',
+ styleUrl: 'grid-cellMerging.component.scss',
+ imports: [
+ FormsModule,
+ IgxColumnComponent,
+ IgxGridComponent,
+ ]
+})
+export class GridCellMergingComponent {
+ public data = [{
+ ProductID: 1,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '10 boxes x 20 bags',
+ UnitPrice: '18.0000',
+ UnitsInStock: 39,
+ UnitsOnOrder: 0,
+ ReorderLevel: 10.567,
+ Discontinued: false,
+ OrderDate: null,
+ OrderDate2: new Date(1991, 2, 12, 18, 40, 50).toISOString()
+ }, {
+ ProductID: 2,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 3,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 4,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '20.0000',
+ UnitsInStock: 20,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 5,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 6,
+ ProductName: 'Chang',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 7,
+ ProductName: 'Chang',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 8,
+ ProductName: 'Chang',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 30,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 9,
+ ProductName: 'Aniseed Syrup',
+ SupplierID: 1,
+ CategoryID: 2,
+ QuantityPerUnit: '12 - 550 ml bottles',
+ UnitPrice: '10.0000',
+ UnitsInStock: 13,
+ UnitsOnOrder: 70,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2006-03-17').toISOString(),
+ OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString()
+ },
+ {
+ ProductID: 10,
+ ProductName: 'Chang',
+ SupplierID: 1,
+ CategoryID: 2,
+ QuantityPerUnit: '12 - 550 ml bottles',
+ UnitPrice: '10.0000',
+ UnitsInStock: 13,
+ UnitsOnOrder: 70,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2006-03-17').toISOString(),
+ OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString()
+ },
+ {
+ ProductID: 11,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 2,
+ QuantityPerUnit: '12 - 550 ml bottles',
+ UnitPrice: '10.0000',
+ UnitsInStock: 13,
+ UnitsOnOrder: 70,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2006-03-17').toISOString(),
+ OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString()
+ },
+ {
+ ProductID: 12,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 2,
+ QuantityPerUnit: '12 - 550 ml bottles',
+ UnitPrice: '10.0000',
+ UnitsInStock: 12,
+ UnitsOnOrder: 70,
+ ReorderLevel: 30,
+ Discontinued: false,
+ OrderDate: new Date('2006-03-17').toISOString(),
+ OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString()
+ },
+ {
+ ProductID: 1,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '10 boxes x 20 bags',
+ UnitPrice: '18.0000',
+ UnitsInStock: 39,
+ UnitsOnOrder: 0,
+ ReorderLevel: 10.567,
+ Discontinued: false,
+ OrderDate: null,
+ OrderDate2: new Date(1991, 2, 12, 18, 40, 50).toISOString()
+ }, {
+ ProductID: 2,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 3,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 4,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '20.0000',
+ UnitsInStock: 20,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 5,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 6,
+ ProductName: 'Chang',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 7,
+ ProductName: 'Chang',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 8,
+ ProductName: 'Chang',
+ SupplierID: 1,
+ CategoryID: 1,
+ QuantityPerUnit: '24 - 12 oz bottles',
+ UnitPrice: '19.0000',
+ UnitsInStock: 17,
+ UnitsOnOrder: 40,
+ ReorderLevel: 30,
+ Discontinued: false,
+ OrderDate: new Date('2003-03-17').toISOString(),
+ OrderDate2: new Date('2003-03-17').toISOString()
+ },
+ {
+ ProductID: 9,
+ ProductName: 'Aniseed Syrup',
+ SupplierID: 1,
+ CategoryID: 2,
+ QuantityPerUnit: '12 - 550 ml bottles',
+ UnitPrice: '10.0000',
+ UnitsInStock: 13,
+ UnitsOnOrder: 70,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2006-03-17').toISOString(),
+ OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString()
+ },
+ {
+ ProductID: 10,
+ ProductName: 'Chang',
+ SupplierID: 1,
+ CategoryID: 2,
+ QuantityPerUnit: '12 - 550 ml bottles',
+ UnitPrice: '10.0000',
+ UnitsInStock: 13,
+ UnitsOnOrder: 70,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2006-03-17').toISOString(),
+ OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString()
+ },
+ {
+ ProductID: 11,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 2,
+ QuantityPerUnit: '12 - 550 ml bottles',
+ UnitPrice: '10.0000',
+ UnitsInStock: 13,
+ UnitsOnOrder: 70,
+ ReorderLevel: 25,
+ Discontinued: false,
+ OrderDate: new Date('2006-03-17').toISOString(),
+ OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString()
+ },
+ {
+ ProductID: 12,
+ ProductName: 'Chai',
+ SupplierID: 1,
+ CategoryID: 2,
+ QuantityPerUnit: '12 - 550 ml bottles',
+ UnitPrice: '10.0000',
+ UnitsInStock: 12,
+ UnitsOnOrder: 70,
+ ReorderLevel: 30,
+ Discontinued: false,
+ OrderDate: new Date('2006-03-17').toISOString(),
+ OrderDate2: new Date(1991, 2, 12, 15, 40, 50).toISOString()
+ }];
+
+}
+