• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 28784046178

06 Jul 2026 10:10AM UTC coverage: 90.14% (+0.01%) from 90.127%
28784046178

Pull #17387

github

web-flow
Merge 9b3797999 into 5ea2a183e
Pull Request #17387: fix(grid): fix zone.onStable patterns broken in zoneless change detection - 21.2.x

14894 of 17358 branches covered (85.8%)

Branch coverage included in aggregate %.

43 of 48 new or added lines in 9 files covered. (89.58%)

5 existing lines in 4 files now uncovered.

29975 of 32419 relevant lines covered (92.46%)

39174.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.93
/projects/igniteui-angular/grids/tree-grid/src/tree-grid.component.ts
1
import { ChangeDetectionStrategy, Component, HostBinding, Input, OnInit, TemplateRef, ContentChild, AfterContentInit, ViewChild, DoCheck, AfterViewInit, CUSTOM_ELEMENTS_SCHEMA, booleanAttribute, inject } from '@angular/core';
2
import { NgClass, NgTemplateOutlet, NgStyle } from '@angular/common';
3
import { IgxTreeGridAPIService } from './tree-grid-api.service';
4
import { IgxGridBaseDirective, IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from 'igniteui-angular/grids/grid';
5
import {
6
    CellType,
7
    GridSelectionMode,
8
    GridType,
9
    IGX_GRID_BASE,
10
    IGX_GRID_SERVICE_BASE,
11
    IgxColumnComponent,
12
    IgxColumnMovingDropDirective,
13
    IgxColumnResizingService,
14
    IgxFilteringService,
15
    IgxGridBodyDirective,
16
    IgxGridCell,
17
    IgxGridColumnResizerComponent,
18
    IgxGridCRUDService,
19
    IgxGridDragSelectDirective,
20
    IgxGridHeaderRowComponent,
21
    IgxGridNavigationService,
22
    IgxGridRowClassesPipe,
23
    IgxGridRowPinningPipe,
24
    IgxGridRowStylesPipe,
25
    IgxGridSelectionService,
26
    IgxGridSummaryService,
27
    IgxGridTransaction,
28
    IgxGridValidationService,
29
    IgxHasVisibleColumnsPipe,
30
    IgxRowEditTabStopDirective,
31
    IgxStringReplacePipe,
32
    IgxSummaryDataPipe,
33
    IgxSummaryRow,
34
    IgxSummaryRowComponent,
35
    IgxTreeGridRow,
36
    IRowDataCancelableEventArgs,
37
    IRowDataEventArgs,
38
    IRowToggleEventArgs,
39
    RowType
40
} from 'igniteui-angular/grids/core';
41
import { first, takeUntil } from 'rxjs/operators';
42
import { IgxRowLoadingIndicatorTemplateDirective } from './tree-grid.directives';
43
import { IgxTreeGridSelectionService } from './tree-grid-selection.service';
44
import { DefaultTreeGridMergeStrategy, HierarchicalState, HierarchicalTransaction, HierarchicalTransactionService, IGridMergeStrategy, IgxHierarchicalTransactionFactory, IgxOverlayOutletDirective, ITreeGridRecord, mergeObjects, StateUpdateEvent, TransactionEventOrigin, TransactionType, TreeGridFilteringStrategy } from 'igniteui-angular/core';
45
import { IgxTreeGridSummaryPipe } from './tree-grid.summary.pipe';
46
import { IgxTreeGridFilteringPipe } from './tree-grid.filtering.pipe';
47
import { IgxTreeGridHierarchizingPipe, IgxTreeGridFlatteningPipe, IgxTreeGridSortingPipe, IgxTreeGridPagingPipe, IgxTreeGridTransactionPipe, IgxTreeGridNormalizeRecordsPipe, IgxTreeGridAddRowPipe } from './tree-grid.pipes';
48
import { IgxTreeGridRowComponent } from './tree-grid-row.component';
49
import { IgxButtonDirective, IgxForOfScrollSyncService, IgxForOfSyncService, IgxGridForOfDirective, IgxRippleDirective, IgxScrollInertiaDirective, IgxTemplateOutletDirective, IgxToggleDirective } from 'igniteui-angular/directives';
50
import { IgxCircularProgressBarComponent } from 'igniteui-angular/progressbar';
51
import { IgxSnackbarComponent } from 'igniteui-angular/snackbar';
52
import { IgxIconComponent } from 'igniteui-angular/icon';
53
import { IgxTreeGridGroupByAreaComponent } from './tree-grid-group-by-area.component';
54

55
let NEXT_ID = 0;
3✔
56

57
/* blazorAdditionalDependency: Column */
58
/* blazorAdditionalDependency: ColumnGroup */
59
/* blazorAdditionalDependency: ColumnLayout */
60
/* blazorAdditionalDependency: GridToolbar */
61
/* blazorAdditionalDependency: GridToolbarActions */
62
/* blazorAdditionalDependency: GridToolbarTitle */
63
/* blazorAdditionalDependency: GridToolbarAdvancedFiltering */
64
/* blazorAdditionalDependency: GridToolbarExporter */
65
/* blazorAdditionalDependency: GridToolbarHiding */
66
/* blazorAdditionalDependency: GridToolbarPinning */
67
/* blazorAdditionalDependency: ActionStrip */
68
/* blazorAdditionalDependency: GridActionsBaseDirective */
69
/* blazorAdditionalDependency: GridEditingActions */
70
/* blazorAdditionalDependency: GridPinningActions */
71
/* blazorIndirectRender */
72
/**
73
 * **Ignite UI for Angular Tree Grid** -
74
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/grid/grid)
75
 *
76
 * The Ignite UI Tree Grid displays and manipulates hierarchical data with consistent schema formatted as a table and
77
 * provides features such as sorting, filtering, editing, column pinning, paging, column moving and hiding.
78
 *
79
 * Example:
80
 * ```html
81
 * <igx-tree-grid [data]="employeeData" primaryKey="employeeID" foreignKey="PID" [autoGenerate]="false">
82
 *   <igx-column field="first" header="First Name"></igx-column>
83
 *   <igx-column field="last" header="Last Name"></igx-column>
84
 *   <igx-column field="role" header="Role"></igx-column>
85
 * </igx-tree-grid>
86
 * ```
87
 */
88
@Component({
89
    changeDetection: ChangeDetectionStrategy.OnPush,
90
    selector: 'igx-tree-grid',
91
    templateUrl: 'tree-grid.component.html',
92
    providers: [
93
        IgxGridCRUDService,
94
        IgxGridValidationService,
95
        IgxGridSummaryService,
96
        IgxGridNavigationService,
97
        { provide: IgxGridSelectionService, useClass: IgxTreeGridSelectionService },
98
        { provide: IGX_GRID_SERVICE_BASE, useClass: IgxTreeGridAPIService },
99
        { provide: IGX_GRID_BASE, useExisting: IgxTreeGridComponent },
100
        IgxFilteringService,
101
        IgxColumnResizingService,
102
        IgxForOfSyncService,
103
        IgxForOfScrollSyncService
104
    ],
105
    imports: [
106
        NgClass,
107
        NgStyle,
108
        NgTemplateOutlet,
109
        IgxGridHeaderRowComponent,
110
        IgxGridBodyDirective,
111
        IgxGridDragSelectDirective,
112
        IgxColumnMovingDropDirective,
113
        IgxGridForOfDirective,
114
        IgxTemplateOutletDirective,
115
        IgxTreeGridRowComponent,
116
        IgxSummaryRowComponent,
117
        IgxOverlayOutletDirective,
118
        IgxToggleDirective,
119
        IgxCircularProgressBarComponent,
120
        IgxSnackbarComponent,
121
        IgxButtonDirective,
122
        IgxRippleDirective,
123
        IgxRowEditTabStopDirective,
124
        IgxIconComponent,
125
        IgxGridColumnResizerComponent,
126
        IgxHasVisibleColumnsPipe,
127
        IgxGridRowPinningPipe,
128
        IgxGridRowClassesPipe,
129
        IgxGridRowStylesPipe,
130
        IgxSummaryDataPipe,
131
        IgxTreeGridHierarchizingPipe,
132
        IgxTreeGridFlatteningPipe,
133
        IgxTreeGridSortingPipe,
134
        IgxTreeGridFilteringPipe,
135
        IgxTreeGridPagingPipe,
136
        IgxTreeGridTransactionPipe,
137
        IgxTreeGridSummaryPipe,
138
        IgxTreeGridNormalizeRecordsPipe,
139
        IgxTreeGridAddRowPipe,
140
        IgxStringReplacePipe,
141
        IgxGridCellMergePipe,
142
        IgxScrollInertiaDirective,
143
        IgxGridUnmergeActivePipe
144
    ],
145
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
146
})
147
export class IgxTreeGridComponent extends IgxGridBaseDirective implements GridType, OnInit, AfterViewInit, DoCheck, AfterContentInit {
3✔
148
    protected override _diTransactions = inject<HierarchicalTransactionService<HierarchicalTransaction, HierarchicalState>>(IgxGridTransaction, { optional: true, });
561✔
149
    protected override transactionFactory = inject(IgxHierarchicalTransactionFactory);
561✔
150

151
    /**
152
     * Sets the child data key of the `IgxTreeGridComponent`.
153
     * ```html
154
     * <igx-tree-grid #grid [data]="employeeData" [childDataKey]="'employees'" [autoGenerate]="true"></igx-tree-grid>
155
     * ```
156
     *
157
     * @memberof IgxTreeGridComponent
158
     */
159
    @Input()
160
    public childDataKey: string;
161

162
    /**
163
     * Sets the foreign key of the `IgxTreeGridComponent`.
164
     * ```html
165
     * <igx-tree-grid #grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'" [autoGenerate]="true">
166
     * </igx-tree-grid>
167
     * ```
168
     *
169
     * @memberof IgxTreeGridComponent
170
     */
171
    @Input()
172
    public foreignKey: string;
173

174
    /**
175
     * Sets the key indicating whether a row has children.
176
     * This property is only used for load on demand scenarios.
177
     * ```html
178
     * <igx-tree-grid #grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'"
179
     *                [loadChildrenOnDemand]="loadChildren"
180
     *                [hasChildrenKey]="'hasEmployees'">
181
     * </igx-tree-grid>
182
     * ```
183
     *
184
     * @memberof IgxTreeGridComponent
185
     */
186
    @Input()
187
    public hasChildrenKey: string;
188

189
    /**
190
     * Sets whether child records should be deleted when their parent gets deleted.
191
     * By default it is set to true and deletes all children along with the parent.
192
     * ```html
193
     * <igx-tree-grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'" cascadeOnDelete="false">
194
     * </igx-tree-grid>
195
     * ```
196
     *
197
     * @memberof IgxTreeGridComponent
198
     */
199
    @Input({ transform: booleanAttribute })
200
    public cascadeOnDelete = true;
561✔
201

202
    /* csSuppress */
203
    /**
204
     * Sets a callback for loading child rows on demand.
205
     * ```html
206
     * <igx-tree-grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'" [loadChildrenOnDemand]="loadChildren">
207
     * </igx-tree-grid>
208
     * ```
209
     * ```typescript
210
     * public loadChildren = (parentID: any, done: (children: any[]) => void) => {
211
     *     this.dataService.getData(parentID, children => done(children));
212
     * }
213
     * ```
214
     *
215
     * @memberof IgxTreeGridComponent
216
     */
217
    @Input()
218
    public loadChildrenOnDemand: (parentID: any, done: (children: any[]) => void) => void;
219

220
    /**
221
     * @hidden @internal
222
     */
223
    @HostBinding('attr.role')
224
    public role = 'treegrid';
561✔
225

226
    /**
227
     * Sets the value of the `id` attribute. If not provided it will be automatically generated.
228
     * ```html
229
     * <igx-tree-grid [id]="'igx-tree-grid-1'"></igx-tree-grid>
230
     * ```
231
     *
232
     * @memberof IgxTreeGridComponent
233
     */
234
    @HostBinding('attr.id')
235
    @Input()
236
    public id = `igx-tree-grid-${NEXT_ID++}`;
561✔
237

238
    /**
239
     * @hidden
240
     * @internal
241
     */
242
    @ContentChild(IgxTreeGridGroupByAreaComponent, { read: IgxTreeGridGroupByAreaComponent })
243
    public treeGroupArea: IgxTreeGridGroupByAreaComponent;
244

245
    /**
246
     * @hidden @internal
247
     */
248
    @ViewChild('record_template', { read: TemplateRef, static: true })
249
    protected recordTemplate: TemplateRef<any>;
250

251
    /**
252
     * @hidden @internal
253
     */
254
    @ViewChild('summary_template', { read: TemplateRef, static: true })
255
    protected summaryTemplate: TemplateRef<any>;
256

257
    /**
258
     * @hidden
259
     */
260
    @ContentChild(IgxRowLoadingIndicatorTemplateDirective, { read: IgxRowLoadingIndicatorTemplateDirective })
261
    protected rowLoadingTemplate: IgxRowLoadingIndicatorTemplateDirective;
262

263
    /**
264
     * @hidden
265
     */
266
    public flatData: any[] | null;
267

268
    /**
269
     * @hidden
270
     */
271
    public processedExpandedFlatData: any[] | null;
272

273
    /**
274
     * Returns an array of the root level `ITreeGridRecord`s.
275
     * ```typescript
276
     * // gets the root record with index=2
277
     * const states = this.grid.rootRecords[2];
278
     * ```
279
     *
280
     * @memberof IgxTreeGridComponent
281
     */
282
    public rootRecords: ITreeGridRecord[];
283

284
    /* blazorSuppress */
285
    /**
286
     * Returns a map of all `ITreeGridRecord`s.
287
     * ```typescript
288
     * // gets the record with primaryKey=2
289
     * const states = this.grid.records.get(2);
290
     * ```
291
     *
292
     * @memberof IgxTreeGridComponent
293
     */
294
    public records: Map<any, ITreeGridRecord> = new Map<any, ITreeGridRecord>();
561✔
295

296
    /**
297
     * Returns an array of processed (filtered and sorted) root `ITreeGridRecord`s.
298
     * ```typescript
299
     * // gets the processed root record with index=2
300
     * const states = this.grid.processedRootRecords[2];
301
     * ```
302
     *
303
     * @memberof IgxTreeGridComponent
304
     */
305
    public processedRootRecords: ITreeGridRecord[];
306

307
    /* blazorSuppress */
308
    /**
309
     * Returns a map of all processed (filtered and sorted) `ITreeGridRecord`s.
310
     * ```typescript
311
     * // gets the processed record with primaryKey=2
312
     * const states = this.grid.processedRecords.get(2);
313
     * ```
314
     *
315
     * @memberof IgxTreeGridComponent
316
     */
317
    public processedRecords: Map<any, ITreeGridRecord> = new Map<any, ITreeGridRecord>();
561✔
318

319
    /**
320
     * @hidden
321
     */
322
    public loadingRows = new Set<any>();
561✔
323

324
    protected override _filterStrategy = new TreeGridFilteringStrategy();
561✔
325
    protected override _transactions: HierarchicalTransactionService<HierarchicalTransaction, HierarchicalState>;
326
    protected override _mergeStrategy: IGridMergeStrategy = new DefaultTreeGridMergeStrategy();
561✔
327
    private _data;
328
    private _rowLoadingIndicatorTemplate: TemplateRef<void>;
329
    private _expansionDepth = Infinity;
561✔
330

331
    /* treatAsRef */
332
    /**
333
     * Gets/Sets the array of data that populates the component.
334
     * ```html
335
     * <igx-tree-grid [data]="Data" [autoGenerate]="true"></igx-tree-grid>
336
     * ```
337
     *
338
     * @memberof IgxTreeGridComponent
339
     */
340
    @Input()
341
    public get data(): any[] | null {
342
        return this._data;
20,003✔
343
    }
344

345
    /* treatAsRef */
346
    public set data(value: any[] | null) {
347
        const oldData = this._data;
578✔
348
        this._data = value || [];
578✔
349
        this.summaryService.clearSummaryCache();
578✔
350
        if (!this._init) {
578✔
351
            this.validation.updateAll(this._data);
25✔
352
        }
353
        if (this.autoGenerate && this._data.length > 0 && this.shouldRecreateColumns(oldData, this._data)) {
578✔
354
            this.setupColumns();
5✔
355
        }
356
        this.checkPrimaryKeyField();
578✔
357
        this.cdr.markForCheck();
578✔
358
    }
359

360
    /** @hidden @internal */
361
    public override get type(): GridType["type"] {
362
        return 'tree';
2,539✔
363
    }
364

365
    /**
366
     * Get transactions service for the grid.
367
     *
368
     * @experimental @hidden
369
     */
370
    public override get transactions() {
371
        if (this._diTransactions && !this.batchEditing) {
1,072,693✔
372
            return this._diTransactions;
85,466✔
373
        }
374
        return this._transactions;
987,227✔
375
    }
376

377
    /**
378
     * Sets the count of levels to be expanded in the `IgxTreeGridComponent`. By default it is
379
     * set to `Infinity` which means all levels would be expanded.
380
     * ```html
381
     * <igx-tree-grid #grid [data]="employeeData" [childDataKey]="'employees'" expansionDepth="1" [autoGenerate]="true"></igx-tree-grid>
382
     * ```
383
     *
384
     * @memberof IgxTreeGridComponent
385
     */
386
    @Input()
387
    public get expansionDepth(): number {
388
        return this._expansionDepth;
15,464✔
389
    }
390

391
    public set expansionDepth(value: number) {
392
        this._expansionDepth = value;
193✔
393
        this.notifyChanges();
193✔
394
    }
395

396
    /**
397
     * Template for the row loading indicator when load on demand is enabled.
398
     * ```html
399
     * <ng-template #rowLoadingTemplate>
400
     *     <igx-icon>loop</igx-icon>
401
     * </ng-template>
402
     *
403
     * <igx-tree-grid #grid [data]="employeeData" [primaryKey]="'ID'" [foreignKey]="'parentID'"
404
     *                [loadChildrenOnDemand]="loadChildren"
405
     *                [rowLoadingIndicatorTemplate]="rowLoadingTemplate">
406
     * </igx-tree-grid>
407
     * ```
408
     *
409
     * @memberof IgxTreeGridComponent
410
     */
411
    @Input()
412
    public get rowLoadingIndicatorTemplate(): TemplateRef<void> {
413
        return this._rowLoadingIndicatorTemplate;
8✔
414
    }
415

416
    public set rowLoadingIndicatorTemplate(value: TemplateRef<void>) {
417
        this._rowLoadingIndicatorTemplate = value;
×
418
        this.notifyChanges();
×
419
    }
420

421
    // Kind of stupid
422
    // private get _gridAPI(): IgxTreeGridAPIService {
423
    //     return this.gridAPI as IgxTreeGridAPIService;
424
    // }
425

426
    /**
427
     * @hidden
428
     */
429
    public override ngOnInit() {
430
        super.ngOnInit();
561✔
431

432
        this.rowToggle.pipe(takeUntil(this.destroy$)).subscribe((args) => {
561✔
433
            this.loadChildrenOnRowExpansion(args);
181✔
434
        });
435

436
        // TODO: cascade selection logic should be refactor to be handled in the already existing subs
437
        this.rowAddedNotifier.pipe(takeUntil(this.destroy$)).subscribe(args => {
561✔
438
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
61✔
439
                let rec = this.gridAPI.get_rec_by_id(this.primaryKey ? args.data[this.primaryKey] : args.data);
6!
440
                if (rec && rec.parent) {
6✔
441
                    this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(
4✔
442
                        new Set([rec.parent]), rec.parent.key);
443
                } else {
444
                    // The record is still not available
445
                    // Wait for the change detection to update records through pipes
446
                    requestAnimationFrame(() => {
2✔
447
                        rec = this.gridAPI.get_rec_by_id(this.primaryKey ?
2!
448
                            args.data[this.primaryKey] : args.data);
449
                        if (rec && rec.parent) {
2✔
450
                            this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(
2✔
451
                                new Set([rec.parent]), rec.parent.key);
452
                        }
453
                        this.notifyChanges();
2✔
454
                    });
455
                }
456
            }
457
        });
458

459
        this.rowDeletedNotifier.pipe(takeUntil(this.destroy$)).subscribe(args => {
561✔
460
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
107✔
461
                if (args.data) {
8!
462
                    const rec = this.gridAPI.get_rec_by_id(
8✔
463
                        this.primaryKey ? args.data[this.primaryKey] : args.data);
8!
464
                    this.handleCascadeSelection(args, rec);
8✔
465
                } else {
466
                    // if a row has been added and before commiting the transaction deleted
467
                    const leafRowsDirectParents = new Set<any>();
×
468
                    this.records.forEach(record => {
×
469
                        if (record && (!record.children || record.children.length === 0) && record.parent) {
×
470
                            leafRowsDirectParents.add(record.parent);
×
471
                        }
472
                    });
473
                    // Wait for the change detection to update records through pipes
474
                    requestAnimationFrame(() => {
×
475
                        this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(leafRowsDirectParents);
×
476
                        this.notifyChanges();
×
477
                    });
478
                }
479
            }
480
        });
481

482
        this.filteringDone.pipe(takeUntil(this.destroy$)).subscribe(() => {
561✔
483
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
59✔
484
                const leafRowsDirectParents = new Set<any>();
15✔
485
                this.records.forEach(record => {
15✔
486
                    if (record && (!record.children || record.children.length === 0) && record.parent) {
151✔
487
                        leafRowsDirectParents.add(record.parent);
91✔
488
                    }
489
                });
490
                this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(leafRowsDirectParents);
15✔
491
                this.notifyChanges();
15✔
492
            }
493
        });
