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

IgniteUI / igniteui-angular / 23354069329

20 Mar 2026 05:11PM UTC coverage: 9.846% (-79.6%) from 89.462%
23354069329

Pull #17071

github

web-flow
Merge 742ad5a5c into 0018afe92
Pull Request #17071: fix(IgxGrid): Do not apply width constraint to groups.

905 of 16404 branches covered (5.52%)

Branch coverage included in aggregate %.

1 of 3 new or added lines in 1 file covered. (33.33%)

24265 existing lines in 328 files now uncovered.

3714 of 30509 relevant lines covered (12.17%)

6.21 hits per line

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

25.98
/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';
3✔
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() {
UNCOV
110
        return this._selection;
×
111
    }
112

113
    public set selection(selectionMode: IgxTreeSelectionType) {
UNCOV
114
        this._selection = selectionMode;
×
UNCOV
115
        this.selectionService.clearNodesSelection();
×
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;
3✔
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;
3✔
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 = {
3✔
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>();
3✔
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>();
3✔
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>();
3✔
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>();
3✔
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>();
3✔
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>>();
3✔
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>>();
3✔
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>[] {
UNCOV
305
        return this.nodes?.filter(node => node.level === 0);
×
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>>();
3✔
314

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

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

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

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

337
    /** @hidden @internal */
338
    public get nativeElement() {
UNCOV
339
        return this.element.nativeElement;
×
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>[]) {
UNCOV
354
        nodes = nodes || this.nodes.toArray();
×
UNCOV
355
        nodes.forEach(e => e.expanded = true);
×
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>[]) {
UNCOV
370
        nodes = nodes || this.nodes.toArray();
×
UNCOV
371
        nodes.forEach(e => e.expanded = false);
×
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>[]) {
UNCOV
388
        this.selectionService.deselectNodesWithNoEvent(nodes);
×
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 {
UNCOV
428
        const compareFunc = comparer || this._comparer;
×
UNCOV
429
        const results = this.nodes.filter(node => compareFunc(searchTerm, node));
×
UNCOV
430
        return results?.length === 0 ? null : results;
×
431
    }
432

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

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

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

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

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

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

502
    private subToChanges() {
UNCOV
503
        this.unsubChildren$.next();
×
UNCOV
504
        const toBeSelected = [...this.forceSelect];
×
UNCOV
505
        if(this.platform.isBrowser) {
×
UNCOV
506
            requestAnimationFrame(() => {
×
UNCOV
507
                this.selectionService.selectNodesWithNoEvent(toBeSelected);
×
508
            });
509
        }
UNCOV
510
        this.forceSelect = [];
×
UNCOV
511
        this.nodes.forEach(node => {
×
UNCOV
512
            node.expandedChange.pipe(takeUntil(this.unsubChildren$)).subscribe(nodeState => {
×
UNCOV
513
                this.navService.update_visible_cache(node, nodeState);
×
514
            });
UNCOV
515
            node.closeAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => {
×
UNCOV
516
                const targetElement = this.navService.focusedNode?.header.nativeElement;
×
UNCOV
517
                this.scrollNodeIntoView(targetElement);
×
518
            });
UNCOV
519
            node.openAnimationDone.pipe(takeUntil(this.unsubChildren$)).subscribe(() => {
×
UNCOV
520
                const targetElement = this.navService.focusedNode?.header.nativeElement;
×
UNCOV
521
                this.scrollNodeIntoView(targetElement);
×
522
            });
523
        });
UNCOV
524
        this.navService.init_invisible_cache();
×
525
    }
526

527
    private scrollNodeIntoView(el: HTMLElement) {
UNCOV
528
        if (!el) {
×
UNCOV
529
            return;
×
530
        }
UNCOV
531
        const nodeRect = el.getBoundingClientRect();
×
UNCOV
532
        const treeRect = this.nativeElement.getBoundingClientRect();
×
UNCOV
533
        const topOffset = treeRect.top > nodeRect.top ? nodeRect.top - treeRect.top : 0;
×
UNCOV
534
        const bottomOffset = treeRect.bottom < nodeRect.bottom ? nodeRect.bottom - treeRect.bottom : 0;
×
UNCOV
535
        const shouldScroll = !!topOffset || !!bottomOffset;
×
UNCOV
536
        if (shouldScroll && this.nativeElement.scrollHeight > this.nativeElement.clientHeight) {
×
537
            // this.nativeElement.scrollTop = nodeRect.y - treeRect.y - nodeRect.height;
UNCOV
538
            this.nativeElement.scrollTop =
×
539
                this.nativeElement.scrollTop + bottomOffset + topOffset + (topOffset ? -1 : +1) * nodeRect.height;
×
540
        }
541
    }
542

543
    private _comparer = <T>(data: T, node: IgxTreeNodeComponent<T>) => node.data === data;
3✔
544

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