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

IgniteUI / igniteui-angular / 17823480788

18 Sep 2025 08:56AM UTC coverage: 91.514% (-0.002%) from 91.516%
17823480788

Pull #16102

github

web-flow
Merge 3073c964b into 2a0fadc67
Pull Request #16102: refactor(tree): add raf check for tree SSR

13520 of 15849 branches covered (85.31%)

4 of 5 new or added lines in 1 file covered. (80.0%)

9 existing lines in 2 files now uncovered.

27242 of 29768 relevant lines covered (91.51%)

34605.65 hits per line

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

95.96
/projects/igniteui-angular/src/lib/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
} from '@angular/core';
18

19
import { Subject } from 'rxjs';
20
import { takeUntil, throttleTime } from 'rxjs/operators';
21

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

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

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

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

98
    @HostBinding('class.igx-tree')
99
    public cssClass = 'igx-tree';
101✔
100

101
    /**
102
     * Gets/Sets tree selection mode
103
     *
104
     * @remarks
105
     * By default the tree selection mode is 'None'
106
     * @param selectionMode: IgxTreeSelectionType
107
     */
108
    @Input()
109
    public get selection() {
110
        return this._selection;
42,777✔
111
    }
112

113
    public set selection(selectionMode: IgxTreeSelectionType) {
114
        this._selection = selectionMode;
79✔
115
        this.selectionService.clearNodesSelection();
79✔
116
    }
117

118
    /** Get/Set how the tree should handle branch expansion.
119
     * If set to `true`, only a single branch can be expanded at a time, collapsing all others
120
     *
121
     * ```html
122
     * <igx-tree [singleBranchExpand]="true">
123
     * ...
124
     * </igx-tree>
125
     * ```
126
     *
127
     * ```typescript
128
     * const tree: IgxTree = this.tree;
129
     * this.tree.singleBranchExpand = false;
130
     * ```
131
     */
132
    @Input({ transform: booleanAttribute })
133
    public singleBranchExpand = false;
101✔
134

135
    /** Get/Set if nodes should be expanded/collapsed when clicking over them.
136
     *
137
     * ```html
138
     * <igx-tree [toggleNodeOnClick]="true">
139
     * ...
140
     * </igx-tree>
141
     * ```
142
     *
143
     * ```typescript
144
     * const tree: IgxTree = this.tree;
145
     * this.tree.toggleNodeOnClick = false;
146
     * ```
147
     */
148
    @Input({ transform: booleanAttribute })
149
    public toggleNodeOnClick = false;
101✔
150

151

152
    /** Get/Set the animation settings that branches should use when expanding/collpasing.
153
     *
154
     * ```html
155
     * <igx-tree [animationSettings]="customAnimationSettings">
156
     * </igx-tree>
157
     * ```
158
     *
159
     * ```typescript
160
     * const animationSettings: ToggleAnimationSettings = {
161
     *      openAnimation: growVerIn,
162
     *      closeAnimation: growVerOut
163
     * };
164
     *
165
     * this.tree.animationSettings = animationSettings;
166
     * ```
167
     */
168
    @Input()
169
    public animationSettings: ToggleAnimationSettings = {
101✔
170
        openAnimation: growVerIn,
171
        closeAnimation: growVerOut
172
    };
173

174
    /** Emitted when the node selection is changed through interaction
175
     *
176
     * ```html
177
     * <igx-tree (nodeSelection)="handleNodeSelection($event)">
178
     * </igx-tree>
179
     * ```
180
     *
181
     *```typescript
182
     * public handleNodeSelection(event: ITreeNodeSelectionEvent) {
183
     *  const newSelection: IgxTreeNode<any>[] = event.newSelection;
184
     *  const added: IgxTreeNode<any>[] = event.added;
185
     *  console.log("New selection will be: ", newSelection);
186
     *  console.log("Added nodes: ", event.added);
187
     * }
188
     *```
189
     */
190
    @Output()
191
    public nodeSelection = new EventEmitter<ITreeNodeSelectionEvent>();
101✔
192

193
    /** Emitted when a node is expanding, before it finishes
194
     *
195
     * ```html
196
     * <igx-tree (nodeExpanding)="handleNodeExpanding($event)">
197
     * </igx-tree>
198
     * ```
199
     *
200
     *```typescript
201
     * public handleNodeExpanding(event: ITreeNodeTogglingEventArgs) {
202
     *  const expandedNode: IgxTreeNode<any> = event.node;
203
     *  if (expandedNode.disabled) {
204
     *      event.cancel = true;
205
     *  }
206
     * }
207
     *```
208
     */
