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

IgniteUI / igniteui-angular / 28360060902

29 Jun 2026 08:50AM UTC coverage: 90.129% (-0.005%) from 90.134%
28360060902

Pull #17328

github

web-flow
Merge 44b7971f4 into 07bdcd752
Pull Request #17328: fix(grid): fix zone.onStable patterns broken in zoneless change detection

14908 of 17376 branches covered (85.8%)

Branch coverage included in aggregate %.

34 of 35 new or added lines in 3 files covered. (97.14%)

2 existing lines in 2 files now uncovered.

29987 of 32436 relevant lines covered (92.45%)

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

141
    /**
142
     * Sets the child data key of the tree grid.
143
     * ```html
144
     * <igx-tree-grid #grid [data]="employeeData" [childDataKey]="'employees'" [autoGenerate]="true"></igx-tree-grid>
145
     * ```
146
     *
147
     * @memberof IgxTreeGridComponent
148
     */
149
    @Input()
150
    public childDataKey: string;
151

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

164
    /**
165
     * Sets the key indicating whether a row has children.
166
     * This property is only used for load on demand scenarios.
167
     * ```html
168
     * <igx-tree-grid #grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'"
169
     *                [loadChildrenOnDemand]="loadChildren"
170
     *                [hasChildrenKey]="'hasEmployees'">
171
     * </igx-tree-grid>
172
     * ```
173
     *
174
     * @memberof IgxTreeGridComponent
175
     */
176
    @Input()
177
    public hasChildrenKey: string;
178

179
    /**
180
     * Sets whether child records should be deleted when their parent gets deleted.
181
     * By default it is set to true and deletes all children along with the parent.
182
     * ```html
183
     * <igx-tree-grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'" cascadeOnDelete="false">
184
     * </igx-tree-grid>
185
     * ```
186
     *
187
     * @memberof IgxTreeGridComponent
188
     */
189
    @Input({ transform: booleanAttribute })
190
    public cascadeOnDelete = true;
548✔
191

192
    /* csSuppress */
193
    /**
194
     * Sets a callback for loading child rows on demand.
195
     * ```html
196
     * <igx-tree-grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'" [loadChildrenOnDemand]="loadChildren">
197
     * </igx-tree-grid>
198
     * ```
199
     * ```typescript
200
     * public loadChildren = (parentID: any, done: (children: any[]) => void) => {
201
     *     this.dataService.getData(parentID, children => done(children));
202
     * }
203
     * ```
204
     *
205
     * @memberof IgxTreeGridComponent
206
     */
207
    @Input()
208
    public loadChildrenOnDemand: (parentID: any, done: (children: any[]) => void) => void;
209

210
    /**
211
     * @hidden @internal
212
     */
213
    @HostBinding('attr.role')
214
    public role = 'treegrid';
548✔
215

216
    /**
217
     * Sets the value of the `id` attribute. If not provided it will be automatically generated.
218
     * ```html
219
     * <igx-tree-grid [id]="'igx-tree-grid-1'"></igx-tree-grid>
220
     * ```
221
     *
222
     * @memberof IgxTreeGridComponent
223
     */
224
    @HostBinding('attr.id')
225
    @Input()
226
    public id = `igx-tree-grid-${NEXT_ID++}`;
548✔
227

228
    /**
229
     * @hidden
230
     * @internal
231
     */
232
    @ContentChild(IgxTreeGridGroupByAreaComponent, { read: IgxTreeGridGroupByAreaComponent })
233
    public treeGroupArea: IgxTreeGridGroupByAreaComponent;
234

235
    /**
236
     * @hidden @internal
237
     */
238
    @ViewChild('record_template', { read: TemplateRef, static: true })
239
    protected recordTemplate: TemplateRef<any>;
240

241
    /**
242
     * @hidden @internal
243
     */
244
    @ViewChild('summary_template', { read: TemplateRef, static: true })
245
    protected summaryTemplate: TemplateRef<any>;
246

247
    /**
248
     * @hidden
249
     */
