Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GridType } from '../grids/common/grid.interface';
export interface IMergeByResult {
rowSpan: number;
root?: any;
childRecords?: any[];
}

/**
Expand Down Expand Up @@ -75,10 +76,11 @@ export class DefaultMergeStrategy implements IGridMergeStrategy {
continue;
}
const recToUpdateData = recData ?? { recordRef: grid.isGhostRecord(rec) ? rec.recordRef : rec, cellMergeMeta: new Map<string, IMergeByResult>(), ghostRecord: rec.ghostRecord };
recToUpdateData.cellMergeMeta.set(field, { rowSpan: 1 });
recToUpdateData.cellMergeMeta.set(field, { rowSpan: 1, childRecords: [] });
if (prev && comparer.call(this, prev.recordRef, recToUpdateData.recordRef, field, isDate, isTime) && prev.ghostRecord === recToUpdateData.ghostRecord) {
const root = prev.cellMergeMeta.get(field)?.root ?? prev;
root.cellMergeMeta.get(field).rowSpan += 1;
root.cellMergeMeta.get(field).childRecords.push(recToUpdateData);
recToUpdateData.cellMergeMeta.get(field).root = root;
}
prev = recToUpdateData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
| gridRowPinning:id:true:pipeTrigger
| gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true
| gridSort:sortingExpressions:groupingExpressions:sortStrategy:id:pipeTrigger:true
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData) {
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger
| gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData) {
@if (pinnedData.length > 0) {
<div #pinContainer
[ngClass]="{
Expand All @@ -96,7 +97,8 @@
| gridDetails:hasDetails:expansionStates:pipeTrigger
| gridAddRow:false:pipeTrigger
| gridRowPinning:id:false:pipeTrigger
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:false:pipeTrigger"
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger
| gridUnmergeActive:columnsToMerge:activeRowIndexes:false:pipeTrigger"
let-rowIndex="index" [igxForScrollOrientation]="'vertical'" [igxForScrollContainer]="verticalScroll"
[igxForContainerSize]="calcHeight"
[igxForItemSize]="hasColumnLayouts ? rowHeight * multiRowLayoutRowSize + 1 : renderedRowHeight"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { IGridGroupingStrategy } from '../common/strategy';
import { IgxGridValidationService } from './grid-validation.service';
import { IgxGridDetailsPipe } from './grid.details.pipe';
import { IgxGridSummaryPipe } from './grid.summary.pipe';
import { IgxGridGroupingPipe, IgxGridPagingPipe, IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe } from './grid.pipes';
import { IgxGridGroupingPipe, IgxGridPagingPipe, IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from './grid.pipes';
import { IgxSummaryDataPipe } from '../summaries/grid-root-summary.pipe';
import { IgxGridTransactionPipe, IgxHasVisibleColumnsPipe, IgxGridRowPinningPipe, IgxGridAddRowPipe, IgxGridRowClassesPipe, IgxGridRowStylesPipe, IgxStringReplacePipe } from '../common/pipes';
import { IgxGridColumnResizerComponent } from '../resizing/resizer.component';
Expand Down Expand Up @@ -154,6 +154,7 @@ export interface IGroupingDoneEventArgs extends IBaseEventArgs {
IgxGridDetailsPipe,
IgxStringReplacePipe,
IgxGridCellMergePipe,
IgxGridUnmergeActivePipe,
IgxScrollInertiaDirective
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
Expand Down
66 changes: 64 additions & 2 deletions projects/igniteui-angular/src/lib/grids/grid/grid.pipes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,76 @@ export class IgxGridCellMergePipe implements PipeTransform {

constructor(@Inject(IGX_GRID_BASE) private grid: GridType) { }

public transform(collection: any, colsToMerge: ColumnType[], mergeMode: GridCellMergeMode, mergeStrategy: IGridMergeStrategy, activeRowIndexes: number[], pinned: boolean, _pipeTrigger: number) {
public transform(collection: any, colsToMerge: ColumnType[], mergeMode: GridCellMergeMode, mergeStrategy: IGridMergeStrategy, _pipeTrigger: number) {
if (colsToMerge.length === 0) {
return collection;
}
const result = DataUtil.merge(collection, colsToMerge, mergeStrategy, [], this.grid);
return result;
}
}

@Pipe({
name: 'gridUnmergeActive',
standalone: true
})
export class IgxGridUnmergeActivePipe implements PipeTransform {

constructor(@Inject(IGX_GRID_BASE) private grid: GridType) { }

public transform(collection: any, colsToMerge: ColumnType[], activeRowIndexes: number[], pinned: boolean, _pipeTrigger: number) {
if (colsToMerge.length === 0) {
return collection;
}
if (this.grid.hasPinnedRecords && !pinned && this.grid.pinning.rows !== RowPinningPosition.Bottom) {
activeRowIndexes = activeRowIndexes.map(x => x - this.grid.pinnedRecordsCount);
}
const result = DataUtil.merge(cloneArray(collection), colsToMerge, mergeStrategy, activeRowIndexes, this.grid);
activeRowIndexes = Array.from(new Set(activeRowIndexes)).filter(x => !isNaN(x));
const rootsToUpdate = [];
activeRowIndexes.forEach(index => {
const target = collection[index];
if (target) {
colsToMerge.forEach(col => {
const colMeta = target.cellMergeMeta.get(col.field);
const root = colMeta.root || (colMeta.rowSpan > 1 ? target : null);
if (root) {
rootsToUpdate.push(root);
}
});
}
});
const uniqueRoots = Array.from(new Set(rootsToUpdate));
if (uniqueRoots.length === 0) {
// if nothing to update, return
return collection;
}
let result = cloneArray(collection) as any;
uniqueRoots.forEach(x => {
const index = result.indexOf(x);
const colKeys = [...x.cellMergeMeta.keys()];
const cols = colsToMerge.filter(col => colKeys.indexOf(col.field) !== -1);
let res = [];
for (const col of cols) {

let childData = x.cellMergeMeta.get(col.field).childRecords;
const childRecs = childData.map(rec => rec.recordRef);
const isDate = col?.dataType === 'date' || col?.dataType === 'dateTime';
const isTime = col?.dataType === 'time' || col?.dataType === 'dateTime';
res = this.grid.mergeStrategy.merge(
[x.recordRef, ...childRecs],
col.field,
col.mergingComparer,
res,
activeRowIndexes.map(ri => ri - index),
isDate,
isTime,
this.grid);

}
result = result.slice(0, index).concat(res, result.slice(index + res.length));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use a regular for-of loop here and instead of creating 3 new array coppies we can mutate the result (which is cloned beforehand) in place?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean going through all items in res and replacing each one in the original array with the unmerged version?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, we could. But it would look a bit ugly and also not sure how much more performant it would be considering that in the particular scenario:

  1. Go to the performance project and enable the merging on all columns. Run the performance samples using npm run start:performance and go to the grid with 1 million records. Sort the Position column. Click on the first cell of the Registered column. An error is thrown with maximum callstack exceeded.
Screenshot 2025-09-29 at 3 53 58 pm

It would need to loop through 300k+ records and replace each one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that

for(let i =0;i<res.length;++i) {
    result[index+i] = res[i];
}

would be faster because there is no array copying but changes the result in place. However, seems like the slice and concat version is perhaps more optimized by the js engine. I have tested this and, indeed, your current version is faster.

});


return result;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
| gridRowPinning:id:true:pipeTrigger
| gridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:id:pipeTrigger:filteringPipeTrigger:true
| gridSort:sortingExpressions:[]:sortStrategy:id:pipeTrigger:true
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger
| gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData
) {
@if (pinnedData.length > 0) {
<div #pinContainer class="igx-grid__tr--pinned"
Expand All @@ -76,7 +77,8 @@
| gridHierarchical:expansionStates:id:primaryKey:childLayoutKeys:pipeTrigger
| gridAddRow:false:pipeTrigger
| gridRowPinning:id:false:pipeTrigger
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:false:pipeTrigger"
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger
| gridUnmergeActive:columnsToMerge:activeRowIndexes:false:pipeTrigger"
[igxForScrollOrientation]="'vertical'" [igxForScrollContainer]="verticalScroll"
[igxForContainerSize]="calcHeight" [igxForItemSize]="renderedRowHeight" [igxForTrackBy]="trackChanges"
#verticalScrollContainer (chunkPreload)="dataLoading($event)" (dataChanging)="dataRebinding($event)" (dataChanged)="dataRebound($event)">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { IgxGridValidationService } from '../grid/grid-validation.service';
import { IgxGridHierarchicalPipe, IgxGridHierarchicalPagingPipe } from './hierarchical-grid.pipes';
import { IgxSummaryDataPipe } from '../summaries/grid-root-summary.pipe';
import { IgxGridTransactionPipe, IgxHasVisibleColumnsPipe, IgxGridRowPinningPipe, IgxGridAddRowPipe, IgxGridRowClassesPipe, IgxGridRowStylesPipe, IgxStringReplacePipe } from '../common/pipes';
import { IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe } from '../grid/grid.pipes';
import { IgxGridSortingPipe, IgxGridFilteringPipe, IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from '../grid/grid.pipes';
import { IgxGridColumnResizerComponent } from '../resizing/resizer.component';
import { IgxRowEditTabStopDirective } from '../grid.rowEdit.directive';
import { IgxIconComponent } from '../../icon/icon.component';
Expand Down Expand Up @@ -353,7 +353,8 @@ export class IgxChildGridRowComponent implements AfterViewInit, OnInit {
IgxGridHierarchicalPagingPipe,
IgxStringReplacePipe,
IgxGridCellMergePipe,
IgxScrollInertiaDirective
IgxScrollInertiaDirective,
IgxGridUnmergeActivePipe
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
| gridRowPinning:id:true:pipeTrigger
| treeGridFiltering:filteringExpressionsTree:filterStrategy:advancedFilteringExpressionsTree:pipeTrigger:filteringPipeTrigger:true
| treeGridSorting:sortingExpressions:treeGroupArea?.expressions:sortStrategy:pipeTrigger:true
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:true:pipeTrigger; as pinnedData
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger
| gridUnmergeActive:columnsToMerge:activeRowIndexes:true:pipeTrigger; as pinnedData
) {
@if (pinnedData.length > 0) {
<div #pinContainer
Expand Down Expand Up @@ -82,7 +83,8 @@
| treeGridSummary:hasSummarizedColumns:summaryCalculationMode:summaryPosition:showSummaryOnCollapse:pipeTrigger:summaryPipeTrigger
| treeGridAddRow:false:pipeTrigger
| gridRowPinning:id:false:pipeTrigger
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:activeRowIndexes:false:pipeTrigger"
| gridCellMerge:columnsToMerge:cellMergeMode:mergeStrategy:pipeTrigger
| gridUnmergeActive:columnsToMerge:activeRowIndexes:false:pipeTrigger"
let-rowIndex="index" [igxForScrollOrientation]="'vertical'" [igxForScrollContainer]='verticalScroll'
[igxForContainerSize]='calcHeight' [igxForItemSize]="renderedRowHeight" #verticalScrollContainer
(dataChanging)="dataRebinding($event)" (dataChanged)="dataRebound($event)">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ import { IgxGridDragSelectDirective } from '../selection/drag-select.directive';
import { IgxGridBodyDirective } from '../grid.common';
import { IgxGridHeaderRowComponent } from '../headers/grid-header-row.component';
import { IgxTextHighlightService } from '../../directives/text-highlight/text-highlight.service';
import { IgxGridCellMergePipe } from '../grid/grid.pipes';
import { IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from '../grid/grid.pipes';
import { DefaultTreeGridMergeStrategy, IGridMergeStrategy } from '../../data-operations/merge-strategy';
import { IgxScrollInertiaDirective } from '../../directives/scroll-inertia/scroll_inertia.directive';

Expand Down Expand Up @@ -173,7 +173,8 @@ let NEXT_ID = 0;
IgxTreeGridAddRowPipe,
IgxStringReplacePipe,
IgxGridCellMergePipe,
IgxScrollInertiaDirective
IgxScrollInertiaDirective,
IgxGridUnmergeActivePipe
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
Expand Down
5 changes: 2 additions & 3 deletions src/app/grid-cellMerging/grid-cellMerging.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ <h4 class="sample-title">Grid with cell merge</h4>
Value: {{val}},Index: {{cell.row.index}}
</ng-template>
</igx-column>
<igx-column field="ShipCountry" header="Ship Country" width="200px" [merge]="true" sortable="true">
<igx-column field="ShipCountry" header="Ship Country" [merge]="true" width="200px" sortable="true">
</igx-column>
<igx-column field="OrderDate" header="Order Date" width="200px" [merge]="true" [groupable]="true" [dataType]="'date'" sortable="true">
</igx-column>
Expand All @@ -70,8 +70,7 @@ <h4 class="sample-title">Grid with cell merge</h4>
</igx-column>
<igx-column field="Quantity" header="Quantity" width="150px" dataType="number">
</igx-column>
<igx-paginator [perPage]="20">
</igx-paginator>

<igx-action-strip>
<igx-grid-pinning-actions></igx-grid-pinning-actions>
</igx-action-strip>
Expand Down
13 changes: 12 additions & 1 deletion src/app/grid-cellMerging/grid-cellMerging.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { INVOICE_DATA } from '../shared/invoiceData';
FormsModule,
IgxColumnComponent,
IgxGridComponent,
IgxPaginatorComponent,
// IgxPaginatorComponent,
IgxActionStripComponent,
IgxGridPinningActionsComponent,
IgxGridToolbarComponent,
Expand Down Expand Up @@ -66,6 +66,17 @@ export class GridCellMergingComponent {
@ViewChild('grid1', { static: true }) public grid: IgxGridComponent;
public data = INVOICE_DATA;

constructor(){
const allData = INVOICE_DATA
const length = INVOICE_DATA.length;
for (let i = 1; i <= 600_000; i++) {
const rnd = Math.floor(Math.random() * length);
allData.push(Object.assign({}, INVOICE_DATA[rnd]));
}

this.data = allData;
}

public toggleStrategy() {
if (this.treeGridMergeStrategy instanceof ByLevelTreeGridMergeStrategy) {
this.treeGridMergeStrategy = new DefaultTreeGridMergeStrategy();
Expand Down
Loading