Angular CDK 是一个 Angular 组件开发工具箱,也是 Material UI 组件库(Angular)的底层基座,是其UI无关或弱UI的局部(tree-control是真正UI无关的外围)。
CDK 尽管是 Material UI 组件库的依赖,但它并不与 Material UI 组件库有耦合,咱们能够独立应用 CDK,咱们的 Ng DevUI 组件库就有应用到 CDK Scrolling 和 CDK Overlay 等能力。
1 先用起来
- 装置 cdk:
npm i @angular/cdk
- 导入 cdk tree 模块
import { CdkTreeModule } from '@angular/cdk/tree'
- 应用
cdk-tree
组件
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl"> <cdk-tree-node *cdkTreeNodeDef="let node" cdkTreeNodePadding [style.display]="shouldRender(node) ? 'flex' : 'none'" class="example-tree-node" > {{node.label}} </cdk-tree-node> <cdk-tree-node *cdkTreeNodeDef="let node; when hasChild" cdkTreeNodePadding [style.display]="shouldRender(node) ? 'flex' : 'none'" class="example-tree-node" > <button cdkTreeNodeToggle (click)="node.isExpanded = !node.isExpanded" > {{treeControl.isExpanded(node) ? '收起' : '开展'}} </button> {{node.label}} </cdk-tree-node></cdk-tree>
import { Component } from '@angular/core';import { FlatTreeControl } from '@angular/cdk/tree';interface ExampleBaseNode { label: string; level: number; isExpanded?: boolean; isExpanded?: boolean;}const TREE_DATA: ExampleBaseNode[] = [ { label: 'Fruit', expandable: true, level: 0 }, { label: 'Apple', expandable: false, level: 1 }, { label: 'Vegetables', expandable: false, level: 0 },];@Component({ selector: 'app-tree-base-demo', templateUrl: './tree-base-demo.component.html', styleUrls: ['./tree-base-demo.component.scss']})export class TreeBaseDemoComponent { // 树控制器,必选 treeControl = new FlatTreeControl<ExampleBaseNode>( node => node.level, node => node.expandable, ); // 数据源,不传没法显示内容 dataSource = TREE_DATA; // 判断是否显示开展/收起按钮 hasChild = (_: number, node: ExampleBaseNode) => node.expandable; // 判断是否显示节点(折叠状态不显示) shouldRender(node: ExampleBaseNode) { let parent = this.getParentNode(node); while (parent) { if (!parent.isExpanded) { return false; } parent = this.getParentNode(parent); } return true; } // 工具办法,获取父节点 getParentNode(node: ExampleBaseNode) { const nodeIndex = TREE_DATA.indexOf(node); for (let i = nodeIndex - 1; i >= 0; i--) { if (TREE_DATA[i].level === node.level - 1) { return TREE_DATA[i]; } } return null; }}
.example-tree-node { display: flex; align-items: center;}
成果如下:
2 源码构造
cdk/tree├── control // TreeControl| ├── base-tree-control.ts // 抽象类| ├── flat-tree-control.ts // 扁平树| ├── nested-tree-control.ts // 嵌套树| └── tree-control.ts // 接口├── index.ts├── nested-node.ts // 嵌套树节点├── node.ts // 树节点组件├── outlet.ts // 节点进口├── padding.ts // 节点padding├── public-api.ts // 对外裸露的api├── toggle.ts // 节点开展/收起├── tree-errors.ts // 谬误日志├── tree-module.ts // 入口模块└── tree.ts // 树组件
3 tree 组件源码解析
Tree
组件最外围的性能:
- 渲染层级构造
- 开展/收起子节点
CdkTree
外围源码剖析步骤:
- 先看极简
demo
的组成 - 从外到内做整体剖析
- 再做要害模块剖析
3.1 极简demo的组成
<cdk-tree>
组件<cdk-tree-node>
组件cdkTreeNodeDef
指令cdkTreeNodePadding
指令cdkTreeNodeToggle
指令dataSource
数据结构treeControl
控制器shouldRender
办法hasChild
办法
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl"> <cdk-tree-node *cdkTreeNodeDef="let node" cdkTreeNodePadding [style.display]="shouldRender(node) ? 'flex' : 'none'" class="example-tree-node" > {{node.label}} </cdk-tree-node> <cdk-tree-node *cdkTreeNodeDef="let node; when hasChild" cdkTreeNodePadding [style.display]="shouldRender(node) ? 'flex' : 'none'" class="example-tree-node" > <button cdkTreeNodeToggle (click)="node.isExpanded = !node.isExpanded" > {{treeControl.isExpanded(node) ? '收起' : '开展'}} </button> {{node.label}} </cdk-tree-node></cdk-tree>
3.2 cdk-tree 组件
cdk-tree
只是一个节点进口的容器
,而后定义了一些
- 输出参数,如数据源
dataSource
和树控制器treeControl
; - 操作树节点的办法,如插入节点的
inserNode
。
@Component({ selector: 'cdk-tree', template: `<ng-container cdkTreeNodeOutlet></ng-container>`,})export class CdkTree { // 数据源,可读写 @Input() get dataSource() { return this._dataSource; } set dataSource(dataSource) { if (this._dataSource !== dataSource) { this._switchDataSource(dataSource); } } private _dataSource; // 树节点进口容器 @ViewChild(CdkTreeNodeOutlet, {static: true}) _nodeOutlet: CdkTreeNodeOutlet; // 所有树节点 @ContentChildren(CdkTreeNodeDef) _nodeDefs: QueryList<CdkTreeNodeDef<T>>; // 树控制器 @Input() treeControl; // 插入节点 insertNode(nodeData, index) {} // 渲染节点 renderNodeChanges(data) {}}
3.3 cdk-tree-node 组件
有两种类型:
cdk-tree-node
是根底树节点,用于扁平树cdk-nested-tree-node
继承自cdk-tree-node
,用于嵌套树
cdk-tree-node
组件比较简单,就定义了几个属性:
- data
- isExpanded
- level
@Directive({ selector: 'cdk-tree-node',})export class CdkTreeNode { // 节点数据,可读写 get data() { return this._data; } set data(value) { this._data = value; } protected _data; // 是否开展,只读 get isExpanded() { return this._tree.treeControl.isExpanded(this._data); } // 以后层级,只读 get level() { return this._tree.treeControl.getLevel(this._data); }
cdk-nested-tree-node
继承自cdk-tree-node
,并增加了一些嵌套树的解决逻辑,如updateChildrenNodes
办法。
@Directive({ selector: 'cdk-nested-tree-node',})export class CdkNestedTreeNode extends CdkTreeNode { // 获取树节点进口 @ContentChildren(CdkTreeNodeOutlet) nodeOutlet: QueryList<CdkTreeNodeOutlet>; ngAfterContentInit() { // 获取以后节点所有的子节点 const childrenNodes = this._tree.treeControl.getChildren(this.data); // 更新子节点 this.updateChildrenNodes(childrenNodes); } /** Add children dataNodes to the NodeOutlet */ updateChildrenNodes(children) {}}
嵌套树的demo:
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl"> <cdk-nested-tree-node *cdkTreeNodeDef="let node" cdkTreeNodePadding class="example-tree-node" > {{node.label}} </cdk-nested-tree-node> <cdk-nested-tree-node *cdkTreeNodeDef="let node; when hasChild" cdkTreeNodePadding class="example-tree-node" > <button cdkTreeNodeToggle (click)="node.isExpanded = !node.isExpanded" > {{treeControl.isExpanded(node) ? '收起' : '开展'}} </button> {{node.label}} <!-- 嵌套树须要减少一个节口进口容器 --> <div [class.example-tree-invisible]="!treeControl.isExpanded(node)"> <ng-container cdkTreeNodeOutlet></ng-container> </div> </cdk-nested-tree-node></cdk-tree>
除了须要减少接口进口容器,嵌套树的数据结构和控制器也和扁平树不同。
// 数据结构interface ExampleBaseNode { label: string; children?: ExampleBaseNode[];}const TREE_DATA: ExampleBaseNode[] = [ { label: 'Fruit', children: [ { label: 'Apple' } ], }, { label: 'Vegetables' },];// 控制器treeControl = new NestedTreeControl<ExampleBaseNode>(node => node.children);
4 tree-control 控制器(外围)
TreeControl
是CdkTree
组件的UI无关的逻辑层,次要分成以下局部:
- tree-control 接口:定义控制器的成员(不蕴含具体实现)
- base-tree-control 抽象类:定义控制器的公共局部,给扁平树和嵌套树控制器继承(不能被间接实例化)
- flat-tree-control 扁平树控制器
- nested-tree-control 嵌套树控制器
接口和类大家可能都很相熟,抽象类和它们有什么区别呢?
抽象类有以下特点:
- 抽象类是能够派生其余类的基类;
- 它不能被间接实例化;
- 与接口不同,一个抽象类能够蕴含它的成员的实现细节;
- abstract 关键字是用来定义抽象类的,同时也是定义它外部的形象办法的。
4.1 tree-control 接口
export interface TreeControl<T, K = T> { dataNodes: T[]; // 树的节点数组 expansionModel: SelectionModel<K>; // 抉择模型 isExpanded(dataNode: T): boolean; // 节点是否开展 getDescendants(dataNode: T): any[]; // 获取节点的所有子节点 toggle(dataNode: T): void; // 切换节点的开展/收起状态 expand(dataNode: T): void; // 开展节点 collapse(dataNode: T): void; // 收起节点 expandAll(): void; // 开展所有节点 collapseAll(): void; // 收起所有节点 toggleDescendants(dataNode: T): void; // 切换所有子节点的开展/收起状态 expandDescendants(dataNode: T): void; // 开展所有子节点 collapseDescendants(dataNode: T): void; // 收起所有子节点 readonly getLevel: (dataNode: T) => number; // 获取节点的层级 readonly isExpandable: (dataNode: T) => boolean; // 判断节点是否能够开展 readonly getChildren: (dataNode: T) => Observable<T[]> | T[] | undefined | null; // 获取子节点}
4.2 base-tree-control 抽象类
export abstract class BaseTreeControl<T, K = T> implements TreeControl<T, K> { abstract getDescendants(dataNode: T): T[]; abstract expandAll(): void; dataNodes: T[]; expansionModel: SelectionModel<K> = new SelectionModel<K>(true); trackBy?: (dataNode: T) => K; getLevel: (dataNode: T) => number; isExpandable: (dataNode: T) => boolean; getChildren: (dataNode: T) => Observable<T[]> | T[] | undefined | null; toggle(dataNode: T): void { this.expansionModel.toggle(this._trackByValue(dataNode)); } expand(dataNode: T): void { this.expansionModel.select(this._trackByValue(dataNode)); } collapse(dataNode: T): void { this.expansionModel.deselect(this._trackByValue(dataNode)); } isExpanded(dataNode: T): boolean { return this.expansionModel.isSelected(this._trackByValue(dataNode)); } toggleDescendants(dataNode: T): void { this.expansionModel.isSelected(this._trackByValue(dataNode)) ? this.collapseDescendants(dataNode) : this.expandDescendants(dataNode); } collapseAll(): void { this.expansionModel.clear(); } expandDescendants(dataNode: T): void { let toBeProcessed = [dataNode]; toBeProcessed.push(...this.getDescendants(dataNode)); this.expansionModel.select(...toBeProcessed.map(value => this._trackByValue(value))); } collapseDescendants(dataNode: T): void { let toBeProcessed = [dataNode]; toBeProcessed.push(...this.getDescendants(dataNode)); this.expansionModel.deselect(...toBeProcessed.map(value => this._trackByValue(value))); } protected _trackByValue(value: T | K): K { return this.trackBy ? this.trackBy(value as T) : (value as K); }}
4.3 flat-tree-control 扁平树控制器
export class FlatTreeControl<T, K = T> extends BaseTreeControl<T, K> { constructor() {} getDescendants(dataNode: T): T[] { // 扁平树的获取全副子节点的逻辑 } expandAll(): void { // 扁平树的开展全副节点逻辑 }}
4.4 nested-tree-control 嵌套树控制器
export class NestedTreeControl<T, K = T> extends BaseTreeControl<T, K> { constructor() {} expandAll(): void { // 嵌套树的开展全副节点逻辑 } getDescendants(dataNode: T): T[] { // 嵌套树的获取全副子节点的逻辑 } protected _getDescendants(descendants: T[], dataNode: T): void {}}
5 selection-model 抉择模型
咱们发现TreeControl
的办法实际上是在调用SelectionModel
实例的办法。
expansionModel: SelectionModel<K> = new SelectionModel<K>(true); // 切换开展/收起状态 toggle(dataNode: T): void { this.expansionModel.toggle(this._trackByValue(dataNode)); } // 开展树节点 expand(dataNode: T): void { this.expansionModel.select(this._trackByValue(dataNode)); } // 收起树节点 collapse(dataNode: T): void { this.expansionModel.deselect(this._trackByValue(dataNode)); } // 节点是否开展 isExpanded(dataNode: T): boolean { return this.expansionModel.isSelected(this._trackByValue(dataNode)); }
selection-model
保护了一个Set
数据结构,并提供了一系列的办法来设置列表的状态,以下是它的外围实现思逻辑。
export class SelectionModel<T> { private _selection = new Set<T>(); isSelected(value: T): boolean { return this._selection.has(value); } private _markSelected(value: T) { if (!this.isSelected(value)) { this._selection.add(value); } } private _unmarkSelected(value: T) { if (this.isSelected(value)) { this._selection.delete(value); } } // 其余办法}
6 参考
- https://github.com/angular/components/tree/master/src/cdk/tree
- https://material.angular.io/cdk/tree/examples