Karam
Karam

Reputation: 336

Expression has changed after it was checked --> checkbox mat-tree

I have the functionality working but when I switch the value of intermediate i get that error. Not sure if Im changing 2 observables so its like which value should i take. IDK lol! I added my code down below. If you need anything else just let me know. Any Ideas on how to solve this? Man Mat tree component is so annoying! enter image description here

import { NestedTreeControl } from '@angular/cdk/tree';
import { Component, ChangeDetectorRef } from '@angular/core';
import { MatTreeNestedDataSource } from '@angular/material/tree';
import { SelectionModel } from '@angular/cdk/collections';

interface ITreeNode {
    children?: ITreeNode[];
    name: string;
    expanded: boolean;
}

const TREE_DATA = [
    {
        name: 'Land Plane',
        expanded: true,
        children: [
            { name: 'Piston', expanded: true, children: [] },
            { name: 'Jet', expanded: true, children: [] },
            { name: 'Turboprop', expanded: true, children: [] }
        ]
    },
    {
        name: 'Helicopter',
        expanded: true,
        children: [
            { name: 'Piston', expanded: true, children: [] },
            { name: 'Turboprop', expanded: true, children: [] }
        ]
    },
    {
        name: 'Amphibian',
        expanded: true,
        children: [{ name: 'Turboprop', expanded: true, children: [] }]
    },
    {
        name: 'Tiltwing',
        expanded: true,
        children: [{ name: 'Turboprop', expanded: true, children: [] }]
    },
    {
        name: 'Gyrocopter',
        expanded: true,
        children: [{ name: 'Piston', expanded: true, children: [] }]
    },
    {
        name: 'Tower',
        expanded: true,
        children: []
    },
    {
        name: 'Gyrocopter',
        expanded: true,
        children: []
    }
];

@Component({
    selector: 'globe-source-facets',
    templateUrl: './globe-source-facets.component.html',
    styleUrls: ['./globe-source-facets.component.scss']
})
export class GlobeSourceFacetsComponent {
    public nestedTreeControl: NestedTreeControl<ITreeNode>;
    public nestedDataSource: MatTreeNestedDataSource<ITreeNode>;
    public checklistSelection = new SelectionModel<ITreeNode>(true);
    constructor(private changeDetectorRef: ChangeDetectorRef) {
        this.nestedTreeControl = new NestedTreeControl<ITreeNode>(
            this.getChildren
        );
        this.nestedDataSource = new MatTreeNestedDataSource();
        this.nestedDataSource.data = TREE_DATA;
    }

    public hasNestedChild = (_: number, nodeData: ITreeNode) =>
        nodeData.children.length > 0;

    public getChildren = (node: ITreeNode) => node.children;

    public changeState(node) {
        node.expanded = !node.expanded;
    }

    descendantsAllSelected(node: ITreeNode): boolean {
        const descendants = this.nestedTreeControl.getDescendants(node);
        if (!descendants.length) {
          return this.checklistSelection.isSelected(node);
        }
        const selected = this.checklistSelection.isSelected(node);
        const allSelected = descendants.every(child => this.checklistSelection.isSelected(child));
        if (!selected && allSelected) {
          this.checklistSelection.select(node);
          this.changeDetectorRef.markForCheck();
        }
        return allSelected;
      }

    public descendantsPartiallySelected(node: ITreeNode): boolean {
        const descendants = this.nestedTreeControl.getDescendants(node);
        if (!descendants.length) {
        return false;
        }
        const result = descendants.some(child => this.checklistSelection.isSelected(child));
        return result && !this.descendantsAllSelected(node);
    }