494
    }
495

496
    /**
497
     * @hidden
498
     */
499
    public override ngAfterViewInit() {
500
        super.ngAfterViewInit();
561✔
501
        // TODO: pipesExectured event
502
        // run after change detection in super triggers pipes for records structure
503
        if (this.rowSelection === GridSelectionMode.multipleCascade && this.selectedRows.length) {
561!
504
            const selRows = this.selectedRows;
×
505
            this.selectionService.clearRowSelection();
×
506
            this.selectRows(selRows, true);
×
507
            this.cdr.detectChanges();
×
508
        }
509
    }
510

511
    /**
512
     * @hidden
513
     */
514
    public override ngAfterContentInit() {
515
        if (this.rowLoadingTemplate) {
561!
516
            this._rowLoadingIndicatorTemplate = this.rowLoadingTemplate.template;
×
517
        }
518
        super.ngAfterContentInit();
561✔
519
    }
520

521
    public override getDefaultExpandState(record: ITreeGridRecord): boolean {
522
        return record.children && record.children.length && record.level < this.expansionDepth;
×
523
    }
524

525
    /**
526
     * Expands all rows.
527
     * ```typescript
528
     * this.grid.expandAll();
529
     * ```
530
     *
531
     * @memberof IgxTreeGridComponent
532
     */
