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

IgniteUI / igniteui-angular / 13287444581

12 Feb 2025 02:18PM UTC coverage: 10.56% (-81.0%) from 91.606%
13287444581

Pull #15359

github

web-flow
Merge a24969adb into 32cfe83f6
Pull Request #15359: fix(time-picker): exclude from SSR toggle events #15135

933 of 15233 branches covered (6.12%)

3037 of 28759 relevant lines covered (10.56%)

352.42 hits per line

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

0.75
/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid.component.ts
1
import {
2
    ChangeDetectionStrategy,
3
    Component,
4
    HostBinding,
5
    Input,
6
    OnInit,
7
    TemplateRef,
8
    ContentChild,
9
    AfterContentInit,
10
    ViewChild,
11
    DoCheck,
12
    AfterViewInit,
13
    ElementRef,
14
    NgZone,
15
    Inject,
16
    ChangeDetectorRef,
17
    IterableDiffers,
18
    ViewContainerRef,
19
    Optional,
20
    LOCALE_ID,
21
    Injector,
22
    EnvironmentInjector,
23
    CUSTOM_ELEMENTS_SCHEMA,
24
    booleanAttribute
25
} from '@angular/core';
26
import { DOCUMENT, NgIf, NgClass, NgFor, NgTemplateOutlet, NgStyle } from '@angular/common';
27

28
import { IgxTreeGridAPIService } from './tree-grid-api.service';
29
import { IgxGridBaseDirective } from '../grid-base.directive';
30
import { ITreeGridRecord } from './tree-grid.interfaces';
31
import { IRowDataCancelableEventArgs, IRowDataEventArgs, IRowToggleEventArgs } from '../common/events';
32
import {
33
    HierarchicalTransaction,
34
    HierarchicalState,
35
    TransactionType,
36
    TransactionEventOrigin,
37
    StateUpdateEvent
38
} from '../../services/transaction/transaction';
39
import { IgxFilteringService } from '../filtering/grid-filtering.service';
40
import { IgxGridSummaryService } from '../summaries/grid-summary.service';
41
import { IgxGridSelectionService } from '../selection/selection.service';
42
import { mergeObjects, PlatformUtil } from '../../core/utils';
43
import { first, takeUntil } from 'rxjs/operators';
44
import { IgxRowLoadingIndicatorTemplateDirective } from './tree-grid.directives';
45
import { IgxForOfSyncService, IgxForOfScrollSyncService } from '../../directives/for-of/for_of.sync.service';
46
import { IgxGridNavigationService } from '../grid-navigation.service';
47
import { CellType, GridServiceType, GridType, IGX_GRID_BASE, IGX_GRID_SERVICE_BASE, RowType } from '../common/grid.interface';
48
import { IgxColumnComponent } from '../columns/column.component';
49
import { IgxTreeGridSelectionService } from './tree-grid-selection.service';
50
import { GridSelectionMode } from '../common/enums';
51
import { IgxSummaryRow, IgxTreeGridRow } from '../grid-public-row';
52
import { IgxGridCRUDService } from '../common/crud.service';
53
import { IgxTreeGridGroupByAreaComponent } from '../grouping/tree-grid-group-by-area.component';
54
import { IgxGridCell } from '../grid-public-cell';
55
import { IgxHierarchicalTransactionFactory } from '../../services/transaction/transaction-factory.service';
56
import { IgxColumnResizingService } from '../resizing/resizing.service';
57
import { HierarchicalTransactionService } from '../../services/transaction/hierarchical-transaction';
58
import { IgxOverlayService } from '../../services/overlay/overlay';
59
import { IgxGridTransaction } from '../common/types';
60
import { TreeGridFilteringStrategy } from './tree-grid.filtering.strategy';
61
import { IgxGridValidationService } from '../grid/grid-validation.service';
62
import { IgxTreeGridSummaryPipe } from './tree-grid.summary.pipe';
63
import { IgxTreeGridFilteringPipe } from './tree-grid.filtering.pipe';
64
import { IgxTreeGridHierarchizingPipe, IgxTreeGridFlatteningPipe, IgxTreeGridSortingPipe, IgxTreeGridPagingPipe, IgxTreeGridTransactionPipe, IgxTreeGridNormalizeRecordsPipe, IgxTreeGridAddRowPipe } from './tree-grid.pipes';
65
import { IgxSummaryDataPipe } from '../summaries/grid-root-summary.pipe';
66
import { IgxHasVisibleColumnsPipe, IgxGridRowPinningPipe, IgxGridRowClassesPipe, IgxGridRowStylesPipe, IgxStringReplacePipe } from '../common/pipes';
67
import { IgxGridColumnResizerComponent } from '../resizing/resizer.component';
68
import { IgxIconComponent } from '../../icon/icon.component';
69
import { IgxRowEditTabStopDirective } from '../grid.rowEdit.directive';
70
import { IgxRippleDirective } from '../../directives/ripple/ripple.directive';
71
import { IgxButtonDirective } from '../../directives/button/button.directive';
72
import { IgxSnackbarComponent } from '../../snackbar/snackbar.component';
73
import { IgxCircularProgressBarComponent } from '../../progressbar/progressbar.component';
74
import { IgxOverlayOutletDirective, IgxToggleDirective } from '../../directives/toggle/toggle.directive';
75
import { IgxSummaryRowComponent } from '../summaries/summary-row.component';
76
import { IgxTreeGridRowComponent } from './tree-grid-row.component';
77
import { IgxTemplateOutletDirective } from '../../directives/template-outlet/template_outlet.directive';
78
import { IgxGridForOfDirective } from '../../directives/for-of/for_of.directive';
79
import { IgxColumnMovingDropDirective } from '../moving/moving.drop.directive';
80
import { IgxGridDragSelectDirective } from '../selection/drag-select.directive';
81
import { IgxGridBodyDirective } from '../grid.common';
82
import { IgxGridHeaderRowComponent } from '../headers/grid-header-row.component';
83
import { IgxTextHighlightService } from '../../directives/text-highlight/text-highlight.service';
84

85
let NEXT_ID = 0;
2✔
86

87
/* blazorAdditionalDependency: Column */
88
/* blazorAdditionalDependency: ColumnGroup */
89
/* blazorAdditionalDependency: ColumnLayout */
90
/* blazorAdditionalDependency: GridToolbar */
91
/* blazorAdditionalDependency: GridToolbarActions */
92
/* blazorAdditionalDependency: GridToolbarTitle */
93
/* blazorAdditionalDependency: GridToolbarAdvancedFiltering */
94
/* blazorAdditionalDependency: GridToolbarExporter */
95
/* blazorAdditionalDependency: GridToolbarHiding */
96
/* blazorAdditionalDependency: GridToolbarPinning */
97
/* blazorAdditionalDependency: ActionStrip */
98
/* blazorAdditionalDependency: GridActionsBaseDirective */
99
/* blazorAdditionalDependency: GridEditingActions */
100
/* blazorAdditionalDependency: GridPinningActions */
101
/* blazorIndirectRender */
102
/**
103
 * **Ignite UI for Angular Tree Grid** -
104
 * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/grid/grid)
105
 *
106
 * The Ignite UI Tree Grid displays and manipulates hierarchical data with consistent schema formatted as a table and
107
 * provides features such as sorting, filtering, editing, column pinning, paging, column moving and hiding.
108
 *
109
 * Example:
110
 * ```html
111
 * <igx-tree-grid [data]="employeeData" primaryKey="employeeID" foreignKey="PID" [autoGenerate]="false">
112
 *   <igx-column field="first" header="First Name"></igx-column>
113
 *   <igx-column field="last" header="Last Name"></igx-column>
114
 *   <igx-column field="role" header="Role"></igx-column>
115
 * </igx-tree-grid>
116
 * ```
117
 */
