File

src/app/actions/ack-tree/ack-tree.component.ts

Description

Tree with checkboxes for a list of alarms

Based on the angular material documentation for the tree component

Implements

OnInit OnChanges

Metadata

selector app-ack-tree
styleUrls ack-tree.component.scss
templateUrl ack-tree.component.html

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor(alarmService: AlarmService)

Instantiates the component

Parameters :
Name Type Optional Description
alarmService AlarmService No

Service used to send the request to acknowledge the alarm

Inputs

selectedAlarm
Type : Alarm

The parent Alarm of the tree

Outputs

alarmsToAckFromSelection
Type : EventEmitter

EventEmitter used to send the selected alarms to the parent component

Methods

Private _getSubTree
_getSubTree(alarm: Alarm)

Auxiliary function used to get the tree data from a given alarm

Parameters :
Name Type Optional Description
alarm Alarm No

the {

Returns : any

the tree data in a JSON format

alarmItemSelectionToggle
alarmItemSelectionToggle(node: AlarmItemFlatNode)

Toggle the alarm item selection. Select/deselect all the descendants node

Parameters :
Name Type Optional Description
node AlarmItemFlatNode No

the node

Returns : void
buildFileTree
buildFileTree(value: any, level: number)

Build the file structure tree. The value is the Json object, or a sub-tree of a Json object.

Parameters :
Name Type Optional Description
value any No

the node as a Json object, or a sub-tree of a Json object.

level number No

the level of the node

Returns : AlarmItemNode[]

the list of AlarmItemNode.

descendantsAllSelected
descendantsAllSelected(node: AlarmItemFlatNode)

Checks whether all the descendants of the node are selected

Parameters :
Name Type Optional Description
node AlarmItemFlatNode No

the node

Returns : boolean

true if all the descendants of the node are selected, false if not

descendantsPartiallySelected
descendantsPartiallySelected(node: AlarmItemFlatNode)

Checks whether part of the descendants are selected

Parameters :
Name Type Optional Description
node AlarmItemFlatNode No

the node

Returns : boolean

true if some of the descendents are selected, false if not

getTreeData
getTreeData()

Get tree data from selected alarm

Returns : any

the tree data in a JSON format

ngOnChanges
ngOnChanges(changes: SimpleChanges)

This function is executed on Component startup and everytime its state changes. It currently builds the tree by reading the data from the alarm (whenever the alarm changes)

Parameters :
Name Type Optional
changes SimpleChanges No
Returns : void
ngOnInit
ngOnInit()

This function is defined by default and executed on Component startup.

Returns : void
noSelectedDescendants
noSelectedDescendants(node: AlarmItemFlatNode)

Checks wether or not the node has selected dependencies

Parameters :
Name Type Optional Description
node AlarmItemFlatNode No

the node

Returns : boolean

true if the node has no selected descendents, false if not

updateAckList
updateAckList()

Update list with ids to ack *

Returns : void
updateData
updateData()

Update the data for the dataSource

Returns : void

Properties

ackList
Type : string[]
Default value : []

List with ids to ack *

checklistSelection
Default value : new SelectionModel<AlarmItemFlatNode>(true /* multiple */)

The selection for checklist

dataSource
Type : MatTreeFlatDataSource<AlarmItemNode | AlarmItemFlatNode>

Angular Material Data source for the flat tree

flatNodeMap
Type : Map<AlarmItemFlatNode | AlarmItemNode>
Default value : new Map<AlarmItemFlatNode, AlarmItemNode>()

Map from flat node to nested node. This helps us finding a nested node to be modified

getChildren
Default value : () => {...}

Retuns the children of the node, as an Observable

Parameters :
Name Description
node

the node

getLevel
Default value : () => {...}

Retuns the level of a given node

Parameters :
Name Description
node

the node

hasChild
Default value : () => {...}

Checks wether or not the node has children

Parameters :
Name Description
node

the node

hasNoContent
Default value : () => {...}

Checks wether or not the node has no content

Parameters :
Name Description
node

the node

isExpandable
Default value : () => {...}

Checks wether or not the node is expandable

Parameters :
Name Description
node

the node

nestedNodeMap
Type : Map<AlarmItemNode | AlarmItemFlatNode>
Default value : new Map<AlarmItemNode, AlarmItemFlatNode>()

Map from nested node to flattened node. This helps us to keep the same object for selection

transformer
Default value : () => {...}

Transformer to convert nested node to flat node. Record the nodes in maps for later use.

Parameters :
Name Description
node

the node

level

the level node

treeControl
Type : FlatTreeControl<AlarmItemFlatNode>

Angular Material Flat tree control. Able to expand/collapse a subtree recursively for flattened tree.

treeFlattener
Type : MatTreeFlattener<AlarmItemNode | AlarmItemFlatNode>

Angular Material Tree flattener to convert a normal type of node to node with children & level information

import { Component, EventEmitter, Input, Output, SimpleChanges, SimpleChange } from '@angular/core';
import { OnInit, OnChanges } from '@angular/core';
import { SelectionModel } from '@angular/cdk/collections';
import { FlatTreeControl } from '@angular/cdk/tree';
import { MatTreeFlattener, MatTreeFlatDataSource } from '@angular/material/tree';
import { of as ofObservable, Observable } from 'rxjs';
import { AlarmService } from '../../data/alarm.service';
import { Alarm } from '../../data/alarm';

/**
 * Node for an alarm item
 */
export class AlarmItemNode {

  /** List of children nodes */
  children: AlarmItemNode[];

  /** Name of the node */
  item: string;
}

/** Flat to-do item node with expandable and level information */
export class AlarmItemFlatNode {

  /** Name of the node */
  item: string;

  /** Level of the node */
  level: number;

  /** True if the node is expandable, false if not */
  expandable: boolean;
}

/**
* Tree with checkboxes for a list of alarms
*
* Based on the angular material documentation for the tree component
*
*/
@Component({
  selector: 'app-ack-tree',
  templateUrl: 'ack-tree.component.html',
  styleUrls: ['ack-tree.component.scss']
})
export class AckTreeComponent implements OnInit, OnChanges {

  /** The parent Alarm of the tree  */
  @Input() selectedAlarm: Alarm;

  /** EventEmitter used to send the selected alarms to the parent component */
  @Output() alarmsToAckFromSelection = new EventEmitter();

  /** List with ids to ack **/
  ackList: string[] = [];

  /** Map from flat node to nested node. This helps us finding a nested node to be modified */
  flatNodeMap: Map<AlarmItemFlatNode, AlarmItemNode> = new Map<AlarmItemFlatNode, AlarmItemNode>();

  /** Map from nested node to flattened node. This helps us to keep the same object for selection */
  nestedNodeMap: Map<AlarmItemNode, AlarmItemFlatNode> = new Map<AlarmItemNode, AlarmItemFlatNode>();

  /** Angular Material Flat tree control. Able to expand/collapse a subtree recursively for flattened tree. */
  treeControl: FlatTreeControl<AlarmItemFlatNode>;

  /** Angular Material Tree flattener to convert a normal type of node to node with children & level information */
  treeFlattener: MatTreeFlattener<AlarmItemNode, AlarmItemFlatNode>;

  /** Angular Material Data source for the flat tree */
  dataSource: MatTreeFlatDataSource<AlarmItemNode, AlarmItemFlatNode>;

  /** The selection for checklist */
  checklistSelection = new SelectionModel<AlarmItemFlatNode>(true /* multiple */);

  /**
   * Instantiates the component
   * @param {AlarmService} alarmService Service used to send the request to acknowledge the alarm
   */
  constructor(private alarmService: AlarmService) {
  }

  /**
   * This function is defined by default and executed on Component startup.
   */
  ngOnInit() {
    this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);
    this.treeControl = new FlatTreeControl<AlarmItemFlatNode>(this.getLevel, this.isExpandable);
    this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
    this.checklistSelection.onChange.subscribe( () => {
      this.updateAckList();
    });
    this.updateData();
  }

  /**
   * This function is executed on Component startup and everytime its state changes.
   * It currently builds the tree by reading the data from the alarm (whenever the alarm changes)
   */
  ngOnChanges(changes: SimpleChanges) {
    if (this.selectedAlarm) {
      if (changes.selectedAlarm.previousValue) {
        const alarm: SimpleChange = changes.selectedAlarm;
        const prevAlarmCoreID = alarm.previousValue.core_id;
        const currentAlarmCoreID = alarm.currentValue.core_id;
        const prevDependenciesString = alarm.previousValue.dependencies.sort().join();
        const currentDependenciesString = alarm.currentValue.dependencies.sort().join();
        const dependenciesChange = (prevDependenciesString !== currentDependenciesString);
        const coreIDChange = (prevAlarmCoreID !== currentAlarmCoreID);
        if ((coreIDChange === true) || (dependenciesChange === true)) {
          this.updateData();
        }
      } else {
        this.updateData();
      }
    }
  }


  /**
   * Update the data for the dataSource
   */
  updateData() {
    if (this.dataSource) {
      const tree_data = this.getTreeData();
      this.dataSource.data = this.buildFileTree(tree_data, 0);
    }
  }

  /**
  * Retuns the level of a given node
  * @param {AlarmItemFlatNode} node the node
  * @returns {number} the level of the node
  */
  getLevel = (node: AlarmItemFlatNode) => node.level;

  /**
  * Checks wether or not the node is expandable
  * @param {AlarmItemFlatNode} node the node
  * @returns {boolean} true if the node is expandable, false if not
  */
  isExpandable = (node: AlarmItemFlatNode) => node.expandable;

  /**
  * Retuns the children of the node, as an Observable
  * @param {AlarmItemFlatNode} node the node
  * @returns {Observable} the children the node
  */
  getChildren = (node: AlarmItemNode): Observable<AlarmItemNode[]> => {
    return ofObservable(node.children);
  }

  /**
  * Checks wether or not the node has children
  * @param {AlarmItemFlatNode} node the node
  * @returns {boolean}  true if the node has a child, false if not
  */
  hasChild = (_: number, _nodeData: AlarmItemFlatNode) => _nodeData.expandable;

  /**
  * Checks wether or not the node has no content
  * @param {AlarmItemFlatNode} node the node
  * @returns {boolean}  true if the node has no content, false if not
  */
  hasNoContent = (_: number, _nodeData: AlarmItemFlatNode) => _nodeData.item === '';

  /**
   * Get tree data from selected alarm
   * @returns {any}  the tree data in a JSON format
   */
  getTreeData(): any {
    const tree_data = {};
    tree_data[this.selectedAlarm.core_id] = this._getSubTree(this.selectedAlarm);
    return tree_data;
  }

  /**
   * Auxiliary function used to get the tree data from a given alarm
   * @param {Alarm} alarm the {@link Alarm}
   * @returns {any}  the tree data in a JSON format
   */
  private _getSubTree(alarm: Alarm): any {
    if (alarm.dependencies.length === 0) {
      return null;
    }
    const subTree = {};
    for (const childId of alarm.dependencies) {
      const childAlarm = this.alarmService.get(childId);
      if (childAlarm.ack) {
        continue;
      }
      const subSubTree = this._getSubTree(childAlarm);
      subTree[childId] = subSubTree;
    }
    const subTreeIsEmpty = Object.keys(subTree).length === 0;
    if ( subTreeIsEmpty ) {
      return null;
    }
    return subTree;
  }

  /**
   * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
   * @param {any} value the node as a Json object, or a sub-tree of a Json object.
   * @param {number} level the level of the node
   * @returns {AlarmItemNode[]} the list of `AlarmItemNode`.
   */
  buildFileTree(value: any, level: number): AlarmItemNode[] {
    const data: any[] = [];
    for (const k in value) {
      if (k in value) {
        const v = value[k];
        const node = new AlarmItemNode();
        node.item = `${k}`;
        if (v === null || v === undefined) {
          // no action
        } else if (typeof v === 'object') {
        node.children = this.buildFileTree(v, level + 1);
      } else {
        node.item = v;
      }
      data.push(node);
      }
    }
    return data;
  }

  /**
   * Transformer to convert nested node to flat node. Record the nodes in maps for later use.
   * @param {AlarmItemNode} node the node
   * @param {number} level the level node
   * @returns {AlarmItemFlatNode} the node converted to a FlatNode
   */
  transformer = (node: AlarmItemNode, level: number) => {
    let flatNode: any;
    if (this.nestedNodeMap.has(node) && this.nestedNodeMap.get(node) !== null && this.nestedNodeMap.get(node).item === node.item) {
      flatNode = this.nestedNodeMap.get(node);
    } else {
      flatNode = new AlarmItemFlatNode();
    }
    flatNode.item = node.item;
    flatNode.level = level;
    flatNode.expandable = !!node.children;
    this.flatNodeMap.set(flatNode, node);
    this.nestedNodeMap.set(node, flatNode);
    return flatNode;
  }

  /**
  * Checks whether all the descendants of the node are selected
  * @param {AlarmItemFlatNode} node the node
  * @returns {boolean} true if all the descendants of the node are selected, false if not
  */
  descendantsAllSelected(node: AlarmItemFlatNode): boolean {
    const descendants = this.treeControl.getDescendants(node);
    return descendants.every(child => this.checklistSelection.isSelected(child));
  }

  /**
  * Checks whether part of the descendants are selected
  * @param {AlarmItemFlatNode} node the node
  * @returns {boolean} true if some of the descendents are selected, false if not
  */
  descendantsPartiallySelected(node: AlarmItemFlatNode): boolean {
    const descendants = this.treeControl.getDescendants(node);

    let selectedDescendants = 0;
    let unSelectedDescendants = 0;
    for (const descendant of descendants) {
      if (this.checklistSelection.isSelected(descendant)) {
        selectedDescendants++;
      } else {
        unSelectedDescendants++;
      }
    }
    if (unSelectedDescendants === 0) {
      this.checklistSelection.select(node);
    } else {
      this.checklistSelection.deselect(node);
    }
    return selectedDescendants > 0 && unSelectedDescendants > 0;
  }

  /**
  * Checks wether or not the node has selected dependencies
  * @param {AlarmItemFlatNode} node the node
  * @returns {boolean} true if the node has no selected descendents, false if not
  */
  noSelectedDescendants(node: AlarmItemFlatNode): boolean {
    const descendants = this.treeControl.getDescendants(node);
    const result = descendants.some(child => this.checklistSelection.isSelected(child));
    return !result;
  }

  /**
  * Toggle the alarm item selection. Select/deselect all the descendants node
  * @param {AlarmItemFlatNode} node the node
  */
  alarmItemSelectionToggle(node: AlarmItemFlatNode): void {
    this.checklistSelection.toggle(node);
    const descendants = this.treeControl.getDescendants(node);
    if (this.checklistSelection.isSelected(node)) {
      this.checklistSelection.select(...descendants);
    } else {
      this.checklistSelection.deselect(...descendants);
    }
  }

  /** Update list with ids to ack **/
  updateAckList(): void {
    this.ackList = [];
    const selected = this.checklistSelection.selected;
    selected.forEach( (flatNode) => {
      if (flatNode.expandable === false) {
        this.ackList.push(flatNode.item);
      }
    });
    this.alarmsToAckFromSelection.emit(this.ackList);
  }

}
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
  <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding>
    <button mat-icon-button disabled></button>
    <mat-checkbox
        class="checklist-leaf-node"
        [checked]="checklistSelection.isSelected(node)"
        (change)="checklistSelection.toggle(node)"
      > {{node.item}}
    </mat-checkbox>
  </mat-tree-node>
  <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
    <button mat-icon-button matTreeNodeToggle>
      <mat-icon class="mat-icon-rtl-mirror">
        {{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
      </mat-icon>
    </button>
    <mat-checkbox
        [ngClass]="{'clear-check-class': noSelectedDescendants(node)}"
        [checked]="descendantsAllSelected(node)"
        [indeterminate]="descendantsPartiallySelected(node)"
        (change)="alarmItemSelectionToggle(node)">
      {{node.item}}
    </mat-checkbox>
  </mat-tree-node>
</mat-tree>

ack-tree.component.scss

mat-checkbox.mat-checkbox-checked {
  &.clear-check-class {

    .mat-checkbox-background {
      display: none;
    }

  }
}

mat-tree {
  background: transparent;
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""