533
    public override expandAll() {
534
        this._expansionDepth = Infinity;
42✔
535
        this.expansionStates = new Map<any, boolean>();
42✔
536
    }
537

538
    /**
539
     * Collapses all rows.
540
     *
541
     * ```typescript
542
     * this.grid.collapseAll();
543
     *  ```
544
     *
545
     * @memberof IgxTreeGridComponent
546
     */
547
    public override collapseAll() {
548
        this._expansionDepth = 0;
14✔
549
        this.expansionStates = new Map<any, boolean>();
14✔
550
    }
551

552
    /**
553
     * @hidden
554
     */
555
    public override refreshGridState(args?: IRowDataEventArgs) {
556
        super.refreshGridState();
65✔
557
        if (this.primaryKey && this.foreignKey && args) {
65✔
558
            const rowID = args.data[this.foreignKey];
35✔
559
            this.summaryService.clearSummaryCache({ rowID });
35✔
560
            this.pipeTrigger++;
35✔
561
            this.cdr.detectChanges();
35✔
562
        }
563
    }
564

565
    /* blazorCSSuppress */
566
    /**
567
     * Creates a new `IgxTreeGridRowComponent` with the given data. If a parentRowID is not specified, the newly created
568
     * row would be added at the root level. Otherwise, it would be added as a child of the row whose primaryKey matches
569
     * the specified parentRowID. If the parentRowID does not exist, an error would be thrown.
570
     * ```typescript
571
     * const record = {
572
     *     ID: this.grid.data[this.grid1.data.length - 1].ID + 1,
573
     *     Name: this.newRecord
574
     * };
575
     * this.grid.addRow(record, 1); // Adds a new child row to the row with ID=1.
576
     * ```
577
     *
578
     * @param data
579
     * @param parentRowID
580
     * @memberof IgxTreeGridComponent
581
     */
