Skip to content
Merged
Show file tree
Hide file tree
Changes from 87 commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
7562cc5
chore(*): Cell merge POC.
Jul 2, 2025
f42bb3c
chore(*): Minor tweaks to suggestion.
Jul 2, 2025
870e7f6
chore(*): Implement with rows that are retained outside the virt.frame.
Jul 3, 2025
bb4e39f
chore(*): Fix border styles.
Jul 7, 2025
e160c65
chore(*): Add handling for variable row height.
Jul 7, 2025
4667bb0
chore(*): Add handling if different rows have different sizes.
Jul 7, 2025
febbd43
chore(*): Extract hardcoded styles in class.
Jul 7, 2025
4c5d922
chore(*): Adjust API and members.
Jul 7, 2025
a42b192
chore(*): Add mergeStrategy for grid and merging comparer for column.
Jul 8, 2025
1361952
chore(*): Adjust pipe triggers.
Jul 9, 2025
407357f
chore(*): Re-use cell templates.
Jul 9, 2025
f23dd6c
chore(*): Fix row indexes in merged row area.
Jul 9, 2025
f6ffa42
chore(*): Adjust some styles for row selection.
Jul 9, 2025
4d1b981
chore(*): Merged cells navigation.
Jul 10, 2025
aca57ee
chore(*): Adjust for scenarios after horizontal nav into merged cell.
Jul 10, 2025
e168c3a
chore(*): Add integration with pinning.
Jul 10, 2025
02b866a
chore(*): Merge cells in pinned col.
Jul 11, 2025
d515e97
chore(*): Adjust check for non-mergeable record types.
Jul 11, 2025
95e5b40
chore(*): Break merge sequence on row activation. Discard custom merg…
Jul 11, 2025
759b465
chore(*): Merge in pinned row area.
Jul 11, 2025
9329079
chore(*): Implement selection per spec.
Jul 14, 2025
19c2efe
chore(*): Add hover styles.
Jul 14, 2025
52860e8
chore(*): Fix duplicate host listeners.
Jul 14, 2025
16d4ac6
chore(*): Merge ghost(pinned) records with same value in main area.
Jul 14, 2025
f81bd27
chore(*): Fix check for merged data.
Jul 15, 2025
2ba947e
chore(*): Refactor to calculate mergedData rows only on chunk change.
Jul 15, 2025
95d0186
chore(*): Cache merge data index and re-use in template.
Jul 15, 2025
2a65bc2
chore(*): Cell merge in hierarchical grid - initial commit.
Jul 15, 2025
5c4587f
chore(*): Implement for tree grid. Add 2 strategies.
Jul 16, 2025
7ac17e4
chore(*): Export strategies in package.
Jul 16, 2025
401370e
chore(*): Fix chip displaying when in merged cell.
Jul 16, 2025
5e1b702
chore(*): Minor tweaks. Console warn on invalid setup.
Jul 16, 2025
58e6bfa
chore(*): Fix lint errors.
Jul 17, 2025
5e81bf9
chore(*): Fix theming lint.
Jul 17, 2025
b2b61a7
chore(*): Fix build and imports.
Jul 17, 2025
9a7a2d2
chore(*): Add null check.
Jul 17, 2025
ce0c854
chore(*): Null check for activeRowIndex.
Jul 17, 2025
a6fc511
chore(*): Generate elements config.
Jul 17, 2025
d025473
chore(*): Break up merge groups on cell selection.
Jul 17, 2025
190f29d
chore(*): Update active indexes on events. Cache result to limit pipe…
Jul 18, 2025
50e7d85
chore(*): When searching, mark merged cells as a single result.
Jul 18, 2025
1e560e8
chore(*): Refresh search if needed only.
Jul 18, 2025
7db787a
chore(*): Fix scrollTo when scrolling to a merged cell that has large…
Jul 21, 2025
86bc021
chore(*): Add basic merging tests.
Jul 21, 2025
91c457b
chore(*): Add some UI tests for merging.
Jul 21, 2025
a5c5566
chore(*): Add some integration tests.
Jul 22, 2025
46ea7ae
chore(*): Add more integration tests.
Jul 23, 2025
f862469
chore(*): Add more integration tests.
Jul 23, 2025
fc46739
chore(*): Add Hgrid and TreeGrid integration tests.
Jul 24, 2025
ea9baa6
chore(*): Fix lint i tests.
Jul 24, 2025
1f4c554
chore(*): Update Changelog.
Jul 24, 2025
94eb0a8
Merge branch 'master' into mkirova/cell-merge-POC-2
Jul 24, 2025
cc47820
chore(*): Fix unrelated respy issue in combo tests.
Jul 24, 2025
35a1bde
chore(*): Fix hardcoded value in unrelated test.
Jul 24, 2025
a7970d1
chore(*): Improve samples a bit.
Jul 25, 2025
5ab08a8
chore(*): In case cell is merged cell placeholder, do not render cont…
Jul 28, 2025
229bcd1
chore(*): Adjust indexes when there are pinned rows to top.
Jul 28, 2025
e64ee01
chore(*): Fix templates in hgrid and tgrid.
Jul 28, 2025
238390f
chore(*): Update external merge container on data changing.
Jul 28, 2025
c071635
chore(*): Adjust selection check for pinned row root.
Jul 28, 2025
9f1ffd0
chore(*): Fix more indexes due to row pinning.
Jul 28, 2025
0ae58e0
chore(*): Fix tgrid check.
Jul 28, 2025
2267e03
chore(*): Clear active row indexes when selection is cleared.
Jul 28, 2025
6178a23
chore(*): Adjust selection check to use pinned view if row is pinned.
Jul 28, 2025
d044219
chore(*): Fix scrollbar disappearing on data changing.
Jul 28, 2025
b8a77e2
chore(*): Notify changes after merge data is updated.
Jul 28, 2025
0351166
chore(*): Apply review comments on styles.
Jul 29, 2025
9cc56b6
chore(*): Extract key from tree grid record.
Jul 29, 2025
cbf4a1c
chore(*): Update merge indexes when in a paged grid context.
Jul 29, 2025
e491681
chore(*): Update size if repaint was requested. Optimize a bit index …
Jul 29, 2025
5d2a5ce
chore(*): Remove unnecessary inherit that overrides selection in pin …
Jul 29, 2025
db23777
chore(*): Fix timing issue between activation and drag selection.
Jul 29, 2025
3f7c27e
chore(*): Make activation and merge tests async.
Jul 29, 2025
2bbd9b8
chore(*): Approximate click position in merge cell to activate closes…
Jul 30, 2025
bcae71b
chore(*): Pass clientY when simulating pointer events in tests.
Jul 30, 2025
c081168
Merge remote-tracking branch 'origin/master' into mkirova/cell-merge-…
ChronosSF Jul 31, 2025
2d6b443
chore(*): Fix background styles when pinned and merged.
Jul 31, 2025
2a9726d
Merge branch 'mkirova/cell-merge-POC-2' of https://github.com/IgniteU…
Jul 31, 2025
f69b105
chore(*): Fix background styles when merged, hovered and selected.
Jul 31, 2025
13b4c24
chore(*): Add merge strategy to pipe trigger so that it can be change…
Jul 31, 2025
05e42c6
chore(*): Change detect on runtime strategy change.
Jul 31, 2025
e46786e
Merge branch 'master' into mkirova/cell-merge-POC-2
ChronosSF Jul 31, 2025
a06132a
fix(*): Fix positioning in pin right scenario. Extract styles in class.
Aug 1, 2025
5528867
chore(*): Fix border styles for cell merging.
Aug 1, 2025
4f75644
Merge branch 'master' into mkirova/cell-merge-POC-2
ChronosSF Aug 5, 2025
968363c
Merge branch 'master' into mkirova/cell-merge-POC-2
ChronosSF Aug 5, 2025
b052b64
Merge pull request #16107 from IgniteUI/mkirova/cell-merge-right-pin-fix
ChronosSF Aug 5, 2025
8e45f72
chore(*): Apply review comments.
MayaKirova Aug 7, 2025
d6b9e8e
chore(*): Update tests since border is now removed.
Aug 7, 2025
e235e83
Merge branch 'mkirova/cell-merge-POC-2' of https://github.com/IgniteU…
Aug 7, 2025
88e01da
Merge branch 'master' into mkirova/cell-merge-POC-2
Aug 14, 2025
e91b097
chore(*): Add explicit notifyChange after activeNode is changed.
Aug 14, 2025
6e103ac
chore(*): Add merge strategy interface api docs.
Aug 14, 2025
75fd965
chore(*): Small review comments.
Aug 14, 2025
69b272c
chore(*): Limit how often visibleColumns array changes.
Aug 14, 2025
1a4fbd9
chore(*): Cache columnsToMerge and use as pipe trigger.
Aug 15, 2025
0483273
chore(*): Remove sortExpr as pipe trigger, since no longer needed.
Aug 15, 2025
6d148a4
chore(*): More optimizations for merged cols eval.
Aug 15, 2025
a17df96
chore(*): Remove change detect on mergeStrategy change.
Aug 18, 2025
a892a57
chore(*): Add handling for different date related dataTypes on column.
Aug 18, 2025
a503659
chore(*): Add test for date column.
Aug 18, 2025
69ba10d
chore(*): Apply review comments.
Aug 20, 2025
638df68
chore(*): Add scroll inertia in merged rows outside of virt.frame.
Aug 20, 2025
173532f
chore(*): merging pinning with cell merging
ChronosSF Aug 26, 2025
4d1c477
chore(*): applying modified merge for pinning
ChronosSF Aug 28, 2025
4c7495e
fix(merging): applying a performance improv for merging pipe
ChronosSF Aug 28, 2025
f624236
Merge pull request #16176 from IgniteUI/sstoychev/merge-pinning-cellm…
ChronosSF Aug 28, 2025
17dd6c0
Merge remote-tracking branch 'origin/master' into mkirova/cell-merge-…
ChronosSF Aug 28, 2025
514ad96
chore(docs): resolving broken changelog merge
ChronosSF Aug 28, 2025
c5507de
Merge branch 'master' into mkirova/cell-merge-POC-2
ChronosSF Aug 28, 2025
7e15c92
chore(docs): removing duplicated changelog entry.
ChronosSF Aug 28, 2025
e406dee
Merge branch 'master' into mkirova/cell-merge-POC-2
ChronosSF Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,32 @@
All notable changes for each version of this project will be documented in this file.

