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

IgniteUI / igniteui-angular / 29572405564

17 Jul 2026 10:07AM UTC coverage: 90.116% (-0.02%) from 90.135%
29572405564

Pull #15125

github

web-flow
Merge 637b7229e into 8ebabbb38
Pull Request #15125: refactor(*): bundle styles with components

14920 of 17397 branches covered (85.76%)

Branch coverage included in aggregate %.

30030 of 32483 relevant lines covered (92.45%)

34495.14 hits per line

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

85.39
/projects/igniteui-angular/grids/tree-grid/src/tree-grid.component.ts
1
import {
2
    AfterContentInit,
3
    AfterViewInit,
4
    booleanAttribute,
5
    ChangeDetectionStrategy,
6
    Component,
7
    ContentChild,
8
    CUSTOM_ELEMENTS_SCHEMA,
9
    DoCheck,
10
    HostBinding,
11
    inject,
12
    Input,
13
    OnInit,
14
    TemplateRef,
15
    ViewChild,
16
    ViewEncapsulation,
17
} from '@angular/core';
18
import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
19
import { IgxTreeGridAPIService } from './tree-grid-api.service';
20
import { IgxGridBaseDirective, IgxGridCellMergePipe, IgxGridUnmergeActivePipe } from 'igniteui-angular/grids/grid';
21
import {
22
    CellType,
23
    GridSelectionMode,
24
    GridType,
25
    IGX_GRID_BASE,
26
    IGX_GRID_SERVICE_BASE,
27
    IgxColumnComponent,
28
    IgxColumnMovingDropDirective,
29
    IgxColumnResizingService,
30
    IgxFilteringService,
31
    IgxGridBodyDirective,
32
    IgxGridCell,
33
    IgxGridColumnResizerComponent,
34
    IgxGridCRUDService,
35
    IgxGridDragSelectDirective,
36
    IgxGridHeaderRowComponent,
37
    IgxGridNavigationService,
38
    IgxGridRowClassesPipe,
39
    IgxGridRowPinningPipe,
40
    IgxGridRowStylesPipe,
41
    IgxGridSelectionService,
42
    IgxGridSummaryService,
43
    IgxGridTransaction,
44
    IgxGridValidationService,
45
    IgxHasVisibleColumnsPipe,
46
    IgxRowEditTabStopDirective,
47
    IgxStringReplacePipe,
48
    IgxSummaryDataPipe,
49
    IgxSummaryRow,
50
    IgxSummaryRowComponent,
51
    IgxTreeGridRow,
52
    IRowDataCancelableEventArgs,
53
    IRowDataEventArgs,
54
    IRowToggleEventArgs,
55
    RowType,
56
} from 'igniteui-angular/grids/core';
57
import { first, takeUntil } from 'rxjs/operators';
58
import { IgxRowLoadingIndicatorTemplateDirective } from './tree-grid.directives';
59
import { IgxTreeGridSelectionService } from './tree-grid-selection.service';
60
import {
61
    DefaultTreeGridMergeStrategy,
62
    HierarchicalState,
63
    HierarchicalTransaction,
64
    HierarchicalTransactionService,
65
    IGridMergeStrategy,
66
    IgxHierarchicalTransactionFactory,
67
    IgxOverlayOutletDirective,
68
    ITreeGridRecord,
69
    mergeObjects,
70
    StateUpdateEvent,
71
    TransactionEventOrigin,
72
    TransactionType,
73
    TreeGridFilteringStrategy,
74
} from 'igniteui-angular/core';
75
import { IgxTreeGridSummaryPipe } from './tree-grid.summary.pipe';
76
import { IgxTreeGridFilteringPipe } from './tree-grid.filtering.pipe';
77
import {
78
    IgxTreeGridAddRowPipe,
79
    IgxTreeGridFlatteningPipe,
80
    IgxTreeGridHierarchizingPipe,
81
    IgxTreeGridNormalizeRecordsPipe,
82
    IgxTreeGridPagingPipe,
83
    IgxTreeGridSortingPipe,
84
    IgxTreeGridTransactionPipe,
85
} from './tree-grid.pipes';
86
import { IgxTreeGridRowComponent } from './tree-grid-row.component';
87
import {
88
    IgxButtonDirective,
89
    IgxForOfScrollSyncService,
90
    IgxForOfSyncService,
91
    IgxGridForOfDirective,
92
    IgxRippleDirective,
93
    IgxScrollInertiaDirective,
94
    IgxTemplateOutletDirective,
95
    IgxToggleDirective,
96
} from 'igniteui-angular/directives';
97
import { IgxCircularProgressBarComponent } from 'igniteui-angular/progressbar';
98
import { IgxSnackbarComponent } from 'igniteui-angular/snackbar';
99
import { IgxIconComponent } from 'igniteui-angular/icon';
100
import { IgxTreeGridGroupByAreaComponent } from './tree-grid-group-by-area.component';
101

102
let NEXT_ID = 0;
3✔
103

104
/* wcAlternateName: TreeGridBase */
105
/* blazorIndirectRender
106
   blazorComponent
107
   omitModule
108
   wcSkipComponentSuffix */
109
/**
110
 * **Ignite UI for Angular Tree Grid** -
111
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/grid/grid)
112
 *
113
 * The Ignite UI Tree Grid displays and manipulates hierarchical data with consistent schema formatted as a table and
114
 * provides features such as sorting, filtering, editing, column pinning, paging, column moving and hiding.
115
 *
116
 * Example:
117
 * ```html
118
 * <igx-tree-grid [data]="employeeData" primaryKey="employeeID" foreignKey="PID" [autoGenerate]="false">
119
 *   <igx-column field="first" header="First Name"></igx-column>
120
 *   <igx-column field="last" header="Last Name"></igx-column>
121
 *   <igx-column field="role" header="Role"></igx-column>
122
 * </igx-tree-grid>
123
 * ```
124
 */