118
@Component({
119
    changeDetection: ChangeDetectionStrategy.OnPush,
120
    selector: 'igx-tree-grid',
121
    templateUrl: 'tree-grid.component.html',
122
    providers: [
123
        IgxGridCRUDService,
124
        IgxGridValidationService,
125
        IgxGridSummaryService,
126
        IgxGridNavigationService,
127
        { provide: IgxGridSelectionService, useClass: IgxTreeGridSelectionService },
128
        { provide: IGX_GRID_SERVICE_BASE, useClass: IgxTreeGridAPIService },
129
        { provide: IGX_GRID_BASE, useExisting: IgxTreeGridComponent },
130
        IgxFilteringService,
131
        IgxColumnResizingService,
132
        IgxForOfSyncService,
133
        IgxForOfScrollSyncService
134
    ],
135
    imports: [
136
        NgIf,
137
        NgFor,
138
        NgClass,
139
        NgStyle,
140
        NgTemplateOutlet,
141
        IgxGridHeaderRowComponent,
142
        IgxGridBodyDirective,
143
        IgxGridDragSelectDirective,
144
        IgxColumnMovingDropDirective,
145
        IgxGridForOfDirective,
146
        IgxTemplateOutletDirective,
147
        IgxTreeGridRowComponent,
148
        IgxSummaryRowComponent,
149
        IgxOverlayOutletDirective,
150
        IgxToggleDirective,
151
        IgxCircularProgressBarComponent,
152
        IgxSnackbarComponent,
153
        IgxButtonDirective,
154
        IgxRippleDirective,
155
        IgxRowEditTabStopDirective,
156
        IgxIconComponent,
157
        IgxGridColumnResizerComponent,
158
        IgxHasVisibleColumnsPipe,
159
        IgxGridRowPinningPipe,
160
        IgxGridRowClassesPipe,
161
        IgxGridRowStylesPipe,
162
        IgxSummaryDataPipe,
163
        IgxTreeGridHierarchizingPipe,
164
        IgxTreeGridFlatteningPipe,
165
        IgxTreeGridSortingPipe,
166
        IgxTreeGridFilteringPipe,
167
        IgxTreeGridPagingPipe,
168
        IgxTreeGridTransactionPipe,
169
        IgxTreeGridSummaryPipe,
170
        IgxTreeGridNormalizeRecordsPipe,
171
        IgxTreeGridAddRowPipe,
172
        IgxStringReplacePipe
173
    ],
174
    schemas: [CUSTOM_ELEMENTS_SCHEMA]
175
})
176
export class IgxTreeGridComponent extends IgxGridBaseDirective implements GridType, OnInit, AfterViewInit, DoCheck, AfterContentInit {
2✔
177
    /**
178
     * Sets the child data key of the `IgxTreeGridComponent`.
179
     * ```html
180
     * <igx-tree-grid #grid [data]="employeeData" [childDataKey]="'employees'" [autoGenerate]="true"></igx-tree-grid>
181
     * ```
182
     *
183
     * @memberof IgxTreeGridComponent
184
     */
185
    @Input()
186
    public childDataKey: string;
187

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

200
    /**
201
     * Sets the key indicating whether a row has children.
202
     * This property is only used for load on demand scenarios.
203
     * ```html
204
     * <igx-tree-grid #grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'"
205
     *                [loadChildrenOnDemand]="loadChildren"
206
     *                [hasChildrenKey]="'hasEmployees'">
207
     * </igx-tree-grid>
208
     * ```
209
     *
210
     * @memberof IgxTreeGridComponent
211
     */
212
    @Input()
213
    public hasChildrenKey: string;
214

215
    /**
216
     * Sets whether child records should be deleted when their parent gets deleted.
217
     * By default it is set to true and deletes all children along with the parent.
218
     * ```html
219
     * <igx-tree-grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'" cascadeOnDelete="false">
220
     * </igx-tree-grid>
221
     * ```
222
     *
223
     * @memberof IgxTreeGridComponent
224
     */
225
    @Input({ transform: booleanAttribute })
226
    public cascadeOnDelete = true;
×
227

228
    /* csSuppress */
229
    /**
230
     * Sets a callback for loading child rows on demand.
231
     * ```html
232
     * <igx-tree-grid [data]="employeeData" [primaryKey]="'employeeID'" [foreignKey]="'parentID'" [loadChildrenOnDemand]="loadChildren">
233
     * </igx-tree-grid>
234
     * ```
235
     * ```typescript
236
     * public loadChildren = (parentID: any, done: (children: any[]) => void) => {
237
     *     this.dataService.getData(parentID, children => done(children));
238
     * }
239
     * ```
240
     *
241
     * @memberof IgxTreeGridComponent
242
     */
243
    @Input()
244
    public loadChildrenOnDemand: (parentID: any, done: (children: any[]) => void) => void;
245

246
    /**
247
     * @hidden @internal
248
     */
249
    @HostBinding('attr.role')
250
    public role = 'treegrid';
×
251

252
    /**
253
     * Sets the value of the `id` attribute. If not provided it will be automatically generated.
254
     * ```html
255
     * <igx-tree-grid [id]="'igx-tree-grid-1'"></igx-tree-grid>
256
     * ```
257
     *
258
     * @memberof IgxTreeGridComponent
259
     */
260
    @HostBinding('attr.id')
261
    @Input()
262
    public id = `igx-tree-grid-${NEXT_ID++}`;
×
263

264
    /**
265
     * @hidden
266
     * @internal
267
     */
268
    @ContentChild(IgxTreeGridGroupByAreaComponent, { read: IgxTreeGridGroupByAreaComponent })
269
    public treeGroupArea: IgxTreeGridGroupByAreaComponent;
270

271
    /**
272
     * @hidden @internal
273
     */
274
    @ViewChild('record_template', { read: TemplateRef, static: true })
275
    protected recordTemplate: TemplateRef<any>;
276

277
    /**
278
     * @hidden @internal
279
     */
280
    @ViewChild('summary_template', { read: TemplateRef, static: true })
281
    protected summaryTemplate: TemplateRef<any>;
282

283
    /**
284
     * @hidden
285
     */
286
    @ContentChild(IgxRowLoadingIndicatorTemplateDirective, { read: IgxRowLoadingIndicatorTemplateDirective })
287
    protected rowLoadingTemplate: IgxRowLoadingIndicatorTemplateDirective;
288

289
    /**
290
     * @hidden
291
     */
292
    public flatData: any[] | null;
293

294
    /**
295
     * @hidden
296
     */
297
    public processedExpandedFlatData: any[] | null;
298

299
    /**
300
     * Returns an array of the root level `ITreeGridRecord`s.
301
     * ```typescript
302
     * // gets the root record with index=2
303
     * const states = this.grid.rootRecords[2];
304
     * ```
305
     *
306
     * @memberof IgxTreeGridComponent
307
     */
308
    public rootRecords: ITreeGridRecord[];
309

310
    /* blazorSuppress */
311
    /**
312
     * Returns a map of all `ITreeGridRecord`s.
313
     * ```typescript
314
     * // gets the record with primaryKey=2
315
     * const states = this.grid.records.get(2);
316
     * ```
317
     *
318
     * @memberof IgxTreeGridComponent
319
     */
320
    public records: Map<any, ITreeGridRecord> = new Map<any, ITreeGridRecord>();
×
321

322
    /**
323
     * Returns an array of processed (filtered and sorted) root `ITreeGridRecord`s.
324
     * ```typescript
325
     * // gets the processed root record with index=2
326
     * const states = this.grid.processedRootRecords[2];
327
     * ```
328
     *
329
     * @memberof IgxTreeGridComponent
330
     */
331
    public processedRootRecords: ITreeGridRecord[];
332

333
    /* blazorSuppress */
334
    /**
335
     * Returns a map of all processed (filtered and sorted) `ITreeGridRecord`s.
336
     * ```typescript
337
     * // gets the processed record with primaryKey=2
338
     * const states = this.grid.processedRecords.get(2);
339
     * ```
340
     *
341
     * @memberof IgxTreeGridComponent
342
     */
343
    public processedRecords: Map<any, ITreeGridRecord> = new Map<any, ITreeGridRecord>();
×
344

345
    /**
346
     * @hidden
347
     */
348
    public loadingRows = new Set<any>();
×
349

350
    protected override _filterStrategy = new TreeGridFilteringStrategy();
×
351
    protected override _transactions: HierarchicalTransactionService<HierarchicalTransaction, HierarchicalState>;
352
    private _data;
353
    private _rowLoadingIndicatorTemplate: TemplateRef<void>;
354
    private _expansionDepth = Infinity;
×
355

356
     /* treatAsRef */
357
    /**
358
     * Gets/Sets the array of data that populates the component.
359
     * ```html
360
     * <igx-tree-grid [data]="Data" [autoGenerate]="true"></igx-tree-grid>
361
     * ```
362
     *
363
     * @memberof IgxTreeGridComponent
364
     */
365
    @Input()
366
    public get data(): any[] | null {
367
        return this._data;
×
368
    }
369

370
     /* treatAsRef */
371
    public set data(value: any[] | null) {
372
        const oldData = this._data;
×
373
        this._data = value || [];
×
374
        this.summaryService.clearSummaryCache();
×
375
        if (!this._init) {
×
376
            this.validation.updateAll(this._data);
×
377
        }
378
        if (this.autoGenerate && this._data.length > 0 && this.shouldRecreateColumns(oldData, this._data)) {
×
379
            this.setupColumns();
×
380
        }
381
        this.checkPrimaryKeyField();
×
382
        this.cdr.markForCheck();
×
383
    }
384

385
    /** @hidden @internal */
386
    public override get type(): GridType["type"] {
387
        return 'tree';
×
388
    }
389

390
    /**
391
     * Get transactions service for the grid.
392
     *
393
     * @experimental @hidden
394
     */
395
    public override get transactions() {
396
        if (this._diTransactions && !this.batchEditing) {
×
397
            return this._diTransactions;
×
398
        }
399
        return this._transactions;
×
400
    }
401

402
    /**
403
     * Sets the count of levels to be expanded in the `IgxTreeGridComponent`. By default it is
404
     * set to `Infinity` which means all levels would be expanded.
405
     * ```html
406
     * <igx-tree-grid #grid [data]="employeeData" [childDataKey]="'employees'" expansionDepth="1" [autoGenerate]="true"></igx-tree-grid>
407
     * ```
408
     *
409
     * @memberof IgxTreeGridComponent
410
     */
411
    @Input()
412
    public get expansionDepth(): number {
413
        return this._expansionDepth;
×
414
    }
415

416
    public set expansionDepth(value: number) {
417
        this._expansionDepth = value;
×
418
        this.notifyChanges();
×
419
    }
420

421
    /**
422
     * Template for the row loading indicator when load on demand is enabled.
423
     * ```html
424
     * <ng-template #rowLoadingTemplate>
425
     *     <igx-icon>loop</igx-icon>
426
     * </ng-template>
427
     *
428
     * <igx-tree-grid #grid [data]="employeeData" [primaryKey]="'ID'" [foreignKey]="'parentID'"
429
     *                [loadChildrenOnDemand]="loadChildren"
430
     *                [rowLoadingIndicatorTemplate]="rowLoadingTemplate">
431
     * </igx-tree-grid>
432
     * ```
433
     *
434
     * @memberof IgxTreeGridComponent
435
     */
436
    @Input()
437
    public get rowLoadingIndicatorTemplate(): TemplateRef<void> {
438
        return this._rowLoadingIndicatorTemplate;
×
439
    }
440

441
    public set rowLoadingIndicatorTemplate(value: TemplateRef<void>) {
442
        this._rowLoadingIndicatorTemplate = value;
×
443
        this.notifyChanges();
×
444
    }
445

446
    // Kind of stupid
447
    // private get _gridAPI(): IgxTreeGridAPIService {
448
    //     return this.gridAPI as IgxTreeGridAPIService;
449
    // }
450

451
    constructor(
452
        validationService: IgxGridValidationService,
453
        selectionService: IgxGridSelectionService,
454
        colResizingService: IgxColumnResizingService,
455
        @Inject(IGX_GRID_SERVICE_BASE) gridAPI: GridServiceType,
456
        // public gridAPI: GridBaseAPIService<IgxGridBaseDirective & GridType>,
457
        transactionFactory: IgxHierarchicalTransactionFactory,
458
        _elementRef: ElementRef<HTMLElement>,
459
        _zone: NgZone,
460
        @Inject(DOCUMENT) document: any,
461
        cdr: ChangeDetectorRef,
462
        differs: IterableDiffers,
463
        viewRef: ViewContainerRef,
464
        injector: Injector,
465
        envInjector: EnvironmentInjector,
466
        navigation: IgxGridNavigationService,
467
        filteringService: IgxFilteringService,
468
        textHighlightService: IgxTextHighlightService,
469
        @Inject(IgxOverlayService) overlayService: IgxOverlayService,
470
        summaryService: IgxGridSummaryService,
471
        @Inject(LOCALE_ID) localeId: string,
472
        platform: PlatformUtil,
473
        @Optional() @Inject(IgxGridTransaction) protected override _diTransactions?:
×
474
            HierarchicalTransactionService<HierarchicalTransaction, HierarchicalState>,
475
    ) {
476
        super(
×
477
            validationService,
478
            selectionService,
479
            colResizingService,
480
            gridAPI,
481
            transactionFactory,
482
            _elementRef,
483
            _zone,
484
            document,
485
            cdr,
486
            differs,
487
            viewRef,
488
            injector,
489
            envInjector,
490
            navigation,
491
            filteringService,
492
            textHighlightService,
493
            overlayService,
494
            summaryService,
495
            localeId,
496
            platform,
497
            _diTransactions,
498
        );
499
    }
500

501
    /**
502
     * @hidden
503
     */
504
    public override ngOnInit() {
505
        super.ngOnInit();
×
506

507
        this.rowToggle.pipe(takeUntil(this.destroy$)).subscribe((args) => {
×
508
            this.loadChildrenOnRowExpansion(args);
×
509
        });
510

511
        // TODO: cascade selection logic should be refactor to be handled in the already existing subs
512
        this.rowAddedNotifier.pipe(takeUntil(this.destroy$)).subscribe(args => {
×
513
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
×
514
                let rec = this.gridAPI.get_rec_by_id(this.primaryKey ? args.data[this.primaryKey] : args.data);
×
515
                if (rec && rec.parent) {
×
516
                    this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(
×
517
                        new Set([rec.parent]), rec.parent.key);
518
                } else {
519
                    // The record is still not available
520
                    // Wait for the change detection to update records through pipes
521
                    requestAnimationFrame(() => {
×
522
                        rec = this.gridAPI.get_rec_by_id(this.primaryKey ?
×
523
                            args.data[this.primaryKey] : args.data);
524
                        if (rec && rec.parent) {
×
525
                            this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(
×
526
                                new Set([rec.parent]), rec.parent.key);
527
                        }
528
                        this.notifyChanges();
×
529
                    });
530
                }
531
            }
532
        });
533

534
        this.rowDeletedNotifier.pipe(takeUntil(this.destroy$)).subscribe(args => {
×
535
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
×
536
                if (args.data) {
×
537
                    const rec = this.gridAPI.get_rec_by_id(
×
538
                        this.primaryKey ? args.data[this.primaryKey] : args.data);
×
539
                    this.handleCascadeSelection(args, rec);
×
540
                } else {
541
                    // if a row has been added and before commiting the transaction deleted
542
                    const leafRowsDirectParents = new Set<any>();
×
543
                    this.records.forEach(record => {
×
544
                        if (record && (!record.children || record.children.length === 0) && record.parent) {
×
545
                            leafRowsDirectParents.add(record.parent);
×
546
                        }
547
                    });
548
                    // Wait for the change detection to update records through pipes
549
                    requestAnimationFrame(() => {
×
550
                        this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(leafRowsDirectParents);
×
551
                        this.notifyChanges();
×
552
                    });
553
                }
554
            }
555
        });
556

557
        this.filteringDone.pipe(takeUntil(this.destroy$)).subscribe(() => {
×
558
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
×
559
                const leafRowsDirectParents = new Set<any>();
×
560
                this.records.forEach(record => {
×
561
                    if (record && (!record.children || record.children.length === 0) && record.parent) {
×
562
                        leafRowsDirectParents.add(record.parent);
×
563
                    }
564
                });
565
                this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(leafRowsDirectParents);
×
566
                this.notifyChanges();
×
567
            }
568
        });
569
    }
570

571
    /**
572
     * @hidden
573
     */
574
    public override ngAfterViewInit() {
575
        super.ngAfterViewInit();
×
576
        // TODO: pipesExectured event
577
        // run after change detection in super triggers pipes for records structure
578
        if (this.rowSelection === GridSelectionMode.multipleCascade && this.selectedRows.length) {
×
579
            const selRows = this.selectedRows;
×
580
            this.selectionService.clearRowSelection();
×
581
            this.selectRows(selRows, true);
×
582
            this.cdr.detectChanges();
×
583
        }
584
    }
585

586
    /**
587
     * @hidden
588
     */
589
    public override ngAfterContentInit() {
590
        if (this.rowLoadingTemplate) {
×
591
            this._rowLoadingIndicatorTemplate = this.rowLoadingTemplate.template;
×
592
        }
593
        super.ngAfterContentInit();
×
594
    }
595

596
    public override getDefaultExpandState(record: ITreeGridRecord) {
597
        return record.children && record.children.length && record.level < this.expansionDepth;
×
598
    }
599

600
    /**
601
     * Expands all rows.
602
     * ```typescript
603
     * this.grid.expandAll();
604
     * ```
605
     *
606
     * @memberof IgxTreeGridComponent
607
     */
608
    public override expandAll() {
609
        this._expansionDepth = Infinity;
×
610
        this.expansionStates = new Map<any, boolean>();
×
611
    }
612

613
    /**
614
     * Collapses all rows.
615
     *
616
     * ```typescript
617
     * this.grid.collapseAll();
618
     *  ```
619
     *
620
     * @memberof IgxTreeGridComponent
621
     */
622
    public override collapseAll() {
623
        this._expansionDepth = 0;
×
624
        this.expansionStates = new Map<any, boolean>();
×
625
    }
626

627
    /**
628
     * @hidden
629
     */
630
    public override refreshGridState(args?: IRowDataEventArgs) {
631
        super.refreshGridState();
×
632
        if (this.primaryKey && this.foreignKey && args) {
×
633
            const rowID = args.data[this.foreignKey];
×
634
            this.summaryService.clearSummaryCache({ rowID });
×
635
            this.pipeTrigger++;
×
636
            this.cdr.detectChanges();
×
637
        }
638
    }
639

640
    /* blazorCSSuppress */
641
    /**
642
     * Creates a new `IgxTreeGridRowComponent` with the given data. If a parentRowID is not specified, the newly created
643
     * row would be added at the root level. Otherwise, it would be added as a child of the row whose primaryKey matches
644
     * the specified parentRowID. If the parentRowID does not exist, an error would be thrown.
645
     * ```typescript
646
     * const record = {
647
     *     ID: this.grid.data[this.grid1.data.length - 1].ID + 1,
648
     *     Name: this.newRecord
649
     * };
650
     * this.grid.addRow(record, 1); // Adds a new child row to the row with ID=1.
651
     * ```
652
     *
653
     * @param data
654
     * @param parentRowID
655
     * @memberof IgxTreeGridComponent
656
     */
657
    // TODO: remove evt emission
658
    public override addRow(data: any, parentRowID?: any) {
659
        this.crudService.endEdit(true);
×
660
        this.gridAPI.addRowToData(data, parentRowID);
×
661

662
        this.rowAddedNotifier.next({
×
663
            data: data,
664
            rowData: data, owner: this,
665
            primaryKey: data[this.primaryKey],
666
            rowKey: data[this.primaryKey]
667
        });
668
        this.pipeTrigger++;
×
669
        this.notifyChanges();
×
670
    }
671

672
    /**
673
     * Enters add mode by spawning the UI with the context of the specified row by index.
674
     *
675
     * @remarks
676
     * Accepted values for index are integers from 0 to this.grid.dataView.length
677
     * @remarks
678
     * When adding the row as a child, the parent row is the specified row.
679
     * @remarks
680
     * To spawn the UI on top, call the function with index = null or a negative number.
681
     * In this case trying to add this row as a child will result in error.
682
     * @example
683
     * ```typescript
684
     * this.grid.beginAddRowByIndex(10);
685
     * this.grid.beginAddRowByIndex(10, true);
686
     * this.grid.beginAddRowByIndex(null);
687
     * ```
688
     * @param index - The index to spawn the UI at. Accepts integers from 0 to this.grid.dataView.length
689
     * @param asChild - Whether the record should be added as a child. Only applicable to igxTreeGrid.
690
     */
691
    public override beginAddRowByIndex(index: number, asChild?: boolean): void {
692
        if (index === null || index < 0) {
×
693
            return this.beginAddRowById(null, asChild);
×
694
        }
695
        return this._addRowForIndex(index - 1, asChild);
×
696
    }
697

698
    /**
699
     * @hidden
700
     */
701
    public getContext(rowData: any, rowIndex: number, pinned?: boolean): any {
702
        return {
×
703
            $implicit: this.isGhostRecord(rowData) ? rowData.recordRef : rowData,
×
704
            index: this.getDataViewIndex(rowIndex, pinned),
705
            templateID: {
706
                type: this.isSummaryRow(rowData) ? 'summaryRow' : 'dataRow',
×
707
                id: null
708
            },
709
            disabled: this.isGhostRecord(rowData) ? rowData.recordRef.isFilteredOutParent === undefined : false
×
710
        };
711
    }
712

713
    /**
714
     * @hidden
715
     * @internal
716
     */
717
    public override getInitialPinnedIndex(rec) {
718
        const id = this.gridAPI.get_row_id(rec);
×
719
        return this._pinnedRecordIDs.indexOf(id);
×
720
    }
721

722
    /**
723
     * @hidden
724
     * @internal
725
     */
726
    public override isRecordPinned(rec) {
727
        return this.getInitialPinnedIndex(rec.data) !== -1;
×
728
    }
729

730
    /**
731
     *
732
     * Returns an array of the current cell selection in the form of `[{ column.field: cell.value }, ...]`.
733
     *
734
     * @remarks
735
     * If `formatters` is enabled, the cell value will be formatted by its respective column formatter (if any).
736
     * If `headers` is enabled, it will use the column header (if any) instead of the column field.
737
     */
738
    public override getSelectedData(formatters = false, headers = false): any[] {
×
739
        let source = [];
×
740

741
        const process = (record) => {
×
742
            if (record.summaries) {
×
743
                source.push(null);
×
744
                return;
×
745
            }
746
            source.push(record.data);
×
747
        };
748

749
        this.unpinnedDataView.forEach(process);
×
750
        source = this.isRowPinningToTop ? [...this.pinnedDataView, ...source] : [...source, ...this.pinnedDataView];
×
751
        return this.extractDataFromSelection(source, formatters, headers);
×
752
    }
753

754
    /**
755
     * @hidden @internal
756
     */
757
    public override getEmptyRecordObjectFor(inTreeRow: RowType) {
758
        const treeRowRec = inTreeRow?.treeRow || null;
×
759
        const row = { ...treeRowRec };
×
760
        const data = treeRowRec?.data || {};
×
761
        row.data = { ...data };
×
762
        Object.keys(row.data).forEach(key => {
×
763
            // persist foreign key if one is set.
764
            if (this.foreignKey && key === this.foreignKey) {
×
765
                row.data[key] = treeRowRec.data[this.crudService.addRowParent?.asChild ? this.primaryKey : key];
×
766
            } else {
767
                row.data[key] = undefined;
×
768
            }
769
        });
770
        let id = this.generateRowID();
×
771
        const rootRecPK = this.foreignKey && this.rootRecords && this.rootRecords.length > 0 ?
×
772
            this.rootRecords[0].data[this.foreignKey] : null;
773
        if (id === rootRecPK) {
×
774
            // safeguard in case generated id matches the root foreign key.
775
            id = this.generateRowID();
×
776
        }
777
        row.key = id;
×
778
        row.data[this.primaryKey] = id;
×
779
        return { rowID: id, data: row.data, recordRef: row };
×
780
    }
781

782
    /** @hidden */
783
    public override deleteRowById(rowId: any): any {
784
        //  if this is flat self-referencing data, and CascadeOnDelete is set to true
785
        //  and if we have transactions we should start pending transaction. This allows
786
        //  us in case of delete action to delete all child rows as single undo action
787
        const args: IRowDataCancelableEventArgs = {
×
788
            rowID: rowId,
789
            primaryKey: rowId,
790
            rowKey: rowId,
791
            cancel: false,
792
            rowData: this.getRowData(rowId),
793
            data: this.getRowData(rowId),
794
            oldValue: null,
795
            owner: this
796
        };
797
        this.rowDelete.emit(args);
×
798
        if (args.cancel) {
×
799
            return;
×
800
        }
801

802
        const record = this.gridAPI.deleteRowById(rowId);
×
803
        const key = record[this.primaryKey];
×
804
        if (record !== null && record !== undefined) {
×
805
            const rowDeletedEventArgs: IRowDataEventArgs = {
×
806
                data: record,
807
                rowData: record,
808
                owner: this,
809
                primaryKey: key,
810
                rowKey: key
811
            };
812
            this.rowDeleted.emit(rowDeletedEventArgs);
×
813
        }
814
        return record;
×
815
    }
816

817
    /**
818
     * Returns the `IgxTreeGridRow` by index.
819
     *
820
     * @example
821
     * ```typescript
822
     * const myRow = treeGrid.getRowByIndex(1);
823
     * ```
824
     * @param index
825
     */
826
    public getRowByIndex(index: number): RowType {
827
        if (index < 0 || index >= this.dataView.length) {
×
828
            return undefined;
×
829
        }
830
        return this.createRow(index);
×
831
    }
832

833
    /**
834
     * Returns the `RowType` object by the specified primary key.
835
     *
836
     * @example
837
     * ```typescript
838
     * const myRow = this.treeGrid.getRowByIndex(1);
839
     * ```
840
     * @param index
841
     */
842
    public getRowByKey(key: any): RowType {
843
        const rec = this.filteredSortedData ? this.primaryKey ? this.filteredSortedData.find(r => r[this.primaryKey] === key) :
×
844
            this.filteredSortedData.find(r => r === key) : undefined;
×
845
        const index = this.dataView.findIndex(r => r.data && r.data === rec);
×
846
        if (index < 0 || index >= this.filteredSortedData.length) {
×
847
            return undefined;
×
848
        }
849
        return new IgxTreeGridRow(this as any, index, rec);
×
850
    }
851

852
    /**
853
     * Returns the collection of all RowType for current page.
854
     *
855
     * @hidden @internal
856
     */
857
    public allRows(): RowType[] {
858
        return this.dataView.map((rec, index) => this.createRow(index));
×
859
    }
860

861
    /**
862
     * Returns the collection of `IgxTreeGridRow`s for current page.
863
     *
864
     * @hidden @internal
865
     */
866
    public dataRows(): RowType[] {
867
        return this.allRows().filter(row => row instanceof IgxTreeGridRow);
×
868
    }
869

870
    /**
871
     * Returns an array of the selected `IgxGridCell`s.
872
     *
873
     * @example
874
     * ```typescript
875
     * const selectedCells = this.grid.selectedCells;
876
     * ```
877
     */
878
    public get selectedCells(): CellType[] {
879
        return this.dataRows().map((row) => row.cells.filter((cell) => cell.selected))
×
880
            .reduce((a, b) => a.concat(b), []);
×
881
    }
882

883
    /**
884
     * Returns a `CellType` object that matches the conditions.
885
     *
886
     * @example
887
     * ```typescript
888
     * const myCell = this.grid1.getCellByColumn(2, "UnitPrice");
889
     * ```
890
     * @param rowIndex
891
     * @param columnField
892
     */
893
    public getCellByColumn(rowIndex: number, columnField: string): CellType {
894
        const row = this.getRowByIndex(rowIndex);
×
895
        const column = this.columns.find((col) => col.field === columnField);
×
896
        if (row && row instanceof IgxTreeGridRow && column) {
×
897
            return new IgxGridCell(this as any, rowIndex, column);
×
898
        }
899
    }
900

901
    /**
902
     * Returns a `CellType` object that matches the conditions.
903
     *
904
     * @remarks
905
     * Requires that the primaryKey property is set.
906
     * @example
907
     * ```typescript
908
     * grid.getCellByKey(1, 'index');
909
     * ```
910
     * @param rowSelector match any rowID
911
     * @param columnField
912
     */
913
    public getCellByKey(rowSelector: any, columnField: string): CellType {
914
        const row = this.getRowByKey(rowSelector);
×
915
        const column = this.columns.find((col) => col.field === columnField);
×
916
        if (row && column) {
×
917
            return new IgxGridCell(this as any, row.index, column);
×
918
        }
919
    }
920

921
    public override pinRow(rowID: any, index?: number): boolean {
922
        const row = this.getRowByKey(rowID);
×
923
        return super.pinRow(rowID, index, row);
×
924
    }
925

926
    public override unpinRow(rowID: any): boolean {
927
        const row = this.getRowByKey(rowID);
×
928
        return super.unpinRow(rowID, row);
×
929
    }
930

931
    /** @hidden */
932
    public generateRowPath(rowId: any): any[] {
933
        const path: any[] = [];
×
934
        let record = this.records.get(rowId);
×
935

936
        while (record.parent) {
×
937
            path.push(record.parent.key);
×
938
            record = record.parent;
×
939
        }
940

941
        return path.reverse();
×
942
    }
943

944
    /** @hidden */
945
    public isTreeRow(record: any): boolean {
946
        return record.key !== undefined && record.data;
×
947
    }
948

949
    /** @hidden */
950
    public override getUnpinnedIndexById(id) {
951
        return this.unpinnedRecords.findIndex(x => x.data[this.primaryKey] === id);
×
952
    }
953

954
    /**
955
     * @hidden
956
     */
957
    public createRow(index: number, data?: any): RowType {
958
        let row: RowType;
959
        const dataIndex = this._getDataViewIndex(index);
×
960
        const rec: any = data ?? this.dataView[dataIndex];
×
961

962
        if (this.isSummaryRow(rec)) {
×
963
            row = new IgxSummaryRow(this as any, index, rec.summaries);
×
964
        }
965

966
        if (!row && rec) {
×
967
            const isTreeRow = this.isTreeRow(rec);
×
968
            const dataRec = isTreeRow ? rec.data : rec;
×
969
            const treeRow = isTreeRow ? rec : undefined;
×
970
            row = new IgxTreeGridRow(this as any, index, dataRec, treeRow);
×
971
        }
972

973
        return row;
×
974
    }
975

976
    protected override generateDataFields(data: any[]): string[] {
977
        return super.generateDataFields(data).filter(field => field !== this.childDataKey);
×
978
    }
979

980
    protected override transactionStatusUpdate(event: StateUpdateEvent) {
981
        let actions = [];
×
982
        if (event.origin === TransactionEventOrigin.REDO) {
×
983
            actions = event.actions ? event.actions.filter(x => x.transaction.type === TransactionType.DELETE) : [];
×
984
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
×
985
                this.handleCascadeSelection(event);
×
986
            }
987
        } else if (event.origin === TransactionEventOrigin.UNDO) {
×
988
            actions = event.actions ? event.actions.filter(x => x.transaction.type === TransactionType.ADD) : [];
×
989
            if (this.rowSelection === GridSelectionMode.multipleCascade) {
×
990
                if (event.actions[0].transaction.type === 'add') {
×
991
                    const rec = this.gridAPI.get_rec_by_id(event.actions[0].transaction.id);
×
992
                    this.handleCascadeSelection(event, rec);
×
993
                } else {
994
                    this.handleCascadeSelection(event);
×
995
                }
996
            }
997
        }
998
        if (actions.length) {
×
999
            for (const action of actions) {
×
1000
                this.deselectChildren(action.transaction.id);
×
1001
            }
1002
        }
1003
        super.transactionStatusUpdate(event);
×
1004
    }
1005

1006
    protected findRecordIndexInView(rec) {
1007
        return this.dataView.findIndex(x => x.data[this.primaryKey] === rec[this.primaryKey]);
×
1008
    }
1009

1010
    /**
1011
     * @hidden @internal
1012
     */
1013
    protected override getDataBasedBodyHeight(): number {
1014
        return !this.flatData || (this.flatData.length < this._defaultTargetRecordNumber) ?
×
1015
            0 : this.defaultTargetBodyHeight;
1016
    }
1017

1018
    /**
1019
     * @hidden
1020
     */
1021
    protected override scrollTo(row: any | number, column: any | number): void {
1022
        let delayScrolling = false;
×
1023
        let record: ITreeGridRecord;
1024

1025
        if (typeof (row) !== 'number') {
×
1026
            const rowData = row;
×
1027
            const rowID = this.gridAPI.get_row_id(rowData);
×
1028
            record = this.processedRecords.get(rowID);
×
1029
            this.gridAPI.expand_path_to_record(record);
×
1030

1031
            if (this.paginator) {
×
1032
                const rowIndex = this.processedExpandedFlatData.indexOf(rowData);
×
1033
                const page = Math.floor(rowIndex / this.perPage);
×
1034

1035
                if (this.page !== page) {
×
1036
                    delayScrolling = true;
×
1037
                    this.page = page;
×
1038
                }
1039
            }
1040
        }
1041

1042
        if (delayScrolling) {
×
1043
            this.verticalScrollContainer.dataChanged.pipe(first()).subscribe(() => {
×
1044
                this.scrollDirective(this.verticalScrollContainer,
×
1045
                    typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(record));
×
1046
            });
1047
        } else {
1048
            this.scrollDirective(this.verticalScrollContainer,
×
1049
                typeof (row) === 'number' ? row : this.unpinnedDataView.indexOf(record));
×
1050
        }
1051

1052
        this.scrollToHorizontally(column);
×
1053
    }
1054

1055
    protected override writeToData(rowIndex: number, value: any) {
1056
        mergeObjects(this.flatData[rowIndex], value);
×
1057
    }
1058

1059
    /**
1060
     * @hidden
1061
     */
1062
    protected override initColumns(collection: IgxColumnComponent[], cb: (args: any) => void = null) {
×
1063
        if (this.hasColumnLayouts) {
×
1064
            // invalid configuration - tree grid should not allow column layouts
1065
            // remove column layouts
1066
            const nonColumnLayoutColumns = this.columns.filter((col) => !col.columnLayout && !col.columnLayoutChild);
×
1067
            this.updateColumns(nonColumnLayoutColumns);
×
1068
        }
1069
        super.initColumns(collection, cb);
×
1070
    }
1071

1072
    /**
1073
     * @hidden @internal
1074
     */
1075
    protected override getGroupAreaHeight(): number {
1076
        return this.treeGroupArea ? this.getComputedHeight(this.treeGroupArea.nativeElement) : 0;
×
1077
    }
1078

1079
    /**
1080
     * @description A recursive way to deselect all selected children of a given record
1081
     * @param recordID ID of the record whose children to deselect
1082
     * @hidden
1083
     * @internal
1084
     */
1085
    private deselectChildren(recordID): void {
1086
        const selectedChildren = [];
×
1087
        // G.E. Apr 28, 2021 #9465 Records which are not in view can also be selected so we need to
1088
        // deselect them as well, hence using 'records' map instead of getRowByKey() method which will
1089
        // return only row components (i.e. records in view).
1090
        const rowToDeselect = this.records.get(recordID);
×
1091
        this.selectionService.deselectRowsWithNoEvent([recordID]);
×
1092
        this.gridAPI.get_selected_children(rowToDeselect, selectedChildren);
×
1093
        if (selectedChildren.length > 0) {
×
1094
            selectedChildren.forEach(x => this.deselectChildren(x));
×
1095
        }
1096
    }
1097

1098
    private addChildRows(children: any[], parentID: any) {
1099
        if (this.primaryKey && this.foreignKey) {
×
1100
            for (const child of children) {
×
1101
                child[this.foreignKey] = parentID;
×
1102
            }
1103
            this.data.push(...children);
×
1104
        } else if (this.childDataKey) {
×
1105
            let parent = this.records.get(parentID);
×
1106
            let parentData = parent.data;
×
1107

1108
            if (this.transactions.enabled && this.transactions.getAggregatedChanges(true).length) {
×
1109
                const path = [];
×
1110
                while (parent) {
×
1111
                    path.push(parent.key);
×
1112
                    parent = parent.parent;
×
1113
                }
1114

1115
                let collection = this.data;
×
1116
                let record: any;
1117
                for (let i = path.length - 1; i >= 0; i--) {
×
1118
                    const pid = path[i];
×
1119
                    record = collection.find(r => r[this.primaryKey] === pid);
×
1120

1121
                    if (!record) {
×
1122
                        break;
×
1123
                    }
1124
                    collection = record[this.childDataKey];
×
1125
                }
1126
                if (record) {
×
1127
                    parentData = record;
×
1128
                }
1129
            }
1130

1131
            parentData[this.childDataKey] = children;
×
1132
        }
1133
        this.selectionService.clearHeaderCBState();
×
1134
        this.pipeTrigger++;
×
1135
        if (this.rowSelection === GridSelectionMode.multipleCascade) {
×
1136
            // Force pipe triggering for building the data structure
1137
            this.cdr.detectChanges();
×
1138
            if (this.selectionService.isRowSelected(parentID)) {
×
1139
                this.selectionService.rowSelection.delete(parentID);
×
1140
                this.selectionService.selectRowsWithNoEvent([parentID]);
×
1141
            }
1142
        }
1143
    }
1144

1145
    private loadChildrenOnRowExpansion(args: IRowToggleEventArgs) {
1146
        if (this.loadChildrenOnDemand) {
×
1147
            const parentID = args.rowID;
×
1148

1149
            if (args.expanded && !this._expansionStates.has(parentID)) {
×
1150
                this.loadingRows.add(parentID);
×
1151

1152
                this.loadChildrenOnDemand(parentID, children => {
×
1153
                    this.loadingRows.delete(parentID);
×
1154
                    this.addChildRows(children, parentID);
×
1155
                    this.notifyChanges();
×
1156
                });
1157
            }
1158
        }
1159
    }
1160

1161
    private handleCascadeSelection(event: IRowDataEventArgs | StateUpdateEvent, rec: ITreeGridRecord = null) {
×
1162
        // Wait for the change detection to update records through the pipes
1163
        requestAnimationFrame(() => {
×
1164
            if (rec === null) {
×
1165
                rec = this.gridAPI.get_rec_by_id((event as StateUpdateEvent).actions[0].transaction.id);
×
1166
            }
1167
            if (rec && rec.parent) {
×
1168
                this.gridAPI.grid.selectionService.updateCascadeSelectionOnFilterAndCRUD(
×
1169
                    new Set([rec.parent]), rec.parent.key
1170
                );
1171
                this.notifyChanges();
×
1172
            }
1173
        });
1174
    }
1175
}
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