209
    @Output()
210
    public nodeExpanding = new EventEmitter<ITreeNodeTogglingEventArgs>();
101✔
211

212
    /** Emitted when a node is expanded, after it finishes
213
     *
214
     * ```html
215
     * <igx-tree (nodeExpanded)="handleNodeExpanded($event)">
216
     * </igx-tree>
217
     * ```
218
     *
219
     *```typescript
220
     * public handleNodeExpanded(event: ITreeNodeToggledEventArgs) {
221
     *  const expandedNode: IgxTreeNode<any> = event.node;
222
     *  console.log("Node is expanded: ", expandedNode.data);
223
     * }
224
     *```
225
     */
226
    @Output()
227
    public nodeExpanded = new EventEmitter<ITreeNodeToggledEventArgs>();
101✔
228

229
    /** Emitted when a node is collapsing, before it finishes
230
     *
231
     * ```html
232
     * <igx-tree (nodeCollapsing)="handleNodeCollapsing($event)">
233
     * </igx-tree>
234
     * ```
235
     *
236
     *```typescript
237
     * public handleNodeCollapsing(event: ITreeNodeTogglingEventArgs) {
238
     *  const collapsedNode: IgxTreeNode<any> = event.node;
239
     *  if (collapsedNode.alwaysOpen) {
240
     *      event.cancel = true;
241
     *  }
242
     * }
243
     *```
244
     */
245
    @Output()
246
    public nodeCollapsing = new EventEmitter<ITreeNodeTogglingEventArgs>();
101✔
247

248
    /** Emitted when a node is collapsed, after it finishes
249
     *
250
     * @example
251
     * ```html
252
     * <igx-tree (nodeCollapsed)="handleNodeCollapsed($event)">
253
     * </igx-tree>
254
     * ```
255
     * ```typescript
256
     * public handleNodeCollapsed(event: ITreeNodeToggledEventArgs) {
257
     *  const collapsedNode: IgxTreeNode<any> = event.node;
258
     *  console.log("Node is collapsed: ", collapsedNode.data);
259
     * }
260
     * ```
261
     */
262
    @Output()
263
    public nodeCollapsed = new EventEmitter<ITreeNodeToggledEventArgs>();
101✔
264

265
    /**
266
     * Emitted when the active node is changed.
267
     *
268
     * @example
269
     * ```
270
     * <igx-tree (activeNodeChanged)="activeNodeChanged($event)"></igx-tree>
271
     * ```
272
     */
273
    @Output()
274
    public activeNodeChanged = new EventEmitter<IgxTreeNode<any>>();
101✔
275

276
    /**
277
     * A custom template to be used for the expand indicator of nodes
278
     * ```html
279
     * <igx-tree>
280
     *  <ng-template igxTreeExpandIndicator let-expanded>
281
     *      <igx-icon>{{ expanded ? "close_fullscreen": "open_in_full"}}</igx-icon>
282
     *  </ng-template>
283
     * </igx-tree>
284
     * ```
285
     */
286
    @ContentChild(IgxTreeExpandIndicatorDirective, { read: TemplateRef })
287
    public expandIndicator: TemplateRef<any>;
288

289
    /** @hidden @internal */
290
    @ContentChildren(IgxTreeNodeComponent, { descendants: true })
291
    public nodes: QueryList<IgxTreeNodeComponent<any>>;
292

293
    /** @hidden @internal */
294
    public disabledChange = new EventEmitter<IgxTreeNode<any>>();
101✔
295

296
    /**
297
     * Returns all **root level** nodes
298
     *
299
     * ```typescript
300
     * const tree: IgxTree = this.tree;
301
     * const rootNodes: IgxTreeNodeComponent<any>[] = tree.rootNodes;
302
     * ```
303
     */
304
    public get rootNodes(): IgxTreeNodeComponent<any>[] {
305
        return this.nodes?.filter(node => node.level === 0);
62✔
306
    }
307

308
    /**
309
     * Emitted when the active node is set through API
310
     *
311
     * @hidden @internal
312
     */
313
    public activeNodeBindingChange = new EventEmitter<IgxTreeNode<any>>();
101✔
314

315
    /** @hidden @internal */
316
    public forceSelect = [];
101✔
317

318
    /** @hidden @internal */
319
    public resizeNotify = new Subject<void>();
101✔
320

321
    private _selection: IgxTreeSelectionType = IgxTreeSelectionType.None;
101✔
322
    private destroy$ = new Subject<void>();
101✔
323
    private unsubChildren$ = new Subject<void>();
101✔
324

325
    constructor(
326
        private navService: IgxTreeNavigationService,
101✔
327
        private selectionService: IgxTreeSelectionService,
101✔
328
        private treeService: IgxTreeService,
101✔
329
        private element: ElementRef<HTMLElement>,
101✔
330
        private platform: PlatformUtil,
101✔
331
    ) {
332
        this.selectionService.register(this);
101✔
333
        this.treeService.register(this);
101✔
334
        this.navService.register(this);
101✔
335
    }
336

337
    /** @hidden @internal */
338
    public get nativeElement() {
339
        return this.element.nativeElement;
757✔
340
    }
341

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

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

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

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

433
    /** @hidden @internal */
434
    public handleKeydown(event: KeyboardEvent) {
435
        this.navService.handleKeydown(event);
38✔
436
    }
437

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

459
    /** @hidden @internal */
460
    public ngAfterViewInit() {
461
        this.nodes.changes.pipe(takeUntil(this.destroy$)).subscribe(() => {
84✔
462
            this.subToChanges();
54✔
463
        });
464
        this.scrollNodeIntoView(this.navService.activeNode?.header?.nativeElement);
84✔
465
        this.subToChanges();
84✔
466
        resizeObservable(this.nativeElement).pipe(takeUntil(this.destroy$)).subscribe(() => this.resizeNotify.next());
84✔
467
    }
468

469
    /** @hidden @internal */
470
    public ngOnDestroy() {
471
        this.unsubChildren$.next();
116✔
472
        this.unsubChildren$.complete();
116✔
473
        this.destroy$.next();
116✔
474
        this.destroy$.complete();
116✔
475
    }
476

477
    private expandToNode(node: IgxTreeNode<any>) {
478
        if (node && node.parentNode) {
175✔
479
            node.path.forEach(n => {
125✔
480
                if (n !== node && !n.expanded) {
326!
481
                    n.expanded = true;
×
482
                }
483
            });
484
        }
485
    }
486

487
    private subToCollapsing() {
488
        this.nodeCollapsing.pipe(takeUntil(this.destroy$)).subscribe(event => {
84✔
489
            if (event.cancel) {
5!
490
                return;
×
491
            }
492
            this.navService.update_visible_cache(event.node, false);
5✔
493
        });
494
        this.nodeExpanding.pipe(takeUntil(this.destroy$)).subscribe(event => {
84✔
495
            if (event.cancel) {
26!
496
                return;
×
497
            }
498
            this.navService.update_visible_cache(event.node, true);
26✔
499
        });
500
    }
501

502
    private subToChanges() {
503
        this.unsubChildren$.next();
138✔
504
        const toBeSelected = [...this.forceSelect];
138✔
505

506
        if(this.platform.isBrowser) {
138!
507
            requestAnimationFrame(() => {
138✔
508
                this.selectionService.selectNodesWithNoEvent(toBeSelected);
138✔
509
            });
510
        } else {
NEW
UNCOV
511
            this.selectionService.selectNodesWithNoEvent(toBeSelected);
×
512
        }
513

514
        this.forceSelect = [];
138✔
515
        this.nodes.forEach(node => {
138✔
516
            node.expandedChange.pipe(takeUntil(this.unsubChildren$)).subscribe(nodeState => {
3,783✔
517
                this.navService.update_visible_cache(node, nodeState);
60✔
518
            });
519
            node.closeAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => {
3,783✔
520
                const targetElement = this.navService.focusedNode?.header.nativeElement;
2✔
521
                this.scrollNodeIntoView(targetElement);
2✔
522
            });
523
            node.openAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => {
3,783✔
524
                const targetElement = this.navService.focusedNode?.header.nativeElement;
11✔
525
                this.scrollNodeIntoView(targetElement);
11✔
526
            });
527
        });
528
        this.navService.init_invisible_cache();
138✔
529
    }
530

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

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

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

© 2025 Coveralls, Inc