582
    // TODO: remove evt emission
583
    public override addRow(data: any, parentRowID?: any) {
584
        this.crudService.endEdit(true);
56✔
585
        this.gridAPI.addRowToData(data, parentRowID);
56✔
586

587
        this.rowAddedNotifier.next({
52✔
588
            data: data,
589
            rowData: data, owner: this,
590
            primaryKey: data[this.primaryKey],
591
            rowKey: data[this.primaryKey]
592
        });
593
        this.pipeTrigger++;
52✔
594
        this.notifyChanges();
52✔
595
    }
596

597
    /**
598
     * Enters add mode by spawning the UI with the context of the specified row by index.
599
     *
600
     * @remarks
601
     * Accepted values for index are integers from 0 to this.grid.dataView.length
602
     * @remarks
603
     * When adding the row as a child, the parent row is the specified row.
604
     * @remarks
605
     * To spawn the UI on top, call the function with index = null or a negative number.
606
     * In this case trying to add this row as a child will result in error.
607
     * @example
608
     * ```typescript
609
     * this.grid.beginAddRowByIndex(10);
610
     * this.grid.beginAddRowByIndex(10, true);
611
     * this.grid.beginAddRowByIndex(null);
612
     * ```
613
     * @param index - The index to spawn the UI at. Accepts integers from 0 to this.grid.dataView.length
614
     * @param asChild - Whether the record should be added as a child. Only applicable to igxTreeGrid.
615
     */
