Building a Tree View in Aurelia

For steps on setting up a new Aurelia project please refer to this link:

https://www.technical-recipes.com/2019/getting-started-with-aurelia-js-part-2/

(For reference I am using the TypeScript option when creating this new Aurelia project and building / running it in Visual Studio Code.)

Some other useful links on this topic can be found here:

https://www.syntaxsuccess.com/viewarticle/building-a-treeview-in-aurelia

https://gist.run/?id=53dc5b81ef1549cdcc56c3b1fa1e67ba (GistRun link)

Anyway the following are the files I added to Visual Studio Code project:

app.html

<template>  
  <require from="styles.css"></require>
  <require from="tree-view"></require>
<tree-view></tree-view>
</template>

app.ts

export class App {}

node-model.ts

export class NodeModel {
  public id: string;
  public icon: string;
  public children: NodeModel[];
  public expanded: boolean;
  public visible: boolean;

  constructor(idVal: string, children: NodeModel[]) {
    this.id = idVal;
    this.visible = true;
    this.children = children || [];

    if (this.hasChildren()) {
      this.icon = "fa fa-minus";
      this.expanded = true;
    }
  }

  hasChildren() {
    return this.children.length > 0;
  }

  toggleNode() {
    for (var i = 0; i < this.children.length; i++) {
      this.children[i].visible = !this.children[i].visible;
      if (this.expanded) {
        this.children[i].toggleNode();
      }
    }
    this.expanded = !this.expanded;
    if (this.expanded === true) {
      this.icon = "fa fa-minus";
    } else {
      this.icon = "fa fa-plus";
    }
  }
}

styles.css

ul{
    list-style-type:none;
    margin:0;
  }
  tree-node  ul:first-child{
    padding-left:0;
  }
  
  .fa.fa-plus{
    width: 16px;
  height: 16px;
  background-repeat: no-repeat;
  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTJDBGvsAAAAmUlEQVQ4T6WTuw2AMBBDXbEDo7AQQ9AgISRYgRUYhIqK3wjsQHWEE5wOiJDCFa+xbEtxEhDRDVRjg7yNHPhC/DrMQr0QqqlD1scOCEEFXDKuKIbEASa4gEvmDeWUOn4WSNF7F/HrMAu+goPHLuLXYRZ84Qu1i/h1mAVfUHPuIn4dZsEX0pgKTEcwjfj7Gk0PyfSUTZ8p6DsTdnMcPQl3gNV4AAAAAElFTkSuQmCC);
  
  display:inline-block;
  }
  
  .fa.fa-minus{
    width: 16px;
  height: 16px;
  background-repeat: no-repeat;
  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTMyIDc5LjE1OTI4NCwgMjAxNi8wNC8xOS0xMzoxMzo0MCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUuNSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6M0I4RDFEMDE5MDc4MTFFNjhBRkU4RjUxMTMwMTFBNDciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6M0I4RDFEMDI5MDc4MTFFNjhBRkU4RjUxMTMwMTFBNDciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDozQjhEMUNGRjkwNzgxMUU2OEFGRThGNTExMzAxMUE0NyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDozQjhEMUQwMDkwNzgxMUU2OEFGRThGNTExMzAxMUE0NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtWsFOoAAACvSURBVHjaYvz//z8DJYBx6BvAwthxZSYDI3MaWbr//53FxPDjei7D/3/HSdcM1APUywRk/mL4/CMYaNpzEmx+DtYD1MsEFXrO8Ps/0JD/vwhrBqoBqQXpAQImJKnjDH//5xI0AKIG7mUmNOlZoIDBF2hgNcjRyFC/Cl0ZGwOH1gEGRiZLzEC75gAOMyTAhMUezEBFCjR0xUw4HIsIVLRAw0hIeIILOVCPD968ABBgAN4Tav6F/rCJAAAAAElFTkSuQmCC);
  display:inline-block;
  }
  
  .StartOfTreeNode > tree-node > .StartOfTreeNode{
    padding-left:20px;
  }
  .mainBack{
    background:#F7F7F7;
  }
  
  
  .left{float:left;}
  .right{float:right;}
  .clear{clear:both;}
  .WorkQueueDropContent{
      color:#343434;
  }
  .EachMiniQueueWithOutCollapse{
      padding:15px 10px;
      border-bottom:1px solid #F1F1F1;
  }
  .eachMinQueueCheckSection{
      margin-right:10px;
  }
  .leftGaptoRightElement{
      margin-left:10px;
  }
  .lightFontForMInQueue{
      color:#A8A8A8; 
  }
  .DateOfMinQueue{
      font-weight:bold;
      color:#0E7EC6;
  }

tree-node.html

<template>   
    <div show.bind="current.visible" class="StartOfTreeNode">       
        <div class="EachMiniQueueWithOutCollapse">
            <span if.bind="current.hasChildren()" 
                click.trigger="current.toggleNode(current)" 
                class="${current.icon} left eachMinQueueCheckSection">
            </span>
            <div class="left eachMinQueueCheckSection" if.bind="!current.hasChildren()">
                <label class="cb_control control--checkbox">
                    <input type="checkbox">
                    <span class="control__indicator"></span>
                </label>
            </div>
            <div class="left">
                <div class="TitleOfMinQueue">
                    <span class="fontBold"><b>${current.id}</b></span>                           
                </div>                       
            </div>                   
        </div>
        
        <tree-node 
            repeat.for="node of current.children" 
            current.bind="node">
        </tree-node>            
    </div>  
</template>

tree-node.ts

import { bindable } from "aurelia-framework";
import { NodeModel } from "node-model";

export class TreeNode {
  @bindable current: NodeModel;
}

tree-view.html

<template>    
    <require from='./tree-node'></require>       
    <tree-node repeat.for="node of nodes" current.bind="node"></tree-node>    
</template>

tree-view.ts

import { NodeModel } from "node-model";

export class TreeView {
  public nodes: NodeModel[] = [];

  constructor() {
    var node1 = new NodeModel("Wiltshire", [
      new NodeModel("Chippenham", null),
      new NodeModel("Devizes", null),
      new NodeModel("Bradford-on-Avon", null)
    ]);

    var node3 = new NodeModel("Somerset", [
      new NodeModel("Yeovil", null),
      new NodeModel("Taunton", [
        new NodeModel("Hestercombe House", null),
        new NodeModel("Vivary Park", null)
      ])
    ]);

    this.nodes = [node1, node3];
  }
}

Giving the following output: