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

IgniteUI / igniteui-angular / 28013325390

23 Jun 2026 08:34AM UTC coverage: 90.139% (-0.02%) from 90.154%
28013325390

Pull #17324

github

web-flow
Merge 690ff31c5 into 01244911c
Pull Request #17324: fix(skills): omit column widths by default in generated grid code

14880 of 17339 branches covered (85.82%)

Branch coverage included in aggregate %.

29947 of 32392 relevant lines covered (92.45%)

34656.81 hits per line

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

95.28
/projects/igniteui-angular/tree/src/tree/tree.component.ts
1
import { Component, QueryList, Input, Output, EventEmitter, ContentChild, Directive, TemplateRef, OnInit, AfterViewInit, ContentChildren, OnDestroy, HostBinding, ElementRef, booleanAttribute, inject, ChangeDetectionStrategy } from '@angular/core';
2

3
import { Subject } from 'rxjs';
4
import { takeUntil, throttleTime } from 'rxjs/operators';
5

6
import {
7
    IGX_TREE_COMPONENT, IgxTreeSelectionType, IgxTree, ITreeNodeToggledEventArgs,
8
    ITreeNodeTogglingEventArgs, ITreeNodeSelectionEvent, IgxTreeNode, IgxTreeSearchResolver
9
} from './common';
10
import { IgxTreeNavigationService } from './tree-navigation.service';
11
import { IgxTreeNodeComponent } from './tree-node/tree-node.component';
12
import { IgxTreeSelectionService } from './tree-selection.service';
13
import { IgxTreeService } from './tree.service';
14
import { growVerIn, growVerOut } from 'igniteui-angular/animations';
15
import { PlatformUtil, resizeObservable } from 'igniteui-angular/core';
16
import { ToggleAnimationSettings } from 'igniteui-angular/expansion-panel';
17

18
/**
19
 * @hidden @internal
20
 * Used for templating the select marker of the tree
21
 */
22
@Directive({
23
    selector: '[igxTreeSelectMarker]',
24
    standalone: true
25
})
26
export class IgxTreeSelectMarkerDirective {
3✔
27
}
28

29
/**
30
 * @hidden @internal
31
 * Used for templating the expand indicator of the tree
32
 */
33
@Directive({
34
    selector: '[igxTreeExpandIndicator]',
35
    standalone: true
36
})
37
export class IgxTreeExpandIndicatorDirective {
3✔
38
}
39

40
/**
41
 * Tree allows a developer to show a set of nodes in a hierarchical fashion.
42
 *
43
 * @igxModule IgxTreeModule
44
 * @igxKeywords tree
45
 * @igxTheme igx-tree-theme
46
 * @igxGroup Grids & Lists
47
 *
48
 * @remark
49
 * The Angular Tree Component allows users to represent hierarchical data in a tree-view structure,
50
 * maintaining parent-child relationships, as well as to define static tree-view structure without a corresponding data model.
51
 * Its primary purpose is to allow end-users to visualize and navigate within hierarchical data structures.
52
 * The Ignite UI for Angular Tree Component also provides load on demand capabilities, item activation,
53
 * bi-state and cascading selection of items through built-in checkboxes, built-in keyboard navigation and more.
54
 *
55
 * @example
56
 * ```html
57
 * <igx-tree>
58
 *   <igx-tree-node>
59
 *      I am a parent node 1
60
 *      <igx-tree-node>
61
 *          I am a child node 1
62
 *      </igx-tree-node>
63
 *      ...
64
 *   </igx-tree-node>
65
 *         ...
66
 * </igx-tree>
67
 * ```
68
 */
69
@Component({
70
    selector: 'igx-tree',
71
    templateUrl: 'tree.component.html',
72
    providers: [
73
        IgxTreeService,
74
        IgxTreeSelectionService,
75
        IgxTreeNavigationService,
76
        { provide: IGX_TREE_COMPONENT, useExisting: IgxTreeComponent },
77
    ],
78
    changeDetection: ChangeDetectionStrategy.Eager,
79
    standalone: true
80
})
81
export class IgxTreeComponent implements IgxTree, OnInit, AfterViewInit, OnDestroy {
3✔
82
    private navService = inject(IgxTreeNavigationService);
101✔
83
    private selectionService = inject(IgxTreeSelectionService);
101✔
84
    private treeService = inject(IgxTreeService);
101✔
85
    private element = inject<ElementRef<HTMLElement>>(ElementRef);
101✔
86
    private platform = inject(PlatformUtil);
101✔
87

88

89
    @HostBinding('class.igx-tree')
90
    public cssClass = 'igx-tree';
101✔
91

92
    /**
93
     * Gets/Sets tree selection mode
94
     *
95
     * @remarks
96
     * By default the tree selection mode is 'None'
97
     * @param selectionMode: IgxTreeSelectionType
98
     */
99
    @Input()
100
    public get selection() {
101
        return this._selection;
42,634✔
102
    }
103

104
    public set selection(selectionMode: IgxTreeSelectionType) {
105
        this._selection = selectionMode;
80✔
106
        this.selectionService.clearNodesSelection();
80✔
107
    }
108

109
    /** Get/Set how the tree should handle branch expansion.
110
     * If set to `true`, only a single branch can be expanded at a time, collapsing all others
111
     *
112
     * ```html
113
     * <igx-tree [singleBranchExpand]="true">
114
     * ...
115
     * </igx-tree>
116
     * ```
117
     *
118
     * ```typescript
119
     * const tree: IgxTree = this.tree;
120
     * this.tree.singleBranchExpand = false;
121
     * ```
122
     */
123
    @Input({ transform: booleanAttribute })
124
    public singleBranchExpand = false;
101✔
125

126
    /** Get/Set if nodes should be expanded/collapsed when clicking over them.
127
     *
128
     * ```html
129
     * <igx-tree [toggleNodeOnClick]="true">
130
     * ...
131
     * </igx-tree>
132
     * ```
133
     *
134
     * ```typescript
135
     * const tree: IgxTree = this.tree;
136
     * this.tree.toggleNodeOnClick = false;
137
     * ```
138
     */
139
    @Input({ transform: booleanAttribute })
140
    public toggleNodeOnClick = false;
101✔
141

142

143
    /** Get/Set the animation settings that branches should use when expanding/collpasing.
144
     *
145
     * ```html
146
     * <igx-tree [animationSettings]="customAnimationSettings">
147
     * </igx-tree>
148
     * ```
149
     *
150
     * ```typescript
151
     * const animationSettings: ToggleAnimationSettings = {
152
     *      openAnimation: growVerIn,
153
     *      closeAnimation: growVerOut
154
     * };
155
     *
156
     * this.tree.animationSettings = animationSettings;
157
     * ```
158
     */
159
    @Input()
160
    public animationSettings: ToggleAnimationSettings = {
101✔
161
        openAnimation: growVerIn,
162
        closeAnimation: growVerOut
163
    };
164

165
    /** Emitted when the node selection is changed through interaction
166
     *
167
     * ```html
168
     * <igx-tree (nodeSelection)="handleNodeSelection($event)">
169
     * </igx-tree>
170
     * ```
171
     *
172
     *```typescript
173
     * public handleNodeSelection(event: ITreeNodeSelectionEvent) {
174
     *  const newSelection: IgxTreeNode<any>[] = event.newSelection;
175
     *  const added: IgxTreeNode<any>[] = event.added;
176
     *  console.log("New selection will be: ", newSelection);
177
     *  console.log("Added nodes: ", event.added);
178
     * }
179
     *```
180
     */
181
    @Output()
182
    public nodeSelection = new EventEmitter<ITreeNodeSelectionEvent>();
101✔
183

184
    /** Emitted when a node is expanding, before it finishes
185
     *
186
     * ```html
187
     * <igx-tree (nodeExpanding)="handleNodeExpanding($event)">
188
     * </igx-tree>
189
     * ```
190
     *
191
     *```typescript
192
     * public handleNodeExpanding(event: ITreeNodeTogglingEventArgs) {
193
     *  const expandedNode: IgxTreeNode<any> = event.node;
194
     *  if (expandedNode.disabled) {
195
     *      event.cancel = true;
196
     *  }
197
     * }
198
     *```
199
     */
200
    @Output()
201
    public nodeExpanding = new EventEmitter<ITreeNodeTogglingEventArgs>();
101✔
202

203
    /** Emitted when a node is expanded, after it finishes
204
     *
205
     * ```html
206
     * <igx-tree (nodeExpanded)="handleNodeExpanded($event)">
207
     * </igx-tree>
208
     * ```
209
     *
210
     *```typescript
211
     * public handleNodeExpanded(event: ITreeNodeToggledEventArgs) {
212
     *  const expandedNode: IgxTreeNode<any> = event.node;
213
     *  console.log("Node is expanded: ", expandedNode.data);
214
     * }
215
     *```
216
     */
217
    @Output()
218
    public nodeExpanded = new EventEmitter<ITreeNodeToggledEventArgs>();
101✔
219

220
    /** Emitted when a node is collapsing, before it finishes
221
     *
222
     * ```html
223
     * <igx-tree (nodeCollapsing)="handleNodeCollapsing($event)">
224
     * </igx-tree>
225
     * ```
226
     *
227
     *```typescript
228
     * public handleNodeCollapsing(event: ITreeNodeTogglingEventArgs) {
229
     *  const collapsedNode: IgxTreeNode<any> = event.node;
230
     *  if (collapsedNode.alwaysOpen) {
231
     *      event.cancel = true;
232
     *  }
233
     * }
234
     *```
235
     */
236
    @Output()
237
    public nodeCollapsing = new EventEmitter<ITreeNodeTogglingEventArgs>();
101✔
238

239
    /** Emitted when a node is collapsed, after it finishes
240
     *
241
     * @example
242
     * ```html
243
     * <igx-tree (nodeCollapsed)="handleNodeCollapsed($event)">
244
     * </igx-tree>
245
     * ```
246
     * ```typescript
247
     * public handleNodeCollapsed(event: ITreeNodeToggledEventArgs) {
248
     *  const collapsedNode: IgxTreeNode<any> = event.node;
249
     *  console.log("Node is collapsed: ", collapsedNode.data);
250
     * }
251
     * ```
252
     */
253
    @Output()
254
    public nodeCollapsed = new EventEmitter<ITreeNodeToggledEventArgs>();
101✔
255

256
    /**
257
     * Emitted when the active node is changed.
258
     *
259
     * @example
260
     * ```
261
     * <igx-tree (activeNodeChanged)="activeNodeChanged($event)"></igx-tree>
262
     * ```
263
     */
264
    @Output()
265
    public activeNodeChanged = new EventEmitter<IgxTreeNode<any>>();
101✔
266

267
    /**
268
     * A custom template to be used for the expand indicator of nodes
269
     * ```html
270
     * <igx-tree>
271
     *  <ng-template igxTreeExpandIndicator let-expanded>
272
     *      <igx-icon>{{ expanded ? "close_fullscreen": "open_in_full"}}</igx-icon>
273
     *  </ng-template>
274
     * </igx-tree>
275
     * ```
276
     */
277
    @ContentChild(IgxTreeExpandIndicatorDirective, { read: TemplateRef })
278
    public expandIndicator: TemplateRef<any>;
279

280
    /** @hidden @internal */
281
    @ContentChildren(IgxTreeNodeComponent, { descendants: true })
282
    public nodes: QueryList<IgxTreeNodeComponent<any>>;
283

284
    /** @hidden @internal */
285
    public disabledChange = new EventEmitter<IgxTreeNode<any>>();
101✔
286

287
    /**
288
     * Returns all **root level** nodes
289
     *
290
     * ```typescript
291
     * const tree: IgxTree = this.tree;
292
     * const rootNodes: IgxTreeNodeComponent<any>[] = tree.rootNodes;
293
     * ```
294
     */
295
    public get rootNodes(): IgxTreeNodeComponent<any>[] {
296
        return this.nodes?.filter(node => node.level === 0);
62✔
297
    }
298

299
    /**
300
     * Emitted when the active node is set through API
301
     *
302
     * @hidden @internal
303
     */
304
    public activeNodeBindingChange = new EventEmitter<IgxTreeNode<any>>();
101✔
305

306
    /** @hidden @internal */
307
    public forceSelect = [];
101✔
308

309
    /** @hidden @internal */
310
    public resizeNotify = new Subject<void>();
101✔
311

312
    private _selection: IgxTreeSelectionType = IgxTreeSelectionType.None;
101✔
313
    private destroy$ = new Subject<void>();
101✔
314
    private unsubChildren$ = new Subject<void>();
101✔
315

316
    constructor() {
317
        this.selectionService.register(this);
101✔
318
        this.treeService.register(this);
101✔
319
        this.navService.register(this);
101✔
320
    }
321

322
    /** @hidden @internal */
323
    public get nativeElement() {
324
        return this.element.nativeElement;
742✔
325
    }
326

327
    /**
328
     * Expands all of the passed nodes.
329
     * If no nodes are passed, expands ALL nodes
330
     *
331
     * @param nodes nodes to be expanded
332
     *
333
     * ```typescript
334
     * const targetNodes: IgxTreeNode<any> = this.tree.findNodes(true, (_data: any, node: IgxTreeNode<any>) => node.data.expandable);
335
     * tree.expandAll(nodes);
336
     * ```
337
     */
338
    public expandAll(nodes?: IgxTreeNode<any>[]) {
339
        nodes = nodes || this.nodes.toArray();
2✔
340
        nodes.forEach(e => e.expanded = true);
6✔
341
    }
342

343
    /**
344
     * Collapses all of the passed nodes.
345
     * If no nodes are passed, collapses ALL nodes
346
     *
347
     * @param nodes nodes to be collapsed
348
     *
349
     * ```typescript
350
     * const targetNodes: IgxTreeNode<any> = this.tree.findNodes(true, (_data: any, node: IgxTreeNode<any>) => node.data.collapsible);
351
     * tree.collapseAll(nodes);
352
     * ```
353
     */
354
    public collapseAll(nodes?: IgxTreeNode<any>[]) {
355
        nodes = nodes || this.nodes.toArray();
25✔
356
        nodes.forEach(e => e.expanded = false);
45✔
357
    }
358

359
    /**
360
     * Deselect all nodes if the nodes collection is empty. Otherwise, deselect the nodes in the nodes collection.
361
     *
362
     * @example
363
     * ```typescript
364
     *  const arr = [
365
     *      this.tree.nodes.toArray()[0],
366
     *      this.tree.nodes.toArray()[1]
367
     *  ];
368
     *  this.tree.deselectAll(arr);
369
     * ```
370
     * @param nodes: IgxTreeNodeComponent<any>[]
371
     */
372
    public deselectAll(nodes?: IgxTreeNodeComponent<any>[]) {
373
        this.selectionService.deselectNodesWithNoEvent(nodes);
4✔
374
    }
375

376
    /**
377
     * Returns all of the nodes that match the passed searchTerm.
378
     * Accepts a custom comparer function for evaluating the search term against the nodes.
379
     *
380
     * @remarks
381
     * Default search compares the passed `searchTerm` against the node's `data` Input.
382
     * When using `findNodes` w/o a `comparer`, make sure all nodes have `data` passed.
383
     *
384
     * @param searchTerm The data of the searched node
385
     * @param comparer A custom comparer function that evaluates the passed `searchTerm` against all nodes.
386
     * @returns Array of nodes that match the search. `null` if no nodes are found.
387
     *
388
     * ```html
389
     * <igx-tree>
390
     *     <igx-tree-node *ngFor="let node of data" [data]="node">
391
     *          {{ node.label }}
392
     *     </igx-tree-node>
393
     * </igx-tree>
394
     * ```
395
     *
396
     * ```typescript
397
     * public data: DataEntry[] = FETCHED_DATA;
398
     * ...
399
     * const matchedNodes: IgxTreeNode<DataEntry>[] = this.tree.findNodes<DataEntry>(searchTerm: data[5]);
400
     * ```
401
     *
402
     * Using a custom comparer
403
     * ```typescript
404
     * public data: DataEntry[] = FETCHED_DATA;
405
     * ...
406
     * const comparer: IgxTreeSearchResolver = (data: any, node: IgxTreeNode<DataEntry>) {
407
     *      return node.data.index % 2 === 0;
408
     * }
409
     * const evenIndexNodes: IgxTreeNode<DataEntry>[] = this.tree.findNodes<DataEntry>(null, comparer);
410
     * ```
411
     */
412
    public findNodes(searchTerm: any, comparer?: IgxTreeSearchResolver): IgxTreeNodeComponent<any>[] | null {
413
        const compareFunc = comparer || this._comparer;
3✔
414
        const results = this.nodes.filter(node => compareFunc(searchTerm, node));
15✔
415
        return results?.length === 0 ? null : results;
3✔
416
    }
417

418
    /** @hidden @internal */
419
    public handleKeydown(event: KeyboardEvent) {
420
        this.navService.handleKeydown(event);
38✔
421
    }
422

423
    /** @hidden @internal */
424
    public ngOnInit() {
425
        this.disabledChange.pipe(takeUntil(this.destroy$)).subscribe((e) => {
85✔
426
            this.navService.update_disabled_cache(e);
86✔
427
        });
428
        this.activeNodeBindingChange.pipe(takeUntil(this.destroy$)).subscribe((node) => {
85✔
429
            this.expandToNode(this.navService.activeNode);
175✔
430
            this.scrollNodeIntoView(node?.header?.nativeElement);
175✔
431
        });
432
        this.subToCollapsing();
85✔
433
        this.resizeNotify.pipe(
85✔
434
            throttleTime(40, null, { trailing: true }),
435
            takeUntil(this.destroy$)
436
        )
437
        .subscribe(() => {
438
            requestAnimationFrame(() => {
9✔
439
                this.scrollNodeIntoView(this.navService.activeNode?.header.nativeElement);
9✔
440
            });
441
        });
442
    }
443

444
    /** @hidden @internal */
445
    public ngAfterViewInit() {
446
        this.nodes.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
85✔
447
            this.subToChanges();
37✔
448
        });
449
        this.scrollNodeIntoView(this.navService.activeNode?.header?.nativeElement);
85✔
450
        this.subToChanges();
85✔
451
        resizeObservable(this.nativeElement).pipe(takeUntil(this.destroy$)).subscribe(() => this.resizeNotify.next());
85✔
452
    }
453

454
    /** @hidden @internal */
455
    public ngOnDestroy() {
456
        this.unsubChildren$.next();
119✔
457
        this.unsubChildren$.complete();
119✔
458
        this.destroy$.next();
119✔
459
        this.destroy$.complete();
119✔
460
    }
461

462
    private expandToNode(node: IgxTreeNode<any>) {
463
        if (node && node.parentNode) {
175✔
464
            node.path.forEach(n => {
130✔
465
                if (n !== node && !n.expanded) {
352!
466
                    n.expanded = true;
×
467
                }
468
            });
469
        }
470
    }
471

472
    private subToCollapsing() {
473
        this.nodeCollapsing.pipe(takeUntil(this.destroy$)).subscribe(event => {
85✔
474
            if (event.cancel) {
5!
475
                return;
×
476
            }
477
            this.navService.update_visible_cache(event.node, false);
5✔
478
        });
479
        this.nodeExpanding.pipe(takeUntil(this.destroy$)).subscribe(event => {
85✔
480
            if (event.cancel) {
26!
481
                return;
×
482
            }
483
            this.navService.update_visible_cache(event.node, true);
26✔
484
        });
485
    }
486

487
    private subToChanges() {
488
        this.unsubChildren$.next();
122✔
489
        const toBeSelected = [...this.forceSelect];
122✔
490
        if(this.platform.isBrowser) {
122✔
491
            requestAnimationFrame(() => {
122✔
492
                this.selectionService.selectNodesWithNoEvent(toBeSelected);
122✔
493
            });
494
        }
495
        this.forceSelect = [];
122✔
496
        this.nodes.forEach(node => {
122✔
497
            node.expandedChange.pipe(takeUntil(this.unsubChildren$)).subscribe(nodeState => {
3,718✔
498
                this.navService.update_visible_cache(node, nodeState);
60✔
499
            });
500
            node.closeAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => {
3,718✔
501
                const targetElement = this.navService.focusedNode?.header.nativeElement;
2✔
502
                this.scrollNodeIntoView(targetElement);
2✔
503
            });
504
            node.openAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => {
3,718✔
505
                const targetElement = this.navService.focusedNode?.header.nativeElement;
11✔
506
                this.scrollNodeIntoView(targetElement);
11✔
507
            });
508
        });
509
        this.navService.init_invisible_cache();
122✔
510
    }
511

512
    private scrollNodeIntoView(el: HTMLElement) {
513
        if (!el) {
282✔
514
            return;
90✔
515
        }
516
        const nodeRect = el.getBoundingClientRect();
192✔
517
        const treeRect = this.nativeElement.getBoundingClientRect();
192✔
518
        const topOffset = treeRect.top > nodeRect.top ? nodeRect.top - treeRect.top : 0;
192✔
519
        const bottomOffset = treeRect.bottom < nodeRect.bottom ? nodeRect.bottom - treeRect.bottom : 0;
192✔
520
        const shouldScroll = !!topOffset || !!bottomOffset;
192✔
521
        if (shouldScroll && this.nativeElement.scrollHeight > this.nativeElement.clientHeight) {
192✔
522
            // this.nativeElement.scrollTop = nodeRect.y - treeRect.y - nodeRect.height;
523
            this.nativeElement.scrollTop =
116✔
524
                this.nativeElement.scrollTop + bottomOffset + topOffset + (topOffset ? -1 : +1) * nodeRect.height;
116✔
525
        }
526
    }
527

528
    private _comparer = <T>(data: T, node: IgxTreeNodeComponent<T>) => node.data === data;
101✔
529

530
}
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