<!DOCTYPE html>
<html>

  <head>
    <base href="." />
    <title>Angular Demo</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" />
    <script src="https://unpkg.com/core-js@^2.4.1/client/shim.js"></script>
    <script src="https://unpkg.com/zone.js@0.8.10/dist/zone.js"></script>
    <script src="https://unpkg.com/zone.js@0.8.10/dist/long-stack-trace-zone.js"></script>
    <script src="https://unpkg.com/reflect-metadata@^0.1.8/Reflect.js"></script>
    <script src="https://unpkg.com/systemjs@^0.19.40/dist/system.js"></script>
    <script src="config.js"></script>
    <script>
    System.import('app').catch(console.error.bind(console));
    </script>
  </head>

  <body>
    <div class="container">
      <div class="row">
        <div class="col-12">
          <app-root>Loading...</app-root>      
        </div>
      </div>
    </div>
  </body>
</html>
var ver = {
    ng: '5.2.6'
  };
  
  System.config({
  //use typescript for compilation
  transpiler: 'typescript',
  //typescript compiler options
  typescriptOptions: {
    emitDecoratorMetadata: true
  },
  meta: {
    'typescript': {
      "exports": "ts"
    }
  },  
  paths: {
    'npm:': 'https://unpkg.com/'
  },
  map: {

    'app': './src',

    '@angular/core': 'npm:@angular/core@' + ver.ng + '/bundles/core.umd.js',
    '@angular/common': 'npm:@angular/common@' + ver.ng + '/bundles/common.umd.js',
    '@angular/common/http': 'npm:@angular/common@' + ver.ng + '/bundles/common-http.umd.js',
    '@angular/compiler': 'npm:@angular/compiler@' + ver.ng + '/bundles/compiler.umd.js',
    '@angular/platform-browser': 'npm:@angular/platform-browser@' + ver.ng + '/bundles/platform-browser.umd.js',
    '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic@' + ver.ng + '/bundles/platform-browser-dynamic.umd.js',
    '@angular/http': 'npm:@angular/http@' + ver.ng + '/bundles/http.umd.js',
    '@angular/router': 'npm:@angular/router@' + ver.ng + '/bundles/router.umd.js',
    '@angular/forms': 'npm:@angular/forms@' + ver.ng + '/bundles/forms.umd.js',

    'rxjs': 'npm:rxjs@^5.5.2',
    'rxjs/operators': 'npm:rxjs@^5.5.2/operators/index.js',
    'tslib': 'npm:tslib/tslib.js',
    'typescript': 'npm:typescript@2.4.2/lib/typescript.js',

    '@ng-bootstrap/ng-bootstrap': 'npm:@ng-bootstrap/ng-bootstrap@1.0.0/bundles/ng-bootstrap.js'
  },
  packages: {
    app: {
      main: './main.ts',
      defaultExtension: 'ts'
    },
    rxjs: {
      defaultExtension: 'js'
    }
  }
});
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app/app.module.ts';

platformBrowserDynamic().bootstrapModule(AppModule);
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { FormsModule } from '@angular/forms';

import { NgbModule } from '@ng-bootstrap/ng-bootstrap';

import { AppComponent } from './app.component';

import { TreeViewComponent } from './tree-view.component';

