【问题标题】:Angular Material mat-tree get checkbox valuesAngular Material mat-tree 获取复选框值
【发布时间】:2018-09-10 07:35:06
【问题描述】:

我正在使用带有复选框的Angular Material v6.0 MatTreeModule (mat-tree)。 但是我很难弄清楚如何确定哪些节点已被检查,哪些节点尚未被检查。 在 Angular Material 的示例中,他们提供了非常好的源代码来设置它,我能够很好地设置它。

但是,我无法确定哪些复选框已被选中,哪些未选中。我已经尝试了几个小时试图解决这个问题。

我的目标是最终用户将选中或取消选中树中的复选框,然后我需要在他们做出选择后运行一些进程。

但我完全无法弄清楚哪些 mat-tree-nodes 被检查了哪些没有被检查,而且我在任何地方都找不到好的工作示例。

找到关键源码here

更多关于 mat-tree 的信息请见here

谁能帮助确定复选框是否已被选中?

谢谢。

根据请求,我从两个代码文件中添加代码:

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

来自“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>

如前所述,要查看完整的源代码及其运行演示,请访问: https://stackblitz.com/angular/gabkadkvybq?file=app%2Ftree-checklist-example.html

谢谢。

【问题讨论】:

  • 您的代码链接很好,但在您的帖子中分享minimal reproducible example 更好。也证明了你调试的努力。
  • 嗨@juan-vega,我真的对答案很感兴趣,我实际上正在处理这种问题。

标签: angular angular-material angular-forms angular-template


【解决方案1】:

我认为选择列表(在您的 Stackblitz 演示链接中)是您想要的。这意味着您必须自己跟踪MatTree 中的选择:它不会为您执行此操作,因为它不知道如何处理您在节点上使用的MatCheckboxes。

在您的演示中,这是通过使用/维护SelectionModel(不属于MatTree@angular/cdk/collections 的集合)来完成的。修改后的 Stackblitz 示例为 here(只需在 MatTree 上选择一些节点with children)。

演示的重要部分是每次点击MatCheckbox 都会触发@Output() change 该复选框用于触发todoItemSelectionToggle 方法,更新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);
}

SelectionModel 是基于Set 的集合,该集合由@angular 团队构建,完全供开发人员使用,使用允许多项选择的组件来帮助他们跟踪这些组件的更改。您可以在此处查看有关此系列的更多详细信息:https://github.com/angular/components/blob/master/src/cdk/collections/selection-model.ts

就像 javascript 中的所有内容一样,这里没有魔法,基本上,它的构造函数接受一个布尔参数来定义 SelectionModel&lt;T&gt;(它是一个泛型)是存储多个值(true)还是一个值。它还具有方便的方法,例如sort(predicate?: (a: T, b: T) =&gt; number)select(...values: T[]) 添加对象、deselect(...values: T[]) 删除对象、toggle(o: T) 添加(如果它不存在)或删除(如果它已经存在)。在内部,默认情况下,比较是通过引用完成的,所以{a:1} != {a:1}

【讨论】:

  • 非常感谢。我真的很感谢你的回答。周日一整天后,我失去了无法使用 MatTree 的希望。但现在不是了。这正是我们正在寻找的,而这个解决方案将会实现。
  • StackBlitz 演示网址已损坏 @jpavel
  • @yaswanthkoneri,我修复了链接。出于某种原因,我删除了我的 stackblitz 示例。我不确定我在该示例中做了什么,但我认为这只是展示如何检索在某些树节点上选择的值。如果是这样,我重建示例..
  • @JuanVega 您是否有可用的最终工作版本(stackblitz 源或类似源),因为我正在寻求相同的分辨率?谢谢。
【解决方案2】:

你可以直接从checklistselection中获取值

值 = this.checklistSelection.selected

这将准确地返回所有检查项目的值

【讨论】:

    【解决方案3】:

    您提到的示例使用了 Angular 材料选择模型。

    当属性 = SelectionModel 和模型类型 = TodoItemFlatNode。 你这样做 ->

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

    现在 checklistSelection 属性将包含并且您可以访问所有这些方法。

    changed、hasValue、isSelected、selection、onChange、toggle 等

    所以现在您可以通过访问上述方法来应用您的选择逻辑。

    例子

    this.checklistSelection.isSelected ?
    

    【讨论】:

      【解决方案4】:

      不完全是一个答案(我还没有),但是由于我处理同样的问题 atm 并且我没有足够的声誉来添加评论,所以我会在这里发布这个(mods,如果这是违反规则的)。

      在我看来 mat-tree 是有问题的。这里已经讨论过了:https://github.com/angular/material2/issues/11400,但是那里提供的解决方案仍然没有解决取消/检查子/父节点时的奇怪行为。这可能会转移到您将从 jpavel 提到的 SelectionModel 获得的值。请注意,因为您正在处理此功能。

      【讨论】:

      • @icepero,我不会将此行为归类为mat-tree 错误。这个例子肯定有问题,但这似乎不是mat-tree 的错。我会尝试(不是在接下来的 2 周内)修复这个例子。它会给todoItemSelectionToggle 方法带来额外的复杂性,但我同意这是必需的。
      【解决方案5】:

      通过进行以下代码更改,我可以从树控件中获得选中的复选框节点:

      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);
          
                const partialSelection = this.treeControl.dataNodes.filter(x => 
                                this.descendantsPartiallySelected(x));
          
                 console.log(this.checklistSelection.selected, partialSelection);
            }
      

      在控制台中,您可以看到所有选定的节点以及部分选定的节点值。

      【讨论】:

        猜你喜欢
        • 2019-05-08
        • 1970-01-01
        • 2019-08-30
        • 2019-03-05
        • 2018-11-09
        • 1970-01-01
        • 2022-11-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多