angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.39k stars 6.76k forks source link

mat-tree with check box and dynamic data does no work fine #14731

Closed elham-sedighi closed 5 years ago

elham-sedighi commented 5 years ago

Hi I'm using mat-tree material angular with checkboxes and dynamic data which comes from a http call service. I have two problems now: 1- all root nodes of tree are checked at the beginning! and select and deselect nodes and parent nodes does not work correctly 2- toggling parent nodes does not work fine either because there may be more that one level deeper in a parent node (which I,m trying to collapse) but only first level will be collapsed according to the count of its child. what is the solution? I even used a recursive function to handle that but it's not working fine too. I would be grateful to help me with. here are my codes: view: `<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" dir="rtl"> <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding>

<mat-checkbox class="checklist-leaf-node"
               [indeterminate]="descendantsPartiallySelected(node)"
              [checked]="checklistSelection.isSelected(node)"
              (change)="todoLeafItemSelectionToggle(node);">
  {{node.Title}}
</mat-checkbox>
<mat-progress-bar *ngIf="node.isLoading"
                  mode="indeterminate"
                  class="example-tree-progress-bar"></mat-progress-bar>

<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding> <button mat-icon-button matTreeNodeToggle [attr.aria-label]="'toggle ' + node.filename">

{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</button>
<mat-checkbox [checked]="descendantsAllSelected(node)"
              [indeterminate]="descendantsPartiallySelected(node)"
              (change)="todoItemSelectionToggle(node)">{{node.Title}}</mat-checkbox>
<mat-progress-bar *ngIf="node.isLoading"
                  mode="indeterminate"
                  class="example-tree-progress-bar"></mat-progress-bar>

my component: import { CollectionViewer, SelectionChange, SelectionModel } from '@angular/cdk/collections'; import { FlatTreeControl } from '@angular/cdk/tree'; import { Component, Injectable } from '@angular/core'; import { Service } from '../service/callajaxandhttp/service.component'; import { BehaviorSubject, Observable, merge } from 'rxjs'; import { map } from 'rxjs/operators';

export class DynamicFlatNode { constructor( public Title: string, public id: string, public PID: number, public Type: string, public s: string, public haschildren: boolean, public level: number = 1, public expandable: boolean = false, public isLoading: boolean = false, public childcount: number = 0, public children: DynamicFlatNode[] = [] ) { } }

@Injectable() export class DynamicDatabase { dfns: DynamicFlatNode[]; Tempchilddfns: DynamicFlatNode[];

public getData(url: string, ptitle: string, parentnode: string): Promise { ptitle = ptitle != '' ? '?ptitle=' + ptitle : ''; parentnode = parentnode != '' ? '?parent=' + parentnode : '';

return this.service.callservice('get', url + ptitle + parentnode, "").then(
  (response: any) =>
    response)
  .catch(error => console.log(error));

}

constructor(private service: Service) { } }

@Injectable() export class DynamicDataSource {

dataChange: BehaviorSubject<DynamicFlatNode[]> = new BehaviorSubject<DynamicFlatNode[]>([]); get data(): DynamicFlatNode[] { return this.dataChange.value; } set data(value: DynamicFlatNode[]) { this.treeControl.dataNodes = value; this.dataChange.next(value); } constructor(private treeControl: FlatTreeControl, private database: DynamicDatabase) { } connect(collectionViewer: CollectionViewer): Observable<DynamicFlatNode[]> { this.treeControl.expansionModel.onChange!.subscribe(change => { if ((change as SelectionChange).added || (change as SelectionChange).removed) { this.handleTreeControl(change as SelectionChange); } });

return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data));

}

handleTreeControl(change: SelectionChange) { if (change.added) {

  change.added.forEach((node) => this.toggleNode(node, true));
}
if (change.removed) {
  change.removed.reverse().forEach((node) => this.toggleNode(node, false));
}

}

toggleNode(node: DynamicFlatNode, expand: boolean) { const index = this.data.indexOf(node); if (expand) { if (node.level == 3) return; node.isLoading = true; if (node.children.length == 0) { const children = this.database.getData('Subject/scripts/Subjectw.ashx?parent=' + node.id, '', "").then( (response: any) => { const nodes = response.map( ContentNode => new DynamicFlatNode( ContentNode.Title, ContentNode.id, ContentNode.PID, ContentNode.Type, ContentNode.s, ContentNode.children, node.level + 1, ContentNode.children)) if (!children || index < 0) { // If no children, or cannot find the node, no op return; }

        node.childcount = nodes.length;
        node.children = nodes;
        this.data.splice(index + 1, 0, ...nodes);
        // notify the change
        this.dataChange.next(this.data);
        node.isLoading = false;
      })
      .catch(error => console.log(error));
  }
  else {
    this.data.splice(index + 1, 0, ...node.children);
    this.dataChange.next(this.data);
    node.isLoading = false;
  }
} else {
  this.data.splice(index + 1, node.childcount,);//children.length
  this.dataChange.next(this.data);
}

} }

@Component({ selector: 'Contenttree', templateUrl: './contenttree.component.html', styleUrls: ['./contenttree.component.css'], providers: [DynamicDatabase] }) export class ContenttreeComponent {

RootContents: DynamicFlatNode[] = []; constructor(database: DynamicDatabase, private service: Service) { this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); this.dataSource = new DynamicDataSource(this.treeControl, database);

database.getData('Subject/scripts/Subjectw.ashx', '', '').then(
  (response: any) =>
    this.dataSource.data = response.map(
      ContentNode =>
        new DynamicFlatNode(
          ContentNode.Title,
          ContentNode.id,
          ContentNode.PID,
          ContentNode.Type,
          ContentNode.s,
          ContentNode.children,
          0,
          ContentNode.children))
)
  .catch(error => console.log(error));

}

dataSource: DynamicDataSource; treeControl: FlatTreeControl; getLevel = (node: DynamicFlatNode) => { return node.level; }; isExpandable = (node: DynamicFlatNode) => { return node.expandable; }; hasChild = (_: number, _nodeData: DynamicFlatNode) => { return nodeData.expandable; }; hasNoContent = (: number, _nodeData: DynamicFlatNode) => { return _nodeData.Title === ''; }; checklistSelection = new SelectionModel(true / multiple /);

/* Whether all the descendants of the node are selected. / descendantsAllSelected(node: DynamicFlatNode): boolean { 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: DynamicFlatNode): 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: DynamicFlatNode): void { this.checklistSelection.toggle(node); const descendants = this.treeControl.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)
);
this.checkAllParentsSelection(node);

}

/* Toggle a leaf to-do item selection. Check all the parents to see if they changed / todoLeafItemSelectionToggle(node: DynamicFlatNode): void { this.checklistSelection.toggle(node); this.checkAllParentsSelection(node); }

/ Checks all the parents when a leaf node is selected/unselected / checkAllParentsSelection(node: DynamicFlatNode): void { let parent: DynamicFlatNode | null = this.getParentNode(node); while (parent) { this.checkRootNodeSelection(parent); parent = this.getParentNode(parent); } }

/* Check root node checked state and change it accordingly / checkRootNodeSelection(node: DynamicFlatNode): void { 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); } }

/ Get the parent node of a node / getParentNode(node: DynamicFlatNode): DynamicFlatNode | null { 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;

} } `

