diff --git a/components/components.less b/components/components.less
index edc88e8b005..02d62f62208 100644
--- a/components/components.less
+++ b/components/components.less
@@ -54,6 +54,7 @@
@import "./upload/style/entry.less";
@import "./auto-complete/style/entry.less";
@import "./cascader/style/entry.less";
+@import "./tree-view/style/entry.less";
@import "./tree/style/entry.less";
@import "./tree-select/style/entry.less";
@import "./calendar/style/entry.less";
diff --git a/components/core/animation/collapse.ts b/components/core/animation/collapse.ts
index c09420bfed8..a3f9eb5fc7d 100644
--- a/components/core/animation/collapse.ts
+++ b/components/core/animation/collapse.ts
@@ -19,17 +19,22 @@ export const collapseMotion: AnimationTriggerMetadata = trigger('collapseMotion'
export const treeCollapseMotion: AnimationTriggerMetadata = trigger('treeCollapseMotion', [
transition('* => *', [
query(
- 'nz-tree-node:leave',
- [style({ overflow: 'hidden' }), stagger(0, [animate(`150ms ${AnimationCurves.EASE_IN_OUT}`, style({ height: 0 }))])],
+ 'nz-tree-node:leave,nz-tree-builtin-node:leave',
+ [
+ style({ overflow: 'hidden' }),
+ stagger(0, [animate(`150ms ${AnimationCurves.EASE_IN_OUT}`, style({ height: 0, opacity: 0, 'padding-bottom': 0 }))])
+ ],
{
optional: true
}
),
query(
- 'nz-tree-node:enter',
+ 'nz-tree-node:enter,nz-tree-builtin-node:enter',
[
- style({ overflow: 'hidden', height: 0 }),
- stagger(0, [animate(`150ms ${AnimationCurves.EASE_IN_OUT}`, style({ overflow: 'hidden', height: '*' }))])
+ style({ overflow: 'hidden', height: 0, opacity: 0, 'padding-bottom': 0 }),
+ stagger(0, [
+ animate(`150ms ${AnimationCurves.EASE_IN_OUT}`, style({ overflow: 'hidden', height: '*', opacity: '*', 'padding-bottom': '*' }))
+ ])
],
{
optional: true
diff --git a/components/style/patch.less b/components/style/patch.less
index b679d658849..f77f20f34a9 100644
--- a/components/style/patch.less
+++ b/components/style/patch.less
@@ -11,6 +11,20 @@
z-index: 1000;
}
+.cdk-visually-hidden {
+ border: 0;
+ clip: rect(0 0 0 0);
+ height: 1px;
+ margin: -1px;
+ overflow: hidden;
+ padding: 0;
+ position: absolute;
+ width: 1px;
+ outline: 0;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+}
+
.cdk-overlay-backdrop {
top: 0;
bottom: 0;
diff --git a/components/tree-select/style/patch.less b/components/tree-select/style/patch.less
index 54a73c7e857..d42e8a8058f 100644
--- a/components/tree-select/style/patch.less
+++ b/components/tree-select/style/patch.less
@@ -1,4 +1,4 @@
-.ant-tree.ant-select-tree.ant-tree-show-line nz-tree-node:not(:last-child) > li::before {
+.ant-tree.ant-select-tree.ant-tree-show-line nz-tree-builtin-node:not(:last-child) > li::before {
content: ' ';
width: 1px;
border-left: 1px solid #d9d9d9;
diff --git a/components/tree-select/tree-select.spec.ts b/components/tree-select/tree-select.spec.ts
index 0cb8b8439f0..aa80102454a 100644
--- a/components/tree-select/tree-select.spec.ts
+++ b/components/tree-select/tree-select.spec.ts
@@ -325,7 +325,7 @@ describe('tree-select component', () => {
treeSelect.nativeElement.click();
fixture.detectChanges();
expect(treeSelectComponent.nzOpen).toBe(true);
- node = overlayContainerElement.querySelector('nz-tree-node')!;
+ node = overlayContainerElement.querySelector('nz-tree-builtin-node')!;
dispatchMouseEvent(node, 'click');
fixture.detectChanges();
flush();
@@ -447,7 +447,7 @@ describe('tree-select component', () => {
fixture.detectChanges();
expect(treeSelectComponent.nzOpen).toBe(true);
fixture.detectChanges();
- const targetNode = overlayContainerElement.querySelectorAll('nz-tree-node')[2];
+ const targetNode = overlayContainerElement.querySelectorAll('nz-tree-builtin-node')[2];
dispatchMouseEvent(targetNode, 'click');
fixture.detectChanges();
flush();
diff --git a/components/tree-view/checkbox.ts b/components/tree-view/checkbox.ts
new file mode 100644
index 00000000000..76e84810963
--- /dev/null
+++ b/components/tree-view/checkbox.ts
@@ -0,0 +1,39 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
+
+import { BooleanInput } from 'ng-zorro-antd/core/types';
+import { InputBoolean } from 'ng-zorro-antd/core/util';
+
+@Component({
+ selector: 'nz-tree-node-checkbox:not([builtin])',
+ template: `
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ preserveWhitespaces: false,
+ host: {
+ class: 'ant-tree-checkbox',
+ '[class.ant-tree-checkbox-checked]': `nzChecked`,
+ '[class.ant-tree-checkbox-indeterminate]': `nzIndeterminate`,
+ '[class.ant-tree-checkbox-disabled]': `nzDisabled`,
+ '(click)': 'onClick($event)'
+ }
+})
+export class NzTreeNodeCheckboxComponent {
+ static ngAcceptInputType_nzDisabled: BooleanInput;
+
+ @Input() nzChecked?: boolean;
+ @Input() nzIndeterminate?: boolean;
+ @Input() @InputBoolean() nzDisabled?: boolean;
+ @Output() readonly nzClick = new EventEmitter();
+
+ onClick(e: MouseEvent): void {
+ if (!this.nzDisabled) {
+ this.nzClick.emit(e);
+ }
+ }
+}
diff --git a/components/tree-view/data-source.ts b/components/tree-view/data-source.ts
new file mode 100644
index 00000000000..2b60e6dc317
--- /dev/null
+++ b/components/tree-view/data-source.ts
@@ -0,0 +1,122 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { CollectionViewer, DataSource } from '@angular/cdk/collections';
+import { FlatTreeControl, TreeControl } from '@angular/cdk/tree';
+import { BehaviorSubject, merge, Observable } from 'rxjs';
+import { map, take } from 'rxjs/operators';
+
+export class NzTreeFlattener {
+ constructor(
+ public transformFunction: (node: T, level: number) => F,
+ public getLevel: (node: F) => number,
+ public isExpandable: (node: F) => boolean,
+ public getChildren: (node: T) => Observable | T[] | undefined | null
+ ) {}
+
+ private flattenNode(node: T, level: number, resultNodes: F[], parentMap: boolean[]): F[] {
+ const flatNode = this.transformFunction(node, level);
+ resultNodes.push(flatNode);
+
+ if (this.isExpandable(flatNode)) {
+ const childrenNodes = this.getChildren(node);
+ if (childrenNodes) {
+ if (Array.isArray(childrenNodes)) {
+ this.flattenChildren(childrenNodes, level, resultNodes, parentMap);
+ } else {
+ childrenNodes.pipe(take(1)).subscribe(children => {
+ this.flattenChildren(children, level, resultNodes, parentMap);
+ });
+ }
+ }
+ }
+ return resultNodes;
+ }
+
+ private flattenChildren(children: T[], level: number, resultNodes: F[], parentMap: boolean[]): void {
+ children.forEach((child, index) => {
+ const childParentMap: boolean[] = parentMap.slice();
+ childParentMap.push(index !== children.length - 1);
+ this.flattenNode(child, level + 1, resultNodes, childParentMap);
+ });
+ }
+
+ /**
+ * Flatten a list of node type T to flattened version of node F.
+ * Please note that type T may be nested, and the length of `structuredData` may be different
+ * from that of returned list `F[]`.
+ */
+ flattenNodes(structuredData: T[]): F[] {
+ const resultNodes: F[] = [];
+ structuredData.forEach(node => this.flattenNode(node, 0, resultNodes, []));
+ return resultNodes;
+ }
+
+ /**
+ * Expand flattened node with current expansion status.
+ * The returned list may have different length.
+ */
+ expandFlattenedNodes(nodes: F[], treeControl: TreeControl): F[] {
+ const results: F[] = [];
+ const currentExpand: boolean[] = [];
+ currentExpand[0] = true;
+
+ nodes.forEach(node => {
+ let expand = true;
+ for (let i = 0; i <= this.getLevel(node); i++) {
+ expand = expand && currentExpand[i];
+ }
+ if (expand) {
+ results.push(node);
+ }
+ if (this.isExpandable(node)) {
+ currentExpand[this.getLevel(node) + 1] = treeControl.isExpanded(node);
+ }
+ });
+ return results;
+ }
+}
+
+export class NzTreeFlatDataSource extends DataSource {
+ _flattenedData = new BehaviorSubject([]);
+
+ _expandedData = new BehaviorSubject([]);
+
+ _data: BehaviorSubject;
+
+ constructor(private _treeControl: FlatTreeControl, private _treeFlattener: NzTreeFlattener, initialData: T[] = []) {
+ super();
+ this._data = new BehaviorSubject(initialData);
+ this.flatNodes();
+ }
+
+ setData(value: T[]): void {
+ this._data.next(value);
+ this.flatNodes();
+ }
+
+ getData(): T[] {
+ return this._data.getValue();
+ }
+
+ connect(collectionViewer: CollectionViewer): Observable {
+ const changes = [collectionViewer.viewChange, this._treeControl.expansionModel.changed, this._flattenedData];
+ return merge(...changes).pipe(
+ map(() => {
+ this._expandedData.next(this._treeFlattener.expandFlattenedNodes(this._flattenedData.value, this._treeControl));
+ return this._expandedData.value;
+ })
+ );
+ }
+
+ disconnect(): void {
+ // no op
+ }
+
+ private flatNodes(): void {
+ this._flattenedData.next(this._treeFlattener.flattenNodes(this.getData()));
+ this._treeControl.dataNodes = this._flattenedData.value;
+ }
+}
diff --git a/components/tree-view/demo/basic.md b/components/tree-view/demo/basic.md
new file mode 100644
index 00000000000..6069b19d236
--- /dev/null
+++ b/components/tree-view/demo/basic.md
@@ -0,0 +1,14 @@
+---
+order: 0
+title:
+ zh-CN: 基本
+ en-US: basic
+---
+
+## zh-CN
+
+最简单的用法,选中,禁用,展开等功能。
+
+## en-US
+
+The most basic usage including select, disable and expand features.
diff --git a/components/tree-view/demo/basic.ts b/components/tree-view/demo/basic.ts
new file mode 100644
index 00000000000..b952a119e3f
--- /dev/null
+++ b/components/tree-view/demo/basic.ts
@@ -0,0 +1,98 @@
+import { SelectionModel } from '@angular/cdk/collections';
+import { FlatTreeControl } from '@angular/cdk/tree';
+import { Component } from '@angular/core';
+
+import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view';
+
+interface TreeNode {
+ name: string;
+ disabled?: boolean;
+ children?: TreeNode[];
+}
+
+const TREE_DATA: TreeNode[] = [
+ {
+ name: 'parent 1',
+ children: [
+ {
+ name: 'parent 1-0',
+ disabled: true,
+ children: [{ name: 'leaf' }, { name: 'leaf' }]
+ },
+ {
+ name: 'parent 1-1',
+ children: [{ name: 'leaf' }]
+ }
+ ]
+ }
+];
+
+interface FlatNode {
+ expandable: boolean;
+ name: string;
+ level: number;
+ disabled: boolean;
+}
+
+@Component({
+ selector: 'nz-demo-tree-view-basic',
+ template: `
+
+
+
+
+ {{ node.name }}
+
+
+
+
+
+
+
+
+ {{ node.name }}
+
+
+
+ `
+})
+export class NzDemoTreeViewBasicComponent {
+ private transformer = (node: TreeNode, level: number) => {
+ return {
+ expandable: !!node.children && node.children.length > 0,
+ name: node.name,
+ level: level,
+ disabled: !!node.disabled
+ };
+ };
+ selectListSelection = new SelectionModel(true);
+
+ treeControl = new FlatTreeControl(
+ node => node.level,
+ node => node.expandable
+ );
+
+ treeFlattener = new NzTreeFlattener(
+ this.transformer,
+ node => node.level,
+ node => node.expandable,
+ node => node.children
+ );
+
+ dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener);
+
+ constructor() {
+ this.dataSource.setData(TREE_DATA);
+ this.treeControl.expandAll();
+ }
+
+ hasChild = (_: number, node: FlatNode) => node.expandable;
+}
diff --git a/components/tree-view/demo/checkbox.md b/components/tree-view/demo/checkbox.md
new file mode 100644
index 00000000000..c3d5b193dbe
--- /dev/null
+++ b/components/tree-view/demo/checkbox.md
@@ -0,0 +1,14 @@
+---
+order: 1
+title:
+ zh-CN: 选择框
+ en-US: checkbox
+---
+
+## zh-CN
+
+带选择框的树。
+
+## en-US
+
+Tree with checkboxes.
diff --git a/components/tree-view/demo/checkbox.ts b/components/tree-view/demo/checkbox.ts
new file mode 100644
index 00000000000..813261ffa85
--- /dev/null
+++ b/components/tree-view/demo/checkbox.ts
@@ -0,0 +1,189 @@
+import { SelectionModel } from '@angular/cdk/collections';
+import { FlatTreeControl } from '@angular/cdk/tree';
+import { AfterViewInit, Component } from '@angular/core';
+
+import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view';
+
+interface TreeNode {
+ name: string;
+ disabled?: boolean;
+ children?: TreeNode[];
+}
+
+const TREE_DATA: TreeNode[] = [
+ {
+ name: '0-0',
+ disabled: true,
+ children: [{ name: '0-0-0' }, { name: '0-0-1' }, { name: '0-0-2' }]
+ },
+ {
+ name: '0-1',
+ children: [
+ {
+ name: '0-1-0',
+ children: [{ name: '0-1-0-0' }, { name: '0-1-0-1' }]
+ },
+ {
+ name: '0-1-1',
+ children: [{ name: '0-1-1-0' }, { name: '0-1-1-1' }]
+ }
+ ]
+ }
+];
+
+interface FlatNode {
+ expandable: boolean;
+ name: string;
+ level: number;
+ disabled: boolean;
+}
+
+@Component({
+ selector: 'nz-demo-tree-view-checkbox',
+ template: `
+
+
+
+
+
+ {{ node.name }}
+
+
+
+
+
+
+
+
+
+ {{ node.name }}
+
+
+
+ `
+})
+export class NzDemoTreeViewCheckboxComponent implements AfterViewInit {
+ private transformer = (node: TreeNode, level: number) => {
+ const existingNode = this.nestedNodeMap.get(node);
+ const flatNode =
+ existingNode && existingNode.name === node.name
+ ? existingNode
+ : {
+ expandable: !!node.children && node.children.length > 0,
+ name: node.name,
+ level: level,
+ disabled: !!node.disabled
+ };
+ this.flatNodeMap.set(flatNode, node);
+ this.nestedNodeMap.set(node, flatNode);
+ return flatNode;
+ };
+ flatNodeMap = new Map();
+ nestedNodeMap = new Map();
+ checklistSelection = new SelectionModel(true);
+
+ treeControl = new FlatTreeControl(
+ node => node.level,
+ node => node.expandable
+ );
+
+ treeFlattener = new NzTreeFlattener(
+ this.transformer,
+ node => node.level,
+ node => node.expandable,
+ node => node.children
+ );
+
+ dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener);
+
+ constructor() {
+ this.dataSource.setData(TREE_DATA);
+ }
+
+ hasChild = (_: number, node: FlatNode) => node.expandable;
+
+ ngAfterViewInit(): void {}
+
+ descendantsAllSelected(node: FlatNode): boolean {
+ const descendants = this.treeControl.getDescendants(node);
+ return (
+ descendants.length > 0 &&
+ descendants.every(child => {
+ return this.checklistSelection.isSelected(child);
+ })
+ );
+ }
+
+ descendantsPartiallySelected(node: FlatNode): boolean {
+ const descendants = this.treeControl.getDescendants(node);
+ const result = descendants.some(child => this.checklistSelection.isSelected(child));
+ return result && !this.descendantsAllSelected(node);
+ }
+
+ leafItemSelectionToggle(node: FlatNode): void {
+ this.checklistSelection.toggle(node);
+ this.checkAllParentsSelection(node);
+ }
+
+ itemSelectionToggle(node: FlatNode): void {
+ this.checklistSelection.toggle(node);
+ const descendants = this.treeControl.getDescendants(node);
+ this.checklistSelection.isSelected(node)
+ ? this.checklistSelection.select(...descendants)
+ : this.checklistSelection.deselect(...descendants);
+
+ descendants.forEach(child => this.checklistSelection.isSelected(child));
+ this.checkAllParentsSelection(node);
+ }
+
+ checkAllParentsSelection(node: FlatNode): void {
+ let parent: FlatNode | null = this.getParentNode(node);
+ while (parent) {
+ this.checkRootNodeSelection(parent);
+ parent = this.getParentNode(parent);
+ }
+ }
+
+ checkRootNodeSelection(node: FlatNode): void {
+ const nodeSelected = this.checklistSelection.isSelected(node);
+ const descendants = this.treeControl.getDescendants(node);
+ const descAllSelected =
+ descendants.length > 0 &&
+ descendants.every(child => {
+ return this.checklistSelection.isSelected(child);
+ });
+ if (nodeSelected && !descAllSelected) {
+ this.checklistSelection.deselect(node);
+ } else if (!nodeSelected && descAllSelected) {
+ this.checklistSelection.select(node);
+ }
+ }
+
+ getParentNode(node: FlatNode): FlatNode | null {
+ const currentLevel = node.level;
+
+ if (currentLevel < 1) {
+ return null;
+ }
+
+ const startIndex = this.treeControl.dataNodes.indexOf(node) - 1;
+
+ for (let i = startIndex; i >= 0; i--) {
+ const currentNode = this.treeControl.dataNodes[i];
+
+ if (currentNode.level < currentLevel) {
+ return currentNode;
+ }
+ }
+ return null;
+ }
+}
diff --git a/components/tree-view/demo/directory.md b/components/tree-view/demo/directory.md
new file mode 100644
index 00000000000..61f27c9e5c6
--- /dev/null
+++ b/components/tree-view/demo/directory.md
@@ -0,0 +1,14 @@
+---
+order: 2
+title:
+ zh-CN: 目录
+ en-US: Directory
+---
+
+## zh-CN
+
+目录树
+
+## en-US
+
+Directory tree.
diff --git a/components/tree-view/demo/directory.ts b/components/tree-view/demo/directory.ts
new file mode 100644
index 00000000000..ff71d3073b6
--- /dev/null
+++ b/components/tree-view/demo/directory.ts
@@ -0,0 +1,113 @@
+import { SelectionModel } from '@angular/cdk/collections';
+import { FlatTreeControl } from '@angular/cdk/tree';
+import { AfterViewInit, Component } from '@angular/core';
+
+import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view';
+
+interface FoodNode {
+ name: string;
+ disabled?: boolean;
+ children?: FoodNode[];
+}
+
+const TREE_DATA: FoodNode[] = [
+ {
+ name: 'Fruit',
+ children: [{ name: 'Apple' }, { name: 'Banana', disabled: true }, { name: 'Fruit loops' }]
+ },
+ {
+ name: 'Vegetables',
+ children: [
+ {
+ name: 'Green',
+ children: [{ name: 'Broccoli' }, { name: 'Brussels sprouts' }]
+ },
+ {
+ name: 'Orange',
+ children: [{ name: 'Pumpkins' }, { name: 'Carrots' }]
+ }
+ ]
+ }
+];
+
+/** Flat node with expandable and level information */
+interface ExampleFlatNode {
+ expandable: boolean;
+ name: string;
+ level: number;
+ disabled: boolean;
+}
+
+@Component({
+ selector: 'nz-demo-tree-view-directory',
+ template: `
+
+
+
+
+
+ {{ node.name }}
+
+
+
+
+
+
+
+
+
+ {{ node.name }}
+
+
+
+ `
+})
+export class NzDemoTreeViewDirectoryComponent implements AfterViewInit {
+ private transformer = (node: FoodNode, level: number) => {
+ return {
+ expandable: !!node.children && node.children.length > 0,
+ name: node.name,
+ level: level,
+ disabled: !!node.disabled
+ };
+ };
+ selectListSelection = new SelectionModel();
+
+ treeControl = new FlatTreeControl(
+ node => node.level,
+ node => node.expandable
+ );
+
+ treeFlattener = new NzTreeFlattener(
+ this.transformer,
+ node => node.level,
+ node => node.expandable,
+ node => node.children
+ );
+
+ dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener);
+
+ constructor() {
+ this.dataSource.setData(TREE_DATA);
+ }
+
+ hasChild = (_: number, node: ExampleFlatNode) => node.expandable;
+
+ ngAfterViewInit(): void {
+ setTimeout(() => {
+ this.treeControl.expand(this.getNode('Vegetables')!);
+ }, 300);
+ }
+
+ getNode(name: string): ExampleFlatNode | null {
+ return this.treeControl.dataNodes.find(n => n.name === name) || null;
+ }
+}
diff --git a/components/tree-view/demo/dynamic.md b/components/tree-view/demo/dynamic.md
new file mode 100644
index 00000000000..c301381d46e
--- /dev/null
+++ b/components/tree-view/demo/dynamic.md
@@ -0,0 +1,14 @@
+---
+order: 3
+title:
+ zh-CN: 异步加载数据
+ en-US: Load data asynchronously
+---
+
+## zh-CN
+
+点击展开节点,动态加载数据。
+
+## en-US
+
+To load data asynchronously when click to expand a treeNode.
diff --git a/components/tree-view/demo/dynamic.ts b/components/tree-view/demo/dynamic.ts
new file mode 100644
index 00000000000..74136fb9e81
--- /dev/null
+++ b/components/tree-view/demo/dynamic.ts
@@ -0,0 +1,158 @@
+import { CollectionViewer, DataSource, SelectionChange } from '@angular/cdk/collections';
+import { FlatTreeControl, TreeControl } from '@angular/cdk/tree';
+import { AfterViewInit, Component } from '@angular/core';
+
+import { BehaviorSubject, merge, Observable, of } from 'rxjs';
+import { delay, map, tap } from 'rxjs/operators';
+
+interface FlatNode {
+ expandable: boolean;
+ id: number;
+ label: string;
+ level: number;
+ loading?: boolean;
+}
+
+const TREE_DATA: FlatNode[] = [
+ {
+ id: 0,
+ label: 'Expand to load',
+ level: 0,
+ expandable: true
+ },
+ {
+ id: 1,
+ label: 'Expand to load',
+ level: 0,
+ expandable: true
+ }
+];
+
+function getChildren(node: FlatNode): Observable {
+ return of([
+ {
+ id: Date.now(),
+ label: `Child Node (level-${node.level + 1})`,
+ level: node.level + 1,
+ expandable: true
+ },
+ {
+ id: Date.now(),
+ label: `Child Node (level-${node.level + 1})`,
+ level: node.level + 1,
+ expandable: true
+ },
+ {
+ id: Date.now(),
+ label: `Leaf Node (level-${node.level + 1})`,
+ level: node.level + 1,
+ expandable: false
+ }
+ ]).pipe(delay(500));
+}
+
+class DynamicDatasource implements DataSource {
+ private flattenedData: BehaviorSubject;
+ private childrenLoadedSet = new Set();
+
+ constructor(private treeControl: TreeControl, initData: FlatNode[]) {
+ this.flattenedData = new BehaviorSubject(initData);
+ treeControl.dataNodes = initData;
+ }
+
+ connect(collectionViewer: CollectionViewer): Observable {
+ const changes = [
+ collectionViewer.viewChange,
+ this.treeControl.expansionModel.changed.pipe(tap(change => this.handleExpansionChange(change))),
+ this.flattenedData
+ ];
+ return merge(...changes).pipe(
+ map(() => {
+ return this.expandFlattenedNodes(this.flattenedData.getValue());
+ })
+ );
+ }
+
+ expandFlattenedNodes(nodes: FlatNode[]): FlatNode[] {
+ const treeControl = this.treeControl;
+ const results: FlatNode[] = [];
+ const currentExpand: boolean[] = [];
+ currentExpand[0] = true;
+
+ nodes.forEach(node => {
+ let expand = true;
+ for (let i = 0; i <= treeControl.getLevel(node); i++) {
+ expand = expand && currentExpand[i];
+ }
+ if (expand) {
+ results.push(node);
+ }
+ if (treeControl.isExpandable(node)) {
+ currentExpand[treeControl.getLevel(node) + 1] = treeControl.isExpanded(node);
+ }
+ });
+ return results;
+ }
+
+ handleExpansionChange(change: SelectionChange): void {
+ if (change.added) {
+ change.added.forEach(node => this.loadChildren(node));
+ }
+ }
+
+ loadChildren(node: FlatNode): void {
+ if (this.childrenLoadedSet.has(node)) {
+ return;
+ }
+ node.loading = true;
+ getChildren(node).subscribe(children => {
+ node.loading = false;
+ const flattenedData = this.flattenedData.getValue();
+ const index = flattenedData.indexOf(node);
+ if (index !== -1) {
+ flattenedData.splice(index + 1, 0, ...children);
+ this.childrenLoadedSet.add(node);
+ }
+ this.flattenedData.next(flattenedData);
+ });
+ }
+
+ disconnect(): void {
+ this.flattenedData.complete();
+ }
+}
+
+@Component({
+ selector: 'nz-demo-tree-view-dynamic',
+ template: `
+
+
+ {{ node.label }}
+
+
+
+
+
+
+
+
+
+ {{ node.label }}
+
+
+ `
+})
+export class NzDemoTreeViewDynamicComponent implements AfterViewInit {
+ treeControl = new FlatTreeControl(
+ node => node.level,
+ node => node.expandable
+ );
+
+ dataSource = new DynamicDatasource(this.treeControl, TREE_DATA);
+
+ constructor() {}
+
+ hasChild = (_: number, node: FlatNode) => node.expandable;
+
+ ngAfterViewInit(): void {}
+}
diff --git a/components/tree-view/demo/editable.md b/components/tree-view/demo/editable.md
new file mode 100644
index 00000000000..3e381231cb9
--- /dev/null
+++ b/components/tree-view/demo/editable.md
@@ -0,0 +1,14 @@
+---
+order: 5
+title:
+ zh-CN: 可编辑
+ en-US: editable
+---
+
+## zh-CN
+
+带添加和删除功能的树。
+
+## en-US
+
+Tree with add and delete actions.
diff --git a/components/tree-view/demo/editable.ts b/components/tree-view/demo/editable.ts
new file mode 100644
index 00000000000..3a27febe840
--- /dev/null
+++ b/components/tree-view/demo/editable.ts
@@ -0,0 +1,145 @@
+import { SelectionModel } from '@angular/cdk/collections';
+import { FlatTreeControl } from '@angular/cdk/tree';
+import { Component } from '@angular/core';
+
+import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view';
+
+interface TreeNode {
+ name: string;
+ key: string;
+ children?: TreeNode[];
+}
+
+const TREE_DATA: TreeNode[] = [
+ {
+ name: 'parent 1',
+ key: '1',
+ children: [
+ {
+ name: 'parent 1-0',
+ key: '1-0',
+ children: [
+ { name: 'leaf', key: '1-0-0' },
+ { name: 'leaf', key: '1-0-1' }
+ ]
+ },
+ {
+ name: 'parent 1-1',
+ key: '1-1',
+ children: [{ name: 'leaf', key: '1-1-0' }]
+ }
+ ]
+ },
+ {
+ key: '2',
+ name: 'parent 2',
+ children: [{ name: 'leaf', key: '2-0' }]
+ }
+];
+
+interface FlatNode {
+ expandable: boolean;
+ name: string;
+ key: string;
+ level: number;
+}
+
+@Component({
+ selector: 'nz-demo-tree-view-editable',
+ template: `
+
+
+
+ {{ node.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ node.name }}
+
+
+
+ `,
+ styles: [``]
+})
+export class NzDemoTreeViewEditableComponent {
+ private transformer = (node: TreeNode, level: number) => {
+ const existingNode = this.nestedNodeMap.get(node);
+ const flatNode =
+ existingNode && existingNode.key === node.key
+ ? existingNode
+ : {
+ expandable: !!node.children && node.children.length > 0,
+ name: node.name,
+ level: level,
+ key: node.key
+ };
+ flatNode.name = node.name;
+ this.flatNodeMap.set(flatNode, node);
+ this.nestedNodeMap.set(node, flatNode);
+ return flatNode;
+ };
+
+ treeData = TREE_DATA;
+ flatNodeMap = new Map();
+ nestedNodeMap = new Map();
+ selectListSelection = new SelectionModel(true);
+
+ treeControl = new FlatTreeControl(
+ node => node.level,
+ node => node.expandable
+ );
+ treeFlattener = new NzTreeFlattener(
+ this.transformer,
+ node => node.level,
+ node => node.expandable,
+ node => node.children
+ );
+
+ dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener);
+
+ constructor() {
+ this.dataSource.setData(this.treeData);
+ this.treeControl.expandAll();
+ }
+
+ hasChild = (_: number, node: FlatNode) => node.expandable;
+ hasNoContent = (_: number, node: FlatNode) => node.name === '';
+ trackBy = (_: number, node: FlatNode) => `${node.key}-${node.name}`;
+
+ addNewNode(node: FlatNode): void {
+ const parentNode = this.flatNodeMap.get(node);
+ if (parentNode) {
+ parentNode.children = parentNode.children || [];
+ parentNode.children.push({
+ name: '',
+ key: `${parentNode.key}-${parentNode.children.length}`
+ });
+ this.dataSource.setData(this.treeData);
+ this.treeControl.expand(node);
+ }
+ }
+
+ saveNode(node: FlatNode, value: string): void {
+ const nestedNode = this.flatNodeMap.get(node);
+ if (nestedNode) {
+ nestedNode.name = value;
+ this.dataSource.setData(this.treeData);
+ }
+ }
+}
diff --git a/components/tree-view/demo/line.md b/components/tree-view/demo/line.md
new file mode 100644
index 00000000000..59c5cbf9e77
--- /dev/null
+++ b/components/tree-view/demo/line.md
@@ -0,0 +1,14 @@
+---
+order: 4
+title:
+ zh-CN: 带连接线的树
+ en-US: Tree with line
+---
+
+## zh-CN
+
+节点之间带连接线的树,常用于文件目录结构展示。
+
+## en-US
+
+Tree with connected line between nodes.
diff --git a/components/tree-view/demo/line.ts b/components/tree-view/demo/line.ts
new file mode 100644
index 00000000000..5e1622d9cda
--- /dev/null
+++ b/components/tree-view/demo/line.ts
@@ -0,0 +1,108 @@
+import { FlatTreeControl } from '@angular/cdk/tree';
+import { AfterViewInit, Component } from '@angular/core';
+
+import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view';
+
+interface TreeNode {
+ name: string;
+ children?: TreeNode[];
+}
+
+const TREE_DATA: TreeNode[] = [
+ {
+ name: 'parent 1',
+ children: [
+ {
+ name: 'parent 1-0',
+ children: [{ name: 'leaf' }, { name: 'leaf' }]
+ },
+ {
+ name: 'parent 1-1',
+ children: [
+ { name: 'leaf' },
+ {
+ name: 'parent 1-1-0',
+ children: [{ name: 'leaf' }, { name: 'leaf' }]
+ },
+ { name: 'leaf' }
+ ]
+ }
+ ]
+ },
+ {
+ name: 'parent 2',
+ children: [{ name: 'leaf' }, { name: 'leaf' }]
+ }
+];
+
+interface FlatNode {
+ expandable: boolean;
+ name: string;
+ level: number;
+}
+
+@Component({
+ selector: 'nz-demo-tree-view-line',
+ template: `
+ Show Leaf Icon:
+
+
+
+
+
+
+
+
+ {{ node.name }}
+
+
+
+
+
+
+
+
+ {{ node.name }}
+
+
+
+ `
+})
+export class NzDemoTreeViewLineComponent implements AfterViewInit {
+ private transformer = (node: TreeNode, level: number) => {
+ return {
+ expandable: !!node.children && node.children.length > 0,
+ name: node.name,
+ level: level
+ };
+ };
+
+ treeControl = new FlatTreeControl(
+ node => node.level,
+ node => node.expandable
+ );
+
+ treeFlattener = new NzTreeFlattener(
+ this.transformer,
+ node => node.level,
+ node => node.expandable,
+ node => node.children
+ );
+
+ dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener);
+
+ showLeafIcon = false;
+ constructor() {
+ this.dataSource.setData(TREE_DATA);
+ }
+
+ hasChild = (_: number, node: FlatNode) => node.expandable;
+
+ ngAfterViewInit(): void {
+ this.treeControl.expandAll();
+ }
+
+ getNode(name: string): FlatNode | null {
+ return this.treeControl.dataNodes.find(n => n.name === name) || null;
+ }
+}
diff --git a/components/tree-view/demo/module b/components/tree-view/demo/module
new file mode 100644
index 00000000000..527e7bae318
--- /dev/null
+++ b/components/tree-view/demo/module
@@ -0,0 +1,10 @@
+import { NzButtonModule } from 'ng-zorro-antd/button';
+import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
+import { NzHighlightModule } from 'ng-zorro-antd/core/highlight';
+import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation';
+import { NzIconModule } from 'ng-zorro-antd/icon';
+import { NzInputModule } from 'ng-zorro-antd/input';
+import { NzSwitchModule } from 'ng-zorro-antd/switch';
+import { NzTreeViewModule } from 'ng-zorro-antd/tree-view';
+
+export const moduleList = [ NzTreeViewModule, NzIconModule, NzCheckboxModule, NzInputModule, NzSwitchModule, NzButtonModule, NzNoAnimationModule, NzHighlightModule ];
\ No newline at end of file
diff --git a/components/tree-view/demo/search.md b/components/tree-view/demo/search.md
new file mode 100644
index 00000000000..b1b7180b1f8
--- /dev/null
+++ b/components/tree-view/demo/search.md
@@ -0,0 +1,14 @@
+---
+order: 6
+title:
+ zh-CN: 搜索
+ en-US: search
+---
+
+## zh-CN
+
+可搜索的树。
+
+## en-US
+
+Searchable Tree.
diff --git a/components/tree-view/demo/search.ts b/components/tree-view/demo/search.ts
new file mode 100644
index 00000000000..bfa32c9cf4c
--- /dev/null
+++ b/components/tree-view/demo/search.ts
@@ -0,0 +1,173 @@
+import { FlatTreeControl } from '@angular/cdk/tree';
+import { Component } from '@angular/core';
+
+import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view';
+import { BehaviorSubject, combineLatest } from 'rxjs';
+import { auditTime, map } from 'rxjs/operators';
+
+interface TreeNode {
+ name: string;
+ children?: TreeNode[];
+}
+
+const TREE_DATA: TreeNode[] = [
+ {
+ name: '0-0',
+ children: [{ name: '0-0-0' }, { name: '0-0-1' }, { name: '0-0-2' }]
+ },
+ {
+ name: '0-1',
+ children: [
+ {
+ name: '0-1-0',
+ children: [{ name: '0-1-0-0' }, { name: '0-1-0-1' }]
+ },
+ {
+ name: '0-1-1',
+ children: [{ name: '0-1-1-0' }, { name: '0-1-1-1' }]
+ }
+ ]
+ }
+];
+
+interface FlatNode {
+ expandable: boolean;
+ name: string;
+ level: number;
+}
+
+class FilteredTreeResult {
+ constructor(public treeData: TreeNode[], public needsToExpanded: TreeNode[] = []) {}
+}
+
+/**
+ * From https://stackoverflow.com/a/45290208/6851836
+ */
+function filterTreeData(data: TreeNode[], value: string): FilteredTreeResult {
+ const needsToExpanded = new Set();
+ const _filter = (node: TreeNode, result: TreeNode[]) => {
+ if (node.name.search(value) !== -1) {
+ result.push(node);
+ return result;
+ }
+ if (Array.isArray(node.children)) {
+ const nodes = node.children.reduce((a, b) => _filter(b, a), [] as TreeNode[]);
+ if (nodes.length) {
+ const parentNode = { ...node, children: nodes };
+ needsToExpanded.add(parentNode);
+ result.push(parentNode);
+ }
+ }
+ return result;
+ };
+ const treeData = data.reduce((a, b) => _filter(b, a), [] as TreeNode[]);
+ return new FilteredTreeResult(treeData, [...needsToExpanded]);
+}
+
+@Component({
+ selector: 'nz-demo-tree-view-search',
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ styles: [
+ `
+ nz-input-group {
+ margin-bottom: 8px;
+ }
+
+ ::ng-deep .highlight {
+ color: red;
+ }
+ `
+ ]
+})
+export class NzDemoTreeViewSearchComponent {
+ flatNodeMap = new Map();
+ nestedNodeMap = new Map();
+ expandedNodes: TreeNode[] = [];
+ searchValue = '';
+ originData$ = new BehaviorSubject(TREE_DATA);
+ searchValue$ = new BehaviorSubject('');
+
+ transformer = (node: TreeNode, level: number) => {
+ const existingNode = this.nestedNodeMap.get(node);
+ const flatNode =
+ existingNode && existingNode.name === node.name
+ ? existingNode
+ : {
+ expandable: !!node.children && node.children.length > 0,
+ name: node.name,
+ level: level
+ };
+ this.flatNodeMap.set(flatNode, node);
+ this.nestedNodeMap.set(node, flatNode);
+ return flatNode;
+ };
+
+ treeControl = new FlatTreeControl(
+ node => node.level,
+ node => node.expandable,
+ {
+ trackBy: flatNode => this.flatNodeMap.get(flatNode)!
+ }
+ );
+
+ treeFlattener = new NzTreeFlattener(
+ this.transformer,
+ node => node.level,
+ node => node.expandable,
+ node => node.children
+ );
+
+ dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener);
+
+ filteredData$ = combineLatest([
+ this.originData$,
+ this.searchValue$.pipe(
+ auditTime(300),
+ map(value => (this.searchValue = value))
+ )
+ ]).pipe(map(([data, value]) => (value ? filterTreeData(data, value) : new FilteredTreeResult(data))));
+
+ constructor() {
+ this.filteredData$.subscribe(result => {
+ this.dataSource.setData(result.treeData);
+
+ const hasSearchValue = !!this.searchValue;
+ if (hasSearchValue) {
+ if (this.expandedNodes.length === 0) {
+ this.expandedNodes = this.treeControl.expansionModel.selected;
+ this.treeControl.expansionModel.clear();
+ }
+ this.treeControl.expansionModel.select(...result.needsToExpanded);
+ } else {
+ if (this.expandedNodes.length) {
+ this.treeControl.expansionModel.clear();
+ this.treeControl.expansionModel.select(...this.expandedNodes);
+ this.expandedNodes = [];
+ }
+ }
+ });
+ }
+
+ hasChild = (_: number, node: FlatNode) => node.expandable;
+}
diff --git a/components/tree-view/demo/virtual-scroll.md b/components/tree-view/demo/virtual-scroll.md
new file mode 100644
index 00000000000..9b4af611654
--- /dev/null
+++ b/components/tree-view/demo/virtual-scroll.md
@@ -0,0 +1,14 @@
+---
+order: 7
+title:
+ zh-CN: 虚拟滚动
+ en-US: Virtual Scroll
+---
+
+## zh-CN
+
+使用虚拟滚动。
+
+## en-US
+
+Use virtual scroll.
diff --git a/components/tree-view/demo/virtual-scroll.ts b/components/tree-view/demo/virtual-scroll.ts
new file mode 100644
index 00000000000..fd37730ec4e
--- /dev/null
+++ b/components/tree-view/demo/virtual-scroll.ts
@@ -0,0 +1,97 @@
+import { FlatTreeControl } from '@angular/cdk/tree';
+import { AfterViewInit, Component } from '@angular/core';
+
+import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view';
+
+interface FoodNode {
+ name: string;
+ children?: FoodNode[];
+}
+
+function dig(path: string = '0', level: number = 3): FoodNode[] {
+ const list: FoodNode[] = [];
+ for (let i = 0; i < 10; i += 1) {
+ const name = `${path}-${i}`;
+ const treeNode: FoodNode = {
+ name
+ };
+
+ if (level > 0) {
+ treeNode.children = dig(name, level - 1);
+ }
+
+ list.push(treeNode);
+ }
+ return list;
+}
+
+const TREE_DATA: FoodNode[] = dig();
+
+/** Flat node with expandable and level information */
+interface ExampleFlatNode {
+ expandable: boolean;
+ name: string;
+ level: number;
+}
+
+@Component({
+ selector: 'nz-demo-tree-view-virtual-scroll',
+ template: `
+
+
+
+ {{ node.name }}
+
+
+
+
+
+
+ {{ node.name }}
+
+
+ `,
+ styles: [
+ `
+ .virtual-scroll-tree {
+ height: 200px;
+ }
+ `
+ ]
+})
+export class NzDemoTreeViewVirtualScrollComponent implements AfterViewInit {
+ private transformer = (node: FoodNode, level: number) => {
+ return {
+ expandable: !!node.children && node.children.length > 0,
+ name: node.name,
+ level: level
+ };
+ };
+
+ treeControl = new FlatTreeControl(
+ node => node.level,
+ node => node.expandable
+ );
+
+ treeFlattener = new NzTreeFlattener(
+ this.transformer,
+ node => node.level,
+ node => node.expandable,
+ node => node.children
+ );
+
+ dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener);
+
+ constructor() {
+ this.dataSource.setData(TREE_DATA);
+ this.treeControl.expandAll();
+ }
+
+ hasChild = (_: number, node: ExampleFlatNode) => node.expandable;
+
+ ngAfterViewInit(): void {}
+
+ getNode(name: string): ExampleFlatNode | null {
+ return this.treeControl.dataNodes.find(n => n.name === name) || null;
+ }
+}
diff --git a/components/tree-view/doc/index.en-US.md b/components/tree-view/doc/index.en-US.md
new file mode 100644
index 00000000000..007769fba27
--- /dev/null
+++ b/components/tree-view/doc/index.en-US.md
@@ -0,0 +1,154 @@
+---
+category: Components
+type: Data Display
+title: Tree View
+cover: https://gw.alipayobjects.com/zos/alicdn/Xh-oWqg9k/Tree.svg
+---
+
+## When To Use
+
+More basic Tree component, allowing each of its parts to be defined in the template, and state to be managed manually.
+With better performance and customizability.
+
+```ts
+import { NzTreeViewModule } from 'ng-zorro-antd/tree-view';
+```
+
+## API
+
+### nz-tree-view
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| [nzTreeControl] | The tree controller | [TreeControl](https://material.angular.io/cdk/tree/api#TreeControl) | - |
+| [nzDataSource] | The data array to render | [DataSource](https://material.angular.io/cdk/tree/overview#data-source)<T> \| Observable \| T[] | - |
+| [nzDirectoryTree] | Whether nodes are displayed as directory style | `boolean` | `false` |
+| [nzBlockNode] | Whether tree nodes fill remaining horizontal space| `boolean` | `false` |
+
+### nz-tree-virtual-scroll-view
+
+The virtual scroll tree view, which can be accessed from
+the [CdkVirtualScrollViewport](https://material.angular.io/cdk/scrolling/api#CdkVirtualScrollViewport) instance through
+the `virtualScrollViewport` member of the component instance.
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| [nzTreeControl] | The tree controller | [TreeControl](https://material.angular.io/cdk/tree/api#TreeControl) | - |
+| [nzDataSource] | The data array to render | [DataSource](https://material.angular.io/cdk/tree/overview#data-source)<T> \| Observable \| T[] | - |
+| [nzDirectoryTree] | Whether nodes are displayed as directory style | `boolean` | `false` |
+| [nzBlockNode] | Whether tree nodes fill remaining horizontal space| `boolean` | `false` |
+| [nzNodeWidth] | The width of nodes in the tree (in pixels) | `number` | `28` |
+| [nzMinBufferPx] | The minimum amount of buffer rendered allowed outside the viewport (in pixels) | `number` | `28 * 5` |
+| [nzMaxBufferPx] | The amount of buffer required for rendering new nodes (in pixels) | `number` | `28 * 10` |
+
+### [nzTreeNodeDef]
+
+Directive to define `nz-tree-node`.
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| [nzTreeNodeDefWhen] | A matching function which indicates whether inputted node should be used. It matches the very first node that makes this function return `true`. If no nodes that makes this function return `true`, the node which does not define this function would be matched instead. | `(index: number, nodeData: T) => boolean` | - |
+
+### nz-tree-node
+
+The tree node container component, which needs to be defined by the `nzTreeNodeDef` directive.
+
+### [nzTreeNodePadding]
+
+```html
+
+
+```
+
+Show node indentation by adding `padding` **Best Performance**.
+
+### nzTreeNodeIndentLine
+
+```html
+
+
+```
+
+Show node indentation by adding indent lines.
+
+### nz-tree-node-toggle
+
+A toggle which is used to expand / collapse the node.
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| [nzTreeNodeToggleRecursive] | Is it recursively expand / collapse | `boolean` | `false` |
+
+### nz-tree-node-toggle[nzTreeNodeNoopToggle]
+
+A toggle that does no actions. This can be used for placeholders or displays icons.
+
+### [nz-icon][nzTreeNodeToggleRotateIcon]
+
+Define an icon in the toggle, which it will automatically rotate depending on the collapse/expand state.
+
+### [nz-icon][nzTreeNodeToggleActiveIcon]
+
+Define an icon in the toggle for an active style, which it can be used for the loading state.
+
+### nz-tree-node-option
+
+Define the selectable feature of a node.
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| [nzSelected] | Whether the option is selected | `boolean` | `false` |
+| [nzDisabled] | Whether the option is disabled | `boolean` | `false` |
+| (nzClick) | Event on click | `EventEmitter` | - |
+
+### nz-tree-node-checkbox
+
+Define the checkbox feature of a node.
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| [nzChecked] | Whether the checkbox is checked | `boolean` | `false` |
+| [nzDisabled] | Whether the checkbox is disabled | `boolean` | `false` |
+| [nzIndeterminate] | Whether the checkbox is indeterminate | `boolean` | `false` |
+| (nzClick) | Event on click | `EventEmitter` | - |
+
+## Classes
+
+### __NzTreeFlatDataSource extends DataSource__
+
+### Construction Parameters
+
+| Name | Description |
+| --- | --- |
+| `treeControl: FlatTreeControl` | The tree controller. |
+| `treeFlattener: NzTreeFlattener` | Flattener for convert nested nodes `T` into flattened nodes `F`. |
+| `initialData: T[] = []` | Initialized data. |
+
+### Methods
+
+| Name | Description |
+| --- | --- |
+| `connect(collectionViewer: CollectionViewer): Observable` | Call from the TreeView component to listen for data updates. |
+| `disconnect(): void` | Call when TreeView component is destroyed. |
+| `setData(value: T[]): void` | Set the origin data |
+| `getData(): T[]` | Get the origin data |
+
+### __NzTreeFlattener__
+
+Convert nested data with child nodes into node data with level information.
+
+### Construction Parameters
+
+| Name | Description |
+| --- | --- |
+| `transformFunction: (node: T, level: number) => F` | Receive a nested node and return a flattened node |
+| `getLevel: (node: F) => number` | Define the method to get the `level` property |
+| `isExpandable: (node: F) => boolean` | Methods for defining whether a node is expandable |
+| `getChildren: (node: T) => Observable \| T[] \| undefined \| null` | Define methods to get children nodes from nested node |
+
+### Methods
+
+| Name | Description |
+| --- | --- |
+| `flattenNodes(structuredData: T[]): F[]` | Receive nested data and return flattened data |
+| `expandFlattenedNodes(nodes: F[], treeControl: TreeControl): F[]` | Get flattened node data based on expansion status |
diff --git a/components/tree-view/doc/index.zh-CN.md b/components/tree-view/doc/index.zh-CN.md
new file mode 100644
index 00000000000..b250b5f3368
--- /dev/null
+++ b/components/tree-view/doc/index.zh-CN.md
@@ -0,0 +1,147 @@
+---
+category: Components
+type: 数据展示
+title: Tree View
+subtitle: 树视图
+cover: https://gw.alipayobjects.com/zos/alicdn/Xh-oWqg9k/Tree.svg
+---
+
+## 何时使用
+
+更基础的 Tree 组件,允许在模版中定义每个组成部分,并手动管理状态。相比封装好的 Tree 组件具有更高的定制度和更好的性能。
+
+```ts
+import { NzTreeViewModule } from 'ng-zorro-antd/tree-view';
+```
+
+## API
+
+### nz-tree-view
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| [nzTreeControl] | 树控制器 | [TreeControl](https://material.angular.io/cdk/tree/api#TreeControl) | - |
+| [nzDataSource] | 用于渲染树的数组数据 | [DataSource](https://material.angular.io/cdk/tree/overview#data-source)<T> \| Observable \| T[] | - |
+| [nzDirectoryTree] | 节点是否以文件夹样式显示 | boolean | `false` |
+| [nzBlockNode] | 节点是否占据整行| boolean | `false` |
+
+### nz-tree-virtual-scroll-view
+
+虚拟滚动的树视图,可以通过组件实例上的 `virtualScrollViewport` 成员访问 [CdkVirtualScrollViewport](https://material.angular.io/cdk/scrolling/api#CdkVirtualScrollViewport) 实例。
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| [nzTreeControl] | 树控制器 | [TreeControl](https://material.angular.io/cdk/tree/api#TreeControl) | - |
+| [nzDataSource] | 用于渲染树的数组数据 | [DataSource](https://material.angular.io/cdk/tree/overview#data-source)<T> \| Observable \| T[] | - |
+| [nzDirectoryTree] | 节点是否以文件夹样式显示 | `boolean` | `false` |
+| [nzBlockNode] | 节点是否占据整行| `boolean` | `false` |
+| [nzNodeWidth] | 节点的宽度(px) | `number` | `28` |
+| [nzMinBufferPx] | 超出渲染区的最小缓存区大小(px) | `number` | `28 * 5` |
+| [nzMaxBufferPx] | 需要渲染新节点时的缓冲区大小(px) | `number` | `28 * 10` |
+
+### [nzTreeNodeDef]
+
+用于定义 `nz-tree-node` 的指令。
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| [nzTreeNodeDefWhen] | 用于定义是否使用此节点的方法,优先匹配第一个返回 `true` 的节点。如果没有返回 `true` 的节点,则匹配未定义此方法的节点。| `(index: number, nodeData: T) => boolean` | - |
+
+
+### nz-tree-node
+
+树节点容器组件,需要通过 `nzTreeNodeDef` 指令定义。
+
+### [nzTreeNodePadding]
+
+```html
+
+```
+
+以添加 `padding` 的方式显示节点缩进 **性能最好**。
+
+### nzTreeNodeIndentLine
+
+```html
+
+```
+
+以添加缩进线的方式显示节点缩进。
+
+### nz-tree-node-toggle
+
+切换部分,用于节点的展开/收起。
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| [nzTreeNodeToggleRecursive] | 是否为递归展开/收起 | `boolean` | `false` |
+
+### nz-tree-node-toggle[nzTreeNodeNoopToggle]
+
+不做任何操作的切换部分,可用于占位或者显示图标。
+
+### [nz-icon][nzTreeNodeToggleRotateIcon]
+
+定义切换部分中的图标,会随着展开收起状态自动旋转。
+
+### [nz-icon][nzTreeNodeToggleActiveIcon]
+
+定义切换部分中的图标,使其具有激活状态的样式,可用于 loading 图标。
+
+### nz-tree-node-option
+
+定义节点中的可选择部分。
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| [nzSelected] | 是否选中| `boolean` | `false` |
+| [nzDisabled] | 是否禁用| `boolean` | `false` |
+| (nzClick) | 点击时的事件 | `EventEmitter` | - |
+
+### nz-tree-node-checkbox
+
+定义节点中的可勾选的部分。
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| [nzChecked] | 是否勾选 | `boolean` | `false` |
+| [nzIndeterminate] | 是否为半选 | `boolean` | `false` |
+| [nzDisabled] | 是否禁用| `boolean` | `false` |
+| (nzClick) | 点击时的事件 | `EventEmitter` | - |
+
+## Classes
+
+### __NzTreeFlatDataSource extends DataSource__
+
+### 构造参数
+| 名称 | 说明 |
+| --- | --- |
+| `treeControl: FlatTreeControl` | Tree 控制器 |
+| `treeFlattener: NzTreeFlattener` | 用于将嵌套节点 `T` 处理为扁平节点 `F` 的展平器 |
+| `initialData: T[] = []` | 初始化数据 |
+
+### 方法
+| 名称 | 说明 |
+| --- | --- |
+| `connect(collectionViewer: CollectionViewer): Observable` | TreeView 组件中调用,用于获取数据的更新 |
+| `disconnect(): void` | TreeView 组件销毁时调用 |
+| `setData(value: T[]): void` | 设置原始数据 |
+| `getData(): T[]` | 获取原始数据 |
+
+### __NzTreeFlattener__
+
+将具有子节点的嵌套数据转换为具有级别(level)信息的转换器类。
+
+### 构造参数
+| 名称 | 说明 |
+| --- | --- |
+| `transformFunction: (node: T, level: number) => F` | 接收一个嵌套节点,返回扁平节点 |
+| `getLevel: (node: F) => number` | 定义获取 `level` 属性的方法 |
+| `isExpandable: (node: F) => boolean` | 定义是否为可展开节点的方法 |
+| `getChildren: (node: T) => Observable \| T[] \| undefined \| null` | 定义从嵌套数据中获取子节点的方法 |
+
+### 方法
+| 名称 | 说明 |
+| --- | --- |
+| `flattenNodes(structuredData: T[]): F[]` | 接收嵌套数据,返回扁平数据 |
+| `expandFlattenedNodes(nodes: F[], treeControl: TreeControl): F[]` | 按 TreeControl 中的展开状态获取节点 |
diff --git a/components/tree-view/indent.ts b/components/tree-view/indent.ts
new file mode 100644
index 00000000000..04c2208d6e4
--- /dev/null
+++ b/components/tree-view/indent.ts
@@ -0,0 +1,123 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { ChangeDetectionStrategy, Component, Directive, Input, OnDestroy } from '@angular/core';
+import { animationFrameScheduler, asapScheduler, merge, Subscription } from 'rxjs';
+import { auditTime } from 'rxjs/operators';
+import { NzTreeNodeComponent } from './node';
+import { NzTreeView } from './tree';
+
+import { getNextSibling, getParent } from './utils';
+
+/**
+ * [true, false, false, true] => 1001
+ */
+function booleanArrayToString(arr: boolean[]): string {
+ return arr.map(i => (i ? 1 : 0)).join('');
+}
+
+const BUILD_INDENTS_SCHEDULER = typeof requestAnimationFrame !== 'undefined' ? animationFrameScheduler : asapScheduler;
+
+@Component({
+ selector: 'nz-tree-node-indents',
+ template: `
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ class: 'ant-tree-indent'
+ }
+})
+export class NzTreeNodeIndentsComponent {
+ @Input() indents: boolean[] = [];
+}
+
+@Directive({
+ selector: 'nz-tree-node[nzTreeNodeIndentLine]',
+ host: {
+ class: 'ant-tree-show-line',
+ '[class.ant-tree-treenode-leaf-last]': 'isLast && isLeaf'
+ }
+})
+export class NzTreeNodeIndentLineDirective implements OnDestroy {
+ isLast: boolean | 'unset' = 'unset';
+ isLeaf = false;
+ private preNodeRef: T | null = null;
+ private nextNodeRef: T | null = null;
+ private currentIndents: string = '';
+ private changeSubscription: Subscription;
+
+ constructor(private treeNode: NzTreeNodeComponent, private tree: NzTreeView) {
+ this.buildIndents();
+ this.checkLast();
+
+ /**
+ * The dependent data (TreeControl.dataNodes) can be set after node instantiation,
+ * and setting the indents can cause frame rate loss if it is set too often.
+ */
+ this.changeSubscription = merge(this.treeNode._dataChanges, tree._dataSourceChanged)
+ .pipe(auditTime(0, BUILD_INDENTS_SCHEDULER))
+ .subscribe(() => {
+ this.buildIndents();
+ this.checkAdjacent();
+ });
+ }
+
+ private getIndents(): boolean[] {
+ const indents = [];
+ const nodes = this.tree.treeControl.dataNodes;
+ const getLevel = this.tree.treeControl.getLevel;
+ let parent = getParent(nodes, this.treeNode.data, getLevel);
+ while (parent) {
+ const parentNextSibling = getNextSibling(nodes, parent, getLevel);
+ if (parentNextSibling) {
+ indents.unshift(true);
+ } else {
+ indents.unshift(false);
+ }
+ parent = getParent(nodes, parent, getLevel);
+ }
+ return indents;
+ }
+
+ private buildIndents(): void {
+ if (this.treeNode.data) {
+ const indents = this.getIndents();
+ const diffString = booleanArrayToString(indents);
+ if (diffString !== this.currentIndents) {
+ this.treeNode.setIndents(this.getIndents());
+ this.currentIndents = diffString;
+ }
+ }
+ }
+
+ /**
+ * We need to add an class name for the last child node,
+ * this result can also be affected when the adjacent nodes are changed.
+ */
+ private checkAdjacent(): void {
+ const nodes = this.tree.treeControl.dataNodes;
+ const index = nodes.indexOf(this.treeNode.data);
+ const preNode = nodes[index - 1] || null;
+ const nextNode = nodes[index + 1] || null;
+ if (this.nextNodeRef !== nextNode || this.preNodeRef !== preNode) {
+ this.checkLast(index);
+ }
+ this.preNodeRef = preNode;
+ this.nextNodeRef = nextNode;
+ }
+
+ private checkLast(index?: number): void {
+ const nodes = this.tree.treeControl.dataNodes;
+ this.isLeaf = this.treeNode.isLeaf;
+ this.isLast = !getNextSibling(nodes, this.treeNode.data, this.tree.treeControl.getLevel, index);
+ }
+
+ ngOnDestroy(): void {
+ this.preNodeRef = null;
+ this.nextNodeRef = null;
+ this.changeSubscription.unsubscribe();
+ }
+}
diff --git a/components/tree-view/index.ts b/components/tree-view/index.ts
new file mode 100644
index 00000000000..97717c1c837
--- /dev/null
+++ b/components/tree-view/index.ts
@@ -0,0 +1,6 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+export * from './public-api';
diff --git a/components/tree-view/node.ts b/components/tree-view/node.ts
new file mode 100644
index 00000000000..90a0e0c9839
--- /dev/null
+++ b/components/tree-view/node.ts
@@ -0,0 +1,177 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { CdkTreeNode, CdkTreeNodeDef, CdkTreeNodeOutletContext } from '@angular/cdk/tree';
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ Directive,
+ ElementRef,
+ EmbeddedViewRef,
+ Input,
+ OnChanges,
+ OnDestroy,
+ OnInit,
+ Renderer2,
+ SimpleChange,
+ SimpleChanges,
+ ViewContainerRef
+} from '@angular/core';
+
+import { NzSafeAny } from 'ng-zorro-antd/core/types';
+
+import { NzTreeView } from './tree';
+
+export interface NzTreeVirtualNodeData {
+ data: T;
+ context: CdkTreeNodeOutletContext;
+ nodeDef: CdkTreeNodeDef;
+}
+
+@Component({
+ selector: 'nz-tree-node:not([builtin])',
+ exportAs: 'nzTreeNode',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [{ provide: CdkTreeNode, useExisting: NzTreeNodeComponent }],
+ template: `
+
+
+
+
+
+
+
+
+ `,
+ host: {
+ '[class.ant-tree-treenode-switcher-open]': 'isExpanded',
+ '[class.ant-tree-treenode-switcher-close]': '!isExpanded'
+ }
+})
+export class NzTreeNodeComponent extends CdkTreeNode implements OnDestroy, OnInit {
+ indents: boolean[] = [];
+ disabled = false;
+ selected = false;
+ isLeaf = false;
+
+ constructor(
+ protected elementRef: ElementRef,
+ protected tree: NzTreeView,
+ private renderer: Renderer2,
+ private cdr: ChangeDetectorRef
+ ) {
+ super(elementRef, tree);
+ this._elementRef.nativeElement.classList.add('ant-tree-treenode');
+ }
+
+ ngOnInit(): void {
+ this.isLeaf = !this.tree.treeControl.isExpandable(this.data);
+ }
+
+ disable(): void {
+ this.disabled = true;
+ this.updateDisabledClass();
+ }
+
+ enable(): void {
+ this.disabled = false;
+ this.updateDisabledClass();
+ }
+
+ select(): void {
+ this.selected = true;
+ this.updateSelectedClass();
+ }
+
+ deselect(): void {
+ this.selected = false;
+ this.updateSelectedClass();
+ }
+
+ setIndents(indents: boolean[]): void {
+ this.indents = indents;
+ this.cdr.markForCheck();
+ }
+
+ private updateSelectedClass(): void {
+ if (this.selected) {
+ this.renderer.addClass(this.elementRef.nativeElement, 'ant-tree-treenode-selected');
+ } else {
+ this.renderer.removeClass(this.elementRef.nativeElement, 'ant-tree-treenode-selected');
+ }
+ }
+
+ private updateDisabledClass(): void {
+ if (this.disabled) {
+ this.renderer.addClass(this.elementRef.nativeElement, 'ant-tree-treenode-disabled');
+ } else {
+ this.renderer.removeClass(this.elementRef.nativeElement, 'ant-tree-treenode-disabled');
+ }
+ }
+}
+
+@Directive({
+ selector: '[nzTreeNodeDef]',
+ providers: [{ provide: CdkTreeNodeDef, useExisting: NzTreeNodeDefDirective }]
+})
+export class NzTreeNodeDefDirective extends CdkTreeNodeDef {
+ @Input('nzTreeNodeDefWhen') when!: (index: number, nodeData: T) => boolean;
+}
+
+@Directive({
+ selector: '[nzTreeVirtualScrollNodeOutlet]'
+})
+export class NzTreeVirtualScrollNodeOutletDirective implements OnChanges {
+ private _viewRef: EmbeddedViewRef | null = null;
+ @Input() data!: NzTreeVirtualNodeData;
+
+ constructor(private _viewContainerRef: ViewContainerRef) {}
+
+ ngOnChanges(changes: SimpleChanges): void {
+ const recreateView = this.shouldRecreateView(changes);
+ if (recreateView) {
+ const viewContainerRef = this._viewContainerRef;
+
+ if (this._viewRef) {
+ viewContainerRef.remove(viewContainerRef.indexOf(this._viewRef));
+ }
+
+ this._viewRef = this.data ? viewContainerRef.createEmbeddedView(this.data.nodeDef.template, this.data.context) : null;
+
+ if (CdkTreeNode.mostRecentTreeNode && this._viewRef) {
+ CdkTreeNode.mostRecentTreeNode.data = this.data.data;
+ }
+ } else if (this._viewRef && this.data.context) {
+ this.updateExistingContext(this.data.context);
+ }
+ }
+
+ private shouldRecreateView(changes: SimpleChanges): boolean {
+ const ctxChange = changes.data;
+ return !!changes.data || (ctxChange && this.hasContextShapeChanged(ctxChange));
+ }
+
+ private hasContextShapeChanged(ctxChange: SimpleChange): boolean {
+ const prevCtxKeys = Object.keys(ctxChange.previousValue || {});
+ const currCtxKeys = Object.keys(ctxChange.currentValue || {});
+
+ if (prevCtxKeys.length === currCtxKeys.length) {
+ for (const propName of currCtxKeys) {
+ if (prevCtxKeys.indexOf(propName) === -1) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private updateExistingContext(ctx: NzSafeAny): void {
+ for (const propName of Object.keys(ctx)) {
+ this._viewRef!.context[propName] = (this.data.context as NzSafeAny)[propName];
+ }
+ }
+}
diff --git a/components/tree-view/option.ts b/components/tree-view/option.ts
new file mode 100644
index 00000000000..f0ac57296b8
--- /dev/null
+++ b/components/tree-view/option.ts
@@ -0,0 +1,63 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
+import { BooleanInput } from 'ng-zorro-antd/core/types';
+import { InputBoolean } from 'ng-zorro-antd/core/util';
+
+import { NzTreeNodeComponent } from './node';
+
+@Component({
+ selector: 'nz-tree-node-option',
+ template: `
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ class: 'ant-tree-node-content-wrapper',
+ '[class.ant-tree-node-content-wrapper-open]': 'isExpanded',
+ '[class.ant-tree-node-selected]': 'nzSelected',
+ '(click)': 'onClick($event)'
+ }
+})
+export class NzTreeNodeOptionComponent implements OnChanges {
+ static ngAcceptInputType_nzSelected: BooleanInput;
+ static ngAcceptInputType_nzDisabled: BooleanInput;
+
+ @Input() @InputBoolean() nzSelected = false;
+ @Input() @InputBoolean() nzDisabled = false;
+ @Output() readonly nzClick = new EventEmitter();
+
+ constructor(private treeNode: NzTreeNodeComponent) {}
+
+ get isExpanded(): boolean {
+ return this.treeNode.isExpanded;
+ }
+
+ onClick(e: MouseEvent): void {
+ if (!this.nzDisabled) {
+ this.nzClick.emit(e);
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ const { nzDisabled, nzSelected } = changes;
+ if (nzDisabled) {
+ if (nzDisabled.currentValue) {
+ this.treeNode.disable();
+ } else {
+ this.treeNode.enable();
+ }
+ }
+
+ if (nzSelected) {
+ if (nzSelected.currentValue) {
+ this.treeNode.select();
+ } else {
+ this.treeNode.deselect();
+ }
+ }
+ }
+}
diff --git a/components/tree-view/outlet.ts b/components/tree-view/outlet.ts
new file mode 100644
index 00000000000..dec72f1b98f
--- /dev/null
+++ b/components/tree-view/outlet.ts
@@ -0,0 +1,22 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { CdkTreeNodeOutlet, CDK_TREE_NODE_OUTLET_NODE } from '@angular/cdk/tree';
+import { Directive, Inject, Optional, ViewContainerRef } from '@angular/core';
+
+import { NzSafeAny } from 'ng-zorro-antd/core/types';
+
+@Directive({
+ selector: '[nzTreeNodeOutlet]',
+ providers: [
+ {
+ provide: CdkTreeNodeOutlet,
+ useExisting: NzTreeNodeOutletDirective
+ }
+ ]
+})
+export class NzTreeNodeOutletDirective implements CdkTreeNodeOutlet {
+ constructor(public viewContainer: ViewContainerRef, @Inject(CDK_TREE_NODE_OUTLET_NODE) @Optional() public _node?: NzSafeAny) {}
+}
diff --git a/components/tree-view/package.json b/components/tree-view/package.json
new file mode 100644
index 00000000000..ded1e7a9fdf
--- /dev/null
+++ b/components/tree-view/package.json
@@ -0,0 +1,7 @@
+{
+ "ngPackage": {
+ "lib": {
+ "entryFile": "public-api.ts"
+ }
+ }
+}
diff --git a/components/tree-view/padding.ts b/components/tree-view/padding.ts
new file mode 100644
index 00000000000..c68d3afb2b8
--- /dev/null
+++ b/components/tree-view/padding.ts
@@ -0,0 +1,31 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { CdkTreeNodePadding } from '@angular/cdk/tree';
+import { Directive, Input } from '@angular/core';
+
+@Directive({
+ selector: '[nzTreeNodePadding]',
+ providers: [{ provide: CdkTreeNodePadding, useExisting: NzTreeNodePaddingDirective }]
+})
+export class NzTreeNodePaddingDirective extends CdkTreeNodePadding {
+ _indent = 24;
+
+ @Input('nzTreeNodePadding')
+ get level(): number {
+ return this._level;
+ }
+ set level(value: number) {
+ this._setLevelInput(value);
+ }
+
+ @Input('nzTreeNodePaddingIndent')
+ get indent(): number | string {
+ return this._indent;
+ }
+ set indent(indent: number | string) {
+ this._setIndentInput(indent);
+ }
+}
diff --git a/components/tree-view/public-api.ts b/components/tree-view/public-api.ts
new file mode 100644
index 00000000000..2c4eb14131d
--- /dev/null
+++ b/components/tree-view/public-api.ts
@@ -0,0 +1,17 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+export * from './tree-view.module';
+export * from './checkbox';
+export * from './utils';
+export * from './data-source';
+export * from './indent';
+export * from './node';
+export * from './option';
+export * from './outlet';
+export * from './padding';
+export * from './toggle';
+export * from './tree-view';
+export * from './tree-virtual-scroll-view';
diff --git a/components/tree-view/style/entry.less b/components/tree-view/style/entry.less
new file mode 100644
index 00000000000..d2fc8c63656
--- /dev/null
+++ b/components/tree-view/style/entry.less
@@ -0,0 +1,6 @@
+/*
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+@import 'index.less';
diff --git a/components/tree-view/style/index.less b/components/tree-view/style/index.less
new file mode 100644
index 00000000000..1bebb559128
--- /dev/null
+++ b/components/tree-view/style/index.less
@@ -0,0 +1,23 @@
+/*
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+nz-tree-virtual-scroll-view {
+ display: block;
+ position: relative;
+ overflow: auto;
+ contain: strict;
+ transform: translateZ(0);
+ will-change: scroll-position;
+ -webkit-overflow-scrolling: touch;
+ .ant-tree-list, .ant-tree-list-holder-inner {
+ height: 100%;
+ }
+}
+
+nz-tree-virtual-scroll-view, nz-tree-view {
+ .ant-tree-switcher + .ant-tree-switcher.nz-tree-leaf-line-icon {
+ display: none;
+ }
+}
diff --git a/components/tree-view/toggle.ts b/components/tree-view/toggle.ts
new file mode 100644
index 00000000000..ea05c74daf7
--- /dev/null
+++ b/components/tree-view/toggle.ts
@@ -0,0 +1,57 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { coerceBooleanProperty } from '@angular/cdk/coercion';
+import { CdkTreeNodeToggle } from '@angular/cdk/tree';
+import { Directive, Input } from '@angular/core';
+import { BooleanInput } from 'ng-zorro-antd/core/types';
+
+@Directive({
+ selector: 'nz-tree-node-toggle[nzTreeNodeNoopToggle], [nzTreeNodeNoopToggle]',
+ host: {
+ class: 'ant-tree-switcher ant-tree-switcher-noop'
+ }
+})
+export class NzTreeNodeNoopToggleDirective {}
+
+@Directive({
+ selector: 'nz-tree-node-toggle:not([nzTreeNodeNoopToggle]), [nzTreeNodeToggle]',
+ providers: [{ provide: CdkTreeNodeToggle, useExisting: NzTreeNodeToggleDirective }],
+ host: {
+ class: 'ant-tree-switcher',
+ '[class.ant-tree-switcher_open]': 'isExpanded',
+ '[class.ant-tree-switcher_close]': '!isExpanded'
+ }
+})
+export class NzTreeNodeToggleDirective extends CdkTreeNodeToggle {
+ static ngAcceptInputType_recursive: BooleanInput;
+ @Input('nzTreeNodeToggleRecursive')
+ get recursive(): boolean {
+ return this._recursive;
+ }
+ set recursive(value: boolean) {
+ this._recursive = coerceBooleanProperty(value);
+ }
+
+ get isExpanded(): boolean {
+ return this._treeNode.isExpanded;
+ }
+}
+
+@Directive({
+ selector: '[nz-icon][nzTreeNodeToggleRotateIcon]',
+ host: {
+ class: 'ant-tree-switcher-icon'
+ }
+})
+export class NzTreeNodeToggleRotateIconDirective {}
+
+@Directive({
+ selector: '[nz-icon][nzTreeNodeToggleActiveIcon]',
+ host: {
+ class: 'ant-tree-switcher-loading-icon'
+ }
+})
+export class NzTreeNodeToggleActiveIconDirective {}
diff --git a/components/tree-view/tree-view.module.ts b/components/tree-view/tree-view.module.ts
new file mode 100644
index 00000000000..f39d6344032
--- /dev/null
+++ b/components/tree-view/tree-view.module.ts
@@ -0,0 +1,52 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { ScrollingModule } from '@angular/cdk/scrolling';
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation';
+
+import { NzTreeNodeCheckboxComponent } from './checkbox';
+import { NzTreeNodeIndentLineDirective, NzTreeNodeIndentsComponent } from './indent';
+import { NzTreeNodeComponent, NzTreeNodeDefDirective, NzTreeVirtualScrollNodeOutletDirective } from './node';
+import { NzTreeNodeOptionComponent } from './option';
+import { NzTreeNodeOutletDirective } from './outlet';
+import { NzTreeNodePaddingDirective } from './padding';
+import {
+ NzTreeNodeNoopToggleDirective,
+ NzTreeNodeToggleActiveIconDirective,
+ NzTreeNodeToggleDirective,
+ NzTreeNodeToggleRotateIconDirective
+} from './toggle';
+import { NzTreeView } from './tree';
+import { NzTreeViewComponent } from './tree-view';
+import { NzTreeVirtualScrollViewComponent } from './tree-virtual-scroll-view';
+
+const treeWithControlComponents = [
+ NzTreeView,
+ NzTreeNodeOutletDirective,
+ NzTreeViewComponent,
+ NzTreeNodeDefDirective,
+ NzTreeNodeComponent,
+ NzTreeNodeToggleDirective,
+ NzTreeNodePaddingDirective,
+ NzTreeNodeToggleRotateIconDirective,
+ NzTreeNodeToggleActiveIconDirective,
+ NzTreeNodeOptionComponent,
+ NzTreeNodeNoopToggleDirective,
+ NzTreeNodeCheckboxComponent,
+ NzTreeNodeIndentsComponent,
+ NzTreeVirtualScrollViewComponent,
+ NzTreeVirtualScrollNodeOutletDirective,
+ NzTreeNodeIndentLineDirective
+];
+
+@NgModule({
+ imports: [CommonModule, NzNoAnimationModule, ScrollingModule],
+ declarations: [treeWithControlComponents],
+ exports: [treeWithControlComponents]
+})
+export class NzTreeViewModule {}
diff --git a/components/tree-view/tree-view.ts b/components/tree-view/tree-view.ts
new file mode 100644
index 00000000000..9ca18db2588
--- /dev/null
+++ b/components/tree-view/tree-view.ts
@@ -0,0 +1,51 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { CdkTree } from '@angular/cdk/tree';
+import { AfterViewInit, ChangeDetectionStrategy, Component, ViewChild, ViewEncapsulation } from '@angular/core';
+
+import { treeCollapseMotion } from 'ng-zorro-antd/core/animation';
+
+import { NzTreeNodeOutletDirective } from './outlet';
+import { NzTreeView } from './tree';
+
+@Component({
+ selector: 'nz-tree-view',
+ exportAs: 'nzTreeView',
+ template: `
+
+ `,
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ { provide: CdkTree, useExisting: NzTreeViewComponent },
+ { provide: NzTreeView, useExisting: NzTreeViewComponent }
+ ],
+ host: {
+ class: 'ant-tree',
+ '[class.ant-tree-block-node]': 'nzDirectoryTree || nzBlockNode',
+ '[class.ant-tree-directory]': 'nzDirectoryTree'
+ },
+ animations: [treeCollapseMotion]
+})
+export class NzTreeViewComponent extends NzTreeView implements AfterViewInit {
+ @ViewChild(NzTreeNodeOutletDirective, { static: true }) nodeOutlet!: NzTreeNodeOutletDirective;
+ _afterViewInit = false;
+ ngAfterViewInit(): void {
+ Promise.resolve().then(() => {
+ this._afterViewInit = true;
+ this.changeDetectorRef.markForCheck();
+ });
+ }
+}
diff --git a/components/tree-view/tree-virtual-scroll-view.ts b/components/tree-view/tree-virtual-scroll-view.ts
new file mode 100644
index 00000000000..0225136d6df
--- /dev/null
+++ b/components/tree-view/tree-virtual-scroll-view.ts
@@ -0,0 +1,73 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
+import { CdkTree, CdkTreeNodeOutletContext } from '@angular/cdk/tree';
+import { ChangeDetectionStrategy, Component, Input, ViewChild, ViewEncapsulation } from '@angular/core';
+
+import { NzTreeVirtualNodeData } from './node';
+import { NzTreeNodeOutletDirective } from './outlet';
+
+import { NzTreeView } from './tree';
+
+@Component({
+ selector: 'nz-tree-virtual-scroll-view',
+ exportAs: 'nzTreeVirtualScrollView',
+ template: `
+
+
+
+
+
+
+
+
+ `,
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ { provide: NzTreeView, useExisting: NzTreeVirtualScrollViewComponent },
+ { provide: CdkTree, useExisting: NzTreeVirtualScrollViewComponent }
+ ],
+ host: {
+ class: 'ant-tree',
+ '[class.ant-tree-block-node]': 'nzDirectoryTree || nzBlockNode',
+ '[class.ant-tree-directory]': 'nzDirectoryTree'
+ }
+})
+export class NzTreeVirtualScrollViewComponent extends NzTreeView {
+ @ViewChild(NzTreeNodeOutletDirective, { static: true }) nodeOutlet!: NzTreeNodeOutletDirective;
+ @ViewChild(CdkVirtualScrollViewport, { static: true }) virtualScrollViewport!: CdkVirtualScrollViewport;
+
+ @Input() nzNodeWidth = 28;
+ @Input() nzMinBufferPx = 28 * 5;
+ @Input() nzMaxBufferPx = 28 * 10;
+
+ nodes: Array> = [];
+
+ renderNodeChanges(data: T[] | ReadonlyArray): void {
+ this.nodes = new Array(...data).map((n, i) => this.createNode(n, i));
+ }
+
+ private createNode(nodeData: T, index: number): NzTreeVirtualNodeData {
+ const node = this._getNodeDef(nodeData, index);
+ const context = new CdkTreeNodeOutletContext(nodeData);
+ if (this.treeControl.getLevel) {
+ context.level = this.treeControl.getLevel(nodeData);
+ } else {
+ context.level = 0;
+ }
+ return {
+ data: nodeData,
+ context,
+ nodeDef: node
+ };
+ }
+}
diff --git a/components/tree-view/tree.ts b/components/tree-view/tree.ts
new file mode 100644
index 00000000000..707f73ebef8
--- /dev/null
+++ b/components/tree-view/tree.ts
@@ -0,0 +1,45 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+import { DataSource } from '@angular/cdk/collections';
+import { CdkTree, TreeControl } from '@angular/cdk/tree';
+import { ChangeDetectorRef, Component, Host, Input, IterableDiffer, IterableDiffers, Optional, ViewContainerRef } from '@angular/core';
+import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation';
+
+import { Observable, Subject } from 'rxjs';
+
+import { BooleanInput, NzSafeAny } from 'ng-zorro-antd/core/types';
+import { InputBoolean } from 'ng-zorro-antd/core/util';
+
+@Component({ template: '' })
+// tslint:disable-next-line: component-class-suffix
+export class NzTreeView extends CdkTree {
+ static ngAcceptInputType_nzDirectoryTree: BooleanInput;
+ static ngAcceptInputType_nzBlockNode: BooleanInput;
+
+ _dataSourceChanged = new Subject();
+ @Input('nzTreeControl') treeControl!: TreeControl;
+ @Input('nzDataSource')
+ get dataSource(): DataSource | Observable | T[] {
+ return super.dataSource;
+ }
+ set dataSource(dataSource: DataSource | Observable | T[]) {
+ super.dataSource = dataSource;
+ }
+ @Input() @InputBoolean() nzDirectoryTree = false;
+ @Input() @InputBoolean() nzBlockNode = false;
+
+ constructor(
+ protected differs: IterableDiffers,
+ protected changeDetectorRef: ChangeDetectorRef,
+ @Host() @Optional() public noAnimation?: NzNoAnimationDirective
+ ) {
+ super(differs, changeDetectorRef);
+ }
+ renderNodeChanges(data: T[] | ReadonlyArray, dataDiffer?: IterableDiffer, viewContainer?: ViewContainerRef, parentData?: T): void {
+ super.renderNodeChanges(data, dataDiffer, viewContainer, parentData);
+ this._dataSourceChanged.next();
+ }
+}
diff --git a/components/tree-view/utils.ts b/components/tree-view/utils.ts
new file mode 100644
index 00000000000..4d6faaa5094
--- /dev/null
+++ b/components/tree-view/utils.ts
@@ -0,0 +1,41 @@
+/**
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
+ */
+
+export function getParent(nodes: T[], node: T, getLevel: (dataNode: T) => number): T | null {
+ let index = nodes.indexOf(node);
+ if (index < 0) {
+ return null;
+ }
+ const level = getLevel(node);
+ for (index--; index >= 0; index--) {
+ const preLevel = getLevel(nodes[index]);
+ if (preLevel + 1 === level) {
+ return nodes[index];
+ }
+ if (preLevel + 1 < level) {
+ return null;
+ }
+ }
+ return null;
+}
+
+export function getNextSibling(nodes: T[], node: T, getLevel: (dataNode: T) => number, _index?: number): T | null {
+ let index = typeof _index !== 'undefined' ? _index : nodes.indexOf(node);
+ if (index < 0) {
+ return null;
+ }
+ const level = getLevel(node);
+
+ for (index++; index < nodes.length; index++) {
+ const nextLevel = getLevel(nodes[index]);
+ if (nextLevel < level) {
+ return null;
+ }
+ if (nextLevel === level) {
+ return nodes[index];
+ }
+ }
+ return null;
+}
diff --git a/components/tree/demo/basic.ts b/components/tree/demo/basic.ts
index ab285c3dccb..042385c39c2 100644
--- a/components/tree/demo/basic.ts
+++ b/components/tree/demo/basic.ts
@@ -15,8 +15,7 @@ import { NzFormatEmitEvent, NzTreeComponent, NzTreeNodeOptions } from 'ng-zorro-
(nzContextMenu)="nzClick($event)"
(nzCheckBoxChange)="nzCheck($event)"
(nzExpandChange)="nzCheck($event)"
- >
-
+ >
`
})
export class NzDemoTreeBasicComponent implements AfterViewInit {
diff --git a/components/tree/tree-node-checkbox.component.ts b/components/tree/tree-node-checkbox.component.ts
index 31ed2bdc502..a5ed7b51bb6 100644
--- a/components/tree/tree-node-checkbox.component.ts
+++ b/components/tree/tree-node-checkbox.component.ts
@@ -6,8 +6,10 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
- selector: 'nz-tree-node-checkbox',
- template: ` `,
+ selector: 'nz-tree-node-checkbox[builtin]',
+ template: `
+
+ `,
changeDetection: ChangeDetectionStrategy.OnPush,
preserveWhitespaces: false,
host: {
@@ -21,7 +23,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
'[class.ant-tree-checkbox-disabled]': `!nzSelectMode && (isDisabled || isDisableCheckbox)`
}
})
-export class NzTreeNodeCheckboxComponent {
+export class NzTreeNodeBuiltinCheckboxComponent {
@Input() nzSelectMode = false;
@Input() isChecked?: boolean;
@Input() isHalfChecked?: boolean;
diff --git a/components/tree/tree-node.component.ts b/components/tree/tree-node.component.ts
index ca391299dc5..9af17e8771e 100644
--- a/components/tree/tree-node.component.ts
+++ b/components/tree/tree-node.component.ts
@@ -30,8 +30,8 @@ import { fromEvent, Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
- selector: 'nz-tree-node',
- exportAs: 'nzTreeNode',
+ selector: 'nz-tree-node[builtin]',
+ exportAs: 'nzTreeBuiltinNode',
template: `
=> {
return createComponentBed(componentInstance, {
- declarations: [NzTreeNodeComponent],
+ declarations: [NzTreeNodeBuiltinComponent],
providers: [],
imports: [NzTreeModule, NoopAnimationsModule, FormsModule, ReactiveFormsModule, NzIconTestModule]
});
@@ -37,7 +37,7 @@ describe('tree', () => {
describe('basic tree under default value', () => {
it('basic initial data', () => {
const { nativeElement } = testBed;
- const shownNodes = nativeElement.querySelectorAll('nz-tree-node');
+ const shownNodes = nativeElement.querySelectorAll('nz-tree-node[builtin]');
const enableCheckbox = nativeElement.querySelectorAll('.ant-tree-checkbox');
expect(shownNodes.length).toEqual(3);
expect(enableCheckbox.length).toEqual(3);
@@ -45,7 +45,7 @@ describe('tree', () => {
it('should initialize properly', () => {
const { nativeElement } = testBed;
- const shownNodes = nativeElement.querySelectorAll('nz-tree-node');
+ const shownNodes = nativeElement.querySelectorAll('nz-tree-node[builtin]');
const enableCheckbox = nativeElement.querySelectorAll('.ant-tree-checkbox');
expect(shownNodes.length).toEqual(3);
expect(enableCheckbox.length).toEqual(3);
@@ -55,7 +55,7 @@ describe('tree', () => {
const { component, fixture, nativeElement } = testBed;
component.defaultExpandedKeys = ['0-1'];
fixture.detectChanges();
- const shownNodes = nativeElement.querySelectorAll('nz-tree-node');
+ const shownNodes = nativeElement.querySelectorAll('nz-tree-node[builtin]');
expect(shownNodes.length).toEqual(4);
tick(300);
fixture.detectChanges();
@@ -67,7 +67,7 @@ describe('tree', () => {
const { component, fixture, nativeElement } = testBed;
component.expandAll = true;
fixture.detectChanges();
- const shownNodes = nativeElement.querySelectorAll('nz-tree-node');
+ const shownNodes = nativeElement.querySelectorAll('nz-tree-node[builtin]');
expect(shownNodes.length).toEqual(7);
tick(300);
fixture.detectChanges();
@@ -181,7 +181,7 @@ describe('tree', () => {
tick();
fixture.detectChanges();
// 0-1 0-2 hidden, others are not shown because not expanded
- const hiddenNodes = nativeElement.querySelectorAll('nz-tree-node[style*="display: none;"]');
+ const hiddenNodes = nativeElement.querySelectorAll('nz-tree-node[builtin][style*="display: none;"]');
expect(hiddenNodes.length).toEqual(2);
}));
});
@@ -379,7 +379,7 @@ describe('tree', () => {
dispatchMouseEvent(dragNode, 'dragstart');
fixture.detectChanges();
expect(dragStartSpy).toHaveBeenCalledTimes(1);
- let shownNodes = nativeElement.querySelectorAll('nz-tree-node');
+ let shownNodes = nativeElement.querySelectorAll('nz-tree-node[builtin]');
expect(shownNodes.length).toEqual(3);
// ============ dragenter ==============
@@ -407,7 +407,7 @@ describe('tree', () => {
fixture.detectChanges();
// dragenter expands 0-1/0-1
- shownNodes = nativeElement.querySelectorAll('nz-tree-node');
+ shownNodes = nativeElement.querySelectorAll('nz-tree-node[builtin]');
expect(shownNodes.length).toEqual(7);
}));
@@ -437,7 +437,7 @@ describe('tree', () => {
// ============ dragstart ==============
dispatchMouseEvent(dragNode, 'dragstart');
fixture.detectChanges();
- let shownNodes = nativeElement.querySelectorAll('nz-tree-node');
+ let shownNodes = nativeElement.querySelectorAll('nz-tree-node[builtin]');
expect(shownNodes.length).toEqual(3);
// ============ dragenter ==============
@@ -448,7 +448,7 @@ describe('tree', () => {
// =========== dragover with different position ===========
// drag-over-gap-top
dispatchMouseEvent(passedNode, 'dragover', 300, 340);
- elementNode = nativeElement.querySelector('nz-tree-node:nth-child(2)') as HTMLElement;
+ elementNode = nativeElement.querySelector('nz-tree-node[builtin]:nth-child(2)') as HTMLElement;
expect(elementNode.classList).toContain('drag-over-gap-top');
// drag-over
@@ -458,14 +458,14 @@ describe('tree', () => {
// drag-over-gap-bottom
dispatchMouseEvent(passedNode, 'dragover', 300, 570);
- elementNode = nativeElement.querySelector('nz-tree-node:nth-child(2)') as HTMLElement;
+ elementNode = nativeElement.querySelector('nz-tree-node[builtin]:nth-child(2)') as HTMLElement;
expect(elementNode.classList).toContain('drag-over-gap-bottom');
// ======= enter check, expand passing nodes ========
expect(dragEnterSpy).toHaveBeenCalledTimes(1);
expect(dragOverSpy).toHaveBeenCalledTimes(3);
fixture.detectChanges();
- shownNodes = nativeElement.querySelectorAll('nz-tree-node');
+ shownNodes = nativeElement.querySelectorAll('nz-tree-node[builtin]');
expect(shownNodes.length).toEqual(4);
}));
@@ -584,8 +584,7 @@ describe('tree', () => {
(nzContextMenu)="nzEvent($event)"
(nzExpandChange)="nzEvent($event)"
(nzCheckBoxChange)="nzEvent($event)"
- >
-
+ >
@@ -664,8 +663,7 @@ export class NzTestTreeBasicControlledComponent {
(nzOnDragOver)="onDragOver()"
(nzOnDrop)="onDrop()"
(nzOnDragEnd)="onDragEnd()"
- >
-
+ >
`
})
export class NzTestTreeDraggableComponent {
@@ -729,8 +727,7 @@ export class NzTestTreeDraggableComponent {
[nzExpandAll]="expandAll"
[nzAsyncData]="asyncData"
[nzHideUnMatched]="hideUnMatched"
- >
-
+ >
`
})
export class NzTestTreeBasicSearchComponent {