250
    @ContentChild(IgxRowLoadingIndicatorTemplateDirective, { read: IgxRowLoadingIndicatorTemplateDirective })
251
    protected rowLoadingTemplate: IgxRowLoadingIndicatorTemplateDirective;
252

253
    /**
254
     * @hidden
255
     */
256
    public flatData: any[] | null;
257

258
    /**
259
     * @hidden
260
     */
261
    public processedExpandedFlatData: any[] | null;
262

263
    /**
264
     * Returns an array of the root level `ITreeGridRecord`s.
265
     * ```typescript
266
     * // gets the root record with index=2
267
     * const states = this.grid.rootRecords[2];
268
     * ```
269
     *
270
     * @memberof IgxTreeGridComponent
271
     */
272
    public rootRecords: ITreeGridRecord[];
273

274
    /* blazorSuppress */
275
    /**
276
     * Returns a map of all `ITreeGridRecord`s.
277
     * ```typescript
278
     * // gets the record with primaryKey=2
279
     * const states = this.grid.records.get(2);
280
     * ```
281
     *
282
     * @memberof IgxTreeGridComponent
283
     */
284
    public records: Map<any, ITreeGridRecord> = new Map<any, ITreeGridRecord>();
548✔
285

286
    /**
287
     * Returns an array of processed (filtered and sorted) root `ITreeGridRecord`s.
288
     * ```typescript
289
     * // gets the processed root record with index=2
290
     * const states = this.grid.processedRootRecords[2];
291
     * ```
292
     *
293
     * @memberof IgxTreeGridComponent
294
     */
295
    public processedRootRecords: ITreeGridRecord[];
296

297
    /* blazorSuppress */
298
    /**
299
     * Returns a map of all processed (filtered and sorted) `ITreeGridRecord`s.
300
     * ```typescript
301
     * // gets the processed record with primaryKey=2
302
     * const states = this.grid.processedRecords.get(2);
303
     * ```
304
     *
305
     * @memberof IgxTreeGridComponent
306
     */
307
    public processedRecords: Map<any, ITreeGridRecord> = new Map<any, ITreeGridRecord>();
548✔
308

309
    /**
310
     * @hidden
311
     */
312
    public loadingRows = new Set<any>();
548✔
313

314
    protected override _filterStrategy = new TreeGridFilteringStrategy();
548✔
315
    protected override _transactions: HierarchicalTransactionService<HierarchicalTransaction, HierarchicalState>;
316
    protected override _mergeStrategy: IGridMergeStrategy = new DefaultTreeGridMergeStrategy();
548✔
317
    private _data;
318
    private _rowLoadingIndicatorTemplate: TemplateRef<void>;
319
    private _expansionDepth = Infinity;
548✔
320

321
    /* treatAsRef */
322
    /**
323
     * Gets/Sets the array of data that populates the component.
324
     * ```html
325
     * <igx-tree-grid [data]="Data" [autoGenerate]="true"></igx-tree-grid>
326
     * ```
327
     *
328
     * @memberof IgxTreeGridComponent
329
     */
330
    @Input()
331
    public get data(): any[] | null {
332
        return this._data;
17,937✔
333
    }
334

335
    /* treatAsRef */
336
    public set data(value: any[] | null) {
337
        const oldData = this._data;
564✔
338
        this._data = value || [];
564✔
339
        this.summaryService.clearSummaryCache();
564✔
340
        if (!this._init) {
564✔
341
            this.validation.updateAll(this._data);
23✔
342
        }
343
        if (this.autoGenerate && this._data.length > 0 && this.shouldRecreateColumns(oldData, this._data)) {
564✔
344
            this.setupColumns();
4✔
345
        }
346
        this.checkPrimaryKeyField();
564✔
347
        this.cdr.markForCheck();
564✔
348
    }
349

350
    /** @hidden @internal */
351
    public override get type(): GridType["type"] {
352
        return 'tree';
2,519✔
353
    }
354

355
    /**
356
     * Get transactions service for the grid.
357
     *
358
     * @experimental @hidden
359
     */
