
Angular Material mat-tree 完全指南从扁平/嵌套树到无障碍键盘导航【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/componentsmat-tree是 Angular Material 提供的、遵循 Material Design 规范的树形组件用于展示具有层级关系的数据。本指南以 src/material/tree/tree.md 文档为核心结合仓库中的源码与官方示例系统讲解树模板的编写、两种数据接入方式levelAccessor与childrenAccessor、展开/折叠与缩进控制以及 ARIA 无障碍支持与键盘导航帮助你快速在项目中落地一个可访问、高性能的层级列表组件。mat-tree 与 CDK Tree 的关系mat-tree建立在 Angular CDK 的CdkTree基础之上使用与 CDK Tree 几乎一致的接口来接收数据源和定义模板差别仅在于选择器前缀Material 版本使用mat-前缀而 CDK 版本使用cdk-前缀。从源码实现看这一关系是直接的继承与包装关系tree.ts 中的MatTree组件直接extends CdkTreeT, K模板只是一个ng-container matTreeNodeOutlet并通过providers: [{provide: CdkTree, useExisting: MatTree}]复用 CDK 的能力node.ts 中的MatTreeNode、MatTreeNodeDef、MatNestedTreeNode分别包装了 CDK 的CdkTreeNode、CdkTreeNodeDef、CdkNestedTreeNodepadding.ts、toggle.ts、outlet.ts 同样是 CDK 指令的 Material 风格包装。使用MatTreeModule即可引入全部相关指令与组件可在 tree-module.ts 中查看导出清单。树的两大形态扁平树与嵌套树树存在两种类型扁平树Flat tree与嵌套树Nested tree二者的 DOM 结构完全不同。扁平树Flat tree层级被压平节点不是嵌套渲染在彼此内部而是按顺序作为兄弟节点依次渲染mat-tree mat-tree-node parent node /mat-tree-node mat-tree-node -- child node1 /mat-tree-node mat-tree-node -- child node2 /mat-tree-node /mat-tree扁平树通常更易于样式化和检查 DOM也更容易适配滚动相关的变体例如无限滚动或虚拟滚动。嵌套树Nested tree子节点在 DOM 中位于父节点内部父节点包含一个节点出口node outlet子节点被投影进该出口mat-tree mat-nested-tree-node parent node mat-nested-tree-node -- child node1 /mat-nested-tree-node mat-nested-tree-node -- child node2 /mat-nested-tree-node /mat-nested-tree-node /mat-tree当层级关系需要用扁平节点难以表达的可视化方式呈现时嵌套树更容易处理。需要特别注意的是源码中CdkTree会对节点类型进行校验当同一棵树混用扁平与嵌套节点时会发出警告见 tree.ts 的_setNodeTypeIfUnset因此请确保整棵树只使用一种节点类型。编写树的模板要使用树组件必须先定义一个树节点模板。树节点分两种mat-tree-node用于扁平树与mat-nested-tree-node用于嵌套树。节点模板决定了节点的外观、展开/折叠控件以及嵌套子节点的结构。节点定义通过任意带有matTreeNodeDef指令的元素指定。该指令会把节点数据导出供模板内的绑定使用mat-tree-node *matTreeNodeDeflet node {{node.key}}: {{node.value}} /mat-tree-node在源码中MatTreeNodeDef声明于 node.ts输入属性matTreeNodeDefWhen对应模板中的when谓词matTreeNode输入则用于传入节点数据。扁平树节点模板扁平树使用节点的level层级来渲染节点同时为屏幕阅读器确定层级关系。层级可以通过levelAccessor提供如果提供了childrenAccessorMatTree会自动计算层级。缩进可以有两种方式实现在节点上应用matNodePadding指令推荐或基于aria-level属性编写自定义样式。aria-level由组件自动输出MatTreeNode的 host 绑定中包含[attr.aria-level]: level 1见 node.ts因此 CSS 可以这样使用.mat-tree-node[aria-level2] { margin-left: 80px; }嵌套树节点模板使用嵌套树节点时节点模板内必须包含一个matTreeNodeOutlet它标记了该节点子节点的渲染位置mat-nested-tree-node *matTreeNodeDeflet node {{node.value}} ng-container matTreeNodeOutlet/ng-container /mat-nested-tree-nodeMatTreeNodeOutlet的实现位于 outlet.ts它注入ViewContainerRef作为子节点的容器嵌套层级正是通过这种出口机制逐层展开实现的。添加展开/折叠功能matTreeNodeToggle指令用于为树节点添加展开/折叠功能。toggle 会调用matTree上的展开/折叠方法通过设置[matTreeNodeToggleRecursive]为true还可以递归地展开/折叠整个子树mat-tree-node *matTreeNodeDeflet node button matTreeNodeToggle aria-labeltoggle tree node [matTreeNodeToggleRecursive]true mat-iconexpand/mat-icon /button {{node.value}} /mat-tree-nodematTreeNodeToggle应挂载在按钮元素上在点击或键盘激活时触发。对于纯图标按钮务必提供aria-label这与 Material Design 无障碍规范一致。其 Material 包装见 toggle.ts底层逻辑是 CDK 的CdkTreeNodeToggle。Toggle 可以放在树节点模板中的任意位置并且仅通过click动作触发。官方示例 tree-flat-child-accessor-overview 展示了一个更完整的写法叶子节点使用禁用的图标按钮占位以保持对齐可展开节点使用matTreeNodeToggle按钮图标根据tree.isExpanded(node)在expand_more与chevron_right之间切换mat-tree #tree [dataSource]dataSource [childrenAccessor]childrenAccessor !-- 叶子节点模板 -- mat-tree-node *matTreeNodeDeflet node matTreeNodePadding button matIconButton disabled/button {{node.name}} /mat-tree-node !-- 可展开节点模板 -- mat-tree-node *matTreeNodeDeflet node;when: hasChild matTreeNodePadding matTreeNodeToggle [cdkTreeNodeTypeaheadLabel]node.name button matIconButton matTreeNodeToggle [attr.aria-label]Toggle node.name mat-icon classmat-icon-rtl-mirror {{tree.isExpanded(node) ? expand_more : chevron_right}} /mat-icon /button {{node.name}} /mat-tree-node /mat-tree缩进指令仅扁平树matTreeNodePadding可放在扁平树的节点模板中用来展示扁平树节点的层级缩进mat-tree-node *matTreeNodeDeflet node matNodePadding {{node.value}} /mat-tree-node嵌套树则不需要该指令因为 DOM 的层级结构天然可以通过 CSS 添加缩进。MatTreeNodePadding见 padding.ts提供两个输入matTreeNodePadding节点的层级深度缩进量 level * indent像素matTreeNodePaddingIndent每级缩进像素数默认值为40px来自 Material Design 菜单子菜单规范。mat-tree-node *matTreeNodeDeflet node matTreeNodePadding2 matTreeNodePaddingIndent32 {{node.value}} /mat-tree-node条件模板when 谓词树可以包含多个节点模板通过模板的when谓词为特定数据节点选择对应模板。当多个模板存在时CdkTree会在渲染时逐一调用谓词找到第一个返回true的模板如果都没有命中则回退到没有when的默认模板见 tree.ts 的_getNodeDefmat-tree-node *matTreeNodeDeflet node matTreeNodePadding {{node.value}} /mat-tree-node mat-tree-node *matTreeNodeDeflet node; when: isSpecial matTreeNodePadding [ A special node {{node.value}} ] /mat-tree-nodeisSpecial (_: number, node: TreeNode) node.special true;注意树中最多只能有一个不带when的默认节点模板否则会抛出getTreeMultipleDefaultNodeDefsError见 tree-errors.ts。数据源把树连接到数据与mat-table类似你可以通过DataSource向树提供数据。当树收到DataSource后会调用其connect()方法该方法返回一个会发射数据数组的 Observable。每当数据源向该流发射新数据时树都会重新渲染。正因为数据源掌握着这条数据流所以更新树的时机完全由数据源负责可以是节点展开变化、WebSocket 连接、用户交互、模型更新、定时器等等任何时机。向树提供数据有两种主要方式方式数据形态适用场景扁平数据 levelAccessor数据源已将嵌套结构展开成单个数组数据源自身已经做了扁平化处理仅根节点数据 childrenAccessor数据源直接提供嵌套结构数据源以嵌套结构存在在模板上两种方式对应的写法如下!-- 方式一扁平数据 levelAccessor -- mat-tree [dataSource]dataSource [levelAccessor]levelAccessor ... /mat-tree !-- 方式二根节点数据 childrenAccessor -- mat-tree [dataSource]dataSource [childrenAccessor]childrenAccessor ... /mat-treelevelAccessorlevelAccessor是一个函数给定一个数据项返回该数据在树结构中所处的层级。如果提供了levelAccessor则dataSource提供的数据应包含全部可渲染节点的单个数组。此时数据源负责处理节点的展开/折叠事件可以通过mat-tree-node和mat-nested-tree-node上的(expansionChange)事件监听并在必要时提供更新后的可渲染节点数组levelAccessor (node: FileNode) node.level;mat-tree-node *matTreeNodeDeflet node (expansionChange)onNodeExpansionChange($event) ... /mat-tree-node从源码看当树使用levelAccessor时CdkTree会基于扁平数组中相邻节点的层级差推导父子关系_findParentByLevel从当前节点向前回溯找到第一个层级更低的节点作为父节点见 tree.ts 的_findChildrenByLevel与_findParentForNode。因此扁平数组必须保持先父后子、兄弟相邻的顺序并且节点层级单调递减地排列。childrenAccessorchildrenAccessor是一个函数给定一个数据项返回该数据项的子节点。如果提供了childrenAccessor则dataSource提供的数据应只包含树的根节点。childrenAccessor (node: FoodNode) node.children ?? [];官方示例 tree-flat-child-accessor-overview 与 tree-nested-child-accessor-overview 都使用这种模式数据结构如下interface FoodNode { name: string; children?: FoodNode[]; } const EXAMPLE_DATA: FoodNode[] [ { name: Fruit, children: [{name: Apple}, {name: Banana}, {name: Fruit loops}], }, { name: Vegetables, children: [ { name: Green, children: [{name: Broccoli}, {name: Brussels sprouts}], }, { name: Orange, children: [{name: Pumpkins}, {name: Carrots}], }, ], }, ];注意childrenAccessor的返回值既可以是数组也可以是ObservableT[]——源码中通过coerceObservable统一处理见 tree.ts 的类型声明与 tree.ts 的调用这使得异步加载子节点如按需请求后端数据成为可能。两种接入方式不可混用levelAccessor与childrenAccessor只能二选一treeControl同理。CdkTree在ngOnInit阶段会运行_checkTreeControlUsage校验三者均未提供时抛出getTreeControlMissingError提供超过一个时抛出getMultipleTreeControlsError见 tree.ts。trackBy 提升性能为提升渲染性能可以向树提供trackBy函数其用法与 AngularngFor的trackBy一致。它告诉树如何唯一标识节点以便在每次数据更新时跟踪数据如何变化节点的新增/移除/移动/身份变化mat-tree [dataSource]dataSource [treeControl]treeControl [trackBy]trackByFntrackByFn (_: number, node: FileNode) node.id;源码中的内部实现细节值得了解若未提供trackByCdkTree会默认使用展开键expansionKey未提供时即节点对象本身作为追踪键见 tree.ts 与_getExpansionKey数据差异检测使用 Angular 的IterableDiffer在renderNodeChanges中依据 diff 结果执行节点的插入、移除与移动并通过forEachIdentityChange在数据对象变化但键不变时更新模板上下文见 tree.ts通过expansionKey输入可以为展开状态指定稳定标识避免因节点对象重建而丢失展开状态。自定义 DataSource 的完整示例参考仓库中的 tree-loadmore 示例按需加载更多与 tree-dynamic 示例动态增删节点一个基于childrenAccessor的 DataSource 骨架如下import {CollectionViewer, DataSource} from angular/cdk/collections; import {BehaviorSubject, Observable} from rxjs; export class TreeDataSource implements DataSourceFoodNode { private dataSubject new BehaviorSubjectFoodNode[](ROOT_NODES); connect(): ObservableFoodNode[] { return this.dataSubject.asObservable(); } disconnect(): void { this.dataSubject.complete(); } addNode(parent: FoodNode, child: FoodNode): void { // 更新数据后通过 dataSubject.next(...) 触发树的重新渲染 } }无障碍支持mat-tree实现了 WAI-ARIA 的 tree 控件模式包括键盘导航以及合适的 roles 与 ARIA 属性。这些能力由底层的CdkTree提供host 上设置了roletree并将keydown事件转发给内部的TreeKeyManager见 tree.ts每个节点 host 自动输出aria-expanded、aria-level0 基层级 1、aria-posinset、aria-setsize属性见 node.ts其中posinset/setsize依据节点在父节点子集合中的位置与数量计算见 tree.ts键盘导航由TreeKeyManager承担支持方向键、Home/End、输入字母的 typeahead 查找、禁用节点跳过等策略见 tree.ts 的_initializeKeyManager。注意为了使用上述新的无障碍特性迁移到levelAccessor与childrenAccessor是必须的。仍然使用treeControl的树出于向后兼容的考虑不会实现正确的无障碍特性。isExpandable为了让树正确判断某个节点是否可展开必须在所有可展开的mat-tree-node或mat-nested-tree-node上设置isExpandable属性mat-tree-node *matTreeNodeDeflet node matTreeNodePadding matTreeNodeToggle [isExpandable]hasChild(0, node) {{node.name}} /mat-tree-node该属性用于驱动aria-expanded等属性的正确输出——不可展开的节点不应暴露已展开/已折叠的状态。激活动作activation对于节点在激活或点击时有动作的树mat-tree-node会发射(activation)事件可在用户通过键盘交互激活节点时监听点击仍走(click)mat-tree-node *matTreeNodeDeflet node (click)performAction(node) (activation)performAction($event) /mat-tree-node在这个例子中$event包含节点的数据等价于matTreeNodeDef上下文中隐式传递的数据。键盘激活与鼠标点击通过两条路径汇聚到同一个业务方法确保两种交互方式行为一致。键盘导航速查基于 WAI-ARIA tree 模式与TreeKeyManager的实现mat-tree默认支持的键盘操作包括按键行为↑ / ↓移动到上一个 / 下一个可见节点→展开节点若可展开或移动到第一个子节点←折叠节点若已展开或移动到父节点Home / End移动到第一个 / 最后一个可见节点字符键typeahead 查找输入字母跳转到以该字符开头的节点Enter / Space触发节点激活结语mat-tree在 CDK Tree 的坚实基础上为 Angular 应用提供了开箱即用的 Material Design 树形组件。掌握两个关键决策点即可覆盖绝大多数场景选型上根据数据形态已扁平化的数组或嵌套结构决定levelAccessor或childrenAccessor呈现上根据可视化需求选择扁平树便于虚拟滚动与调试或嵌套树DOM 天然体现层级。在此基础上配合matTreeNodeToggle、matTreeNodePadding、trackBy以及isExpandable/activation等无障碍接口即可构建出高性能、可访问的生产级层级列表。仓库中还提供了大量可直接参考的官方示例src/components-examples/material/tree包括扁平/嵌套树的 child-accessor 总览、动态增删tree-dynamic、按需加载tree-loadmore、传统键盘接口tree-legacy-keyboard-interface以及组件测试 Harnesstree-harness建议结合本文逐一代入实践。【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考