616
    public override beginAddRowByIndex(index: number, asChild?: boolean): void {
617
        if (index === null || index < 0) {
×
618
            return this.beginAddRowById(null, asChild);
×
619
        }
620
        return this._addRowForIndex(index - 1, asChild);
×
621
    }
622

623
    /**
624
     * @hidden
625
     */
626
    public getContext(rowData: any, rowIndex: number, pinned?: boolean): any {
627
        return {
52,111✔
628
            $implicit: this.isGhostRecord(rowData) || this.isRecordMerged(rowData) ? rowData.recordRef : rowData,
156,170✔
629
            index: this.getDataViewIndex(rowIndex, pinned),
630
            templateID: {
631
                type: this.isSummaryRow(rowData) ? 'summaryRow' : 'dataRow',
52,111✔
632
                id: null
633
            },
634
            disabled: this.isGhostRecord(rowData) ? rowData.recordRef.isFilteredOutParent === undefined : false,
52,111✔
635
            metaData: this.isRecordMerged(rowData) ? rowData : null
52,111✔
636
        };
637
    }
638

639
    /**
640
     * @hidden
641
     * @internal
642
     */
643
    public override getInitialPinnedIndex(rec) {
644
        const id = this.gridAPI.get_row_id(rec);
233,197✔
645
        return this._pinnedRecordIDs.indexOf(id);
233,197✔
646
    }
647

648
    /**
649
     * @hidden
650
     * @internal
651
     */
652
    public override isRecordPinned(rec) {
653
        return this.getInitialPinnedIndex(rec.data) !== -1;
233,147✔
654
    }
655

656
    /**
657
     *
658
     * Returns an array of the current cell selection in the form of `[{ column.field: cell.value }, ...]`.
659
     *
660
     * @remarks
661
     * If `formatters` is enabled, the cell value will be formatted by its respective column formatter (if any).
662
     * If `headers` is enabled, it will use the column header (if any) instead of the column field.
663
     */
664
    public override getSelectedData(formatters = false, headers = false): any[] {
128✔
665
        let source = [];
64✔
666

667
        const process = (record) => {
64✔
668
            if (record.summaries) {
1,091✔
669
                source.push(null);
21✔
670
                return;
21✔
671
            }
672
            source.push(record.data);
1,070✔
673
        };
674

675
        this.unpinnedDataView.forEach(process);
64✔
676
        source = this.isRowPinningToTop ? [...this.pinnedDataView, ...source] : [...source, ...this.pinnedDataView];
64!
677
        return this.extractDataFromSelection(source, formatters, headers);
64✔
678
    }
679

680
    /**
681
     * @hidden @internal
682
     */
683
    public override getEmptyRecordObjectFor(inTreeRow: RowType) {
684
        const treeRowRec = inTreeRow?.treeRow || null;
10✔
685
        const row = { ...treeRowRec };
10✔
686
        const data = treeRowRec?.data || {};
10✔
687
        row.data = { ...data };
10✔
688
        Object.keys(row.data).forEach(key => {
10✔
689
            // persist foreign key if one is set.
690
            if (this.foreignKey && key === this.foreignKey) {
45✔
691
                row.data[key] = treeRowRec.data[this.crudService.addRowParent?.asChild ? this.primaryKey : key];
8✔
692
            } else {
693
                row.data[key] = undefined;
37✔
694
            }
695
        });
696
        let id = this.generateRowID();
10✔
697
        const rootRecPK = this.foreignKey && this.rootRecords && this.rootRecords.length > 0 ?
10✔
698
            this.rootRecords[0].data[this.foreignKey] : null;
699
        if (id === rootRecPK) {
10!
700
            // safeguard in case generated id matches the root foreign key.
UNCOV
701
            id = this.generateRowID();
×
702
        }
703
        row.key = id;
10✔
704
        row.data[this.primaryKey] = id;
10✔
705
        return { rowID: id, data: row.data, recordRef: row };
10✔
706
    }
707

708
    /** @hidden */
709
    public override deleteRowById(rowId: any): any {
710
        //  if this is flat self-referencing data, and CascadeOnDelete is set to true
711
        //  and if we have transactions we should start pending transaction. This allows
712
        //  us in case of delete action to delete all child rows as single undo action
713
        const args: IRowDataCancelableEventArgs = {
64✔
714
            rowID: rowId,
715
            primaryKey: rowId,
716
            rowKey: rowId,
717
            cancel: false,
718
            rowData: this.getRowData(rowId),
719
            data: this.getRowData(rowId),
720
            oldValue: null,
721
            owner: this
722
        };
723
        this.rowDelete.emit(args);
64✔
724
        if (args.cancel) {
64✔
725
            return;
1✔
726
        }
727

728
        const record = this.gridAPI.deleteRowById(rowId);
63✔
729
        const key = record[this.primaryKey];
63✔
730
        if (record !== null && record !== undefined) {
63✔
731
            const rowDeletedEventArgs: IRowDataEventArgs = {
63✔
732
                data: record,
733
                rowData: record,
734
                owner: this,
735
                primaryKey: key,
736
                rowKey: key
737
            };
738
            this.rowDeleted.emit(rowDeletedEventArgs);
63✔
739
        }
740
        return record;
63✔
741
    }
742

743
    /**
744
     * Returns the `IgxTreeGridRow` by index.
745
     *
746
     * @example
747
     * ```typescript
748
     * const myRow = treeGrid.getRowByIndex(1);
749
     * ```
750
     * @param index
751
     */
752
    public getRowByIndex(index: number): RowType {
753
        if (index < 0 || index >= this.dataView.length) {
903✔
754
            return undefined;
1✔
755
        }
756
        return this.createRow(index);
902✔
757
    }
758

759
    /**
760
     * Returns the `RowType` object by the specified primary key.
761
     *
762
     * @example
763
     * ```typescript
764
     * const myRow = this.treeGrid.getRowByIndex(1);
765
     * ```
766
     * @param index
767
     */
768
    public getRowByKey(key: any): RowType {
769
        const rec = this.filteredSortedData ? this.primaryKey ? this.filteredSortedData.find(r => r[this.primaryKey] === key) :
308✔
770
            this.filteredSortedData.find(r => r === key) : undefined;
10✔
771
        const index = this.dataView.findIndex(r => r.data && r.data === rec);
363✔
772
        if (index < 0 || index >= this.filteredSortedData.length) {
109✔
773
            return undefined;
7✔
774
        }
775
        return new IgxTreeGridRow(this as any, index, rec);
102✔
776
    }
777

778
    /**
779
     * Returns the collection of all RowType for current page.
780
     *
781
     * @hidden @internal
782
     */
783
    public allRows(): RowType[] {
784
        return this.dataView.map((rec, index) => this.createRow(index));
2,303✔
785
    }
786

787
    /**
788
     * Returns the collection of `IgxTreeGridRow`s for current page.
789
     *
790
     * @hidden @internal
791
     */
792
    public dataRows(): RowType[] {
793
        return this.allRows().filter(row => row instanceof IgxTreeGridRow);
2,303✔
794
    }
795

796
    /**
797
     * Returns an array of the selected `IgxGridCell`s.
798
     *
799
     * @example
800
     * ```typescript
801
     * const selectedCells = this.grid.selectedCells;
802
     * ```
803
     */
804
    public get selectedCells(): CellType[] {
805
        return this.dataRows().map((row) => row.cells.filter((cell) => cell.selected))
11,098✔
806
            .reduce((a, b) => a.concat(b), []);
2,303✔
807
    }
808

809
    /**
810
     * Returns a `CellType` object that matches the conditions.
811
     *
812
     * @example
813
     * ```typescript
814
     * const myCell = this.grid1.getCellByColumn(2, "UnitPrice");
815
     * ```
816
     * @param rowIndex
817
     * @param columnField
818
     */
819
    public getCellByColumn(rowIndex: number, columnField: string): CellType {
820
        const row = this.getRowByIndex(rowIndex);
222✔
821
        const column = this.columns.find((col) => col.field === columnField);
533✔
822
        if (row && row instanceof IgxTreeGridRow && column) {
222✔
823
            return new IgxGridCell(this as any, rowIndex, column);
222✔
824
        }
825
    }