360
    public override get transactions() {
361
        if (this._diTransactions && !this.batchEditing) {
935,270✔
362
            return this._diTransactions;
82,695✔
363
        }
364
        return this._transactions;
852,575✔
365
    }
366

367
    /**
368
     * Sets the count of levels to be expanded in the tree grid. By default it is
369
     * set to `Infinity` which means all levels would be expanded.
370
     * ```html
371
     * <igx-tree-grid #grid [data]="employeeData" [childDataKey]="'employees'" expansionDepth="1" [autoGenerate]="true"></igx-tree-grid>
372
     * ```
373
     *
374
     * @memberof IgxTreeGridComponent
375
     */
376
    @Input()
377
    public get expansionDepth(): number {
378
        return this._expansionDepth;
14,507✔
379
    }
380

381
    public set expansionDepth(value: number) {
382
        this._expansionDepth = value;
192✔
383
        this.notifyChanges();
192✔
384
    }
385

386
    /**
387
     * Template for the row loading indicator when load on demand is enabled.
388
     * ```html
389
     * <ng-template #rowLoadingTemplate>
390
     *     <igx-icon>loop</igx-icon>
391
     * </ng-template>
392
     *
393
     * <igx-tree-grid #grid [data]="employeeData" [primaryKey]="'ID'" [foreignKey]="'parentID'"
394
     *                [loadChildrenOnDemand]="loadChildren"
395
     *                [rowLoadingIndicatorTemplate]="rowLoadingTemplate">
396
     * </igx-tree-grid>
397
     * ```
398
     *
399
     * @memberof IgxTreeGridComponent
400
     */
401
    @Input()
402
    public get rowLoadingIndicatorTemplate(): TemplateRef<void> {
403
        return this._rowLoadingIndicatorTemplate;
8✔
404
    }
405

406
    public set rowLoadingIndicatorTemplate(value: TemplateRef<void>) {
407
        this._rowLoadingIndicatorTemplate = value;
×
408
        this.notifyChanges();
×
409
    }
410

411
    // Kind of stupid
412
    // private get _gridAPI(): IgxTreeGridAPIService {
413
    //     return this.gridAPI as IgxTreeGridAPIService;
414
    // }
415

416
    /**
417
     * @hidden
418
     */
419
    public override ngOnInit() {
420
        super.ngOnInit();
548✔
421

422
        this.rowToggle.pipe(takeUntil(this.destroy$)).subscribe((args) => {
548✔
423
            this.loadChildrenOnRowExpansion(args);
175✔
424
        });
425

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

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

472
        this.filteringDone.pipe(takeUntil(this.destroy$)).subscribe(() => {
548✔
473
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
58✔
474
                const leafRowsDirectParents = new Set<any>();
15✔
475
                this.records.forEach(record => {
15✔
476
                    if (record && (!record.children || record.children.length === 0) && record.parent) {
151✔
477
                        leafRowsDirectParents.add(record.parent);
91✔
478
                    }
479
                });
480
                this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(leafRowsDirectParents);
15✔
481
                this.notifyChanges();
15✔
482
            }
483
        });
484
    }
485

486
    /**
487
     * @hidden
488
     */
489
    public override ngAfterViewInit() {
490
        super.ngAfterViewInit();
548✔
491
        // TODO: pipesExectured event
492
        // run after change detection in super triggers pipes for records structure
493
        if (this.rowSelection === GridSelectionMode.multipleCascade && this.selectedRows.length) {
548!
494
            const selRows = this.selectedRows;
×
495
            this.selectionService.clearRowSelection();
×
496
            this.selectRows(selRows, true);
×
497
            this.cdr.detectChanges();
×
498
        }
499
    }
500

501
    /**
502
     * @hidden
503
     */
504
    public override ngAfterContentInit() {
505
        if (this.rowLoadingTemplate) {
548!
506
            this._rowLoadingIndicatorTemplate = this.rowLoadingTemplate.template;
×
507
        }
508
        super.ngAfterContentInit();
548✔
509
    }
510

511
    public override getDefaultExpandState(record: ITreeGridRecord): boolean {
512
        return record.children && record.children.length && record.level < this.expansionDepth;
×
513
    }