125
@Component({
126
    changeDetection: ChangeDetectionStrategy.OnPush,
127
    selector: 'igx-tree-grid',
128
    templateUrl: 'tree-grid.component.html',
129
    encapsulation: ViewEncapsulation.None,
130
    providers: [
131
        IgxGridCRUDService,
132
        IgxGridValidationService,
133
        IgxGridSummaryService,
134
        IgxGridNavigationService,
135
        { provide: IgxGridSelectionService, useClass: IgxTreeGridSelectionService },
136
        { provide: IGX_GRID_SERVICE_BASE, useClass: IgxTreeGridAPIService },
137
        { provide: IGX_GRID_BASE, useExisting: IgxTreeGridComponent },
138
        IgxFilteringService,
139
        IgxColumnResizingService,
140
        IgxForOfSyncService,
141
        IgxForOfScrollSyncService
142
    ],
143
    imports: [
144
        NgClass,
145
        NgStyle,
146
        NgTemplateOutlet,
147
        IgxGridHeaderRowComponent,
148
        IgxGridBodyDirective,
149
        IgxGridDragSelectDirective,
150
        IgxColumnMovingDropDirective,
151
        IgxGridForOfDirective,
152
        IgxTemplateOutletDirective,
153
        IgxTreeGridRowComponent,
154
        IgxSummaryRowComponent,
155
        IgxOverlayOutletDirective,
156
        IgxToggleDirective,
157
        IgxCircularProgressBarComponent,
158
        IgxSnackbarComponent,
159
        IgxButtonDirective,
160
        IgxRippleDirective,
161
        IgxRowEditTabStopDirective,
162
        IgxIconComponent,
163
        IgxGridColumnResizerComponent,
164
        IgxHasVisibleColumnsPipe,
165
        IgxGridRowPinningPipe,
166
        IgxGridRowClassesPipe,
167
        IgxGridRowStylesPipe,
168
        IgxSummaryDataPipe,
169
        IgxTreeGridHierarchizingPipe,
170
        IgxTreeGridFlatteningPipe,
171
        IgxTreeGridSortingPipe,
172
        IgxTreeGridFilteringPipe,
173
        IgxTreeGridPagingPipe,
174
        IgxTreeGridTransactionPipe,
175
        IgxTreeGridSummaryPipe,
176
        IgxTreeGridNormalizeRecordsPipe,
177
        IgxTreeGridAddRowPipe,
178
        IgxStringReplacePipe,
179
        IgxGridCellMergePipe,
180
        IgxScrollInertiaDirective,
181
        IgxGridUnmergeActivePipe
182
    ],
183
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
184
})
185
export class IgxTreeGridComponent extends IgxGridBaseDirective implements GridType, OnInit, AfterViewInit, DoCheck, AfterContentInit {
3✔
186
    protected override _diTransactions = inject<HierarchicalTransactionService<HierarchicalTransaction, HierarchicalState>>(IgxGridTransaction, { optional: true, });
548✔
187
    protected override transactionFactory = inject(IgxHierarchicalTransactionFactory);
548✔
188

189
    /**
190
     * Sets the child data key of the tree grid.
191
     * ```html
192
     * <igx-tree-grid #grid [data]="employeeData" [childDataKey]="'employees'" [autoGenerate]="true"></igx-tree-grid>
193
     * ```
194
     *
195
     * @memberof IgxTreeGridComponent
196
     */
197
    @Input()
198
    public childDataKey: string;
199

200
    /**
201
     * Sets the foreign key of the tree grid.
202
     * ```html
203
     * <igx-tree-grid #grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'" [autoGenerate]="true">
204
     * </igx-tree-grid>
205
     * ```
206
     *
207
     * @memberof IgxTreeGridComponent
208
     */
209
    @Input()
210
    public foreignKey: string;
211

212
    /**
213
     * Sets the key indicating whether a row has children.
214
     * This property is only used for load on demand scenarios.
215
     * ```html
216
     * <igx-tree-grid #grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'"
217
     *                [loadChildrenOnDemand]="loadChildren"
218
     *                [hasChildrenKey]="'hasEmployees'">
219
     * </igx-tree-grid>
220
     * ```
221
     *
222
     * @memberof IgxTreeGridComponent
223
     */
224
    @Input()
225
    public hasChildrenKey: string;
226

227
    /**
228
     * Sets whether child records should be deleted when their parent gets deleted.
229
     * By default it is set to true and deletes all children along with the parent.
230
     * ```html
231
     * <igx-tree-grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'" cascadeOnDelete="false">
232
     * </igx-tree-grid>
233
     * ```
234
     *
235
     * @memberof IgxTreeGridComponent
236
     */
237
    @Input({ transform: booleanAttribute })
238
    public cascadeOnDelete = true;
548✔
239

240
    /* csSuppress */
241
    /**
242
     * Sets a callback for loading child rows on demand.
243
     * ```html
244
     * <igx-tree-grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'" [loadChildrenOnDemand]="loadChildren">
245
     * </igx-tree-grid>
246
     * ```
247
     * ```typescript
248
     * public loadChildren = (parentID: any, done: (children: any[]) => void) => {
249
     *     this.dataService.getData(parentID, children => done(children));
250
     * }
251
     * ```
252
     *
253
     * @memberof IgxTreeGridComponent
254
     */
255
    @Input()
256
    public loadChildrenOnDemand: (parentID: any, done: (children: any[]) => void) => void;
257

258
    /**
259
     * @hidden @internal
260
     */
261
    @HostBinding('attr.role')
262
    public role = 'treegrid';
548✔
263

264
    /**
265
     * Sets the value of the `id` attribute. If not provided it will be automatically generated.
266
     * ```html
267
     * <igx-tree-grid [id]="'igx-tree-grid-1'"></igx-tree-grid>
268
     * ```
269
     *
270
     * @memberof IgxTreeGridComponent
271
     */
272
    @HostBinding('attr.id')
273
    @Input()
274
    public id = `igx-tree-grid-${NEXT_ID++}`;
548✔
275

276
    /**
277
     * @hidden
278
     * @internal
279
     */
280
    @ContentChild(IgxTreeGridGroupByAreaComponent, { read: IgxTreeGridGroupByAreaComponent })
281
    public treeGroupArea: IgxTreeGridGroupByAreaComponent;
282

283
    /**
284
     * @hidden @internal
285
     */
286
    @ViewChild('record_template', { read: TemplateRef, static: true })
287
    protected recordTemplate: TemplateRef<any>;
288

289
    /**
290
     * @hidden @internal
291
     */
292
    @ViewChild('summary_template', { read: TemplateRef, static: true })
293
    protected summaryTemplate: TemplateRef<any>;
294

295
    /**
296
     * @hidden
297
     */
298
    @ContentChild(IgxRowLoadingIndicatorTemplateDirective, { read: IgxRowLoadingIndicatorTemplateDirective })
299
    protected rowLoadingTemplate: IgxRowLoadingIndicatorTemplateDirective;
300

301
    /**
302
     * @hidden
303
     */
304
    public flatData: any[] | null;
305

306
    /**
307
     * @hidden
308
     */
309
    public processedExpandedFlatData: any[] | null;
310

311
    /**
312
     * Returns an array of the root level `ITreeGridRecord`s.
313
     * ```typescript
314
     * // gets the root record with index=2
315
     * const states = this.grid.rootRecords[2];
316
     * ```
317
     *
318
     * @memberof IgxTreeGridComponent
319
     */
320
    public rootRecords: ITreeGridRecord[];
321

322
    /* blazorSuppress */
323
    /**
324
     * Returns a map of all `ITreeGridRecord`s.
325
     * ```typescript
326
     * // gets the record with primaryKey=2
327
     * const states = this.grid.records.get(2);
328
     * ```
329
     *
330
     * @memberof IgxTreeGridComponent
331
     */
332
    public records: Map<any, ITreeGridRecord> = new Map<any, ITreeGridRecord>();
548✔
333

334
    /**
335
     * Returns an array of processed (filtered and sorted) root `ITreeGridRecord`s.
336
     * ```typescript
337
     * // gets the processed root record with index=2
338
     * const states = this.grid.processedRootRecords[2];
339
     * ```
340
     *
341
     * @memberof IgxTreeGridComponent
342
     */
343
    public processedRootRecords: ITreeGridRecord[];
344

345
    /* blazorSuppress */
346
    /**
347
     * Returns a map of all processed (filtered and sorted) `ITreeGridRecord`s.
348
     * ```typescript
349
     * // gets the processed record with primaryKey=2
350
     * const states = this.grid.processedRecords.get(2);
351
     * ```
352
     *
353
     * @memberof IgxTreeGridComponent
354
     */
355
    public processedRecords: Map<any, ITreeGridRecord> = new Map<any, ITreeGridRecord>();
548✔
356

357
    /**
358
     * @hidden
359
     */
360
    public loadingRows = new Set<any>();
548✔
361

362
    protected override _filterStrategy = new TreeGridFilteringStrategy();
548✔
363
    protected override _transactions: HierarchicalTransactionService<HierarchicalTransaction, HierarchicalState>;
364
    protected override _mergeStrategy: IGridMergeStrategy = new DefaultTreeGridMergeStrategy();
548✔
365
    private _data;
366
    private _rowLoadingIndicatorTemplate: TemplateRef<void>;
367
    private _expansionDepth = Infinity;
548✔
368

369
    /* treatAsRef */
370
    /**
371
     * Gets/Sets the array of data that populates the component.
372
     * ```html
373
     * <igx-tree-grid [data]="Data" [autoGenerate]="true"></igx-tree-grid>
374
     * ```
375
     *
376
     * @memberof IgxTreeGridComponent
377
     */
378
    @Input()
379
    public get data(): any[] | null {
380
        return this._data;
18,354✔
381
    }
382

383
    /* treatAsRef */
384
    public set data(value: any[] | null) {
385
        const oldData = this._data;
564✔
386
        this._data = value || [];
564✔
387
        this.summaryService.clearSummaryCache();
564✔
388
        if (!this._init) {
564✔
389
            this.validation.updateAll(this._data);
23✔
390
        }
391
        if (this.autoGenerate && this._data.length > 0 && this.shouldRecreateColumns(oldData, this._data)) {
564✔
392
            this.setupColumns();
4✔
393
        }
394
        this.checkPrimaryKeyField();
564✔
395
        this.cdr.markForCheck();
564✔
396
    }
397

398
    /** @hidden @internal */
399
    public override get type(): GridType["type"] {
400
        return 'tree';
2,519✔
401
    }
402

403
    /**
404
     * Get transactions service for the grid.
405
     *
406
     * @experimental @hidden
407
     */
408
    public override get transactions() {
409
        if (this._diTransactions && !this.batchEditing) {
934,779✔
410
            return this._diTransactions;
83,019✔
411
        }
412
        return this._transactions;
851,760✔
413
    }
414

415
    /**
416
     * Sets the count of levels to be expanded in the tree grid. By default it is
417
     * set to `Infinity` which means all levels would be expanded.
418
     * ```html
419
     * <igx-tree-grid #grid [data]="employeeData" [childDataKey]="'employees'" expansionDepth="1" [autoGenerate]="true"></igx-tree-grid>
420
     * ```
421
     *
422
     * @memberof IgxTreeGridComponent
423
     */
424
    @Input()
425
    public get expansionDepth(): number {
426
        return this._expansionDepth;
14,503✔
427
    }
428

429
    public set expansionDepth(value: number) {
430
        this._expansionDepth = value;
192✔
431
        this.notifyChanges();
192✔
432
    }
433

434
    /**
435
     * Template for the row loading indicator when load on demand is enabled.
436
     * ```html
437
     * <ng-template #rowLoadingTemplate>
438
     *     <igx-icon>loop</igx-icon>
439
     * </ng-template>
440
     *
441
     * <igx-tree-grid #grid [data]="employeeData" [primaryKey]="'ID'" [foreignKey]="'parentID'"
442
     *                [loadChildrenOnDemand]="loadChildren"
443
     *                [rowLoadingIndicatorTemplate]="rowLoadingTemplate">
444
     * </igx-tree-grid>
445
     * ```
446
     *
447
     * @memberof IgxTreeGridComponent
448
     */
449
    @Input()
450
    public get rowLoadingIndicatorTemplate(): TemplateRef<void> {
451
        return this._rowLoadingIndicatorTemplate;
8✔
452
    }
453

454
    public set rowLoadingIndicatorTemplate(value: TemplateRef<void>) {
455
        this._rowLoadingIndicatorTemplate = value;
×
456
        this.notifyChanges();
×
457
    }
458

459
    // Kind of stupid
460
    // private get _gridAPI(): IgxTreeGridAPIService {
461
    //     return this.gridAPI as IgxTreeGridAPIService;
462
    // }
463

464
    /**
465
     * @hidden
466
     */
467
    public override ngOnInit() {
468
        super.ngOnInit();
548✔
469

470
        this.rowToggle.pipe(takeUntil(this.destroy$)).subscribe((args) => {
548✔
471
            this.loadChildrenOnRowExpansion(args);
175✔
472
        });
473

474
        // TODO: cascade selection logic should be refactor to be handled in the already existing subs
475
        this.rowAddedNotifier.pipe(takeUntil(this.destroy$)).subscribe(args => {
548✔
476
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
61✔
477
                let rec = this.gridAPI.get_rec_by_id(this.primaryKey ? args.data[this.primaryKey] : args.data);
6!
478
                if (rec && rec.parent) {
6✔
479
                    this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(
4✔
480
                        new Set([rec.parent]), rec.parent.key);
481
                } else {
482
                    // The record is still not available
483
                    // Wait for the change detection to update records through pipes
484
                    requestAnimationFrame(() => {
2✔
485
                        rec = this.gridAPI.get_rec_by_id(this.primaryKey ?
2!
486
                            args.data[this.primaryKey] : args.data);
487
                        if (rec && rec.parent) {
2✔
488
                            this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(
2✔
489
                                new Set([rec.parent]), rec.parent.key);
490
                        }
491
                        this.notifyChanges();
2✔
492
                    });
493
                }
494
            }
495
        });
496

497
        this.rowDeletedNotifier.pipe(takeUntil(this.destroy$)).subscribe(args => {
548✔
498
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
107✔
499
                if (args.data) {
8!
500
                    const rec = this.gridAPI.get_rec_by_id(
8✔
501
                        this.primaryKey ? args.data[this.primaryKey] : args.data);
8!
502
                    this.handleCascadeSelection(args, rec);
8✔
503
                } else {
504
                    // if a row has been added and before commiting the transaction deleted
505
                    const leafRowsDirectParents = new Set<any>();
×
506
                    this.records.forEach(record => {
×
507
                        if (record && (!record.children || record.children.length === 0) && record.parent) {
×
508
                            leafRowsDirectParents.add(record.parent);
×
509
                        }
510
                    });
511
                    // Wait for the change detection to update records through pipes
512
                    requestAnimationFrame(() => {
×
513
                        this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(leafRowsDirectParents);
×
514
                        this.notifyChanges();
×
515
                    });
516
                }
517
            }
518
        });
519

520
        this.filteringDone.pipe(takeUntil(this.destroy$)).subscribe(() => {
548✔
521
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
60✔
522
                const leafRowsDirectParents = new Set<any>();
15✔
523
                this.records.forEach(record => {
15✔
524
                    if (record && (!record.children || record.children.length === 0) && record.parent) {
151✔
525
                        leafRowsDirectParents.add(record.parent);
91✔
526
                    }
527
                });
528
                this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(leafRowsDirectParents);
15✔
529
                this.notifyChanges();
15✔
530
            }
531
        });
532
    }
533

534
    /**
535
     * @hidden
536
     */
537
    public override ngAfterViewInit() {
538
        super.ngAfterViewInit();
548✔
539
        // TODO: pipesExectured event
540
        // run after change detection in super triggers pipes for records structure
541
        if (this.rowSelection === GridSelectionMode.multipleCascade && this.selectedRows.length) {
548!
542
            const selRows = this.selectedRows;
×
543
            this.selectionService.clearRowSelection();
×
544
            this.selectRows(selRows, true);
×
545
            this.cdr.detectChanges();
×
546
        }
547
    }
548

549
    /**
550
     * @hidden
551
     */
552
    public override ngAfterContentInit() {
553
        if (this.rowLoadingTemplate) {
548!
554
            this._rowLoadingIndicatorTemplate = this.rowLoadingTemplate.template;
×
555
        }
556
        super.ngAfterContentInit();
548✔
557
    }