    public todoItemSelectionToggle(node: ITreeNode): void {
        this.checklistSelection.toggle(node);
        const descendants = this.nestedTreeControl.getDescendants(node);
        this.checklistSelection.isSelected(node)
            ? this.checklistSelection.select(...descendants)
            : this.checklistSelection.deselect(...descendants);
    }
}
<div class="facets-container">
    <div class="tree-container">
        <mat-tree
            [dataSource]="nestedDataSource"
            [treeControl]="nestedTreeControl"
            class="example-tree"
        >
            <mat-tree-node *matTreeNodeDef="let node" disabled="true">
                <li class="mat-tree-node">
                    <button mat-icon-button disabled></button>
                    <mat-checkbox
                        class="checklist-leaf-node"
                        [checked]="checklistSelection.isSelected(node)"
                        (change)="todoItemSelectionToggle(node)"
                        >{{ node.name }}</mat-checkbox
                    >
                </li>
            </mat-tree-node>

            <mat-nested-tree-node
                *matTreeNodeDef="let node; when: hasNestedChild"
            >
                <li>
                    <div class="mat-tree-node">
                        <button
                            mat-icon-button
                            [attr.aria-label]="'toggle ' + node.name"
                            (click)="changeState(node)"
                        >
                            <mat-icon class="mat-icon-rtl-mirror">
                                {{
                                    node.expanded
                                        ? 'chevron_right'
                                        : 'expand_more'
                                }}
                            </mat-icon>
                        </button>
                        <mat-checkbox
                            *ngIf="node.name !== ''"
                            class="checklist-leaf-node"
                            [checked]="checklistSelection.isSelected(node)"
                            [indeterminate]="descendantsPartiallySelected(node)"
                            (change)="todoItemSelectionToggle(node)"
                            >{{ node.name }}</mat-checkbox
                        >
                    </div>
                    <ul [class.example-tree-invisible]="node.expanded">
                        <ng-container matTreeNodeOutlet></ng-container>
                    </ul>
                </li>
            </mat-nested-tree-node>
        </mat-tree>
    </div>
    <div class="facet-actions">
        <button mat-button>CLEAR</button>
        <button mat-button color="primary">APPLY</button>
    </div>
</div>

Upvotes: 2

Views: 3012

Answers (2)

aycanadal
aycanadal

Reputation: 1146

Changing the checked condition in the nested nodes like following in the view gets rid of the error:

   <div class="facets-container">
<div class="tree-container">
  <mat-tree
    [dataSource]="nestedDataSource"
    [treeControl]="nestedTreeControl"
    class="example-tree"
  >
    <mat-tree-node *matTreeNodeDef="let node" disabled="true">
      <li class="mat-tree-node">
        <button mat-icon-button disabled></button>
        <mat-checkbox
          class="checklist-leaf-node"
          [checked]="checklistSelection.isSelected(node)"
          (change)="todoItemSelectionToggle(node)"
        >{{ node.name }}</mat-checkbox
        >
      </li>
    </mat-tree-node>

    <mat-nested-tree-node
      *matTreeNodeDef="let node; when: hasNestedChild"
    >
      <li>
        <div class="mat-tree-node">
          <button
            mat-icon-button
            [attr.aria-label]="'toggle ' + node.name"
            (click)="changeState(node)"
          >
            <mat-icon class="mat-icon-rtl-mirror">
              {{
              node.expanded
                ? 'chevron_right'
                : 'expand_more'
              }}
            </mat-icon>
          </button>
          <mat-checkbox
            *ngIf="node.name !== ''"
            class="checklist-leaf-node"
            [checked]="descendantsAllSelected(node)"
            [indeterminate]="descendantsPartiallySelected(node)"
            (change)="todoItemSelectionToggle(node)"
          >{{ node.name }}</mat-checkbox
          >
        </div>
        <ul [class.example-tree-invisible]="node.expanded">
          <ng-container matTreeNodeOutlet></ng-container>
        </ul>
      </li>
    </mat-nested-tree-node>
  </mat-tree>
</div>
<div class="facet-actions">
  <button mat-button>CLEAR</button>
  <button mat-button color="primary">APPLY</button>
</div>
</div>

To fix the bug with the parent node state after last child node is deselected, change todoItemSelectionToggle() method like following:

import {NestedTreeControl} from '@angular/cdk/tree';
import {Component, ChangeDetectorRef, OnInit} from '@angular/core';
import {MatTreeNestedDataSource} from '@angular/material/tree';
import {SelectionModel} from '@angular/cdk/collections';