514

515
    /**
516
     * Expands all rows.
517
     * ```typescript
518
     * this.grid.expandAll();
519
     * ```
520
     *
521
     * @memberof IgxTreeGridComponent
522
     */
523
    public override expandAll() {
524
        this._expansionDepth = Infinity;
41✔
525
        this.expansionStates = new Map<any, boolean>();
41✔
526
    }
527

528
    /**
529
     * Collapses all rows.
530
     *
531
     * ```typescript
532
     * this.grid.collapseAll();
533
     *  ```
534
     *
535
     * @memberof IgxTreeGridComponent
536
     */
537
    public override collapseAll() {
538
        this._expansionDepth = 0;
14✔
539
        this.expansionStates = new Map<any, boolean>();
14✔
540
    }
541

542
    /**
543
     * @hidden
544
     */
545
    public override refreshGridState(args?: IRowDataEventArgs) {
546
        super.refreshGridState();
65✔
547
        if (this.primaryKey && this.foreignKey && args) {
65✔
548
            const rowID = args.data[this.foreignKey];
35✔
549
            this.summaryService.clearSummaryCache({ rowID });
35✔
550
            this.pipeTrigger++;
35✔
551
            this.cdr.detectChanges();
35✔
552
        }
553
    }
554

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

577
        this.rowAddedNotifier.next({
52✔
578
            data: data,
579
            rowData: data, owner: this,
580
            primaryKey: data[this.primaryKey],
581
            rowKey: data[this.primaryKey]
582
        });
583
        this.pipeTrigger++;
52✔
584
        this.notifyChanges();
52✔
585
    }
586

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

613
    /**
614
     * @hidden
615
     */
616
    public getContext(rowData: any, rowIndex: number, pinned?: boolean): any {
617
        return {
45,005✔
618
            $implicit: this.isGhostRecord(rowData) || this.isRecordMerged(rowData) ? rowData.recordRef : rowData,
134,852✔
619
            index: this.getDataViewIndex(rowIndex, pinned),
620
            templateID: {
621
                type: this.isSummaryRow(rowData) ? 'summaryRow' : 'dataRow',
45,005✔
622
                id: null
623
            },
624
            disabled: this.isGhostRecord(rowData) ? rowData.recordRef.isFilteredOutParent === undefined : false,
45,005✔
625
            metaData: this.isRecordMerged(rowData) ? rowData : null
45,005✔
626
        };
627
    }
628

629
    /**
630
     * @hidden
631
     * @internal
632
     */
633
    public override getInitialPinnedIndex(rec) {
634
        const id = this.gridAPI.get_row_id(rec);
200,662✔
635
        return this._pinnedRecordIDs.indexOf(id);
200,662✔
636
    }
637

638
    /**
639
     * @hidden
640
     * @internal
641
     */
642
    public override isRecordPinned(rec) {
643
        return this.getInitialPinnedIndex(rec.data) !== -1;
200,612✔
644
    }
645

646
    /**
647
     *
648
     * Returns an array of the current cell selection in the form of `[{ column.field: cell.value }, ...]`.
649
     *
650
     * @remarks
651
     * If `formatters` is enabled, the cell value will be formatted by its respective column formatter (if any).
652
     * If `headers` is enabled, it will use the column header (if any) instead of the column field.
653
     */
654
    public override getSelectedData(formatters = false, headers = false): any[] {
128✔
655
        let source = [];
64✔
656

657
        const process = (record) => {
64✔
658
            if (record.summaries) {
1,091✔
659
                source.push(null);
21✔
660
                return;
21✔
661
            }
662
            source.push(record.data);
1,070✔
663
        };
664

665
        this.unpinnedDataView.forEach(process);
64✔
666
        source = this.isRowPinningToTop ? [...this.pinnedDataView, ...source] : [...source, ...this.pinnedDataView];
64!
667
        return this.extractDataFromSelection(source, formatters, headers);
64✔
668
    }
669

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

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

718
        const record = this.gridAPI.deleteRowById(rowId);
63✔
719
        const key = record[this.primaryKey];