558

559
    public override getDefaultExpandState(record: ITreeGridRecord): boolean {
560
        return record.children && record.children.length && record.level < this.expansionDepth;
×
561
    }
562

563
    /**
564
     * Expands all rows.
565
     * ```typescript
566
     * this.grid.expandAll();
567
     * ```
568
     *
569
     * @memberof IgxTreeGridComponent
570
     */
571
    public override expandAll() {
572
        this._expansionDepth = Infinity;
41✔
573
        this.expansionStates = new Map<any, boolean>();
41✔
574
    }
575

576
    /**
577
     * Collapses all rows.
578
     *
579
     * ```typescript
580
     * this.grid.collapseAll();
581
     *  ```
582
     *
583
     * @memberof IgxTreeGridComponent
584
     */
585
    public override collapseAll() {
586
        this._expansionDepth = 0;
14✔
587
        this.expansionStates = new Map<any, boolean>();
14✔
588
    }
589

590
    /**
591
     * @hidden
592
     */
593
    public override refreshGridState(args?: IRowDataEventArgs) {
594
        super.refreshGridState();
65✔
595
        if (this.primaryKey && this.foreignKey && args) {
65✔
596
            const rowID = args.data[this.foreignKey];
35✔
597
            this.summaryService.clearSummaryCache({ rowID });
35✔
598
            this.pipeTrigger++;
35✔
599
            this.cdr.detectChanges();
35✔
600
        }
601
    }
602

603
    /* blazorCSSuppress */
604
    /**
605
     * Creates a new tree grid row with the given data. If a parentRowID is not specified, the newly created
606
     * row would be added at the root level. Otherwise, it would be added as a child of the row whose primaryKey matches
607
     * the specified parentRowID. If the parentRowID does not exist, an error would be thrown.
608
     * ```typescript
609
     * const record = {
610
     *     ID: this.grid.data[this.grid1.data.length - 1].ID + 1,
611
     *     Name: this.newRecord
612
     * };
613
     * this.grid.addRow(record, 1); // Adds a new child row to the row with ID=1.
614
     * ```
615
     *
616
     * @param data
617
     * @param parentRowID
618
     * @memberof IgxTreeGridComponent
619
     */
620
    // TODO: remove evt emission
621
    public override addRow(data: any, parentRowID?: any) {
622
        this.crudService.endEdit(true);
56✔
623
        this.gridAPI.addRowToData(data, parentRowID);
56✔
624

625
        this.rowAddedNotifier.next({
52✔
626
            data: data,
627
            rowData: data, owner: this,
628
            primaryKey: data[this.primaryKey],
629
            rowKey: data[this.primaryKey]
630
        });
631
        this.pipeTrigger++;
52✔
632
        this.notifyChanges();
52✔
633
    }
634

635
    /**
636
     * Enters add mode by spawning the UI with the context of the specified row by index.
637
     *
638
     * @remarks
639
     * Accepted values for index are integers from 0 to this.grid.dataView.length
640
     * @remarks
641
     * When adding the row as a child, the parent row is the specified row.
642
     * @remarks
643
     * To spawn the UI on top, call the function with index = null or a negative number.
644
     * In this case trying to add this row as a child will result in error.
645
     * @example
646
     * ```typescript
647
     * this.grid.beginAddRowByIndex(10);
648
     * this.grid.beginAddRowByIndex(10, true);
649
     * this.grid.beginAddRowByIndex(null);
650
     * ```
651
     * @param index - The index to spawn the UI at. Accepts integers from 0 to this.grid.dataView.length
652
     * @param asChild - Whether the record should be added as a child. Only applicable to igxTreeGrid.
653
     */
654
    public override beginAddRowByIndex(index: number, asChild?: boolean): void {
655
        if (index === null || index < 0) {
×
656
            return this.beginAddRowById(null, asChild);
×
657
        }
658
        return this._addRowForIndex(index - 1, asChild);
×
659
    }
660

661
    /**
662
     * @hidden
663
     */
664
    public getContext(rowData: any, rowIndex: number, pinned?: boolean): any {
665
        return {
44,960✔
666
            $implicit: this.isGhostRecord(rowData) || this.isRecordMerged(rowData) ? rowData.recordRef : rowData,
134,717✔
667
            index: this.getDataViewIndex(rowIndex, pinned),
668
            templateID: {
669
                type: this.isSummaryRow(rowData) ? 'summaryRow' : 'dataRow',
44,960✔
670
                id: null
671
            },
672
            disabled: this.isGhostRecord(rowData) ? rowData.recordRef.isFilteredOutParent === undefined : false,
44,960✔
673
            metaData: this.isRecordMerged(rowData) ? rowData : null
44,960✔
674
        };
675
    }
676

677
    /**
678
     * @hidden
679
     * @internal
680
     */
681
    public override getInitialPinnedIndex(rec) {
682
        const id = this.gridAPI.get_row_id(rec);
200,540✔
683
        return this._pinnedRecordIDs.indexOf(id);
200,540✔
684
    }
685

686
    /**
687
     * @hidden
688
     * @internal
689
     */
690
    public override isRecordPinned(rec) {
691
        return this.getInitialPinnedIndex(rec.data) !== -1;
200,490✔
692
    }
693

694
    /**
695
     *
696
     * Returns an array of the current cell selection in the form of `[{ column.field: cell.value }, ...]`.
697
     *
698
     * @remarks
699
     * If `formatters` is enabled, the cell value will be formatted by its respective column formatter (if any).
700
     * If `headers` is enabled, it will use the column header (if any) instead of the column field.
701
     */
702
    public override getSelectedData(formatters = false, headers = false): any[] {
128✔
703
        let source = [];
64✔
704

705
        const process = (record) => {
64✔
706
            if (record.summaries) {
1,091✔
707
                source.push(null);
21✔
708
                return;
21✔
709
            }
710
            source.push(record.data);
1,070✔
711
        };
712

713
        this.unpinnedDataView.forEach(process);
64✔
714
        source = this.isRowPinningToTop ? [...this.pinnedDataView, ...source] : [...source, ...this.pinnedDataView];
64!
715
        return this.extractDataFromSelection(source, formatters, headers);
64✔
716
    }
