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

IgniteUI / igniteui-angular / 30820539303

03 Aug 2026 01:59PM UTC coverage: 90.157% (-0.03%) from 90.19%
30820539303

Pull #15125

github

web-flow
Merge cf62d17ef into 75c7853d3
Pull Request #15125: refactor(*): bundle styles with components

14962 of 17433 branches covered (85.83%)

Branch coverage included in aggregate %.

30113 of 32563 relevant lines covered (92.48%)

37531.2 hits per line

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

95.35
/projects/igniteui-angular/tree/src/tree/tree.component.ts
1
import {
2
    Component,
3
    QueryList,
4
    Input,
5
    Output,
6
    EventEmitter,
7
    ContentChild,
8
    Directive,
9
    TemplateRef,
10
    OnInit,
11
    AfterViewInit,
12
    ContentChildren,
13
    OnDestroy,
14
    HostBinding,
15
    ElementRef,
16
    booleanAttribute,
17
    inject,
18
    ChangeDetectionStrategy,
19
    ViewEncapsulation,
20
    ChangeDetectorRef
21
} from '@angular/core';
22

23
import { Subject } from 'rxjs';
24
import { takeUntil, throttleTime } from 'rxjs/operators';
25

26
import {
27
    IGX_TREE_COMPONENT, IgxTreeSelectionType, IgxTree, ITreeNodeToggledEventArgs,
28
    ITreeNodeTogglingEventArgs, ITreeNodeSelectionEvent, IgxTreeNode, IgxTreeSearchResolver
29
} from './common';
30
import { IgxTreeNavigationService } from './tree-navigation.service';
31
import { IgxTreeNodeComponent } from './tree-node/tree-node.component';
32
import { IgxTreeSelectionService } from './tree-selection.service';
33
import { IgxTreeService } from './tree.service';
34
import { growVerIn, growVerOut } from 'igniteui-angular/animations';
35
import { PlatformUtil, resizeObservable } from 'igniteui-angular/core';
36
import { ToggleAnimationSettings } from 'igniteui-angular/expansion-panel';
37

38
/**
39
 * @hidden @internal
40
 * Used for templating the select marker of the tree
41
 */
42
@Directive({
43
    selector: '[igxTreeSelectMarker]',
44
    standalone: true
45
})
46
export class IgxTreeSelectMarkerDirective {
3✔
47
}
48

49
/**
50
 * @hidden @internal
51
 * Used for templating the expand indicator of the tree
52
 */
53
@Directive({
54
    selector: '[igxTreeExpandIndicator]',
55
    standalone: true
56
})
57
export class IgxTreeExpandIndicatorDirective {
3✔
58
}
59

60
/**
61
 * Tree allows a developer to show a set of nodes in a hierarchical fashion.
62
 *
63
 * @igxModule IgxTreeModule
64
 * @igxKeywords tree
65
 * @igxTheme igx-tree-theme
66
 * @igxGroup Grids & Lists
67
 *
68
 * @remark
69
 * The Angular Tree Component allows users to represent hierarchical data in a tree-view structure,
70
 * maintaining parent-child relationships, as well as to define static tree-view structure without a corresponding data model.
71
 * Its primary purpose is to allow end-users to visualize and navigate within hierarchical data structures.
72
 * The Ignite UI for Angular Tree Component also provides load on demand capabilities, item activation,
73
 * bi-state and cascading selection of items through built-in checkboxes, built-in keyboard navigation and more.
74
 *
75
 * @example
76
 * ```html
77
 * <igx-tree>
78
 *   <igx-tree-node>
79
 *      I am a parent node 1
80
 *      <igx-tree-node>
81
 *          I am a child node 1
82
 *      </igx-tree-node>
83
 *      ...
84
 *   </igx-tree-node>
85
 *         ...
86
 * </igx-tree>
87
 * ```
88
 */
89
@Component({
90
    selector: 'igx-tree',
91
    templateUrl: 'tree.component.html',
92
    styleUrl: 'tree.component.css',
93
    encapsulation: ViewEncapsulation.None,
94
    providers: [
95
        IgxTreeService,
96
        IgxTreeSelectionService,
97
        IgxTreeNavigationService,
98
        { provide: IGX_TREE_COMPONENT, useExisting: IgxTreeComponent },
99
    ],
100
    changeDetection: ChangeDetectionStrategy.Eager,
101
    standalone: true
102
})
103
export class IgxTreeComponent implements IgxTree, OnInit, AfterViewInit, OnDestroy {
3✔
104
    private navService = inject(IgxTreeNavigationService);
103✔
105
    private selectionService = inject(IgxTreeSelectionService);
103✔
106
    private treeService = inject(IgxTreeService);
103✔
107
    private element = inject<ElementRef<HTMLElement>>(ElementRef);
103✔
108
    private platform = inject(PlatformUtil);
103✔
109
    private cdr = inject(ChangeDetectorRef, { optional: true });
103✔
110

111

112
    @HostBinding('class.igx-tree')
113
    public cssClass = 'igx-tree';
103✔
114

115
    /**
116
     * Gets/Sets tree selection mode
117
     *
118
     * @remarks
119
     * By default the tree selection mode is 'None'
120
     * @param selectionMode: IgxTreeSelectionType
121
     */
122
    @Input()
123
    public get selection() {
124
        return this._selection;
42,760✔
125
    }
126

127
    public set selection(selectionMode: IgxTreeSelectionType) {
128
        this._selection = selectionMode;
82✔
129
        this.selectionService.clearNodesSelection();
82✔
130
    }
131

132
    /** Get/Set how the tree should handle branch expansion.
133
     * If set to `true`, only a single branch can be expanded at a time, collapsing all others
134
     *
135
     * ```html
136
     * <igx-tree [singleBranchExpand]="true">
137
     * ...
138
     * </igx-tree>
139
     * ```
140
     *
141
     * ```typescript
142
     * const tree: IgxTree = this.tree;
143
     * this.tree.singleBranchExpand = false;
144
     * ```
145
     */
146
    @Input({ transform: booleanAttribute })
147
    public singleBranchExpand = false;
103✔
148

149
    /** Get/Set if nodes should be expanded/collapsed when clicking over them.
150
     *
151
     * ```html
152
     * <igx-tree [toggleNodeOnClick]="true">
153
     * ...
154
     * </igx-tree>
155
     * ```
156
     *
157
     * ```typescript
158
     * const tree: IgxTree = this.tree;
159
     * this.tree.toggleNodeOnClick = false;
160
     * ```
161
     */
162
    @Input({ transform: booleanAttribute })
163
    public toggleNodeOnClick = false;
103✔
164

165

166
    /** Get/Set the animation settings that branches should use when expanding/collpasing.
167
     *
168
     * ```html
169
     * <igx-tree [animationSettings]="customAnimationSettings">
170
     * </igx-tree>
171
     * ```
172
     *
173
     * ```typescript
174
     * const animationSettings: ToggleAnimationSettings = {
175
     *      openAnimation: growVerIn,
176
     *      closeAnimation: growVerOut
177
     * };
178
     *
179
     * this.tree.animationSettings = animationSettings;
180
     * ```
181
     */
182
    @Input()
183
    public animationSettings: ToggleAnimationSettings = {
103✔
184
        openAnimation: growVerIn,
185
        closeAnimation: growVerOut
186
    };
187

188
    /** Emitted when the node selection is changed through interaction
189
     *
190
     * ```html
191
     * <igx-tree (nodeSelection)="handleNodeSelection($event)">
192
     * </igx-tree>
193
     * ```
194
     *
195
     *```typescript
196
     * public handleNodeSelection(event: ITreeNodeSelectionEvent) {
197
     *  const newSelection: IgxTreeNode<any>[] = event.newSelection;
198
     *  const added: IgxTreeNode<any>[] = event.added;
199
     *  console.log("New selection will be: ", newSelection);
200
     *  console.log("Added nodes: ", event.added);
201
     * }
202
     *```
203
     */
204
    @Output()
205
    public nodeSelection = new EventEmitter<ITreeNodeSelectionEvent>();
103✔
206

207
    /** Emitted when a node is expanding, before it finishes
208
     *
209
     * ```html
210
     * <igx-tree (nodeExpanding)="handleNodeExpanding($event)">
211
     * </igx-tree>
212
     * ```
213
     *
214
     *```typescript
215
     * public handleNodeExpanding(event: ITreeNodeTogglingEventArgs) {
216
     *  const expandedNode: IgxTreeNode<any> = event.node;
217
     *  if (expandedNode.disabled) {
218
     *      event.cancel = true;
219
     *  }
220
     * }
221
     *```
222
     */
223
    @Output()
224
    public nodeExpanding = new EventEmitter<ITreeNodeTogglingEventArgs>();
103✔
225

226
    /** Emitted when a node is expanded, after it finishes
227
     *
228
     * ```html
229
     * <igx-tree (nodeExpanded)="handleNodeExpanded($event)">
230
     * </igx-tree>
231
     * ```
232
     *
233
     *```typescript
234
     * public handleNodeExpanded(event: ITreeNodeToggledEventArgs) {
235
     *  const expandedNode: IgxTreeNode<any> = event.node;
236
     *  console.log("Node is expanded: ", expandedNode.data);
237
     * }
238
     *```
239
     */
240
    @Output()
241
    public nodeExpanded = new EventEmitter<ITreeNodeToggledEventArgs>();
103✔
242

243
    /** Emitted when a node is collapsing, before it finishes
244
     *
245
     * ```html
246
     * <igx-tree (nodeCollapsing)="handleNodeCollapsing($event)">
247
     * </igx-tree>
248
     * ```
249
     *
250
     *```typescript
251
     * public handleNodeCollapsing(event: ITreeNodeTogglingEventArgs) {
252
     *  const collapsedNode: IgxTreeNode<any> = event.node;
253
     *  if (collapsedNode.alwaysOpen) {
254
     *      event.cancel = true;
255
     *  }
256
     * }
257
     *```
258
     */
259
    @Output()
260
    public nodeCollapsing = new EventEmitter<ITreeNodeTogglingEventArgs>();
103✔
261

262
    /** Emitted when a node is collapsed, after it finishes
263
     *
264
     * @example
265
     * ```html
266
     * <igx-tree (nodeCollapsed)="handleNodeCollapsed($event)">
267
     * </igx-tree>
268
     * ```
269
     * ```typescript
270
     * public handleNodeCollapsed(event: ITreeNodeToggledEventArgs) {
271
     *  const collapsedNode: IgxTreeNode<any> = event.node;
272
     *  console.log("Node is collapsed: ", collapsedNode.data);
273
     * }
274
     * ```
275
     */
276
    @Output()
277
    public nodeCollapsed = new EventEmitter<ITreeNodeToggledEventArgs>();
103✔
278

279
    /**
280
     * Emitted when the active node is changed.
281
     *
282
     * @example
283
     * ```
284
     * <igx-tree (activeNodeChanged)="activeNodeChanged($event)"></igx-tree>
285
     * ```
286
     */
287
    @Output()
288
    public activeNodeChanged = new EventEmitter<IgxTreeNode<any>>();
103✔
289

290
    /**
291
     * A custom template to be used for the expand indicator of nodes
292
     * ```html
293
     * <igx-tree>
294
     *  <ng-template igxTreeExpandIndicator let-expanded>
295
     *      <igx-icon>{{ expanded ? "close_fullscreen": "open_in_full"}}</igx-icon>
296
     *  </ng-template>
297
     * </igx-tree>
298
     * ```
299
     */
300
    @ContentChild(IgxTreeExpandIndicatorDirective, { read: TemplateRef })
301
    public expandIndicator: TemplateRef<any>;
302

303
    /** @hidden @internal */
304
    @ContentChildren(IgxTreeNodeComponent, { descendants: true })
305
    public nodes: QueryList<IgxTreeNodeComponent<any>>;
306

307
    /** @hidden @internal */
308
    public disabledChange = new EventEmitter<IgxTreeNode<any>>();
103✔
309

310
    /**
311
     * Returns all **root level** nodes
312
     *
313
     * ```typescript
314
     * const tree: IgxTree = this.tree;
315
     * const rootNodes: IgxTreeNodeComponent<any>[] = tree.rootNodes;
316
     * ```
317
     */
318
    public get rootNodes(): IgxTreeNodeComponent<any>[] {
319
        return this.nodes?.filter(node => node.level === 0);
62✔
320
    }
321

322
    /**
323
     * Emitted when the active node is set through API
324
     *
325
     * @hidden @internal
326
     */
327
    public activeNodeBindingChange = new EventEmitter<IgxTreeNode<any>>();
103✔
328

329
    /** @hidden @internal */
330
    public forceSelect = [];
103✔
331

332
    /** @hidden @internal */
333
    public resizeNotify = new Subject<void>();
103✔
334

335
    private _selection: IgxTreeSelectionType = IgxTreeSelectionType.None;
103✔
336
    private destroy$ = new Subject<void>();
103✔
337
    private unsubChildren$ = new Subject<void>();
103✔
338

339
    constructor() {
340
        this.selectionService.register(this);
103✔
341
        this.treeService.register(this);
103✔
342
        this.navService.register(this);
103✔
343
    }
344

345
    /** @hidden @internal */
346
    public get nativeElement() {
347
        return this.element.nativeElement;
760✔
348
    }
349

350
    /**
351
     * Expands all of the passed nodes.
352
     * If no nodes are passed, expands ALL nodes
353
     *
354
     * @param nodes nodes to be expanded
355
     *
356
     * ```typescript
357
     * const targetNodes: IgxTreeNode<any> = this.tree.findNodes(true, (_data: any, node: IgxTreeNode<any>) => node.data.expandable);
358
     * tree.expandAll(nodes);
359
     * ```
360
     */
361
    public expandAll(nodes?: IgxTreeNode<any>[]) {
362
        nodes = nodes || this.nodes.toArray();
2✔
363
        nodes.forEach(e => e.expanded = true);
6✔
364
    }
365

366
    /**
367
     * Collapses all of the passed nodes.
368
     * If no nodes are passed, collapses ALL nodes
369
     *
370
     * @param nodes nodes to be collapsed
371
     *
372
     * ```typescript
373
     * const targetNodes: IgxTreeNode<any> = this.tree.findNodes(true, (_data: any, node: IgxTreeNode<any>) => node.data.collapsible);
374
     * tree.collapseAll(nodes);
375
     * ```
376
     */
377
    public collapseAll(nodes?: IgxTreeNode<any>[]) {
378
        nodes = nodes || this.nodes.toArray();
25✔
379
        nodes.forEach(e => e.expanded = false);
45✔
380
    }
381

382
    /**
383
     * Deselect all nodes if the nodes collection is empty. Otherwise, deselect the nodes in the nodes collection.
384
     *
385
     * @example
386
     * ```typescript
387
     *  const arr = [
388
     *      this.tree.nodes.toArray()[0],
389
     *      this.tree.nodes.toArray()[1]
390
     *  ];
391
     *  this.tree.deselectAll(arr);
392
     * ```
393
     * @param nodes: IgxTreeNodeComponent<any>[]
394
     */
395
    public deselectAll(nodes?: IgxTreeNodeComponent<any>[]) {
396
        this.selectionService.deselectNodesWithNoEvent(nodes);
4✔
397
    }
398

399
    /**
400
     * Returns all of the nodes that match the passed searchTerm.
401
     * Accepts a custom comparer function for evaluating the search term against the nodes.
402
     *
403
     * @remarks
404
     * Default search compares the passed `searchTerm` against the node's `data` Input.
405
     * When using `findNodes` w/o a `comparer`, make sure all nodes have `data` passed.
406
     *
407
     * @param searchTerm The data of the searched node
408
     * @param comparer A custom comparer function that evaluates the passed `searchTerm` against all nodes.
409
     * @returns Array of nodes that match the search. `null` if no nodes are found.
410
     *
411
     * ```html
412
     * <igx-tree>
413
     *     <igx-tree-node *ngFor="let node of data" [data]="node">
414
     *          {{ node.label }}
415
     *     </igx-tree-node>
416
     * </igx-tree>
417
     * ```
418
     *
419
     * ```typescript
420
     * public data: DataEntry[] = FETCHED_DATA;
421
     * ...
422
     * const matchedNodes: IgxTreeNode<DataEntry>[] = this.tree.findNodes<DataEntry>(searchTerm: data[5]);
423
     * ```
424
     *
425
     * Using a custom comparer
426
     * ```typescript
427
     * public data: DataEntry[] = FETCHED_DATA;
428
     * ...
429
     * const comparer: IgxTreeSearchResolver = (data: any, node: IgxTreeNode<DataEntry>) {
430
     *      return node.data.index % 2 === 0;
431
     * }
432
     * const evenIndexNodes: IgxTreeNode<DataEntry>[] = this.tree.findNodes<DataEntry>(null, comparer);
433
     * ```
434
     */
435
    public findNodes(searchTerm: any, comparer?: IgxTreeSearchResolver): IgxTreeNodeComponent<any>[] | null {
436
        const compareFunc = comparer || this._comparer;
3✔
437
        const results = this.nodes.filter(node => compareFunc(searchTerm, node));
15✔
438
        return results?.length === 0 ? null : results;
3✔
439
    }
440

441
    /** @hidden @internal */
442
    public handleKeydown(event: KeyboardEvent) {
443
        this.navService.handleKeydown(event);
38✔
444
    }
445

446
    /** @hidden @internal */
447
    public ngOnInit() {
448
        this.disabledChange.pipe(takeUntil(this.destroy$)).subscribe((e) => {
87✔
449
            this.navService.update_disabled_cache(e);
86✔
450
        });
451
        this.activeNodeBindingChange.pipe(takeUntil(this.destroy$)).subscribe((node) => {
87✔
452
            this.expandToNode(this.navService.activeNode);
175✔
453
            this.scrollNodeIntoView(node?.header?.nativeElement);
175✔
454
        });
455
        this.subToCollapsing();
87✔
456
        this.resizeNotify.pipe(
87✔
457
            throttleTime(40, null, { trailing: true }),
458
            takeUntil(this.destroy$)
459
        )
460
        .subscribe(() => {
461
            requestAnimationFrame(() => {
11✔
462
                this.scrollNodeIntoView(this.navService.activeNode?.header.nativeElement);
11✔
463
            });
464
        });
465
    }
466

467
    /** @hidden @internal */
468
    public ngAfterViewInit() {
469
        this.nodes.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
87✔
470
            this.subToChanges();
38✔
471
        });
472
        this.scrollNodeIntoView(this.navService.activeNode?.header?.nativeElement);
87✔
473
        this.subToChanges();
87✔
474
        resizeObservable(this.nativeElement).pipe(takeUntil(this.destroy$)).subscribe(() => this.resizeNotify.next());
87✔
475
    }
476

477
    /** @hidden @internal */
478
    public ngOnDestroy() {
479
        this.unsubChildren$.next();
121✔
480
        this.unsubChildren$.complete();
121✔
481
        this.destroy$.next();
121✔
482
        this.destroy$.complete();
121✔
483
    }
484

485
    private expandToNode(node: IgxTreeNode<any>) {
486
        if (node && node.parentNode) {
175✔
487
            node.path.forEach(n => {
131✔
488
                if (n !== node && !n.expanded) {
339!
489
                    n.expanded = true;
×
490
                }
491
            });
492
        }
493
    }
494

495
    private subToCollapsing() {
496
        this.nodeCollapsing.pipe(takeUntil(this.destroy$)).subscribe(event => {
87✔
497
            if (event.cancel) {
5!
498
                return;
×
499
            }
500
            this.navService.update_visible_cache(event.node, false);
5✔
501
        });
502
        this.nodeExpanding.pipe(takeUntil(this.destroy$)).subscribe(event => {
87✔
503
            if (event.cancel) {
26!
504
                return;
×
505
            }
506
            this.navService.update_visible_cache(event.node, true);
26✔
507
        });
508
    }
509

510
    private subToChanges() {
511
        this.unsubChildren$.next();
125✔
512
        const toBeSelected = [...this.forceSelect];
125✔
513
        if (this.platform.isBrowser) {
125✔
514
            requestAnimationFrame(() => {
125✔
515
                this.selectionService.selectNodesWithNoEvent(toBeSelected);
125✔
516
                this.cdr?.markForCheck();
125✔
517
            });
518
        }
519
        this.forceSelect = [];
125✔
520
        this.nodes.forEach(node => {
125✔
521
            node.expandedChange.pipe(takeUntil(this.unsubChildren$)).subscribe(nodeState => {
3,724✔
522
                this.navService.update_visible_cache(node, nodeState);
60✔
523
            });
524
            node.closeAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => {
3,724✔
525
                const targetElement = this.navService.focusedNode?.header.nativeElement;
2✔
526
                this.scrollNodeIntoView(targetElement);
2✔
527
            });
528
            node.openAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => {
3,724✔
529
                const targetElement = this.navService.focusedNode?.header.nativeElement;
11✔
530
                this.scrollNodeIntoView(targetElement);
11✔
531
            });
532
        });
533
        this.navService.init_invisible_cache();
125✔
534
    }
535

536
    private scrollNodeIntoView(el: HTMLElement) {
537
        if (!el) {
286✔
538
            return;
94✔
539
        }
540
        const nodeRect = el.getBoundingClientRect();
192✔
541
        const treeRect = this.nativeElement.getBoundingClientRect();
192✔
542
        const topOffset = treeRect.top > nodeRect.top ? nodeRect.top - treeRect.top : 0;
192✔
543
        const bottomOffset = treeRect.bottom < nodeRect.bottom ? nodeRect.bottom - treeRect.bottom : 0;
192✔
544
        const shouldScroll = !!topOffset || !!bottomOffset;
192✔
545
        if (shouldScroll && this.nativeElement.scrollHeight > this.nativeElement.clientHeight) {
192✔
546
            // this.nativeElement.scrollTop = nodeRect.y - treeRect.y - nodeRect.height;
547
            this.nativeElement.scrollTop =
120✔
548
                this.nativeElement.scrollTop + bottomOffset + topOffset + (topOffset ? -1 : +1) * nodeRect.height;
120✔
549
        }
550
    }
551

552
    private _comparer = <T>(data: T, node: IgxTreeNodeComponent<T>) => node.data === data;
103✔
553

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