thanks alot

elham-sedighi commented 5 years ago

since tree fills data for children of a node after index of its parent node (in this.data), so when you want to remove or collapse children of a node: (this.data.splice(index + 1, node.childcount);//children.length) you just have to count all indexes in data from index of node to reach a node with level equal to the level of the node you wanted to collapse. that count would be used like :
(this.data.splice(index + 1, counter);//children.length)

elham-sedighi commented 5 years ago

checkboxes for tree created dynamically wont work fine still. when collapsing a whole root node it changes its check state randomly. can somebody help me with this? check box states for tree created dynamically ....

lroedal commented 5 years ago

If you want checkboxes for a tree with dynamic data, you should map a boolean to your data source when fetching it, and use that boolean for the checkbox state. Just invert that boolean on checkbox click.

lroedal commented 5 years ago

If you create a stackblitz from your code I can edit it in for you.

elham-sedighi commented 5 years ago

stackblitz

https://stackblitz.com/edit/angular-buzxpd

but it cant get http responses in stackblitz, I put my component there anyway. it has checkbox checking problem and collapse and expand sign problem cause i had to remove all nodes expanded under a node (my tree has 4 level).

lroedal commented 5 years ago

Better to add some mockup-data, kind of hard to add it when it is not running. I created a custom stackblitz which maps in booleans for the checkboxes, as well as a separate boolean for the expand (so the tree-control is not really used - you still have to add it though). This is done on OnInit, in your case you can map it in after you have fetched the data from server, before you connect it to the treedatasource. Now it remembers the state of expand for children as well as the checkboxes.

https://stackblitz.com/edit/nested-tree-with-checkbox-custom-expand

andrewseguin commented 5 years ago

Please re-open your issue following the issue template provided, including a description of the issue you are seeing, the current behavior, the expected behavior, and a reproduction in Stackblitz

hijazikaram commented 5 years ago

Did anyone fix the issue?

angular-automatic-lock-bot[bot] commented 5 years ago

This issue has been automatically locked due to inactivity. Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.