I am creating this recursive nested tree view component in Angular. Goal is that whenever a node is selected, styling should be applied to that node with .node-active class and when other node is selected the previous styling applied should be removed and should be added to current selected node. Its working fine for one sub-tree but when I select other node from other sub-tree the styling gets applied to other sub-tree also. Here is the stackblitz link – Stackblitz
I am applying a class using applyStyle function based on the current node code and selected node code, which works for one sub-tree but breaks for another. Any help would be really helpful.
tree.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-tree',
templateUrl: './tree.component.html',
styleUrls: ['./tree.component.css'],
})
export class TreeComponent {
@Input() tree: any;
@Output() selectedValue = new EventEmitter<any>();
public selectedItem: any;
toggleChild(node) {
this.selectedValue.emit(node);
node.showChildren = !node.showChildren;
node.isOpen = !node.isOpen;
}
/* Events are not bubbled up so emitting the parent event on <app-tree>
* when one of the child emits an event - this will create a new EventEmitter per child.
*/
emitOnChildClicked(node) {
this.selectedValue.emit(node);
}
applyStyle(node) {
console.log(node);
this.selectedItem = node.code;
}
}
tree.component.css
.open-btn {
transform: rotate(90deg);
position: absolute;
left: -30px;
z-index: 10;
}
.close-btn {
left: -30px;
position: absolute;
z-index: 10;
}
.tree li {
list-style-type: none;
margin: 8px;
position: relative;
}
.node-item {
color: var(--lbl-color);
padding: 3px;
}
.green {
background-color: darkred;
}
.node-active {
pointer-events: none;
background: #379df1;
border-radius: 5px;
color: white;
}
.node-item:hover {
background-color: rgb(231, 231, 231);
color: #0f0f0f;
border-radius: 5px;
}
.arrow-btn {
width: 18px;
height: 18px;
}
.tree li::before {
content: '';
position: absolute;
top: -7px;
left: -20px;
border-left: 1px solid #ccc;
border-bottom: 1px solid #ccc;
border-radius: 0 0 0 0px;
width: 20px;
height: 15px;
}
.tree li::after {
position: absolute;
content: '';
top: 8px;
left: -20px;
border-left: 1px solid #ccc;
border-top: 1px solid #ccc;
border-radius: 0px 0 0 0;
width: 20px;
height: 100%;
}
.tree li:last-child::after {
display: none;
}
.tree li:last-child:before {
border-radius: 0 0 0 5px;
}
ul.tree > li:first-child::before {
border-left: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
.label-container {
display: inline-block;
}
tree.component.html
<ul *ngIf="tree" class="tree">
<li *ngFor="let node of tree; let i = index">
<div class="label-container" (click)="toggleChild(node)">
<span *ngIf="node.children != 0">
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/9d/Icons8_flat_document.svg/512px-Icons8_flat_document.svg.png"
class="arrow-btn"
[ngClass]="node.isOpen ? 'open-btn' : 'close-btn'"
/>
</span>
<span
class="node-item"
(click)="applyStyle(node)"
[ngClass]="{ 'node-active': selectedItem == node.code }"
>
{{ node.name }}
</span>
</div>
<app-tree
*ngIf="node.showChildren"
(selectedValue)="emitOnChildClicked($event)"
[tree]="node.children"
></app-tree>
</li>
</ul>
tree.mock.ts
export interface TreeNode {
name: string;
showChildren: boolean;
children: any[];
code: string;
}
export const NODES: TreeNode[] = [
{
code: 'AFRI',
name: 'Africa',
showChildren: false,
children: [
{
code: 'ALGE',
name: 'Algeria',
showChildren: false,
children: [
{
code: 'ARIS',
name: 'Algeris',
showChildren: false,
children: [],
},
{
code: 'ACI2',
name: 'Algeria child 2',
showChildren: false,
children: [],
},
],
},
{
code: 'ANGO',
name: 'Angola',
showChildren: false,
children: [],
},
{
code: 'BENI',
name: 'Benin',
showChildren: false,
children: [],
},
],
},
{
code: 'ASIA',
name: 'Asia',
showChildren: false,
children: [
{
code: 'AFGH',
name: 'Afghanistan',
showChildren: false,
children: [
{
code: 'KABU',
name: 'Kabul',
showChildren: false,
children: [],
},
],
},
{
code: 'ARME',
name: 'Armenia',
showChildren: false,
children: [],
},
{
code: 'AZER',
name: 'Azerbaijan',
showChildren: false,
children: [],
},
],
},
{
code: 'EURO',
name: 'Europe',
showChildren: false,
children: [
{
code: 'ROMA',
name: 'Romania',
showChildren: false,
children: [
{
code: 'BUCU',
name: 'Bucuresti',
showChildren: false,
children: [],
},
],
},
{
code: 'HUNG',
name: 'Hungary',
showChildren: false,
children: [],
},
{
code: 'BNIN',
name: 'Benin',
showChildren: false,
children: [],
},
],
},
{
code: 'NOAM',
name: 'North America',
showChildren: false,
children: [],
},
];
2
Answers
The easiest solution would be to just add a new property to all children (selectable) elements, something like
active
and set it initially tofalse
, then whenever an element is clicked: set it tofalse
for all and totrue
for the selected element.It means you’d need to add it to your
TreeNode
model:(added here as optional, since in the code that follows you’re not going to add the values manually to all the nodes, but programmatically).
Then, the template part of the Tree component needs only one simple change:
from
[ngClass]="{ 'node-active': selectedItem == node.code }"
to[ngClass]="node.active ? 'node-active' : ''"
on line 14. So the css style of the children won’t depend on theselectedItem
, but on their ownactive
property value.Finnally, most changes are needed in ts/logic part of the component:
Create a method to recursively iterate over your
tree
and set all theactive
properties tofalse
:Add ngOnInit to the component and inside it call this recursive method to have all the nodes of that particular tree with
active
set to false initially:Change your
applyStyle
method and callsetAllToFalse
to set all the values tofalse
, and after that you set this particular node’sactive
totrue
:Finally, since you’re also using recursion in template, you’ll need to edit your
emitOnChildClicked
method too, since thetree
in component’s context can mean any sub-tree inside the ‘original’ tree:Working Stackblitz example.
The problem is that in your code you have multiple instances of the
app-tree
component, and each of these has aselectedItem
. That is the reason multiple nodes get selected.The best way to implement communication between components is to create a service.
I would create a little service to hold the selected node code for all your
app-tree
components.something like this should do:
Then you adjust your
app-tree
component to no longer have the current output or the variableselectedItem
, and replace these withand on your
app-tree
template you change the node class logic to: