import { SelectionModel } from '@angular/cdk/collections' ; import { FlatTreeControl } from '@angular/cdk/tree' ; import { Component , Injectable } from '@angular/core' ; import { MatTreeFlatDataSource , MatTreeFlattener } from '@angular/material/tree' ; import { BehaviorSubject } from 'rxjs' ; /**
 * Node for to-do item
 */ export class TodoItemNode { children : TodoItemNode [ ] ; item : string ; } /** Flat to-do item node with expandable and level information */ export class TodoItemFlatNode { item : string ; level : number ; expandable : boolean ; } /**
 * The Json object for to-do list data.
 */ const TREE_DATA = { Groceries : { 'Almond Meal flour' : null , 'Organic eggs' : null , 'Protein Powder' : null , Fruits : { Apple : null , Berries : [ 'Blueberry' , 'Raspberry' ] , Orange : null } } , Reminders : [ 'Cook dinner' , 'Read the Material Design spec' , 'Upgrade Application to Angular' ] } ; /**
 * Checklist database, it can build a tree structured Json object.
 * Each node in Json object represents a to-do item or a category.
 * If a node is a category, it has children items and new items can be added under the category.
 */ @ Injectable ( ) export class ChecklistDatabase { dataChange = new BehaviorSubject < TodoItemNode [ ] > ( [ ] ) ; get data ( ) : TodoItemNode [ ] { return this . dataChange . value ; } constructor ( ) { this . initialize ( ) ; } initialize ( ) { const data = this . buildFileTree ( TREE_DATA , 0 ) ; this . dataChange . next ( data ) ; } /**
     * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
     * The return value is the list of `TodoItemNode`.
     */ buildFileTree ( obj , level ) { return Object . keys ( obj ) . reduce < TodoItemNode [ ] > ( ( accumulator , key ) => { const value = obj [ key ] ; const node = new TodoItemNode ( ) ; node . item = key ; if ( value != null ) { if ( typeof value === 'object' ) { node . children = this . buildFileTree ( value , level + 1 ) ; } else { node . item = value ; } } return accumulator . concat ( node ) ; } , [ ] ) ; } /** Add an item to to-do list */ insertItem ( parent , name ) { if ( parent . children ) { parent . children . push ( { item : name } as TodoItemNode ) ; this . dataChange . next ( this . data ) ; } } updateItem ( node , name ) { node . item = name ; this . dataChange . next ( this . data ) ; } } title @ Component ( { selector : 'tree-checklist-example' , templateUrl : 'tree-checklist-example.html' , styleUrls : [ 'tree-checklist-example.css' ] , providers : [ ChecklistDatabase ] } ) export class TreeChecklistExample { /** Map from flat node to nested node. This helps us finding the nested node to be modified */ flatNodeMap = new Map < TodoItemFlatNode , TodoItemNode > ( ) ; /** Map from nested node to flattened node. This helps us to keep the same object for selection */ nestedNodeMap = new Map < TodoItemNode , TodoItemFlatNode > ( ) ; /** A selected parent node to be inserted */ selectedParent : TodoItemFlatNode | null = null ; /** The new item's name */ newItemName = '' ; treeControl : FlatTreeControl < TodoItemFlatNode > ; treeFlattener : MatTreeFlattener < TodoItemNode , TodoItemFlatNode > ; dataSource : MatTreeFlatDataSource < TodoItemNode , TodoItemFlatNode > ; /** The selection for checklist */ checklistSelection = new SelectionModel < TodoItemFlatNode > ( true ) ; constructor ( private database ) { this . treeFlattener = new MatTreeFlattener ( this . transformer , this . getLevel , this . isExpandable , this . getChildren ) ; this . treeControl = new FlatTreeControl < TodoItemFlatNode > ( this . getLevel , this . isExpandable ) ; this . dataSource = new MatTreeFlatDataSource ( this . treeControl , this . treeFlattener ) ; database . dataChange . subscribe ( data => { this . dataSource . data = data ; } ) ; } getLevel = ( node ) => node . level ; isExpandable = ( node ) => node . expandable ; getChildren = ( node ) : TodoItemNode [ ] => node . children ; hasChild = ( _ , _nodeData ) => _nodeData . expandable ; hasNoContent = ( _ , _nodeData ) => _nodeData . item === '' ; /**
     * Transformer to convert nested node to flat node. Record the nodes in maps for later use.
     */ transformer = ( node , level ) => { const existingNode = this . nestedNodeMap . get ( node ) ; const flatNode = existingNode && existingNode . item === node . item ? existingNode : new TodoItemFlatNode ( ) ; flatNode . item = node . item ; flatNode . level = level ; flatNode . expandable = ! ! node . children ; this . flatNodeMap . set ( flatNode , node ) ; this . nestedNodeMap . set ( node , flatNode ) ; return flatNode ; } ; /** Whether all the descendants of the node are selected. */ descendantsAllSelected ( node ) { const descendants = this . treeControl . getDescendants ( node ) ; const descAllSelected = descendants . every ( child => this . checklistSelection . isSelected ( child ) ) ; return descAllSelected ; } /** Whether part of the descendants are selected */ descendantsPartiallySelected ( node ) { const descendants = this . treeControl . getDescendants ( node ) ; const result = descendants . some ( child => this . checklistSelection . isSelected ( child ) ) ; return result && ! this . descendantsAllSelected ( node ) ; } /** Toggle the to-do item selection. Select/deselect all the descendants node */ todoItemSelectionToggle ( node ) { this . checklistSelection . toggle ( node ) ; const descendants = this . treeControl . getDescendants ( node ) ; this . checklistSelection . isSelected ( node ) ? this . checklistSelection . select ( ... descendants ) : this . checklistSelection . deselect ( ... descendants ) ; descendants . every ( child => this . checklistSelection . isSelected ( child ) ) ; this . checkAllParentsSelection ( node ) ; } /** Toggle a leaf to-do item selection. Check all the parents to see if they changed */ todoLeafItemSelectionToggle ( node ) { this . checklistSelection . toggle ( node ) ; this . checkAllParentsSelection ( node ) ; } checkAllParentsSelection ( node ) { let parent = this . getParentNode ( node ) ; while ( parent ) { this . checkRootNodeSelection ( parent ) ; parent = this . getParentNode ( parent ) ; } } /** Check root node checked state and change it accordingly */ checkRootNodeSelection ( node ) { const nodeSelected = this . checklistSelection . isSelected ( node ) ; const descendants = this . treeControl . getDescendants ( node ) ; const descAllSelected = descendants . every ( child => this . checklistSelection . isSelected ( child ) ) ; if ( nodeSelected && ! descAllSelected ) { this . checklistSelection . deselect ( node ) ; } else if ( ! nodeSelected && descAllSelected ) { this . checklistSelection . select ( node ) ; } } getParentNode ( node ) { const currentLevel = this . getLevel ( node ) ; if ( currentLevel < 1 ) { return null ; } const startIndex = this . treeControl . dataNodes . indexOf ( node ) - 1 ; for ( let i = startIndex ; i >= 0 ; i -- ) { const currentNode = this . treeControl . dataNodes [ i ] ; if ( this . getLevel ( currentNode ) < currentLevel ) { return currentNode ; } } return null ; } /** Select the category so we can insert the new item. */ addNewItem ( node ) { const parentNode = this . flatNodeMap . get ( node ) ; this . database . insertItem ( parentNode ! , '' ) ; this . treeControl . expand ( node ) ; } /** Save the node to database */ saveNode ( node , itemValue ) { const nestedNode = this . flatNodeMap . get ( node ) ; this . database . updateItem ( nestedNode ! , itemValue ) ; } }