63✔
720
        if (record !== null && record !== undefined) {
63✔
721
            const rowDeletedEventArgs: IRowDataEventArgs = {
63✔
722
                data: record,
723
                rowData: record,
724
                owner: this,
725
                primaryKey: key,
726
                rowKey: key
727
            };
728
            this.rowDeleted.emit(rowDeletedEventArgs);
63✔
729
        }
730
        return record;
63✔
731
    }
732

733
    /**
734
     * Returns the tree grid row by index.
735
     *
736
     * @example
737
     * ```typescript
738
     * const myRow = treeGrid.getRowByIndex(1);
739
     * ```
740
     * @param index
741
     */
742
    public getRowByIndex(index: number): RowType {
743
        if (index < 0 || index >= this.dataView.length) {
902✔
744
            return undefined;
1✔
745
        }
746
        return this.createRow(index);
901✔
747
    }
748

749
    /**
750
     * Returns the `RowType` object by the specified primary key.
751
     *
752
     * @example
753
     * ```typescript
754
     * const myRow = this.treeGrid.getRowByIndex(1);
755
     * ```
756
     * @param index
757
     */
758
    public getRowByKey(key: any): RowType {
759
        const rec = this.filteredSortedData ? this.primaryKey ? this.filteredSortedData.find(r => r[this.primaryKey] === key) :
308✔
760
            this.filteredSortedData.find(r => r === key) : undefined;
10✔
761
        const index = this.dataView.findIndex(r => r.data && r.data === rec);
363✔
762
        if (index < 0 || index >= this.filteredSortedData.length) {
109✔
763
            return undefined;
7✔
764
        }
765
        return new IgxTreeGridRow(this as any, index, rec);
102✔
766
    }
767

768
    /**
769
     * Returns the collection of all RowType for current page.
770
     *
771
     * @hidden @internal
772
     */
773
    public allRows(): RowType[] {
774
        return this.dataView.map((_rec, index) => this.createRow(index));
1,579✔
775
    }
776

777
    /**
778
     * Returns the collection of tree grid rows for current page.
779
     *
780
     * @hidden @internal
781
     */
782
    public dataRows(): RowType[] {
783
        return this.allRows().filter(row => row instanceof IgxTreeGridRow);
1,579✔
784
    }
785

786
    /**
787
     * Returns an array of the selected grid cells.
788
     *
789
     * @example
790
     * ```typescript
791
     * const selectedCells = this.grid.selectedCells;
792
     * ```
793
     */
794
    public get selectedCells(): CellType[] {
795
        return this.dataRows().map((row) => row.cells.filter((cell) => cell.selected))
7,478✔
796
            .reduce((a, b) => a.concat(b), []);
1,579✔
797
    }
798

799
    /**
800
     * Returns a `CellType` object that matches the conditions.
801
     *
802
     * @example
803
     * ```typescript
804
     * const myCell = this.grid1.getCellByColumn(2, "UnitPrice");
805
     * ```
806
     * @param rowIndex
807
     * @param columnField
808
     */
809
    public getCellByColumn(rowIndex: number, columnField: string): CellType {
810
        const row = this.getRowByIndex(rowIndex);
221✔
811
        const column = this.columns.find((col) => col.field === columnField);
532✔
812
        if (row && row instanceof IgxTreeGridRow && column) {
221✔
813
            return new IgxGridCell(this as any, rowIndex, column);
221✔
814
        }
815
    }
816

817
    /**
818
     * Returns a `CellType` object that matches the conditions.
819
     *
820
     * @remarks
821
     * Requires that the primaryKey property is set.
822
     * @example
823
     * ```typescript
824
     * grid.getCellByKey(1, 'index');
825
     * ```
826
     * @param rowSelector match any rowID
827
     * @param columnField
828
     */
829
    public getCellByKey(rowSelector: any, columnField: string): CellType {
830
        const row = this.getRowByKey(rowSelector);
5✔
831
        const column = this.columns.find((col) => col.field === columnField);
10✔
832
        if (row && column) {
5✔
833
            return new IgxGridCell(this as any, row.index, column);
5✔
834
        }
835
    }
