Angular Material mat-tree get checkbox values

19,521

Solution 1

I think the selection list (in your Stackblitz demo link) is what you want. This means that you must get track of the selections in MatTree by yourself: it won't do that for you because it doesn't know how to handle the MatCheckboxes you're using at the nodes.

In your demo, this is accomplished by using/maintaining the SelectionModel (a collection of @angular/cdk/collections that is not part of MatTree). The modified Stackblitz example is here (just select some node with children on MatTree).

The important part of the demo is that every click on a MatCheckbox fires an @Output() change on that checkbox which is used to trigger the todoItemSelectionToggle method, that updates the SelectionModel:

/** Toggle the to-do item selection. Select/deselect all the descendants node */
todoItemSelectionToggle(node: TodoItemFlatNode): void {
  // HERE IS WHERE THE PART OF THE MODEL RELATED TO THE CLICKED CHECKBOX IS UPDATED
  this.checklistSelection.toggle(node); 

  // HERE WE GET POTENTIAL CHILDREN OF THE CLICKED NODE
  const descendants = this.treeControl.getDescendants(node);

  // HERE IS WHERE THE REST OF THE MODEL (POTENTIAL CHILDREN OF THE CLICKED NODE) IS UPDATED
  this.checklistSelection.isSelected(node) 
    ? this.checklistSelection.select(...descendants)
    : this.checklistSelection.deselect(...descendants);
}

The SelectionModel is a collection based on a Set, which was built by the @angular team exactly to be used by developers using components that allow for multiple selections to help them tracking changes on those components. You can see more details about this collection here: https://github.com/angular/components/blob/master/src/cdk/collections/selection-model.ts