826

827
    /**
828
     * Returns a `CellType` object that matches the conditions.
829
     *
830
     * @remarks
831
     * Requires that the primaryKey property is set.
832
     * @example
833
     * ```typescript
834
     * grid.getCellByKey(1, 'index');
835
     * ```
836
     * @param rowSelector match any rowID
837
     * @param columnField
838
     */
839
    public getCellByKey(rowSelector: any, columnField: string): CellType {
840
        const row = this.getRowByKey(rowSelector);
5✔
841
        const column = this.columns.find((col) => col.field === columnField);
10✔
842
        if (row && column) {
5✔
843
            return new IgxGridCell(this as any, row.index, column);
5✔
844
        }
845
    }
846

847
    public override pinRow(rowID: any, index?: number): boolean {
848
        const row = this.getRowByKey(rowID);
27✔
849
        return super.pinRow(rowID, index, row);
27✔
850
    }
851

852
    public override unpinRow(rowID: any): boolean {
853
        const row = this.getRowByKey(rowID);
5✔
854
        return super.unpinRow(rowID, row);
5✔
855
    }
856

857
    /** @hidden */
858
    public generateRowPath(rowId: any): any[] {
859
        const path: any[] = [];
52✔
860
        let record = this.records.get(rowId);
52✔
861

862
        while (record.parent) {
52✔
863
            path.push(record.parent.key);
46✔
864
            record = record.parent;
46✔
865
        }
866

867
        return path.reverse();
52✔
868
    }
869

870
    /** @hidden */
871
    public isTreeRow(record: any): boolean {
872
        return record.key !== undefined && record.data;
18,237✔
873
    }
874

875
    /** @hidden */
876
    public override getUnpinnedIndexById(id) {
877
        return this.unpinnedRecords.findIndex(x => x.data[this.primaryKey] === id);
43✔
878
    }
879

880
    /**
881
     * @hidden
882
     */
883
    public createRow(index: number, data?: any): RowType {
884
        let row: RowType;
885
        const dataIndex = this._getDataViewIndex(index);
18,230✔
886
        const rec: any = data ?? this.dataView[dataIndex];
18,230✔
887

888
        if (this.isSummaryRow(rec)) {
18,230✔
889
            row = new IgxSummaryRow(this as any, index, rec.summaries);
7✔
890
        }
891

892
        if (!row && rec) {
18,230✔
893
            const isTreeRow = this.isTreeRow(rec);
18,223✔
894
            const dataRec = isTreeRow ? rec.data : rec;
18,223✔
895
            const treeRow = isTreeRow ? rec : undefined;
18,223✔
896
            row = new IgxTreeGridRow(this as any, index, dataRec, treeRow);
18,223✔
897
        }
898

899
        return row;
18,230✔
900
    }
901

902
    protected override generateDataFields(data: any[]): string[] {
903
        return super.generateDataFields(data).filter(field => field !== this.childDataKey);
21✔
904
    }
905

906
    protected override transactionStatusUpdate(event: StateUpdateEvent) {
907
        let actions = [];
152✔
908
        if (event.origin === TransactionEventOrigin.REDO) {
152✔
909
            actions = event.actions ? event.actions.filter(x => x.transaction.type === TransactionType.DELETE) : [];
25!
910
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
20✔
911
                this.handleCascadeSelection(event);
1✔
912
            }
913
        } else if (event.origin === TransactionEventOrigin.UNDO) {
132✔
914
            actions = event.actions ? event.actions.filter(x => x.transaction.type === TransactionType.ADD) : [];
32!
915
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
27✔
916
                if (event.actions[0].transaction.type === 'add') {
2✔
917
                    const rec = this.gridAPI.get_rec_by_id(event.actions[0].transaction.id);
1✔
918
                    this.handleCascadeSelection(event, rec);
1✔
919
                } else {
920
                    this.handleCascadeSelection(event);
1✔
921
                }
922
            }
923
        }
924
        if (actions.length) {
152✔
925
            for (const action of actions) {
13✔
926
                this.deselectChildren(action.transaction.id);
18✔
927
            }
928
        }
929
        super.transactionStatusUpdate(event);
152✔
930
    }
931

932
    protected findRecordIndexInView(rec) {
933
        return this.dataView.findIndex(x => x.data[this.primaryKey] === rec[this.primaryKey]);
×
934
    }
935

936
    /**
937
     * @hidden @internal
938
     */
939
    protected override getDataBasedBodyHeight(): number {
940
        return !this.flatData || (this.flatData.length < this._defaultTargetRecordNumber) ?
44✔
941
            0 : this.defaultTargetBodyHeight;
942
    }
943

944
    /**
945
     * @hidden
946
     */
947
    protected override scrollTo(row: any | number, column: any | number): void {
948
        let delayScrolling = false;
85✔
949
        let record: ITreeGridRecord;
950

951
        if (typeof (row) !== 'number') {
85✔
952
            const rowData = row;
84✔
953
            const rowID = this.gridAPI.get_row_id(rowData);
84✔
954
            record = this.processedRecords.get(rowID);
84✔
955
            this.gridAPI.expand_path_to_record(record);
84✔
956

957
            if (this.paginator) {
84✔
958
                const rowIndex = this.processedExpandedFlatData.indexOf(rowData);
27✔
959
                const page = Math.floor(rowIndex / this.perPage);
27✔
960

961
                if (this.page !== page) {
27✔
962
                    delayScrolling = true;
7✔
963
                    this.page = page;
7✔
964
                }
965
            }
966
        }
967

968
        if (delayScrolling) {
85✔
969
            this.verticalScrollContainer.dataChanged.pipe(first()).subscribe(() => {
7✔
970
                this.scrollDirective(this.verticalScrollContainer,
7✔
971
                    typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(record));
7!
972
            });
973
        } else {
974
            this.scrollDirective(this.verticalScrollContainer,
78✔
975
                typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(record));
78✔
976
        }
977

978
        this.scrollToHorizontally(column);
85✔
979
    }
