skip to Main Content

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

tree-view component

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


  1. The easiest solution would be to just add a new property to all children (selectable) elements, something like active and set it initially to false, then whenever an element is clicked: set it to false for all and to true for the selected element.

    It means you’d need to add it to your TreeNode model:

    active?: boolean;
    

    (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 the selectedItem, but on their own active 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 the active properties to false:

    setAllToFalse(nodes) {
       for (let child of nodes) {
         child.active = false;
         if (child.children && Array.isArray(child.children) && child.children.length > 0) this.setAllToFalse(child.children);
       }
    }
    

    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:

    ngOnInit() {
       this.setAllToFalse(this.tree);
    }
    

    Change your applyStyle method and call setAllToFalse to set all the values to false, and after that you set this particular node’s active to true:

    applyStyle(node) {
      this.selectedItem = node.code;
      this.setAllToFalse(this.tree);
      node.active = true;
    }
    

    Finally, since you’re also using recursion in template, you’ll need to edit your emitOnChildClicked method too, since the tree in component’s context can mean any sub-tree inside the ‘original’ tree:

    emitOnChildClicked(node) {
      this.selectedValue.emit(node);
      this.setAllToFalse(this.tree);
      node.active = true;
    }
    

    Working Stackblitz example.

    Login or Signup to reply.
  2. The problem is that in your code you have multiple instances of the app-tree component, and each of these has a selectedItem. 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:

    import { BehaviorSubject } from "rxjs";
    
    export class TreeService {
      selectedNodeCode$ = new BehaviorSubject<string>('');
    }
    

    Then you adjust your app-tree component to no longer have the current output or the variable selectedItem, and replace these with

    selectedNodeCode$ = this.treeServer.selectedNodeCode$
    

    and on your app-tree template you change the node class logic to:

    <span
     class="node-item"
     (click)="applyStyle(node)"
     [ngClass]="{
       'node-active': node.code == selectedNodeCode$ | async
     }"
    >{{ node.name }}</span>
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search