深入解读 Angular Material Tree 公共 API:从指令结构到扁平/嵌套数据源实现
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
mat-tree是 Angular Material 提供的数据层级展示组件,它建立在 CDK Tree 的基础上,用 Material Design 风格封装了一套完整、可无障碍访问的树形控件。本篇文章以仓库中由 API Extractor 自动生成的 goldens/material/tree/index.api.md 为骨架,结合 src/material/tree 的源码实现与 src/material/tree/tree.md 使用文档,逐项解析@angular/material_tree的完整公开 API:组件、指令、数据源、模块声明,以及levelAccessor/childrenAccessor带来的新一代无障碍实现路径。读完后你将能准确理解每个公开类型的能力边界与弃用状态,并能在自己的 Angular 项目中正确选用扁平树或嵌套树方案。
一、API 报告概览:一个包裹 CDK Tree 的 Material 封装层
@angular/material_tree的 API 报告由 API Extractor 自动生成(文件头明确标注 "Do not edit this file. It is a report generated by API Extractor"),它精确刻画了包的公开导出面。从报告的 import 列表可以看出,整个 Material Tree 的几乎所有功能都来源于对 CDK 树形基础设施的复用:
CdkTree、CdkTreeNode、CdkNestedTreeNode、CdkTreeNodeDef、CdkTreeNodePadding、CdkTreeNodeToggle、CdkTreeNodeOutlet,均来自@angular/cdk/tree;DataSource、CollectionViewer来自@angular/cdk/collections;FlatTreeControl、TreeControl来自@angular/cdk/tree;- 运行期还依赖
@angular/cdk/bidi(BidiModule,用于 RTL 布局支持)。
报告的 public-api.ts 将 7 个源文件全部重新导出:node、padding、tree、tree-module、toggle、outlet,以及data-source下的扁平/嵌套两个数据源。这 7 个模块对应了报告中的 8 个公开类:
| 公开类 | 声明位置 | 继承自 | 职责 |
|---|---|---|---|
MatTree | tree.ts | CdkTree | 树容器组件,<mat-tree> |
MatTreeNode | node.ts | CdkTreeNode | 扁平树节点,<mat-tree-node> |
MatNestedTreeNode | node.ts | CdkNestedTreeNode | 嵌套树节点,<mat-nested-tree-node> |
MatTreeNodeDef | node.ts | CdkTreeNodeDef | 节点模板定义,[matTreeNodeDef] |
MatTreeNodePadding | padding.ts | CdkTreeNodePadding | 层级缩进,[matTreeNodePadding] |
MatTreeNodeToggle | toggle.ts | CdkTreeNodeToggle | 展开/收起开关,[matTreeNodeToggle] |
MatTreeNodeOutlet | outlet.ts | CdkTreeNodeOutlet | 子节点渲染出口,[matTreeNodeOutlet] |
MatTreeModule | tree-module.ts | — | NgModule 聚合声明 |
这种“薄封装”的设计意味着:Material Tree 与 CDK Tree 使用完全相同的接口与数据流,区别只在于选择器前缀由cdk-换成mat-,并叠加了 Material 的视觉样式与无障碍增强。
二、MatTree组件:树的容器
MatTree<T, K = T>是树的容器组件,继承CdkTree<T, K>。其源码(tree.ts)揭示了几个关键实现细节:
- 选择器与导出名:
selector: 'mat-tree',exportAs: 'matTree'; - 模板:
<ng-container matTreeNodeOutlet></ng-container>,即根节点由树模板中的matTreeNodeOutlet出口渲染; - 样式与封装:
styleUrl: 'tree.css',encapsulation: ViewEncapsulation.None,类名为mat-tree; - 变更检测:注释明确指出沿用
CdkTree的默认变更检测策略(ChangeDetectionStrategy.Eager),因为树的数据流复杂,不适合 OnPush 下的局部更新假设; - DI 提供:
providers: [{provide: CdkTree, useExisting: MatTree}]——这是理解整个封装的钥匙:任何通过CdkTree注入的依赖在mat-tree中都会解析到MatTree实例,CDK 的内部逻辑原封不动地跑在 Material 组件上。
组件内部只声明了一个@ViewChild(MatTreeNodeOutlet, {static: true}) _nodeOutlet字段,作为数据节点插入的出口引用。
三、三种节点指令:模板、扁平节点与嵌套节点
3.1MatTreeNodeDef:节点模板定义
[matTreeNodeDef]指令(node.ts)用于捕获一个节点的模板,并支持matTreeNodeDefWhen谓词(对应 CDK 的when输入),当数据节点满足条件时选择该模板。它还暴露了一个matTreeNode输入,把节点数据导出到模板上下文中,供模板内的绑定使用:
<mat-tree-node *matTreeNodeDef="let node"> {{node.key}}: {{node.value}} </mat-tree-node>同一个树中可以存在多个节点模板,运行时按when谓词逐条匹配(tree.md 中的“Conditional template”示例展示了如何为特殊节点渲染不同外观)。
3.2MatTreeNode:扁平树节点
<mat-tree-node>是扁平树的节点元素,继承CdkTreeNode。其 host 绑定(node.ts)集中体现了无障碍与交互逻辑:
[attr.aria-expanded]:动态反映节点的展开状态;[attr.aria-level]='level + 1':用层级渲染出 ARIA level;[attr.aria-posinset]/[attr.aria-setsize]:节点在兄弟集合中的位置与集合大小;(click)='_focusItem()':点击时聚焦节点;[tabindex]='_getTabindexAttribute()':由TreeKeyManager统一管理焦点。
API 报告中标记的两个弃用成员也在这段源码里得到印证(@deprecated,@breaking-change 21.0.0移除):
tabIndexInputBinding(别名tabIndex):源码注释说明,默认情况下MatTreeNode通过TreeKeyManager管理焦点,直接设置 tabIndex 会让键盘管理器进入意外状态,因此建议避免使用(node.ts);disabled:disabled只是isDisabled的别名,源码注释同样标记为 21.0.0 移除(node.ts)。
另外ngAcceptInputType_disabled与ngAcceptInputType_tabIndexInputBinding两个静态字段是 Angular 编译器生成的类型收窄标记,用于把模板中的字符串输入安全转换为boolean/number。
3.3MatNestedTreeNode:嵌套树节点
<mat-nested-tree-node>用于嵌套树,继承CdkNestedTreeNode,并实现了AfterContentInit、OnInit、OnDestroy三个生命周期接口。它与扁平节点最大的区别是:子节点在 DOM 中直接嵌套在父节点内部,因此父节点的模板必须包含一个matTreeNodeOutlet出口:
<mat-nested-tree-node *matTreeNodeDef="let node"> {{node.value}} <ng-container matTreeNodeOutlet></ng-container> </mat-nested-tree-node>源码中还值得注意 DI 提供关系(node.ts):MatNestedTreeNode同时把自己注册为CdkNestedTreeNode、CdkTreeNode与CDK_TREE_NODE_OUTLET_NODE,这使得嵌套节点的子节点出口能拿到父节点引用。
3.4 生命周期钩子重写的来历
MatTreeNode和MatNestedTreeNode都重写了ngOnInit/ngAfterContentInit/ngOnDestroy并仅调用super。源码注释解释了原因:这是对 Angular 两个历史 issue(#23091、#19145)的规避——AOT 编译下父类的生命周期钩子不会被自动调用,因此需要子类显式桥接。
四、交互与布局指令:Toggle、Padding、Outlet
4.1MatTreeNodeToggle:展开/收起开关
[matTreeNodeToggle]指令(toggle.ts)是CdkTreeNodeToggle的空包装,暴露matTreeNodeToggleRecursive输入。将其附着在按钮上,点击或键盘激活即可触发树的展开/收起;设为true时递归展开/收起整棵子树:
<mat-tree-node *matTreeNodeDef="let node"> <button matTreeNodeToggle aria-label="toggle tree node" [matTreeNodeToggleRecursive]="true"> <mat-icon>expand</mat-icon> </button> {{node.value}} </mat-tree-node>文档特别提醒:toggle 应挂在<button>元素上以保证键盘可达;若使用图标按钮,必须提供aria-label(tree.md "Adding expand/collapse" 一节)。
4.2MatTreeNodePadding:扁平树专属缩进
[matTreeNodePadding](padding.ts)仅用于扁平树,因为扁平树的所有节点在 DOM 中是同级兄弟,无法用 CSS 的嵌套结构表达层级,必须靠缩进呈现深度:
level(别名matTreeNodePadding):节点深度,源码注释明确说明"padding 为level * indent像素",并通过numberAttribute做输入转换;indent(别名matTreeNodePaddingIndent):每级缩进量,默认 40px,注释注明取自 Material Design 菜单子菜单规范。
嵌套树不需要该指令——它的 DOM 天然嵌套,缩进直接用 CSS 实现即可。
4.3MatTreeNodeOutlet:子节点渲染出口
[matTreeNodeOutlet](outlet.ts)实现了CdkTreeNodeOutlet,通过inject(ViewContainerRef)拿到出口位置的视图容器,并把CDK_TREE_NODE_OUTLET_NODE(即宿主嵌套节点,可选)注入_node字段。它在MatTree模板与嵌套节点模板中标记子节点的插入位置,是嵌套树 DOM 结构成立的基础。
五、数据源:扁平化与嵌套数据的两种接入方式
5.1MatTreeNestedDataSource<T>:嵌套数据源
嵌套数据源(nested-data-source.ts)内部用一个BehaviorSubject<T[]>持有根节点数组。connect(collectionViewer)合并viewChange与数据流,直接返回this.data。它的设计哲学在注释中写得很清楚:嵌套数据源不需要考虑扁平化,也不需要处理展开/收起的数据重组——这些交给TreeControl和各非叶节点即可。
5.2MatTreeFlattener<T, F, K>:扁平化引擎
MatTreeFlattener(flat-data-source.ts)负责把嵌套结构T转换为带层级信息的扁平结构F。构造器接收四个函数:
| 参数 | 类型 | 作用 |
|---|---|---|
transformFunction | (node: T, level: number) => F | 节点转换,附加expandable、level等字段 |
getLevel | (node: F) => number | 读取扁平节点的层级 |
isExpandable | (node: F) => boolean | 判断节点是否可展开 |
getChildren | (node: T) => Observable<T[]> \| T[] \| undefined \| null | 取出节点的子节点(支持同步数组或 Observable) |
其核心算法是三个方法:
_flattenNode:先变换当前节点并压入结果数组;若可展开则取出子节点,同步数组直接递归,Observable 则pipe(take(1))订阅一次后再递归(flat-data-source.ts);_flattenChildren:遍历子节点并维护parentMap(记录每层是否是最后一个兄弟,供树状连线等场景使用)(flat-data-source.ts);expandFlattenedNodes:结合TreeControl的展开状态,把扁平节点过滤成当前可见列表(flat-data-source.ts)。
文件头部的注释给出了一个直观示例:{key: 'Fruits', children: [...]}会被展开成{key: 'Fruits', expandable: true, level: 1}、{key: 'Apple', expandable: false, level: 2}等扁平节点。
5.3MatTreeFlatDataSource<T, F, K>:扁平数据源
MatTreeFlatDataSource(flat-data-source.ts)在内部维护三个BehaviorSubject(_data、_flattenedData、_expandedData):
- 设置
data时依次触发:更新_data→ 用 flattener 生成全量扁平节点 → 同步到treeControl.dataNodes(flat-data-source.ts); connect合并三个信号源:collectionViewer.viewChange(滚动/视图变化)、treeControl.expansionModel.changed(展开状态变化)、_flattenedData(数据变更),每次重算可见节点并推送给树(flat-data-source.ts)。
这套机制的优点是滚动友好:由于输出永远是单层数组,可以无缝配合虚拟滚动等场景。API 报告与源码都将MatTreeFlattener、MatTreeFlatDataSource标记为@deprecated,建议改用childrenAccessor方式,预计 21.0.0 移除。
5.4 新一代接入方式:levelAccessor与childrenAccessor
tree.md 明确给出了两种推荐的接入方式:
levelAccessor:传入一个函数,给定数据项返回其所在层级。数据源输出的是已扁平化的单数组,数据源需要监听(expansionChange)事件并在展开/收起时重新提供可见节点数组;childrenAccessor:传入一个函数,给定数据项返回其子节点。此时数据源只需提供根节点数组,树的层级关系由该访问器即时解析;trackBy:与ngFor的trackBy类似,告诉树如何唯一标识节点,用于在数据更新时高效复用 DOM 节点:
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" [trackBy]="trackByFn">5.5 无障碍(Accessibility)
<mat-tree>实现了 WAI-ARIA APG 的 tree widget 模式(tree.md Accessibility 一节),包含键盘导航、正确的 roles 与 ARIA 属性。新无障碍特性要求使用levelAccessor/childrenAccessor;使用旧式treeControl的树因向后兼容原因无法实现正确的无障碍行为。两个关键点:
isExpandable:所有可展开的mat-tree-node/mat-nested-tree-node必须设置该属性,树才能正确判断节点可展开性;(activation)事件:树节点通过键盘激活时会触发activation输出,可用于执行与点击等价的操作:
<mat-tree-node *matTreeNodeDef="let node" (click)="performAction(node)" (activation)="performAction($event)"> </mat-tree-node>这里$event携带节点数据,与matTreeNodeDef隐式导出的数据一致。对应地,MatTreeNode与MatNestedTreeNode的指令声明中都声明了outputs: ['activation', 'expandedChange'](node.ts),API 报告里也能看到这两个事件。
六、MatTreeModule模块声明与使用方式
MatTreeModule(tree-module.ts)的ɵmod声明揭示了完整的依赖图:
- imports:
CdkTreeModule+ 7 个 Material 指令(MatNestedTreeNode、MatTreeNodeDef、MatTreeNodePadding、MatTreeNodeToggle、MatTree、MatTreeNode、MatTreeNodeOutlet); - exports:
BidiModule(提供 RTL 方向支持)+ 同样的 7 个指令。
因此在实际项目中,使用树功能只需在模块中导入MatTreeModule即可,无需额外导入 CDK Tree 模块或 Bidi 模块:
import {MatTreeModule} from '@angular/material/tree'; @NgModule({ imports: [MatTreeModule], }) export class MyModule {}由于MatTree、MatTreeNode等均通过providers: [{provide: CdkXxx, useExisting: MatXxx}]覆盖了 CDK 的注入令牌,即便同时导入了CdkTreeModule也不会产生重复逻辑。
七、进一步探索:从 API 报告到源码与测试
API 报告是了解@angular/material_tree公开面的最快入口,但要真正掌握行为细节,建议继续阅读仓库内以下资源:
- 使用文档:src/material/tree/tree.md——包含扁平/嵌套树模板、toggle、padding、条件模板、数据源接入、无障碍等完整示例;
- 核心实现:tree.ts、node.ts、padding.ts、toggle.ts、outlet.ts;
- 数据源:flat-data-source.ts、nested-data-source.ts;
- 测试用例:tree.spec.ts、tree-using-tree-control.spec.ts、tree-using-legacy-key-manager.spec.ts,分别覆盖新式访问器、
treeControl与旧式键盘管理器三条路径; - 测试 Harness:testing/tree-harness.ts 与 testing/tree-harness.spec.ts,可用于组件测试中定位树节点、读取展开状态等;
- 样式与主题:tree.scss、_tree-theme.scss、_m2-tree.scss 与 _m3-tree.scss(M2/M3 双主题实现);
- Golden 基准:goldens/material/tree/index.api.md 本身即公开 API 的权威基准,仓库通过 API Extractor 校验源码与基准的一致性,任何 API 变更都会在此文件中体现。
总结
@angular/material_tree的公开 API 高度凝练:7 个指令/组件加上 2 个(其中一个已弃用的)数据源与 1 个扁平化工具类,全部建立在 CDK Tree 之上。通过 API 报告,可以清晰分辨出当前推荐使用的MatTree+MatTreeNode+levelAccessor/childrenAccessor/isExpandable新式组合,与已标记弃用的MatTreeFlatDataSource/MatTreeFlattener/tabIndex/disabled(计划在 21.0.0 移除)。理解这份 API 报告,就掌握了这一组件的全部能力边界与演进方向——这正是一份由 API Extractor 生成的 API 报告相较于普通使用文档的独特价值所在。
【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考