interface ITreeNode {
  children?: ITreeNode[];
  name: string;
  expanded: boolean;
}

const TREE_DATA = [
  {
    name: 'Land Plane',
    expanded: true,
    children: [
      {name: 'Piston', expanded: true, children: []},
      {name: 'Jet', expanded: true, children: []},
      {name: 'Turboprop', expanded: true, children: []}
    ]
  },
  {
    name: 'Helicopter',
    expanded: true,
    children: [
      {name: 'Piston', expanded: true, children: []},
      {name: 'Turboprop', expanded: true, children: []}
    ]
  },
  {
    name: 'Amphibian',
    expanded: true,
    children: [{name: 'Turboprop', expanded: true, children: []}]
  },
  {
    name: 'Tiltwing',
    expanded: true,
    children: [{name: 'Turboprop', expanded: true, children: []}]
  },
  {
    name: 'Gyrocopter',
    expanded: true,
    children: [{name: 'Piston', expanded: true, children: []}]
  },
  {
    name: 'Tower',
    expanded: true,
    children: []
  },
  {
    name: 'Gyrocopter',
    expanded: true,
    children: []
  }
];

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  public nestedTreeControl: NestedTreeControl<ITreeNode>;
  public nestedDataSource: MatTreeNestedDataSource<ITreeNode>;
  public checklistSelection = new SelectionModel<ITreeNode>(true);

  constructor(private changeDetectorRef: ChangeDetectorRef) {

  }

  ngOnInit() {
    this.nestedTreeControl = new NestedTreeControl<ITreeNode>(
      this.getChildren
    );
    this.nestedDataSource = new MatTreeNestedDataSource();
    this.nestedDataSource.data = TREE_DATA;
  }

  public hasNestedChild = (_: number, nodeData: ITreeNode) =>
    nodeData.children.length > 0

  public getChildren = (node: ITreeNode) => node.children;

  public changeState(node) {
    node.expanded = !node.expanded;
  }

  descendantsAllSelected(node: ITreeNode): boolean {
    const descendants = this.nestedTreeControl.getDescendants(node);
    if (!descendants.length) {
      return this.checklistSelection.isSelected(node);
    }
    const selected = this.checklistSelection.isSelected(node);
    const allSelected = descendants.every(child => this.checklistSelection.isSelected(child));
    if (!selected && allSelected) {
      this.checklistSelection.select(node);
      this.changeDetectorRef.markForCheck();
    }
    return allSelected;
  }

  public descendantsPartiallySelected(node: ITreeNode): boolean {
    const descendants = this.nestedTreeControl.getDescendants(node);
    if (!descendants.length) {
      return false;
    }
    const result = descendants.some(child => this.checklistSelection.isSelected(child));
    return result && !this.descendantsAllSelected(node);
  }

  public todoItemSelectionToggle(node: ITreeNode): void {
    this.checklistSelection.toggle(node);
    const descendants = this.nestedTreeControl.getDescendants(node);
    this.checklistSelection.isSelected(node)
      ? this.checklistSelection.select(...descendants)
      : this.checklistSelection.deselect(...descendants);


    // Force update for the parent
    descendants.every(child =>
      this.checklistSelection.isSelected(child)
    );

  }

}

These are from the example with the checkboxes at https://material.angular.io/components/tree/examples

Check the difference in flat tree view for nodes when they have child nodes and not.

Upvotes: 0

Leandro Lima
Leandro Lima

Reputation: 1164

This issue is more complex than it seems. This is related to how the Angular works.

Basically, the angular life cycle starts on the parent component, goes to children components, than come back to the parent and finishes.

What is happening is, the life cycle started at the parent and some variable value is A, it goes all the way down to the components tree and when it comes back to the parent the value now is B, the Angular knows the value has changed and shows this error. This error means: "Man, the value has changed while I was doing my life cycle, It not sure if the view was painted with the lasted value"

To fix it? Try to use the lifecycle functions instead of doing things on the constructor.

Upvotes: 1

Related Questions