980

981
    protected override writeToData(rowIndex: number, value: any) {
982
        mergeObjects(this.flatData[rowIndex], value);
×
983
    }
984

985
    /**
986
     * @hidden
987
     */
988
    protected override initColumns(collection: IgxColumnComponent[], cb: (args: any) => void = null) {
×
989
        if (this.hasColumnLayouts) {
566!
990
            // invalid configuration - tree grid should not allow column layouts
991
            // remove column layouts
992
            const nonColumnLayoutColumns = this.columns.filter((col) => !col.columnLayout && !col.columnLayoutChild);
×
993
            this.updateColumns(nonColumnLayoutColumns);
×
994
        }
995
        super.initColumns(collection, cb);
566✔
996
    }
997

998
    /**
999
     * @hidden @internal
1000
     */
1001
    protected override getGroupAreaHeight(): number {
1002
        return this.treeGroupArea ? this.getComputedHeight(this.treeGroupArea.nativeElement) : 0;
1,594✔
1003
    }
1004

1005
    /** {@link triggerPipes} will re-create pinnedData on CRUD operations */
1006
    protected trackPinnedRowData(record: ITreeGridRecord) {
1007
        // TODO FIX: pipeline data doesn't match end interface (¬_¬ )
1008
        // return record.key || (record as any).rowID;
1009
        return record;
547✔
1010
    }
1011

1012
    /**
1013
     * @description A recursive way to deselect all selected children of a given record
1014
     * @param recordID ID of the record whose children to deselect
1015
     * @hidden
1016
     * @internal
1017
     */
1018
    private deselectChildren(recordID): void {
1019
        const selectedChildren = [];
21✔
1020
        // G.E. Apr 28, 2021 #9465 Records which are not in view can also be selected so we need to
1021
        // deselect them as well, hence using 'records' map instead of getRowByKey() method which will
1022
        // return only row components (i.e. records in view).
1023
        const rowToDeselect = this.records.get(recordID);
21✔
1024
        this.selectionService.deselectRowsWithNoEvent([recordID]);
21✔
1025
        this.gridAPI.get_selected_children(rowToDeselect, selectedChildren);
21✔
1026
        if (selectedChildren.length > 0) {
21✔
1027
            selectedChildren.forEach(x => this.deselectChildren(x));
3✔
1028
        }
1029
    }
1030

1031
    private addChildRows(children: any[], parentID: any) {
1032
        if (this.primaryKey && this.foreignKey) {
8✔
1033
            for (const child of children) {
6✔
1034
                child[this.foreignKey] = parentID;
10✔
1035
            }
1036
            this.data.push(...children);
6✔
1037
        } else if (this.childDataKey) {
2✔
1038
            let parent = this.records.get(parentID);
2✔
1039
            let parentData = parent.data;
2✔
1040

1041
            if (this.transactions.enabled && this.transactions.getAggregatedChanges(true).length) {
2!
1042
                const path = [];
×
1043
                while (parent) {
×
1044
                    path.push(parent.key);
×
1045
                    parent = parent.parent;
×
1046
                }
1047

1048
                let collection = this.data;
×
1049
                let record: any;
1050
                for (let i = path.length - 1; i >= 0; i--) {
×
1051
                    const pid = path[i];
×
1052
                    record = collection.find(r => r[this.primaryKey] === pid);
×
1053

1054
                    if (!record) {
×
1055
                        break;
×
1056
                    }
1057
                    collection = record[this.childDataKey];
×
1058
                }
1059
                if (record) {
×
1060
                    parentData = record;
×
1061
                }
1062
            }
1063

1064
            parentData[this.childDataKey] = children;
2✔
1065
        }
1066
        this.selectionService.clearHeaderCBState();
8✔
1067
        this.pipeTrigger++;
8✔
1068
        if (this.rowSelection === GridSelectionMode.multipleCascade) {
8✔
1069
            // Force pipe triggering for building the data structure
1070
            this.cdr.detectChanges();
2✔
1071
            if (this.selectionService.isRowSelected(parentID)) {
2✔
1072
                this.selectionService.rowSelection.delete(parentID);
2✔
1073
                this.selectionService.selectRowsWithNoEvent([parentID]);
2✔
1074
            }
1075
        }
1076
    }
1077

1078
    private loadChildrenOnRowExpansion(args: IRowToggleEventArgs) {
1079
        if (this.loadChildrenOnDemand) {
181✔
1080
            const parentID = args.rowID;
11✔
1081

1082
            if (args.expanded && !this._expansionStates.has(parentID)) {
11✔
1083
                this.loadingRows.add(parentID);
8✔
1084

1085
                this.loadChildrenOnDemand(parentID, children => {
8✔
1086
                    this.loadingRows.delete(parentID);
8✔
1087
                    this.addChildRows(children, parentID);
8✔
1088
                    this.notifyChanges();
8✔
1089
                });
1090
            }
1091
        }
1092
    }
1093

1094
    private handleCascadeSelection(event: IRowDataEventArgs | StateUpdateEvent, rec: ITreeGridRecord = null) {
2✔
1095
        // Wait for the change detection to update records through the pipes
1096
        requestAnimationFrame(() => {
11✔
1097
            if (rec === null) {
11✔
1098
                rec = this.gridAPI.get_rec_by_id((event as StateUpdateEvent).actions[0].transaction.id);
2✔
1099
            }
1100
            if (rec && rec.parent) {
11✔
1101
                this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(
11✔
1102
                    new Set([rec.parent]), rec.parent.key
1103
                );
1104
                this.notifyChanges();
11✔
1105
            }
1106
        });
1107
    }
1108
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc