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

IgniteUI / igniteui-angular / 29587398373

17 Jul 2026 02:17PM UTC coverage: 90.116% (-0.02%) from 90.135%
29587398373

Pull #15125

github

web-flow
Merge ac93ee972 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%)

34494.72 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 {
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
} from '@angular/core';
21

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

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

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

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

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

109

110
    @HostBinding('class.igx-tree')
111
    public cssClass = 'igx-tree';
101✔
112

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

125
    public set selection(selectionMode: IgxTreeSelectionType) {
126
        this._selection = selectionMode;
80✔
127
        this.selectionService.clearNodesSelection();
80✔
128
    }
129

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

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

163

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

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

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

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

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

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

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

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

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

305
    /** @hidden @internal */
306
    public disabledChange = new EventEmitter<IgxTreeNode<any>>();
101✔
307

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

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

327
    /** @hidden @internal */
328
    public forceSelect = [];
101✔
329

330
    /** @hidden @internal */
331
    public resizeNotify = new Subject<void>();
101✔
332

333
    private _selection: IgxTreeSelectionType = IgxTreeSelectionType.None;
101✔
334
    private destroy$ = new Subject<void>();
101✔
335
    private unsubChildren$ = new Subject<void>();
101✔
336

337
    constructor() {
338
        this.selectionService.register(this);
101✔
339
        this.treeService.register(this);
101✔
340
        this.navService.register(this);
101✔
341
    }
342

343
    /** @hidden @internal */
344
    public get nativeElement() {
345
        return this.element.nativeElement;
766✔
346
    }
347

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

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

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

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

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

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

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

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

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

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

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

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

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

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