717

718
    /**
719
     * @hidden @internal
720
     */
721
    public override getEmptyRecordObjectFor(inTreeRow: RowType) {
722
        const treeRowRec = inTreeRow?.treeRow || null;
10✔
723
        const row = { ...treeRowRec };
10✔
724
        const data = treeRowRec?.data || {};
10✔
725
        row.data = { ...data };
10✔
726
        Object.keys(row.data).forEach(key => {
10✔
727
            // persist foreign key if one is set.
728
            if (this.foreignKey && key === this.foreignKey) {
45✔
729
                row.data[key] = treeRowRec.data[this.crudService.addRowParent?.asChild ? this.primaryKey : key];
8✔
730
            } else {
731
                row.data[key] = undefined;
37✔
732
            }
733
        });
734
        let id = this.generateRowID();
10✔
735
        const rootRecPK = this.foreignKey && this.rootRecords && this.rootRecords.length > 0 ?
10✔
736
            this.rootRecords[0].data[this.foreignKey] : null;
737
        if (id === rootRecPK) {
10✔
738
            // safeguard in case generated id matches the root foreign key.
739
            id = this.generateRowID();
1✔
740
        }
741
        row.key = id;
10✔
742
        row.data[this.primaryKey] = id;
10✔
743
        return { rowID: id, data: row.data, recordRef: row };
10✔
744
    }
745

746
    /** @hidden */
747
    public override deleteRowById(rowId: any): any {
748
        //  if this is flat self-referencing data, and CascadeOnDelete is set to true
749
        //  and if we have transactions we should start pending transaction. This allows
750
        //  us in case of delete action to delete all child rows as single undo action
751
        const args: IRowDataCancelableEventArgs = {
64✔
752
            rowID: rowId,
753
            primaryKey: rowId,
754
            rowKey: rowId,
755
            cancel: false,
756
            rowData: this.getRowData(rowId),
757
            data: this.getRowData(rowId),
758
            oldValue: null,
759
            owner: this
760
        };
761
        this.rowDelete.emit(args);
64✔
762
        if (args.cancel) {
64✔
763
            return;
1✔
764
        }
765

766
        const record = this.gridAPI.deleteRowById(rowId);
63✔
767
        const key = record[this.primaryKey];
63✔
768
        if (record !== null && record !== undefined) {
63✔
769
            const rowDeletedEventArgs: IRowDataEventArgs = {
63✔
770
                data: record,
771
                rowData: record,
772
                owner: this,
773
                primaryKey: key,
774
                rowKey: key
775
            };
776
            this.rowDeleted.emit(rowDeletedEventArgs);
63✔
777
        }
778
        return record;
63✔
779
    }
780

781
    /**
782
     * Returns the tree grid row by index.
783
     *
784
     * @example
785
     * ```typescript
786
     * const myRow = treeGrid.getRowByIndex(1);
787
     * ```
788
     * @param index
789
     */
790
    public getRowByIndex(index: number): RowType {
791
        if (index < 0 || index >= this.dataView.length) {
902✔
792
            return undefined;
1✔
793
        }
794
        return this.createRow(index);
901✔
795
    }
796

797
    /**
798
     * Returns the `RowType` object by the specified primary key.
799
     *
800
     * @example
801
     * ```typescript
802
     * const myRow = this.treeGrid.getRowByIndex(1);
803
     * ```
804
     * @param index
805
     */
806
    public getRowByKey(key: any): RowType {
807
        const rec = this.filteredSortedData ? this.primaryKey ? this.filteredSortedData.find(r => r[this.primaryKey] === key) :
308✔
808
            this.filteredSortedData.find(r => r === key) : undefined;
10✔
809
        const index = this.dataView.findIndex(r => r.data && r.data === rec);
363✔
810
        if (index < 0 || index >= this.filteredSortedData.length) {
109✔
811
            return undefined;
7✔
812
        }
813
        return new IgxTreeGridRow(this as any, index, rec);
102✔
814
    }
815

816
    /**
817
     * Returns the collection of all RowType for current page.
818
     *
819
     * @hidden @internal
820
     */
821
    public allRows(): RowType[] {
822
        return this.dataView.map((_rec, index) => this.createRow(index));
1,579✔
823
    }
824

825
    /**
826
     * Returns the collection of tree grid rows for current page.
827
     *
828
     * @hidden @internal
829
     */
830
    public dataRows(): RowType[] {
831
        return this.allRows().filter(row => row instanceof IgxTreeGridRow);
1,579✔
832
    }
833

834
    /**
835
     * Returns an array of the selected grid cells.
836
     *
837
     * @example
838
     * ```typescript
839
     * const selectedCells = this.grid.selectedCells;
840
     * ```
841
     */
842
    public get selectedCells(): CellType[] {
843
        return this.dataRows().map((row) => row.cells.filter((cell) => cell.selected))
7,478✔
844
            .reduce((a, b) => a.concat(b), []);
1,579✔
845
    }
846

847
    /**
848
     * Returns a `CellType` object that matches the conditions.
849
     *
850
     * @example
851
     * ```typescript
852
     * const myCell = this.grid1.getCellByColumn(2, "UnitPrice");
853
     * ```
854
     * @param rowIndex
855
     * @param columnField
856
     */
857
    public getCellByColumn(rowIndex: number, columnField: string): CellType {
858
        const row = this.getRowByIndex(rowIndex);
221✔
859
        const column = this.columns.find((col) => col.field === columnField);
532✔
860
        if (row && row instanceof IgxTreeGridRow && column) {
221✔
861
            return new IgxGridCell(this as any, rowIndex, column);
221✔
862
        }
863
    }
864

