【问题标题】:Angular Material Tree Add Child Button to Every NodeAngular Material Tree 向每个节点添加子按钮
【发布时间】:2021-04-11 01:07:49
【问题描述】:

https://stackblitz.com/angular/qkjdynkykodo?file=src%2Fapp%2Ftree-checklist-example.html

在此示例中,“添加子按钮”仅显示在父节点上,我想在所有子节点和父节点上显示添加按钮,这样我就可以为所有节点创建新级别,而不仅仅是父节点。

sample output image at link

What i want and transform this sample to my requirement

My 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)="todoLeafItemSelectionToggle(node)">{{node.item}}</mat-checkbox>
                     <button mat-icon-button (click)="addNewItem(node)"><mat-icon>add</mat-icon></button>
  </mat-tree-node>

  <mat-tree-node *matTreeNodeDef="let node; when: hasNoContent" matTreeNodePadding>
    <button mat-icon-button disabled></button>
    <mat-form-field>
      <mat-label>New item...</mat-label>
      <input matInput #itemValue placeholder="Ex. Lettuce">
    </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.item">
      <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>


<!-- Copyright 2020 Google LLC. All Rights Reserved.
    Use of this source code is governed by an MIT-style license that
    can be found in the LICENSE file at http://angular.io/license -->

My TS Code
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: {[key: string]: any}, 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?.length;
    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);
    const descAllSelected = descendants.length > 0 && descendants.every(child => {
      return this.checklistSelection.isSelected(child);
    });
    return descAllSelected;
  }

  /** 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);

    // Force update for the parent
    descendants.forEach(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: TodoItemFlatNode): void {
    this.checklistSelection.toggle(node);
    this.checkAllParentsSelection(node);
  }

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

  /** Check root node checked state and change it accordingly */
  checkRootNodeSelection(node: TodoItemFlatNode): void {
    const nodeSelected = this.checklistSelection.isSelected(node);
    const descendants = this.treeControl.getDescendants(node);
    const descAllSelected = descendants.length > 0 && descendants.every(child => {
      return 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: TodoItemFlatNode): TodoItemFlatNode | 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;
  }

  /** 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);
  }
}


/**  Copyright 2020 Google LLC. All Rights Reserved.
    Use of this source code is governed by an MIT-style license that
    can be found in the LICENSE file at http://angular.io/license */

谢谢

【问题讨论】:

    标签: angular tree angular-material treeview


    【解决方案1】:

    我没有找到任何直接的解决方案。但我们可以破解它。 Angular 考虑树中的两种节点类型,一种是可扩展的,另一种是不可扩展的。如果一个节点没有子节点,则它是不可扩展的,因此不会显示添加子节点按钮。我们可以欺骗角度认为每个节点都是可扩展的。所以我们需要对代码进行一些修改。

    首先,在TodoItemFlatNode类中添加hasChild:boolean属性:

    export class TodoItemFlatNode {
        item: string;
        level: number;
        expandable: boolean;
        hasChild:boolean; // new property
    }
    

    然后在ChecklistDatabase类中编辑insertItem()函数如下(否则会抛出错误):

    insertItem(parent: TodoItemNode, name: string) {
        if (!parent.children) parent.children=[];
        parent.children.push({ item: name } as TodoItemNode);
        this.dataChange.next(this.data);
    }
    

    像这样编辑TreeChecklistExample组件类中的transfer()函数:

    transformer = (node: TodoItemNode, level: number) => {
        ...
        flatNode.expandable = true;                   // edit this to true to make it always expandable
        flatNode.hasChild = !!node.children?.length;  // add this line. this property will help 
                                                         //us to hide the expand button in a node
        ...
    };
    

    我们完成了.ts,现在我们将对html代码进行一些修改。

    1. 删除第一个&lt;mat-tree-node&gt;,因为我们不再需要它了。
    2. 在第三个&lt;mat-tree-node&gt;*matTreeNodeDef 中删除; when: hasChild
    3. 在切换按钮中添加*ngIf="node.hasChild",使其在节点没有子节点时消失
    4. 在按钮下方添加&lt;div *ngIf="!node.hasChild" class="pad"&gt;&lt;/div&gt;,在展开按钮隐藏时填充空间。
    <mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
      /////// delete the first <mat-tree-node>
    
      <mat-tree-node *matTreeNodeDef="let node; when: hasNoContent" matTreeNodePadding>
        <button mat-icon-button disabled></button>
        <mat-form-field>
          <mat-label>New item...</mat-label>
          <input matInput #itemValue placeholder="Ex. Lettuce">
        </mat-form-field>
        <button mat-button (click)="saveNode(node, itemValue.value)">Save</button>
      </mat-tree-node>
    
      <mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
                       ///////////// remove "; when: hasChild" from *matTreeNodeDef
    
        <button mat-icon-button matTreeNodeToggle
                [attr.aria-label]="'Toggle ' + node.item"
                *ngIf="node.hasChild">   ////////// add this to make expand button 
                                                 // appear when has child
    
          <mat-icon class="mat-icon-rtl-mirror">
            {{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
          </mat-icon>
        </button>
    
        //////////// add this line to make space for removed button
        <div *ngIf="!node.hasChild" class="pad"></div>
    
        <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>
    

    最后在.scss中添加这个样式:

    .pad {
        width: 40px;
    }
    

    https://stackblitz.com/edit/angular-hdejsg?file=src/app/tree-checklist-example.html

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-01-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多