## 20.1.0

### New Features

- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid`
- Introduced a new cell merging feature that allows you to configure and merge cells in a column based on same data or other custom condition, into a single cell.

It can be enabled on the individual columns:

```html
<igx-column field="field" [merge]="true"></igx-column>
```
The merging can be configured on the grid level to apply either:
- `onSort` - only when the column is sorted.
- `always` - always, regardless of data operations.

```html
<igx-grid [cellMergeMode]="'always'">
</igx-grid>
```

The default `cellMergeMode` is `onSort`.

The functionality can be modified by setting a custom `mergeStrategy` on the grid, in case some other merge conditions or logic is needed for a custom scenario.

It's possible also to set a `mergeComparer` on the individual columns, in case some custom handling is needed for a particular data field.

- `IgxCarousel`
- Added `select` method overload accepting index.
```ts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export var registerConfig = [
],
numericProps: ["rowEnd", "colEnd", "rowStart", "colStart"],
boolProps: [
"merge",
"sortable",
"selectable",
"groupable",
Expand Down Expand Up @@ -158,6 +159,7 @@ export var registerConfig = [
"expanded",
"searchable",
"hidden",
"merge",
"sortable",
"groupable",
"editable",
Expand Down Expand Up @@ -213,6 +215,7 @@ export var registerConfig = [
"collapsible",
"expanded",
"searchable",
"merge",
"sortable",
"groupable",
"editable",
Expand Down
3 changes: 2 additions & 1 deletion projects/igniteui-angular-elements/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { IgxPivotDateDimension } from 'projects/igniteui-angular/src/lib/grids/p
import { PivotDimensionType } from 'projects/igniteui-angular/src/lib/grids/pivot-grid/pivot-grid.interface';
import { IgxDateSummaryOperand, IgxNumberSummaryOperand, IgxSummaryOperand, IgxTimeSummaryOperand } from 'projects/igniteui-angular/src/lib/grids/summaries/grid-summary';
import { HorizontalAlignment, VerticalAlignment } from 'projects/igniteui-angular/src/lib/services/overlay/utilities';

import { ByLevelTreeGridMergeStrategy } from 'projects/igniteui-angular/src/lib/data-operations/merge-strategy';

/** Export Public API, TODO: reorganize, Generate all w/ renames? */
export {
Expand All @@ -35,6 +35,7 @@ export {

NoopSortingStrategy as IgcNoopSortingStrategy,
NoopFilteringStrategy as IgcNoopFilteringStrategy,
ByLevelTreeGridMergeStrategy as IgcByLevelTreeGridMergeStrategy,

// Pivot API
IgxPivotDateDimension as IgcPivotDateDimension,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,14 @@
@extend %igx-grid__tr--pinned !optional;
}

@include e(tr, $m: merged) {
@extend %igx-grid__tr--merged !optional;
}

@include e(tr, $m: merged-top) {
@extend %igx-grid__tr--merged-top !optional;
}

@include e(tr, $m: pinned-top) {
@extend %igx-grid__tr--pinned-top !optional;
}
Expand All @@ -296,6 +304,22 @@
@extend %igx-grid__td--edited !optional;
}

@include e(td, $m: merged) {
@extend %igx-grid__td--merged !optional;
}

@include e(td, $mods: (merged-selected, merged-hovered)) {
@extend %igx-grid__td--merged-selected-hovered !optional;
}

@include e(td, $m: merged-selected) {
@extend %igx-grid__td--merged-selected !optional;
}

@include e(td, $m: merged-hovered) {
@extend %igx-grid__td--merged-hovered !optional;
}

@include e(td, $m: editing) {
@extend %igx-grid__td--editing !optional;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@

%grid-row--mrl {
%igx-grid__hierarchical-expander--header,
%igx-grid__hierarchical-expander,
%igx-grid__header-indentation,
%igx-grid__row-indentation,
%grid__cbx-selection {
Expand Down Expand Up @@ -754,6 +755,7 @@
}

%grid__cbx-selection,
%igx-grid__hierarchical-expander,
%igx-grid__row-indentation,
%igx-grid__drag-indicator {
border-bottom: rem(1px) solid var-get($theme, 'row-border-color');
Expand Down Expand Up @@ -1338,6 +1340,35 @@
}
}

%igx-grid__tr--merged {
border-bottom: 0px;
}

%igx-grid__tr--merged-top {
position: absolute;
width: 100%;
}

%igx-grid__td--merged {
z-index: 1;
grid-row: 1 / -1;
}

%igx-grid__td--merged-selected {
color: var-get($theme, 'row-selected-text-color');
background: var-get($theme, 'row-selected-background') !important;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is important needed here and in the consequent placeholder selectors?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's because if the pinned cell style:

    %grid-cell--pinned {
        position: relative;
        background: inherit;
        z-index: 9999;
    }

It overwrites the background, when the cell is also pinned.

}

%igx-grid__td--merged-hovered {
background: var-get($theme, 'row-hover-background') !important;
color: var-get($theme, 'row-hover-text-color');
}

%igx-grid__td--merged-selected-hovered {
background: var-get($theme, 'row-selected-hover-background') !important;
color: var-get($theme, 'row-selected-hover-text-color');
}

%igx-grid__tr--deleted {
%grid-cell-text {
font-style: italic;
Expand Down
12 changes: 11 additions & 1 deletion projects/igniteui-angular/src/lib/data-operations/data-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { IGroupingState } from './groupby-state.interface';
import { mergeObjects } from '../core/utils';
import { Transaction, TransactionType, HierarchicalTransaction } from '../services/transaction/transaction';
import { getHierarchy, isHierarchyMatch } from './operations';
import { GridType } from '../grids/common/grid.interface';
import { ColumnType, GridType } from '../grids/common/grid.interface';
import { ITreeGridRecord } from '../grids/tree-grid/tree-grid.interfaces';
import { ISortingExpression } from './sorting-strategy';
import {
Expand All @@ -20,6 +20,7 @@ import {
} from '../grids/common/strategy';
import { DefaultDataCloneStrategy, IDataCloneStrategy } from '../data-operations/data-clone-strategy';
import { IGroupingExpression } from './grouping-expression.interface';
import { DefaultMergeStrategy, IGridMergeStrategy } from './merge-strategy';

/**
* @hidden
Expand Down Expand Up @@ -90,6 +91,15 @@ export class DataUtil {
return grouping.groupBy(data, state, grid, groupsRecords, fullResult);
}

public static merge<T>(data: T[], columns: ColumnType[], strategy: IGridMergeStrategy = new DefaultMergeStrategy(), activeRowIndexes = [], grid: GridType = null,
): any[] {
let result = [];
for (const col of columns) {
strategy.merge(data, col.field, col.mergingComparer, result, activeRowIndexes, grid);
}
return result;
}

public static page<T>(data: T[], state: IPagingState, dataLength?: number): T[] {
if (!state) {
return data;
Expand Down
128 changes: 128 additions & 0 deletions projects/igniteui-angular/src/lib/data-operations/merge-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { GridType } from '../grids/common/grid.interface';




export interface IMergeByResult {
rowSpan: number;
root?: any;
prev?: any;
}

export interface IGridMergeStrategy {
/* blazorSuppress */
merge: (
data: any[],
field: string,
comparer: (prevRecord: any, currentRecord: any, field: string) => boolean,
result: any[],
activeRowIndexes : number[],
grid?: GridType
) => any[];
comparer: (prevRecord: any, record: any, field: string) => boolean;
}

export class DefaultMergeStrategy implements IGridMergeStrategy {
protected static _instance: DefaultMergeStrategy = null;

public static instance(): DefaultMergeStrategy {
return this._instance || (this._instance = new this());
}

/* blazorSuppress */
public merge(
data: any[],
field: string,
comparer: (prevRecord: any, record: any, field: string) => boolean = this.comparer,
result: any[],
activeRowIndexes : number[],
grid?: GridType
) {
let prev = null;
let index = 0;
for (const rec of data) {

const recData = result[index];
// if this is active row or some special record type - add and skip merging
if (activeRowIndexes.indexOf(index) != -1 || (grid && grid.isDetailRecord(rec) || grid.isGroupByRecord(rec) || grid.isChildGridRecord(rec))) {
if(!recData) {
result.push(rec);
}
prev = null;
index++;
continue;
}
let recToUpdateData = recData ?? { recordRef: grid.isGhostRecord(rec) ? rec.recordRef : rec, cellMergeMeta: new Map<string, IMergeByResult>(), ghostRecord: rec.ghostRecord };
recToUpdateData.cellMergeMeta.set(field, { rowSpan: 1 });
if (prev && comparer(prev.recordRef, recToUpdateData.recordRef, field) && prev.ghostRecord === recToUpdateData.ghostRecord) {
const root = prev.cellMergeMeta.get(field)?.root ?? prev;
root.cellMergeMeta.get(field).rowSpan += 1;
recToUpdateData.cellMergeMeta.get(field).root = root;
}
prev = recToUpdateData;
if (!recData) {
result.push(recToUpdateData);
}
index++;
}
return result;
}

/* blazorSuppress */
public comparer(prevRecord: any, record: any, field: string): boolean {
const a = prevRecord[field];
const b = record[field];
const an = (a === null || a === undefined);
const bn = (b === null || b === undefined);
if (an) {
if (bn) {
return true;
}
return false;
} else if (bn) {
return false;
}
return a === b;
}
}


export class DefaultTreeGridMergeStrategy extends DefaultMergeStrategy {
/* blazorSuppress */
public override comparer(prevRecord: any, record: any, field: string): boolean {
const a = prevRecord.data[field];
const b = record.data[field];
const an = (a === null || a === undefined);
const bn = (b === null || b === undefined);
if (an) {
if (bn) {
return true;
}
return false;
} else if (bn) {
return false;
}
return a === b;
}
}

export class ByLevelTreeGridMergeStrategy extends DefaultMergeStrategy {
/* blazorSuppress */
public override comparer(prevRecord: any, record: any, field: string): boolean {
const a = prevRecord.data[field];
const b = record.data[field];
const levelA = prevRecord.level;
const levelB = record.level;
const an = (a === null || a === undefined);
const bn = (b === null || b === undefined);
if (an) {
if (bn) {
return true;
}
return false;
} else if (bn) {
return false;
}
return a === b && levelA === levelB;
}
}
2 changes: 2 additions & 0 deletions projects/igniteui-angular/src/lib/grids/cell.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
<img [src]="value" [alt]="value | igxCellImageAlt" />
}
</ng-template>

<ng-template #emptyCell></ng-template>
<ng-template #addRowCell let-cell="cell">
@if (column.dataType !== 'boolean' || (column.dataType === 'boolean' && this.formatter)) {
<div
Expand Down
31 changes: 31 additions & 0 deletions projects/igniteui-angular/src/lib/grids/cell.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,18 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT
@Input()
public column: ColumnType;

/**
* @hidden
* @internal
*/
@Input()
public isPlaceholder: boolean;

/**
Gets whether this cell is a merged cell.
*/
@Input()
public isMerged: boolean;

/**
* @hidden
Expand Down Expand Up @@ -286,6 +298,9 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT
* @memberof IgxGridCellComponent
*/
public get template(): TemplateRef<any> {
if (this.isPlaceholder) {
return this.emptyCellTemplate;
}
if (this.editMode && this.formGroup) {
const inlineEditorTemplate = this.column.inlineEditorTemplate;
return inlineEditorTemplate ? inlineEditorTemplate : this.inlineEditorTemplate;
Expand Down Expand Up @@ -706,6 +721,9 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT
@ViewChild('defaultCell', { read: TemplateRef, static: true })
protected defaultCellTemplate: TemplateRef<any>;

@ViewChild('emptyCell', { read: TemplateRef, static: true })
protected emptyCellTemplate: TemplateRef<any>;

@ViewChild('defaultPinnedIndicator', { read: TemplateRef, static: true })
protected defaultPinnedIndicator: TemplateRef<any>;

Expand Down Expand Up @@ -1005,6 +1023,19 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT
* @internal
*/
public pointerdown = (event: PointerEvent) => {

if (this.isMerged) {
// need an approximation of where in the cell the user clicked to get actual index to be activated.
const scrollOffset = this.grid.verticalScrollContainer.scrollPosition + (event.y - this.grid.tbody.nativeElement.getBoundingClientRect().y);
const targetRowIndex = this.grid.verticalScrollContainer.getIndexAtScroll(scrollOffset);
if (targetRowIndex != this.rowIndex) {
const row = this.grid.rowList.toArray().find(x => x.index === targetRowIndex);
const actualTarget = row.cells.find(x => x.column === this.column);
actualTarget.pointerdown(event);
return;
}
}

if (this.cellSelectionMode !== GridSelectionMode.multiple) {
this.activate(event);
return;
Expand Down
Loading
Loading