Like everything in javascript, there's no magic here, basically, its constructor accepts a boolean argument to define whether the SelectionModel<T> (it's a generics) will store multiple values (true) or a single value. It also has convenient methods like sort(predicate?: (a: T, b: T) => number), select(...values: T[]) to add objects, deselect(...values: T[]) to remove objects, toggle(o: T) to add (if it doesn't exist) or remove (if it already exists). Internally, the comparisons are, by default, done by reference, so {a:1} != {a:1}.

Solution 2

You can get the value directly from the checklistselection

values = this.checklistSelection.selected

This will return you exactly the value of all checked items

Solution 3

The example which you mentioned used Angular material Selection Model.

When a property = SelectionModel and type of model = TodoItemFlatNode. you do that by ->

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

Now the checklistSelection property would comprise and you can access all these methods.

changed, hasValue, isSelected, selection, onChange, toggle etc.

So now you can apply your selection logic by accessing those methods above.

example

this.checklistSelection.isSelected ?
Share:
19,521

Related videos on Youtube

Juan Vega
Author by

Juan Vega

Updated on June 04, 2022

Comments

  • Juan Vega
    Juan Vega almost 2 years

    I'm using Angular Material v6.0 MatTreeModule (mat-tree) with checkboxes. But I'm having a hard time figuring out how to determine which nodes have been checked and which nodes have not been checked. In the example by Angular Material, they give the very good source code to set it up which I was able to set up just fine.

    However, I'm unable to determine which checkboxes have been checked and which ones have not. I've tried for hours trying to figure this out with no luck.

    My goal is end-users will check or uncheck the checkboxes in the tree and from there, I need to run some process after they have made their selections.

    But I'm completely stuck trying to figure out which mat-tree-nodes are checked and not checked and there's no good working example that I could find anywhere.

    The key source code is found here

    More info on mat-tree is found here

    Can anyone help on how to determine if the checkboxes have been checked?

    Thanks.

    Per request, I'm adding the code here from the two code files:

    app/tree-checklist-example.ts app/tree-checklist-example.html

    Typescript source code from "app/tree-checklist-example.ts"

    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() {
        // Build the tree nodes from Json object. The result is a list of `TodoItemNode` with nested
        //     file node as children.
        const data = this.buildFileTree(TREE_DATA, 0);
    
        // Notify the change.
        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: object, level: number): TodoItemNode[] {
        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: TodoItemNode, name: string) {
        if (parent.children) {
          parent.children.push({item: name} as TodoItemNode);
          this.dataChange.next(this.data);
        }
      }
    
      updateItem(node: TodoItemNode, name: string) {
        node.item = name;
        this.dataChange.next(this.data);
      }
    }
    
    /**
     * @title Tree with checkboxes
     */
    @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 /* multiple */);
    
      constructor(private database: ChecklistDatabase) {
        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: TodoItemFlatNode) => node.level;
    
      isExpandable = (node: TodoItemFlatNode) => node.expandable;
    
      getChildren = (node: TodoItemNode): TodoItemNode[] => node.children;
    
      hasChild = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.expandable;
    
      hasNoContent = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.item === '';
    
      /**
       * Transformer to convert nested node to flat node. Record the nodes in maps for later use.
       */
      transformer = (node: TodoItemNode, level: number) => {
        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: TodoItemFlatNode): boolean {
        const descendants = this.treeControl.getDescendants(node);
        return descendants.every(child => this.checklistSelection.isSelected(child));
      }
    
      /** Whether part of the descendants are selected */
      descendantsPartiallySelected(node: TodoItemFlatNode): boolean {
        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: TodoItemFlatNode): void {
        this.checklistSelection.toggle(node);
        const descendants = this.treeControl.getDescendants(node);
        this.checklistSelection.isSelected(node)
          ? this.checklistSelection.select(...descendants)
          : this.checklistSelection.deselect(...descendants);
      }
    
      /** Select the category so we can insert the new item. */
      addNewItem(node: TodoItemFlatNode) {
        const parentNode = this.flatNodeMap.get(node);
        this.database.insertItem(parentNode!, '');
        this.treeControl.expand(node);
      }
    
      /** Save the node to database */
      saveNode(node: TodoItemFlatNode, itemValue: string) {
        const nestedNode = this.flatNodeMap.get(node);
        this.database.updateItem(nestedNode!, itemValue);
      }
    }
    
    
    
    HTML source code from "app/tree-checklist-example.html":
    
    <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: hasNoContent" matTreeNodePadding>
        <button mat-icon-button disabled></button>
        <mat-form-field>
          <input matInput #itemValue placeholder="New item...">
        </mat-form-field>
        <button mat-button (click)="saveNode(node, itemValue.value)">Save</button>
      </mat-tree-node>
    
      <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
        <button mat-icon-button matTreeNodeToggle
                [attr.aria-label]="'toggle ' + node.filename">
          <mat-icon class="mat-icon-rtl-mirror">
            {{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
          </mat-icon>
        </button>
        <mat-checkbox [checked]="descendantsAllSelected(node)"
                      [indeterminate]="descendantsPartiallySelected(node)"
                      (change)="todoItemSelectionToggle(node)">{{node.item}}</mat-checkbox>
        <button mat-icon-button (click)="addNewItem(node)"><mat-icon>add</mat-icon></button>
      </mat-tree-node>
    </mat-tree>
    

    As mentioned earlier to see the complete source code and a demo of it working, please go to: https://stackblitz.com/angular/gabkadkvybq?file=app%2Ftree-checklist-example.html

    Thanks.

    • Vega
      Vega over 5 years
      The link to your code is good, but sharing in your post minimal reproducible example is much better. It also proves your efforts for debugging.
  • Juan Vega
    Juan Vega over 5 years
    Thanks so much. I really appreciate your answer. After hours all day Sunday I was losing hope that I would not be able to use the MatTree. But not anymore. It's exactly what we're looking for and this solution will make it happen.
  • julianobrasil
    julianobrasil over 5 years
    @icepero, I wouldn't classify this behavior as a mat-tree bug. The example is definitely bugged, but it doesn't seems to be mat-tree's fault. I'll try (not in the next 2 weeks) to fix the example. It'll bring an additional complexity to the todoItemSelectionToggle method, but I agree this is a required thing.
  • Admin
    Admin over 5 years
  • yaswanthkoneri
    yaswanthkoneri almost 5 years
    StackBlitz demo url is broken @jpavel
  • julianobrasil
    julianobrasil almost 5 years
    @yaswanthkoneri, I fixed the link. I had removed my stackblitz example for some reason. I'm not sure what I had done in that example, but I think it was just something to show how to retrieve the values selected on some tree nodes. If so, I rebuild the example..
  • Efron A.
    Efron A. over 4 years
    @JuanVega do you have the final working version available (stackblitz source or similar) since I am seeking the same resolution? Thank you.
  • Packet Tracer
    Packet Tracer about 4 years
    link is broken again @julianobrasil