You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Angular Material 6.0.1的mat-tree组件实现过滤功能?

哈哈,我之前做项目的时候刚好遇到过这个需求!Angular Material的mat-tree确实没内置树形过滤功能,但咱们完全可以自己实现,而且思路不算复杂,我给你分享一个我常用的可行方案:

实现mat-tree过滤功能的完整方案

核心思路是递归过滤节点数据,同时保留匹配节点的所有父节点(让用户能看到匹配节点的层级关系),然后更新树的数据源,最后自动展开相关父节点来提升体验。

1. 搭建基础结构

先确保你的mat-tree已经正常运行,比如嵌套树用NestedTreeControl和MatTreeNestedDataSource,然后添加一个过滤输入框:

<mat-form-field appearance="fill" class="filter-input">
  <mat-label>过滤节点</mat-label>
  <input matInput [(ngModel)]="filterText" (input)="onFilterInput()">
  <mat-icon matSuffix>search</mat-icon>
</mat-form-field>

<!-- 你的mat-tree核心结构 -->
<mat-tree [dataSource]="filteredDataSource" [treeControl]="treeControl">
  <!-- 普通节点模板 -->
  <mat-nested-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
    <li class="mat-tree-node">
      <button mat-icon-button matTreeNodeToggle>
        <mat-icon>{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}</mat-icon>
      </button>
      <!-- 可选:高亮匹配文本 -->
      <span *ngIf="isNodeMatch(node, filterText)" class="matched-text">{{node.name}}</span>
      <span *ngIf="!isNodeMatch(node, filterText)">{{node.name}}</span>
    </li>
  </mat-nested-tree-node>

  <!-- 包含子节点的模板 -->
  <mat-tree-node *matTreeNodeDef="let node; when: hasChild">
    <li>
      <div class="mat-tree-node">
        <button mat-icon-button matTreeNodeToggle>
          <mat-icon>{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}</mat-icon>
        </button>
        <span *ngIf="isNodeMatch(node, filterText)" class="matched-text">{{node.name}}</span>
        <span *ngIf="!isNodeMatch(node, filterText)">{{node.name}}</span>
      </div>
      <ul [class.example-tree-invisible]="!treeControl.isExpanded(node)">
        <ng-container matTreeNodeOutlet></ng-container>
      </ul>
    </li>
  </mat-tree-node>
</mat-tree>

2. 编写过滤核心逻辑

在组件类里,我们需要完成几个关键操作:保存原始数据、递归过滤节点、自动展开父节点:

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

// 定义你的节点类型
interface TreeItem {
  name: string;
  children?: TreeItem[];
}

@Component({
  selector: 'app-tree-filter',
  templateUrl: './tree-filter.component.html',
  styleUrls: ['./tree-filter.component.scss']
})
export class TreeFilterComponent {
  treeControl = new NestedTreeControl<TreeItem>(node => node.children);
  originalDataSource = new MatTreeNestedDataSource<TreeItem>();
  filteredDataSource = new MatTreeNestedDataSource<TreeItem>();
  filterText = '';

  // 示例原始树数据
  private readonly originalTreeData: TreeItem[] = [
    {
      name: '前端框架',
      children: [
        { name: 'Angular' },
        { name: 'React' },
        { name: 'Vue' }
      ]
    },
    {
      name: '后端语言',
      children: [
        { name: 'Node.js' },
        { name: 'Python' },
        { name: 'Java' }
      ]
    }
  ];

  constructor() {
    this.originalDataSource.data = this.originalTreeData;
    this.filteredDataSource.data = [...this.originalTreeData];
  }

  hasChild = (_: number, node: TreeItem) => !!node.children && node.children.length > 0;

  // 过滤输入触发的函数
  onFilterInput() {
    const filterValue = this.filterText.trim().toLowerCase();
    if (!filterValue) {
      // 清空过滤时恢复原始数据
      this.filteredDataSource.data = [...this.originalTreeData];
      this.treeControl.collapseAll(); // 可选:清空后折叠所有节点
      return;
    }

    // 递归过滤节点
    const filteredData = this.recursiveFilter([...this.originalTreeData], filterValue);
    this.filteredDataSource.data = filteredData;

    // 自动展开所有包含匹配节点的父节点
    this.expandFilteredNodes(filteredData);
  }

  // 递归过滤的核心函数
  private recursiveFilter(nodes: TreeItem[], filter: string): TreeItem[] {
    return nodes.map(node => {
      const clonedNode = { ...node };
      // 递归处理子节点
      if (clonedNode.children) {
        clonedNode.children = this.recursiveFilter(clonedNode.children, filter);
      }
      // 判断当前节点是否匹配,或者子节点有保留的(父节点需要显示)
      const isMatch = clonedNode.name.toLowerCase().includes(filter);
      const hasValidChildren = clonedNode.children && clonedNode.children.length > 0;
      return isMatch || hasValidChildren ? clonedNode : null;
    }).filter(node => node !== null) as TreeItem[];
  }

  // 自动展开过滤后的节点
  private expandFilteredNodes(nodes: TreeItem[]) {
    nodes.forEach(node => {
      this.treeControl.expand(node);
      if (node.children) {
        this.expandFilteredNodes(node.children);
      }
    });
  }

  // 可选:判断节点是否匹配,用于高亮文本
  isNodeMatch(node: TreeItem, filter: string): boolean {
    return node.name.toLowerCase().includes(filter.trim().toLowerCase());
  }
}

3. 可选优化:高亮匹配文本

给匹配的文本添加高亮样式,在SCSS文件中:

.matched-text {
  background-color: #ffeb3b;
  padding: 2px 4px;
  border-radius: 3px;
}

注意事项

  • 必须克隆原始数据进行过滤,避免修改原数据导致清空过滤时无法恢复
  • 如果是平级树(Flat Tree),思路类似,只需调整过滤逻辑适配FlatTree的结构(比如处理level属性)
  • 对于大数据量的树,建议添加防抖处理(比如用RxJS的debounceTime),避免每次输入都触发过滤,提升性能

内容的提问来源于stack exchange,提问作者SatAj

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:25:11