865
    /**
866
     * Returns a `CellType` object that matches the conditions.
867
     *
868
     * @remarks
869
     * Requires that the primaryKey property is set.
870
     * @example
871
     * ```typescript
872
     * grid.getCellByKey(1, 'index');
873
     * ```
874
     * @param rowSelector match any rowID
875
     * @param columnField
876
     */
877
    public getCellByKey(rowSelector: any, columnField: string): CellType {
878
        const row = this.getRowByKey(rowSelector);
5✔
879
        const column = this.columns.find((col) => col.field === columnField);
10✔
880
        if (row && column) {
5✔
881
            return new IgxGridCell(this as any, row.index, column);
5✔
882
        }
883
    }
884

885
    public override pinRow(rowID: any, index?: number): boolean {
886
        const row = this.getRowByKey(rowID);
27✔
887
        return super.pinRow(rowID, index, row);
27✔
888
    }
889

890
    public override unpinRow(rowID: any): boolean {
891
        const row = this.getRowByKey(rowID);
5✔
892
        return super.unpinRow(rowID, row);
5✔
893
    }
894

895
    /** @hidden */
896
    public generateRowPath(rowId: any): any[] {
897
        const path: any[] = [];
52✔
898
        let record = this.records.get(rowId);
52✔
899

900
        while (record.parent) {
52✔
901
            path.push(record.parent.key);
46✔
902
            record = record.parent;
46✔
903
        }
904

905
        return path.reverse();
52✔
906
    }
907

908
    /** @hidden */
909
    public isTreeRow(record: any): boolean {
910
        return record.key !== undefined && record.data;
13,584✔
911
    }
912

913
    /** @hidden */
914
    public override getUnpinnedIndexById(id) {
915
        return this.unpinnedRecords.findIndex(x => x.data[this.primaryKey] === id);
43✔
916
    }
917

918
    /**
919
     * @hidden
920
     */
921
    public createRow(index: number, data?: any): RowType {
922
        let row: RowType;
923
        const dataIndex = this._getDataViewIndex(index);
13,577✔
924
        const rec: any = data ?? this.dataView[dataIndex];
13,577✔
925

926
        if (this.isSummaryRow(rec)) {
13,577✔
927
            row = new IgxSummaryRow(this as any, index, rec.summaries);
7✔
928
        }
929

930
        if (!row && rec) {
13,577✔
931
            const isTreeRow = this.isTreeRow(rec);
13,570✔
932
            const dataRec = isTreeRow ? rec.data : rec;
13,570✔
933
            const treeRow = isTreeRow ? rec : undefined;
13,570✔
934
            row = new IgxTreeGridRow(this as any, index, dataRec, treeRow);
13,570✔
935
        }
936

937
        return row;
13,577✔
938
    }
939

940
    protected override generateDataFields(data: any[]): string[] {
941
        return super.generateDataFields(data).filter(field => field !== this.childDataKey);
16✔
942
    }
943

944
    protected override transactionStatusUpdate(event: StateUpdateEvent) {
945
        let actions = [];
152✔
946
        if (event.origin === TransactionEventOrigin.REDO) {
152✔
947
            actions = event.actions ? event.actions.filter(x => x.transaction.type === TransactionType.DELETE) : [];
25!
948
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
20✔
949
                this.handleCascadeSelection(event);
1✔
950
            }
951
        } else if (event.origin === TransactionEventOrigin.UNDO) {
132✔
952
            actions = event.actions ? event.actions.filter(x => x.transaction.type === TransactionType.ADD) : [];
32!
953
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
27✔
954
                if (event.actions[0].transaction.type === 'add') {
2✔
955
                    const rec = this.gridAPI.get_rec_by_id(event.actions[0].transaction.id);
1✔
956
                    this.handleCascadeSelection(event, rec);
1✔
957
                } else {
958
                    this.handleCascadeSelection(event);
1✔
959
                }
960
            }
961
        }
962
        if (actions.length) {
152✔
963
            for (const action of actions) {
13✔
964
                this.deselectChildren(action.transaction.id);
18✔
965
            }
966
        }
967
        super.transactionStatusUpdate(event);
152✔
968
    }
969

970
    protected findRecordIndexInView(rec) {
971
        return this.dataView.findIndex(x => x.data[this.primaryKey] === rec[this.primaryKey]);
×
972
    }
973

974
    /**
975
     * @hidden @internal
976
     */
977
    protected override getDataBasedBodyHeight(): number {
978
        return !this.flatData || (this.flatData.length < this._defaultTargetRecordNumber) ?
46✔
979
            0 : this.defaultTargetBodyHeight;
980
    }
981

982
    /**
983
     * @hidden
984
     */
985
    protected override scrollTo(row: any | number, column: any | number): void {
986
        let delayScrolling = false;
85✔
987
        let record: ITreeGridRecord;
988

989
        if (typeof (row) !== 'number') {
85✔
990
            const rowData = row;
84✔
991
            const rowID = this.gridAPI.get_row_id(rowData);
84✔
992
            record = this.processedRecords.get(rowID);
84✔
993
            this.gridAPI.expand_path_to_record(record);
84✔
994

995
            if (this.paginator) {
84✔
996
                const rowIndex = this.processedExpandedFlatData.indexOf(rowData);
27✔
997
                const page = Math.floor(rowIndex / this.perPage);
27✔
998

999
                if (this.page !== page) {
27✔
1000
                    delayScrolling = true;
7✔
1001
                    this.page = page;
7✔
1002
                }
1003
            }
1004
        }
1005

1006
        if (delayScrolling) {
85✔
1007
            this.verticalScrollContainer.dataChanged.pipe(first()).subscribe(() => {
7✔
1008
                this.scrollDirective(this.verticalScrollContainer,
7✔
1009
                    typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(record));
7!
1010
            });
1011
        } else {
1012
            this.scrollDirective(this.verticalScrollContainer,
78✔
1013
                typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(record));
78✔
1014
        }
1015

1016
        this.scrollToHorizontally(column);
85✔
1017
    }