@NgModule({
  declarations: [
    AppComponent,
    TreeViewComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    NgbModule.forRoot()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
      <app-tree-view [data]="treeData" [collapseAll]="collapseAll" [selectAll]="selectAll" (onClick)="click($event)" (onChange)="onChange($event)">
      </app-tree-view>
      <div class="btn-toolbar" role="toolbar" aria-label="Toolbar with button groups">
        <div class="btn-group mr-2" role="group" aria-label="First group">
            <button type="button" class="btn btn-sm btn-secondary" (click)="collapseAll = true">Collapse All</button>
            <button type="button" class="btn btn-sm btn-secondary" (click)="collapseAll = false">Expand All</button>
        </div>
        <div class="btn-group" role="group" aria-label="Second group">
            <button type="button" class="btn btn-sm btn-secondary" (click)="selectAll = true">Check All</button>
            <button type="button" class="btn btn-sm btn-secondary" (click)="selectAll = false">Uncheck All</button>
        </div>
      </div>  
      
  `,
  styles: [`
  `]
})
export class AppComponent {
  
  public treeData = [
    {'ID': 1, 'NAME': 'ROOT_1'}, {'ID': 2, 'NAME': 'ROOT_2'},
    {'ID': 3, 'NAME': 'ROOT_3', 'PARENT_ID': 2},
    {'ID': 4, 'NAME': 'ROOT_4', 'PARENT_ID': 3},
    {'ID': 5, 'NAME': 'ROOT_5', 'PARENT_ID': 4},
    {'ID': 6, 'NAME': 'ROOT_6', 'PARENT_ID': 7}, 
    {'ID': 7, 'NAME': 'ROOT_7'},
    {'ID': 8, 'NAME': 'ROOT_8', 'PARENT_ID': 7},
    {'ID': 9, 'NAME': 'ROOT_9', 'PARENT_ID': 7},
    {'ID': 10, 'NAME': 'ROOT_10', 'PARENT_ID': 7},
    {'ID': 11, 'NAME': 'ROOT_11', 'PARENT_ID': 7},
    {'ID': 12, 'NAME': 'ROOT_12', 'PARENT_ID': 7},
    {'ID': 13, 'NAME': 'ROOT_13'},
    {'ID': 14, 'NAME': 'ROOT_14'},
    {'ID': 15, 'NAME': 'ROOT_15', 'PARENT_ID': 4},
    {'ID': 16, 'NAME': 'ROOT_16', 'PARENT_ID': 4},
    {'ID': 17, 'NAME': 'ROOT_17', 'PARENT_ID': 16},
    {'ID': 18, 'NAME': 'ROOT_18', 'PARENT_ID': 16},
    {'ID': 19, 'NAME': 'ROOT_19', 'PARENT_ID': 18},
    {'ID': 20, 'NAME': 'ROOT_20', 'PARENT_ID': 2},
    {'ID': 21, 'NAME': 'ROOT_21', 'PARENT_ID': 2},
    {'ID': 22, 'NAME': 'ROOT_22', 'PARENT_ID': 3},
    {'ID': 23, 'NAME': 'ROOT_23', 'PARENT_ID': 3},
    {'ID': 24, 'NAME': 'ROOT_24', 'PARENT_ID': 3},
    {'ID': 25, 'NAME': 'ROOT_25', 'PARENT_ID': 4},
    {'ID': 26, 'NAME': 'ROOT_26', 'PARENT_ID': 18}
  ];
  public collapseAll: boolean;
  public selectAll: boolean;
  
  click(node: any){
    console.log(node);
  }

  onChange(data){
    console.log(data);
  }
}
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';

/**
 * The NgbTreeview a simple way to create tree view in html.
 */
@Component({
  selector: 'app-tree-view',
  template: `
    <ul class="treeview" *ngIf="nodes.length">
      <li *ngFor="let node of nodes">

          <span *ngIf="node[childrenAttr].length" [ngClass]="{'node-opened': !node[collapseAttr]}">></span>
          <span *ngIf="!node[childrenAttr].length">&#9679;</span>
          
          <input type="checkbox" 
            [(ngModel)]="node[selectAttr]" 
            [indeterminate]="node[inDeterminateAttr]" 
            (change)="onModelChange(node)" />

          <span [ngClass]="{'parent': node[childrenAttr].length}"
              (click)="click(node)">
            {{node.NAME}}
          </span>
          
          <app-tree-view *ngIf="node[childrenAttr].length"  
              [data]="node[childrenAttr]" 
              [prepareData]="false" 
              [ngbCollapse]="node[collapseAttr]"
              (onChange)="change($event)"
              >
         </app-tree-view> 

      </li>
    </ul>
  `,
  styles: [`
    .treeview {
      list-style-type: none;
    }
    .treeview .parent {
      font-weight: bold;
      cursor: pointer;
    }
    .treeview span {
      display: inline-block;
    }
    .treeview .node-opened {
      transform: rotate(90deg);
    }
`]
})
export class TreeViewComponent implements OnInit {
  private _collapseAll: boolean;
  private _selectAll: boolean;

  public nodes: any[] = [];
  public collapseAttr: string = 'isCollapsed';
  public selectAttr: string = 'isSelected';
  public inDeterminateAttr: string = 'isIndeterminate';

  /**
   * Providen data for tree.
   */
  @Input('data') data: any[];

  /**
   * A flag indicating data is flatten in array and prepare is required.(Default
   * is true).
   */
  @Input('prepareData') prepareData: boolean = true;

  /**
   * Name of ID property in input data.
   */
  @Input('idAttr') idAttr: string = 'ID';

  /**
   * Name of parent property in input data.
   */
  @Input('parentAttr') parentAttr: string = 'PARENT_ID';


  /**
   * Name of children list property in input data.
   */
  @Input('childrenAttr') childrenAttr: string = 'CHILDREN';


  /**
   * Collapse or expand all parent nodes.
   */
  @Input('collapseAll')
  set collapseAll(value: boolean) {
    this._collapseAll = value;
    this._recursiveEdit(
        this.nodes, this.childrenAttr, this.collapseAttr, value);
  }

  /**
   * Select or deselect all nodes.
   */
  @Input('selectAll')
  set selectAll(value: boolean) {
    this._selectAll = value;
    this._recursiveEdit(this.nodes, this.childrenAttr, this.selectAttr, value);
    this._recursiveEdit(
        this.nodes, this.childrenAttr, this.inDeterminateAttr, false);
  }

  /**
   * When change a node model this event will be emit.
   */
  @Output() onChange = new EventEmitter<any>();

  /**
   * On click node.
   */
  @Output() onClick = new EventEmitter<any>();

  constructor() {}

  ngOnInit() {
    // Clone input data for protect.
    const cloned = this.data.map(x => Object.assign([], x));

    // If data is flat, prepare data with recursive function.
    this.nodes = this.prepareData ? this._getPreparedData(cloned) : this.data;
  }

  onModelChange(node) {
    if (node[this.childrenAttr].length) {
      this._recursiveEdit(
          [node], this.childrenAttr, this.selectAttr, node[this.selectAttr]);
    }
    this.onChange.emit(node);
  }

  click(node: any) {
    if (node[this.childrenAttr].length) {
      node[this.collapseAttr] = !node[this.collapseAttr]
    }
    this.onClick.emit(node);
  }

  change(value: any) {
    const parent = this.nodes.filter(
        (item) => {return item.ID === value[this.parentAttr]})[0];
    if (parent) {
      let hasDifferent = false, duplicate = {},
          isIndeterminate = value[this.inDeterminateAttr] || false;

      parent[this.childrenAttr].forEach((item) => {
        duplicate[item[this.selectAttr]] =
            (duplicate[item[this.selectAttr]] || 0) + 1;
        if (item[this.inDeterminateAttr]) {
          isIndeterminate = true;
        }
      });
      if (Object.keys(duplicate).length === 1 && !isIndeterminate) {
        parent[this.inDeterminateAttr] = false;
        parent[this.selectAttr] = JSON.parse(Object.keys(duplicate)[0]);
        this.onChange.emit(parent);
      } else {
        parent[this.inDeterminateAttr] = true;
        this.onChange.emit(parent);
      }
    }
  }

  private _recursiveEdit(list, childrenAttr, attr, value) {
    if (Array.isArray(list)) {
      for (let i = 0, len = list.length; i < len; i++) {
        list[i][attr] = value;
        if (list[i][childrenAttr].length) {
          this._recursiveEdit(list[i][childrenAttr], childrenAttr, attr, value);
        }
      }
    }
  }

  private _getPreparedData(list) {
    let tree = [], lookup = {};
    for (let i = 0, len = list.length; i < len; i++) {
      lookup[list[i][this.idAttr]] = list[i];
      list[i][this.childrenAttr] = [];
      list[i][this.collapseAttr] = true;
      list[i][this.selectAttr] = false;
      list[i][this.inDeterminateAttr] = false;
    }
    for (let i = 0, len = list.length; i < len; i++) {
      if (list[i][this.parentAttr]) {
        lookup[list[i][this.parentAttr]][this.childrenAttr].push(list[i]);
      } else {
        tree.push(list[i]);
      }
    }
    return tree;
  };
}