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

IgniteUI / igniteui-angular / 13331632524

14 Feb 2025 02:51PM CUT coverage: 22.015% (-69.6%) from 91.622%
13331632524

Pull #15372

github

web-flow
Merge d52d57714 into bcb78ae0a
Pull Request #15372: chore(*): test ci passing

1990 of 15592 branches covered (12.76%)

431 of 964 new or added lines in 18 files covered. (44.71%)

19956 existing lines in 307 files now uncovered.

6452 of 29307 relevant lines covered (22.02%)

249.17 hits per line

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

33.33
/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 { 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 {
2✔
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 {
2✔
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 {
2✔
97

98
    @HostBinding('class.igx-tree')
99
    public cssClass = 'igx-tree';
2✔
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;
2✔
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;
2✔
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 = {
2✔
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>();
2✔
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>();
2✔
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>();
2✔
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>();
2✔
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>();
2✔
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>>();
2✔
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>>();
2✔
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>>();
2✔
314

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

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

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

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

336
    /** @hidden @internal */
337
    public get nativeElement() {
UNCOV
338
        return this.element.nativeElement;
×
339
    }
340

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

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

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

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

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

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

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

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

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

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

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

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

540
    private _comparer = <T>(data: T, node: IgxTreeNodeComponent<T>) => node.data === data;
2✔
541

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