836

837
    public override pinRow(rowID: any, index?: number): boolean {
838
        const row = this.getRowByKey(rowID);
27✔
839
        return super.pinRow(rowID, index, row);
27✔
840
    }
841

842
    public override unpinRow(rowID: any): boolean {
843
        const row = this.getRowByKey(rowID);
5✔
844
        return super.unpinRow(rowID, row);
5✔
845
    }
846

847
    /** @hidden */
848
    public generateRowPath(rowId: any): any[] {
849
        const path: any[] = [];
52✔
850
        let record = this.records.get(rowId);
52✔
851

852
        while (record.parent) {
52✔
853
            path.push(record.parent.key);
46✔
854
            record = record.parent;
46✔
855
        }
856

857
        return path.reverse();
52✔
858
    }
859

860
    /** @hidden */
861
    public isTreeRow(record: any): boolean {
862
        return record.key !== undefined && record.data;
13,584✔
863
    }
864

865
    /** @hidden */
866
    public override getUnpinnedIndexById(id) {
867
        return this.unpinnedRecords.findIndex(x => x.data[this.primaryKey] === id);
43✔
868
    }
869

870
    /**
871
     * @hidden
872
     */
873
    public createRow(index: number, data?: any): RowType {
874
        let row: RowType;
875
        const dataIndex = this._getDataViewIndex(index);
13,577✔
876
        const rec: any = data ?? this.dataView[dataIndex];
13,577✔
877

878
        if (this.isSummaryRow(rec)) {
13,577✔
879
            row = new IgxSummaryRow(this as any, index, rec.summaries);
7✔
880
        }
881

882
        if (!row && rec) {
13,577✔
883
            const isTreeRow = this.isTreeRow(rec);
13,570✔
884
            const dataRec = isTreeRow ? rec.data : rec;
13,570✔
885
            const treeRow = isTreeRow ? rec : undefined;
13,570✔
886
            row = new IgxTreeGridRow(this as any, index, dataRec, treeRow);
13,570✔
887
        }
888

889
        return row;
13,577✔
890
    }
891

892
    protected override generateDataFields(data: any[]): string[] {
893
        return super.generateDataFields(data).filter(field => field !== this.childDataKey);
16✔
894
    }
895

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

922
    protected findRecordIndexInView(rec) {
923
        return this.dataView.findIndex(x => x.data[this.primaryKey] === rec[this.primaryKey]);
×
924
    }
925

926
    /**
927
     * @hidden @internal
928
     */
929
    protected override getDataBasedBodyHeight(): number {
930
        return !this.flatData || (this.flatData.length < this._defaultTargetRecordNumber) ?
46✔
931
            0 : this.defaultTargetBodyHeight;
932
    }
933

934
    /**
935
     * @hidden
936
     */
937
    protected override scrollTo(row: any | number, column: any | number): void {
938
        let delayScrolling = false;
85✔
939
        let record: ITreeGridRecord;
940

941
        if (typeof (row) !== 'number') {
85✔
942
            const rowData = row;
84✔
943
            const rowID = this.gridAPI.get_row_id(rowData);
84✔
944
            record = this.processedRecords.get(rowID);
84✔
945
            this.gridAPI.expand_path_to_record(record);
84✔
946

947
            if (this.paginator) {
84✔
948
                const rowIndex = this.processedExpandedFlatData.indexOf(rowData);
27✔
949
                const page = Math.floor(rowIndex / this.perPage);
27✔
950

951
                if (this.page !== page) {
27✔
952
                    delayScrolling = true;
7✔
953
                    this.page = page;
7✔
954
                }
955
            }
956
        }
957

958
        if (delayScrolling) {
85✔
959
            this.verticalScrollContainer.dataChanged.pipe(first()).subscribe(() => {
7✔
960
                this.scrollDirective(this.verticalScrollContainer,
7✔
961
                    typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(record));
7!
962
            });
963
        } else {
964
            this.scrollDirective(this.verticalScrollContainer,
78✔
965
                typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(record));
78✔
966
        }
967

968
        this.scrollToHorizontally(column);
85✔
969
    }
