Angular CDK is an Angular component development toolkit and the underlying base of the Material UI component library (Angular), its UI-agnostic or weak UI part (tree-control is the truly UI-agnostic core).
Although CDK is a dependency of Material UI component library, it is not coupled with Material UI component library. We can use CDK independently. Our Ng DevUI component library has the capabilities of CDK Scrolling and CDK Overlay.
1 Use it first
- Install cdk:
npm i @angular/cdk
- Import cdk tree module
import { CdkTreeModule } from '@angular/cdk/tree'
- Use
cdk-tree
component
<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;
}
The effect is as follows:
2 Source code structure
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 component source code analysis
Tree
The core function of the component:
- render hierarchy
- Expand/collapse child nodes
CdkTree
Core source code analysis steps:
- First look at the composition of minimalist
demo
- Do an overall analysis from the outside to the inside
- Do key module analysis again
3.1 The composition of the minimalist demo
-
<cdk-tree>
Component -
<cdk-tree-node>
component -
cdkTreeNodeDef
command -
cdkTreeNodePadding
command -
cdkTreeNodeToggle
instruction -
dataSource
Data structure -
treeControl
Controller -
shouldRender
method -
hasChild
method
<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 components
cdk-tree
just a 节点出口的容器
and then define some
- Input parameters such as data source
dataSource
and tree controllertreeControl
; - The method of manipulating tree nodes, such as
inserNode
for inserting nodes.
@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 component
There are two types:
-
cdk-tree-node
is the base tree node for flattened trees -
cdk-nested-tree-node
inherited fromcdk-tree-node
for nested trees
cdk-tree-node
component is relatively simple, it defines several properties:
- 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
inherits from cdk-tree-node
and adds some nested tree processing logic, such as updateChildrenNodes
method.
@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 of nested tree:
<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>
In addition to the need to increase the interface exit container, the data structure and controller of the nested tree are also different from the flat 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 controller (core)
TreeControl
Yes CdkTree
The UI-independent logic layer of the component is mainly divided into the following parts:
- tree-control interface: define the members of the controller (without the concrete implementation)
- base-tree-control abstract class: defines the public part of the controller, inherits from flat tree and nested tree controllers (cannot be directly instantiated)
- flat-tree-control flat tree controller
- nested-tree-control nested tree controller
You may be familiar with interfaces and classes. What is the difference between abstract classes and them?
Abstract classes have the following characteristics:
- An abstract class is a base class from which other classes can be derived;
- It cannot be instantiated directly;
- Unlike an interface, an abstract class can contain implementation details of its members;
- The abstract keyword is used to define an abstract class, and it is also used to define abstract methods within it.
4.1 tree-control interface
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 abstract class
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 flat tree controller
export class FlatTreeControl<T, K = T> extends BaseTreeControl<T, K> {
constructor() {}
getDescendants(dataNode: T): T[] {
// 扁平树的获取全部子节点的逻辑
}
expandAll(): void {
// 扁平树的展开全部节点逻辑
}
}
4.4 nested-tree-control Nested tree controller
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 selection model
We found that the method of TreeControl
is actually calling the method of the SelectionModel
instance.
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
maintains a Set
data structure, and provides a series of methods to set the state of the list, the following is its core implementation logic.
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);
}
}
// 其他方法
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。