1018

1019
    protected override writeToData(rowIndex: number, value: any) {
1020
        mergeObjects(this.flatData[rowIndex], value);
×
1021
    }
1022

1023
    /**
1024
     * @hidden
1025
     */
1026
    protected override initColumns(collection: IgxColumnComponent[], cb: (args: any) => void = null) {
×
1027
        if (this.hasColumnLayouts) {
552!
1028
            // invalid configuration - tree grid should not allow column layouts
1029
            // remove column layouts
1030
            const nonColumnLayoutColumns = this.columns.filter((col) => !col.columnLayout && !col.columnLayoutChild);
×
1031
            this.updateColumns(nonColumnLayoutColumns);
×
1032
        }
1033
        super.initColumns(collection, cb);
552✔
1034
    }
1035

1036
    /**
1037
     * @hidden @internal
1038
     */
1039
    protected override getGroupAreaHeight(): number {
1040
        return this.treeGroupArea ? this.getComputedHeight(this.treeGroupArea.nativeElement) : 0;
1,532✔
1041
    }
1042

1043
    /** {@link triggerPipes} will re-create pinnedData on CRUD operations */
1044
    protected trackPinnedRowData(record: ITreeGridRecord) {
1045
        // TODO FIX: pipeline data doesn't match end interface (¬_¬ )
1046
        // return record.key || (record as any).rowID;
1047
        return record;
547✔
1048
    }
1049

1050
    /**
1051
     * @description A recursive way to deselect all selected children of a given record
1052
     * @param recordID ID of the record whose children to deselect
1053
     * @hidden
1054
     * @internal
1055
     */
1056
    private deselectChildren(recordID): void {
1057
        const selectedChildren = [];
21✔
1058
        // G.E. Apr 28, 2021 #9465 Records which are not in view can also be selected so we need to
1059
        // deselect them as well, hence using 'records' map instead of getRowByKey() method which will
1060
        // return only row components (i.e. records in view).
1061
        const rowToDeselect = this.records.get(recordID);
21✔
1062
        this.selectionService.deselectRowsWithNoEvent([recordID]);
21✔
1063
        this.gridAPI.get_selected_children(rowToDeselect, selectedChildren);
21✔
1064
        if (selectedChildren.length > 0) {
21✔
1065
            selectedChildren.forEach(x => this.deselectChildren(x));
3✔
1066
        }
1067
    }
1068

1069
    private addChildRows(children: any[], parentID: any) {
1070
        if (this.primaryKey && this.foreignKey) {
8✔
1071
            for (const child of children) {
6✔
1072
                child[this.foreignKey] = parentID;
10✔
1073
            }
1074
            this.data.push(...children);
6✔
1075
        } else if (this.childDataKey) {
2✔
1076
            let parent = this.records.get(parentID);
2✔
1077
            let parentData = parent.data;
2✔
1078

1079
            if (this.transactions.enabled && this.transactions.getAggregatedChanges(true).length) {
2!
1080
                const path = [];
×
1081
                while (parent) {
×
1082
                    path.push(parent.key);
×
1083
                    parent = parent.parent;
×
1084
                }
1085

1086
                let collection = this.data;
×
1087
                let record: any;
1088
                for (let i = path.length - 1; i >= 0; i--) {
×
1089
                    const pid = path[i];
×
1090
                    record = collection.find(r => r[this.primaryKey] === pid);
×
1091

1092
                    if (!record) {
×
1093
                        break;
×
1094
                    }
1095
                    collection = record[this.childDataKey];
×
1096
                }
1097
                if (record) {
×
1098
                    parentData = record;
×
1099
                }
1100
            }
1101

1102
            parentData[this.childDataKey] = children;
2✔
1103
        }
1104
        this.selectionService.clearHeaderCBState();
8✔
1105
        this.pipeTrigger++;
8✔
1106
        if (this.rowSelection === GridSelectionMode.multipleCascade) {
8✔
1107
            // Force pipe triggering for building the data structure
1108
            this.cdr.detectChanges();
2✔
1109
            if (this.selectionService.isRowSelected(parentID)) {
2✔
1110
                this.selectionService.rowSelection.delete(parentID);
2✔
1111
                this.selectionService.selectRowsWithNoEvent([parentID]);
2✔
1112
            }
1113
        }
1114
    }
1115

1116
    private loadChildrenOnRowExpansion(args: IRowToggleEventArgs) {
1117
        if (this.loadChildrenOnDemand) {
175✔
1118
            const parentID = args.rowID;
11✔
1119

1120
            if (args.expanded && !this._expansionStates.has(parentID)) {
11✔
1121
                this.loadingRows.add(parentID);
8✔
1122

1123
                this.loadChildrenOnDemand(parentID, children => {
8✔
1124
                    this.loadingRows.delete(parentID);
8✔
1125
                    this.addChildRows(children, parentID);
8✔
1126
                    this.notifyChanges();
8✔
1127
                });
1128
            }
1129
        }
1130
    }
1131

1132
    private handleCascadeSelection(event: IRowDataEventArgs | StateUpdateEvent, rec: ITreeGridRecord = null) {
2✔
1133
        // Wait for the change detection to update records through the pipes
1134
        requestAnimationFrame(() => {
11✔
1135
            if (rec === null) {
11✔
1136
                rec = this.gridAPI.get_rec_by_id((event as StateUpdateEvent).actions[0].transaction.id);
2✔
1137
            }
1138
            if (rec && rec.parent) {
11✔
1139
                this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(
11✔
1140
                    new Set([rec.parent]), rec.parent.key
1141
                );
1142
                this.notifyChanges();
11✔
1143
            }
1144
        });
1145
    }
1146
}
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