970

971
    protected override writeToData(rowIndex: number, value: any) {
972
        mergeObjects(this.flatData[rowIndex], value);
×
973
    }
974

975
    /**
976
     * @hidden
977
     */
978
    protected override initColumns(collection: IgxColumnComponent[], cb: (args: any) => void = null) {
×
979
        if (this.hasColumnLayouts) {
552!
980
            // invalid configuration - tree grid should not allow column layouts
981
            // remove column layouts
982
            const nonColumnLayoutColumns = this.columns.filter((col) => !col.columnLayout && !col.columnLayoutChild);
×
983
            this.updateColumns(nonColumnLayoutColumns);
×
984
        }
985
        super.initColumns(collection, cb);
552✔
986
    }
987

988
    /**
989
     * @hidden @internal
990
     */
991
    protected override getGroupAreaHeight(): number {
992
        return this.treeGroupArea ? this.getComputedHeight(this.treeGroupArea.nativeElement) : 0;
1,533✔
993
    }
994

995
    /** {@link triggerPipes} will re-create pinnedData on CRUD operations */
996
    protected trackPinnedRowData(record: ITreeGridRecord) {
997
        // TODO FIX: pipeline data doesn't match end interface (¬_¬ )
998
        // return record.key || (record as any).rowID;
999
        return record;
547✔
1000
    }
1001

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

1021
    private addChildRows(children: any[], parentID: any) {
1022
        if (this.primaryKey && this.foreignKey) {
8✔
1023
            for (const child of children) {
6✔
1024
                child[this.foreignKey] = parentID;
10✔
1025
            }
1026
            this.data.push(...children);
6✔
1027
        } else if (this.childDataKey) {
2✔
1028
            let parent = this.records.get(parentID);
2✔
1029
            let parentData = parent.data;
2✔
1030

1031
            if (this.transactions.enabled && this.transactions.getAggregatedChanges(true).length) {
2!
1032
                const path = [];
×
1033
                while (parent) {
×
1034
                    path.push(parent.key);
×
1035
                    parent = parent.parent;
×
1036
                }
1037

1038
                let collection = this.data;
×
1039
                let record: any;
1040
                for (let i = path.length - 1; i >= 0; i--) {
×
1041
                    const pid = path[i];
×
1042
                    record = collection.find(r => r[this.primaryKey] === pid);
×
1043

1044
                    if (!record) {
×
1045
                        break;
×
1046
                    }
1047
                    collection = record[this.childDataKey];
×
1048
                }
1049
                if (record) {
×
1050
                    parentData = record;
×
1051
                }
1052
            }
1053

1054
            parentData[this.childDataKey] = children;
2✔
1055
        }
1056
        this.selectionService.clearHeaderCBState();
8✔
1057
        this.pipeTrigger++;
8✔
1058
        if (this.rowSelection === GridSelectionMode.multipleCascade) {
8✔
1059
            // Force pipe triggering for building the data structure
1060
            this.cdr.detectChanges();
2✔
1061
            if (this.selectionService.isRowSelected(parentID)) {
2✔
1062
                this.selectionService.rowSelection.delete(parentID);
2✔
1063
                this.selectionService.selectRowsWithNoEvent([parentID]);
2✔
1064
            }
1065
        }
1066
    }
1067

1068
    private loadChildrenOnRowExpansion(args: IRowToggleEventArgs) {
1069
        if (this.loadChildrenOnDemand) {
175✔
1070
            const parentID = args.rowID;
11✔
1071

1072
            if (args.expanded && !this._expansionStates.has(parentID)) {
11✔
1073
                this.loadingRows.add(parentID);
8✔
1074

1075
                this.loadChildrenOnDemand(parentID, children => {
8✔
1076
                    this.loadingRows.delete(parentID);
8✔
1077
                    this.addChildRows(children, parentID);
8✔
1078
                    this.notifyChanges();
8✔
1079
                });
1080
            }
1081
        }
1082
    }
1083

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

© 2026 Coveralls, Inc