From 7c4c7dcf05220337e15fc184e5f1fbfd31cee1ea Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:16 -0500 Subject: [PATCH 01/26] feat(cdk/tree): add demos to the dev-app --- .../cdk-tree-flat-level-accessor-example.css | 4 + .../cdk-tree-flat-level-accessor-example.html | 25 ++++ .../cdk-tree-flat-level-accessor-example.ts | 108 ++++++++++++++++++ src/components-examples/cdk/tree/index.ts | 5 +- 4 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css create mode 100644 src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html create mode 100644 src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css new file mode 100644 index 000000000000..a88255f0d954 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css @@ -0,0 +1,4 @@ +.example-tree-node { + display: flex; + align-items: center; +} diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html new file mode 100644 index 000000000000..8f2bc9e6a598 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html @@ -0,0 +1,25 @@ + + + + + + {{node.name}} + + + + + {{node.name}} + + diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts new file mode 100644 index 000000000000..b7026df3609d --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts @@ -0,0 +1,108 @@ +import {ArrayDataSource} from '@angular/cdk/collections'; +import {FlatTreeControl} from '@angular/cdk/tree'; +import {Component} from '@angular/core'; + +const TREE_DATA: ExampleFlatNode[] = [ + { + name: 'Fruit', + expandable: true, + level: 0, + }, + { + name: 'Apple', + expandable: false, + level: 1, + }, + { + name: 'Banana', + expandable: false, + level: 1, + }, + { + name: 'Fruit loops', + expandable: false, + level: 1, + }, + { + name: 'Vegetables', + expandable: true, + level: 0, + }, + { + name: 'Green', + expandable: true, + level: 1, + }, + { + name: 'Broccoli', + expandable: false, + level: 2, + }, + { + name: 'Brussels sprouts', + expandable: false, + level: 2, + }, + { + name: 'Orange', + expandable: true, + level: 1, + }, + { + name: 'Pumpkins', + expandable: false, + level: 2, + }, + { + name: 'Carrots', + expandable: false, + level: 2, + }, +]; + +/** Flat node with expandable and level information */ +interface ExampleFlatNode { + expandable: boolean; + name: string; + level: number; + isExpanded?: boolean; +} + +/** + * @title Tree with flat nodes + */ +@Component({ + selector: 'cdk-tree-flat-level-accessor-example', + templateUrl: 'cdk-tree-flat-level-accessor-example.html', + styleUrls: ['cdk-tree-flat-level-accessor-example.css'], +}) +export class CdkTreeFlatLevelAccessorExample { + levelAccessor = (dataNode: ExampleFlatNode) => dataNode.level; + + dataSource = new ArrayDataSource(TREE_DATA); + + hasChild = (_: number, node: ExampleFlatNode) => node.expandable; + + getParentNode(node: ExampleFlatNode) { + 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; + } + + shouldRender(node: ExampleFlatNode) { + let parent = this.getParentNode(node); + while (parent) { + if (!parent.isExpanded) { + return false; + } + parent = this.getParentNode(parent); + } + return true; + } +} diff --git a/src/components-examples/cdk/tree/index.ts b/src/components-examples/cdk/tree/index.ts index a7f2305695de..75ac02f133df 100644 --- a/src/components-examples/cdk/tree/index.ts +++ b/src/components-examples/cdk/tree/index.ts @@ -3,11 +3,12 @@ import {NgModule} from '@angular/core'; import {MatButtonModule} from '@angular/material/button'; import {MatIconModule} from '@angular/material/icon'; import {CdkTreeFlatExample} from './cdk-tree-flat/cdk-tree-flat-example'; +import {CdkTreeFlatLevelAccessorExample} from './cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example'; import {CdkTreeNestedExample} from './cdk-tree-nested/cdk-tree-nested-example'; -export {CdkTreeFlatExample, CdkTreeNestedExample}; +export {CdkTreeFlatExample, CdkTreeNestedExample, CdkTreeFlatLevelAccessorExample}; -const EXAMPLES = [CdkTreeFlatExample, CdkTreeNestedExample]; +const EXAMPLES = [CdkTreeFlatExample, CdkTreeNestedExample, CdkTreeFlatLevelAccessorExample]; @NgModule({ imports: [CdkTreeModule, MatButtonModule, MatIconModule], From 7835cdb935e48d249c6a0c157238c01e14fb1f59 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:16 -0500 Subject: [PATCH 02/26] feat(cdk/tree): add flat-node with levelAccessor example to the demo page --- .../cdk-tree-flat-level-accessor-example.ts | 1 - src/dev-app/tree/tree-demo.html | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts index b7026df3609d..e675a6f5817b 100644 --- a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts @@ -1,5 +1,4 @@ import {ArrayDataSource} from '@angular/cdk/collections'; -import {FlatTreeControl} from '@angular/cdk/tree'; import {Component} from '@angular/core'; const TREE_DATA: ExampleFlatNode[] = [ diff --git a/src/dev-app/tree/tree-demo.html b/src/dev-app/tree/tree-demo.html index 97160db7c1cb..7a6313e505ea 100644 --- a/src/dev-app/tree/tree-demo.html +++ b/src/dev-app/tree/tree-demo.html @@ -7,6 +7,10 @@ CDK Flat tree + + CDK Flat tree (levelAccessor) + + Nested tree From 25cde665f069efd30ed56cc7d3392acf0451862f Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:16 -0500 Subject: [PATCH 03/26] feat(cdk/tree): move new demos to cdk-tree-redesign dir --- .../cdk-tree-flat-level-accessor-example.ts | 107 ------------------ .../cdk-tree-flat-level-accessor-example.css | 0 .../cdk-tree-flat-level-accessor-example.html | 0 .../cdk-tree-flat-level-accessor-example.ts | 42 +++++++ .../cdk/tree/cdk-tree-redesign/tree-data.ts | 94 +++++++++++++++ src/components-examples/cdk/tree/index.ts | 2 +- 6 files changed, 137 insertions(+), 108 deletions(-) delete mode 100644 src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts rename src/components-examples/cdk/tree/{cdk-tree-flat-level-accessor => cdk-tree-redesign}/cdk-tree-flat-level-accessor-example.css (100%) rename src/components-examples/cdk/tree/{cdk-tree-flat-level-accessor => cdk-tree-redesign}/cdk-tree-flat-level-accessor-example.html (100%) create mode 100644 src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts create mode 100644 src/components-examples/cdk/tree/cdk-tree-redesign/tree-data.ts diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts deleted file mode 100644 index e675a6f5817b..000000000000 --- a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts +++ /dev/null @@ -1,107 +0,0 @@ -import {ArrayDataSource} from '@angular/cdk/collections'; -import {Component} from '@angular/core'; - -const TREE_DATA: ExampleFlatNode[] = [ - { - name: 'Fruit', - expandable: true, - level: 0, - }, - { - name: 'Apple', - expandable: false, - level: 1, - }, - { - name: 'Banana', - expandable: false, - level: 1, - }, - { - name: 'Fruit loops', - expandable: false, - level: 1, - }, - { - name: 'Vegetables', - expandable: true, - level: 0, - }, - { - name: 'Green', - expandable: true, - level: 1, - }, - { - name: 'Broccoli', - expandable: false, - level: 2, - }, - { - name: 'Brussels sprouts', - expandable: false, - level: 2, - }, - { - name: 'Orange', - expandable: true, - level: 1, - }, - { - name: 'Pumpkins', - expandable: false, - level: 2, - }, - { - name: 'Carrots', - expandable: false, - level: 2, - }, -]; - -/** Flat node with expandable and level information */ -interface ExampleFlatNode { - expandable: boolean; - name: string; - level: number; - isExpanded?: boolean; -} - -/** - * @title Tree with flat nodes - */ -@Component({ - selector: 'cdk-tree-flat-level-accessor-example', - templateUrl: 'cdk-tree-flat-level-accessor-example.html', - styleUrls: ['cdk-tree-flat-level-accessor-example.css'], -}) -export class CdkTreeFlatLevelAccessorExample { - levelAccessor = (dataNode: ExampleFlatNode) => dataNode.level; - - dataSource = new ArrayDataSource(TREE_DATA); - - hasChild = (_: number, node: ExampleFlatNode) => node.expandable; - - getParentNode(node: ExampleFlatNode) { - 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; - } - - shouldRender(node: ExampleFlatNode) { - let parent = this.getParentNode(node); - while (parent) { - if (!parent.isExpanded) { - return false; - } - parent = this.getParentNode(parent); - } - return true; - } -} diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css b/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.css similarity index 100% rename from src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css rename to src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.css diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.html similarity index 100% rename from src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html rename to src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.html diff --git a/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts new file mode 100644 index 000000000000..ff24cf7498e2 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts @@ -0,0 +1,42 @@ +import {ArrayDataSource} from '@angular/cdk/collections'; +import {Component} from '@angular/core'; +import {FlatFoodNode, FLAT_DATA, NESTED_DATA} from './tree-data'; + +/** + * @title Tree with flat nodes + */ +@Component({ + selector: 'cdk-tree-flat-level-accessor-example', + templateUrl: 'cdk-tree-flat-level-accessor-example.html', + styleUrls: ['cdk-tree-flat-level-accessor-example.css'], +}) +export class CdkTreeFlatLevelAccessorExample { + levelAccessor = (dataNode: FlatFoodNode) => dataNode.level; + + dataSource = new ArrayDataSource(FLAT_DATA); + + hasChild = (_: number, node: FlatFoodNode) => node.expandable; + + getParentNode(node: FlatFoodNode) { + const nodeIndex = FLAT_DATA.indexOf(node); + + for (let i = nodeIndex - 1; i >= 0; i--) { + if (FLAT_DATA[i].level === node.level - 1) { + return FLAT_DATA[i]; + } + } + + return null; + } + + shouldRender(node: FlatFoodNode) { + let parent = this.getParentNode(node); + while (parent) { + if (!parent.isExpanded) { + return false; + } + parent = this.getParentNode(parent); + } + return true; + } +} diff --git a/src/components-examples/cdk/tree/cdk-tree-redesign/tree-data.ts b/src/components-examples/cdk/tree/cdk-tree-redesign/tree-data.ts new file mode 100644 index 000000000000..780c969d532f --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-redesign/tree-data.ts @@ -0,0 +1,94 @@ +/** Flat node with expandable and level information */ +export interface FlatFoodNode { + expandable: boolean; + name: string; + level: number; + isExpanded?: boolean; +} + +export const FLAT_DATA: FlatFoodNode[] = [ + { + name: 'Fruit', + expandable: true, + level: 0, + }, + { + name: 'Apple', + expandable: false, + level: 1, + }, + { + name: 'Banana', + expandable: false, + level: 1, + }, + { + name: 'Fruit loops', + expandable: false, + level: 1, + }, + { + name: 'Vegetables', + expandable: true, + level: 0, + }, + { + name: 'Green', + expandable: true, + level: 1, + }, + { + name: 'Broccoli', + expandable: false, + level: 2, + }, + { + name: 'Brussels sprouts', + expandable: false, + level: 2, + }, + { + name: 'Orange', + expandable: true, + level: 1, + }, + { + name: 'Pumpkins', + expandable: false, + level: 2, + }, + { + name: 'Carrots', + expandable: false, + level: 2, + }, +]; + +/** + * Food data with nested structure. + * Each node has a name and an optional list of children. + */ +export interface NestedFoodNode { + name: string; + children?: NestedFoodNode[]; +} + +export const NESTED_DATA: NestedFoodNode[] = [ + { + name: 'Fruit', + children: [{name: 'Apple'}, {name: 'Banana'}, {name: 'Fruit loops'}], + }, + { + name: 'Vegetables', + children: [ + { + name: 'Green', + children: [{name: 'Broccoli'}, {name: 'Brussels sprouts'}], + }, + { + name: 'Orange', + children: [{name: 'Pumpkins'}, {name: 'Carrots'}], + }, + ], + }, +]; diff --git a/src/components-examples/cdk/tree/index.ts b/src/components-examples/cdk/tree/index.ts index 75ac02f133df..2efc299e401d 100644 --- a/src/components-examples/cdk/tree/index.ts +++ b/src/components-examples/cdk/tree/index.ts @@ -3,8 +3,8 @@ import {NgModule} from '@angular/core'; import {MatButtonModule} from '@angular/material/button'; import {MatIconModule} from '@angular/material/icon'; import {CdkTreeFlatExample} from './cdk-tree-flat/cdk-tree-flat-example'; -import {CdkTreeFlatLevelAccessorExample} from './cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example'; import {CdkTreeNestedExample} from './cdk-tree-nested/cdk-tree-nested-example'; +import {CdkTreeFlatLevelAccessorExample} from './cdk-tree-redesign/cdk-tree-flat-level-accessor-example'; export {CdkTreeFlatExample, CdkTreeNestedExample, CdkTreeFlatLevelAccessorExample}; From 06d47b0515ad64ed38b48e8d5b37c99b7905db41 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:17 -0500 Subject: [PATCH 04/26] fix(cdk/tree): fix unused error --- .../cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts index ff24cf7498e2..d38c40e0d89c 100644 --- a/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts +++ b/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts @@ -1,6 +1,6 @@ import {ArrayDataSource} from '@angular/cdk/collections'; import {Component} from '@angular/core'; -import {FlatFoodNode, FLAT_DATA, NESTED_DATA} from './tree-data'; +import {FlatFoodNode, FLAT_DATA} from './tree-data'; /** * @title Tree with flat nodes From 66185a391814d4d973781f930b7726591c56e46b Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:17 -0500 Subject: [PATCH 05/26] feat(cdk/tree): move demos back to their own dirs --- .../cdk-tree-flat-level-accessor-example.css | 4 + .../cdk-tree-flat-level-accessor-example.html | 25 +++++ .../cdk-tree-flat-level-accessor-example.ts | 42 +++++++++ ...cdk-tree-nested-level-accessor-example.css | 4 + ...dk-tree-nested-level-accessor-example.html | 22 +++++ .../cdk-tree-nested-level-accessor-example.ts | 42 +++++++++ src/components-examples/cdk/tree/index.ts | 17 +++- src/components-examples/cdk/tree/tree-data.ts | 94 +++++++++++++++++++ src/dev-app/tree/tree-demo.html | 4 + 9 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css create mode 100644 src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html create mode 100644 src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts create mode 100644 src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css create mode 100644 src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html create mode 100644 src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.ts create mode 100644 src/components-examples/cdk/tree/tree-data.ts diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css new file mode 100644 index 000000000000..a88255f0d954 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.css @@ -0,0 +1,4 @@ +.example-tree-node { + display: flex; + align-items: center; +} diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html new file mode 100644 index 000000000000..8f2bc9e6a598 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html @@ -0,0 +1,25 @@ + + + + + + {{node.name}} + + + + + {{node.name}} + + diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts new file mode 100644 index 000000000000..5df6972e12eb --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts @@ -0,0 +1,42 @@ +import {ArrayDataSource} from '@angular/cdk/collections'; +import {Component} from '@angular/core'; +import {FlatFoodNode, FLAT_DATA} from '../tree-data'; + +/** + * @title Tree with flat nodes + */ +@Component({ + selector: 'cdk-tree-flat-level-accessor-example', + templateUrl: 'cdk-tree-flat-level-accessor-example.html', + styleUrls: ['cdk-tree-flat-level-accessor-example.css'], +}) +export class CdkTreeFlatLevelAccessorExample { + levelAccessor = (dataNode: FlatFoodNode) => dataNode.level; + + dataSource = new ArrayDataSource(FLAT_DATA); + + hasChild = (_: number, node: FlatFoodNode) => node.expandable; + + getParentNode(node: FlatFoodNode) { + const nodeIndex = FLAT_DATA.indexOf(node); + + for (let i = nodeIndex - 1; i >= 0; i--) { + if (FLAT_DATA[i].level === node.level - 1) { + return FLAT_DATA[i]; + } + } + + return null; + } + + shouldRender(node: FlatFoodNode) { + let parent = this.getParentNode(node); + while (parent) { + if (!parent.isExpanded) { + return false; + } + parent = this.getParentNode(parent); + } + return true; + } +} diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css new file mode 100644 index 000000000000..a88255f0d954 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css @@ -0,0 +1,4 @@ +.example-tree-node { + display: flex; + align-items: center; +} diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html new file mode 100644 index 000000000000..07cc5d05fa99 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html @@ -0,0 +1,22 @@ + + + + + + {{node.name}} + + + + + {{node.name}} + + diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.ts new file mode 100644 index 000000000000..5867ea78cfc3 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.ts @@ -0,0 +1,42 @@ +import {ArrayDataSource} from '@angular/cdk/collections'; +import {Component} from '@angular/core'; +import {FlatFoodNode, FLAT_DATA} from '../tree-data'; + +/** + * @title Tree with nested nodes + */ +@Component({ + selector: 'cdk-tree-nested-level-accessor-example', + templateUrl: 'cdk-tree-nested-level-accessor-example.html', + styleUrls: ['cdk-tree-nested-level-accessor-example.css'], +}) +export class CdkTreeNestedLevelAccessorExample { + levelAccessor = (dataNode: FlatFoodNode) => dataNode.level; + + dataSource = new ArrayDataSource(FLAT_DATA); + + hasChild = (_: number, node: FlatFoodNode) => node.expandable; + + getParentNode(node: FlatFoodNode) { + const nodeIndex = FLAT_DATA.indexOf(node); + + for (let i = nodeIndex - 1; i >= 0; i--) { + if (FLAT_DATA[i].level === node.level - 1) { + return FLAT_DATA[i]; + } + } + + return null; + } + + shouldRender(node: FlatFoodNode) { + let parent = this.getParentNode(node); + while (parent) { + if (!parent.isExpanded) { + return false; + } + parent = this.getParentNode(parent); + } + return true; + } +} diff --git a/src/components-examples/cdk/tree/index.ts b/src/components-examples/cdk/tree/index.ts index 2efc299e401d..b0ddfdf36457 100644 --- a/src/components-examples/cdk/tree/index.ts +++ b/src/components-examples/cdk/tree/index.ts @@ -2,13 +2,24 @@ import {CdkTreeModule} from '@angular/cdk/tree'; import {NgModule} from '@angular/core'; import {MatButtonModule} from '@angular/material/button'; import {MatIconModule} from '@angular/material/icon'; +import {CdkTreeFlatLevelAccessorExample} from './cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example'; import {CdkTreeFlatExample} from './cdk-tree-flat/cdk-tree-flat-example'; +import {CdkTreeNestedLevelAccessorExample} from './cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example'; import {CdkTreeNestedExample} from './cdk-tree-nested/cdk-tree-nested-example'; -import {CdkTreeFlatLevelAccessorExample} from './cdk-tree-redesign/cdk-tree-flat-level-accessor-example'; -export {CdkTreeFlatExample, CdkTreeNestedExample, CdkTreeFlatLevelAccessorExample}; +export { + CdkTreeFlatExample, + CdkTreeNestedExample, + CdkTreeFlatLevelAccessorExample, + CdkTreeNestedLevelAccessorExample, +}; -const EXAMPLES = [CdkTreeFlatExample, CdkTreeNestedExample, CdkTreeFlatLevelAccessorExample]; +const EXAMPLES = [ + CdkTreeFlatExample, + CdkTreeNestedExample, + CdkTreeFlatLevelAccessorExample, + CdkTreeNestedLevelAccessorExample, +]; @NgModule({ imports: [CdkTreeModule, MatButtonModule, MatIconModule], diff --git a/src/components-examples/cdk/tree/tree-data.ts b/src/components-examples/cdk/tree/tree-data.ts new file mode 100644 index 000000000000..780c969d532f --- /dev/null +++ b/src/components-examples/cdk/tree/tree-data.ts @@ -0,0 +1,94 @@ +/** Flat node with expandable and level information */ +export interface FlatFoodNode { + expandable: boolean; + name: string; + level: number; + isExpanded?: boolean; +} + +export const FLAT_DATA: FlatFoodNode[] = [ + { + name: 'Fruit', + expandable: true, + level: 0, + }, + { + name: 'Apple', + expandable: false, + level: 1, + }, + { + name: 'Banana', + expandable: false, + level: 1, + }, + { + name: 'Fruit loops', + expandable: false, + level: 1, + }, + { + name: 'Vegetables', + expandable: true, + level: 0, + }, + { + name: 'Green', + expandable: true, + level: 1, + }, + { + name: 'Broccoli', + expandable: false, + level: 2, + }, + { + name: 'Brussels sprouts', + expandable: false, + level: 2, + }, + { + name: 'Orange', + expandable: true, + level: 1, + }, + { + name: 'Pumpkins', + expandable: false, + level: 2, + }, + { + name: 'Carrots', + expandable: false, + level: 2, + }, +]; + +/** + * Food data with nested structure. + * Each node has a name and an optional list of children. + */ +export interface NestedFoodNode { + name: string; + children?: NestedFoodNode[]; +} + +export const NESTED_DATA: NestedFoodNode[] = [ + { + name: 'Fruit', + children: [{name: 'Apple'}, {name: 'Banana'}, {name: 'Fruit loops'}], + }, + { + name: 'Vegetables', + children: [ + { + name: 'Green', + children: [{name: 'Broccoli'}, {name: 'Brussels sprouts'}], + }, + { + name: 'Orange', + children: [{name: 'Pumpkins'}, {name: 'Carrots'}], + }, + ], + }, +]; diff --git a/src/dev-app/tree/tree-demo.html b/src/dev-app/tree/tree-demo.html index 7a6313e505ea..c07d10b21ec4 100644 --- a/src/dev-app/tree/tree-demo.html +++ b/src/dev-app/tree/tree-demo.html @@ -19,6 +19,10 @@ CDK Nested tree + + CDK Nested tree (levelAccessor) + + Todo list tree From cb105aec60705de5473219010c154a6ccde0dbf5 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:17 -0500 Subject: [PATCH 06/26] fix(cdk/tree): address review comments --- ...cdk-tree-nested-level-accessor-example.css | 5 + ...dk-tree-nested-level-accessor-example.html | 5 +- .../cdk-tree-nested-level-accessor-example.ts | 15 ++- .../cdk-tree-flat-level-accessor-example.css | 4 - .../cdk-tree-flat-level-accessor-example.html | 25 ----- .../cdk-tree-flat-level-accessor-example.ts | 42 --------- .../cdk/tree/cdk-tree-redesign/tree-data.ts | 94 ------------------- 7 files changed, 13 insertions(+), 177 deletions(-) delete mode 100644 src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.css delete mode 100644 src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.html delete mode 100644 src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts delete mode 100644 src/components-examples/cdk/tree/cdk-tree-redesign/tree-data.ts diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css index a88255f0d954..39f0d83f7741 100644 --- a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css @@ -2,3 +2,8 @@ display: flex; align-items: center; } + +.example-tree-node:not(.example-expandable) { + line-height: 40px; + padding-left: 40px; +} diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html index 07cc5d05fa99..b17a39873f99 100644 --- a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html @@ -1,14 +1,13 @@ - - {{node.name}} + [style.display]="shouldRender(node) ? 'flex' : 'none'" + class="example-tree-node example-expandable"> - {{node.name}} - - - - - {{node.name}} - - diff --git a/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts deleted file mode 100644 index d38c40e0d89c..000000000000 --- a/src/components-examples/cdk/tree/cdk-tree-redesign/cdk-tree-flat-level-accessor-example.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {ArrayDataSource} from '@angular/cdk/collections'; -import {Component} from '@angular/core'; -import {FlatFoodNode, FLAT_DATA} from './tree-data'; - -/** - * @title Tree with flat nodes - */ -@Component({ - selector: 'cdk-tree-flat-level-accessor-example', - templateUrl: 'cdk-tree-flat-level-accessor-example.html', - styleUrls: ['cdk-tree-flat-level-accessor-example.css'], -}) -export class CdkTreeFlatLevelAccessorExample { - levelAccessor = (dataNode: FlatFoodNode) => dataNode.level; - - dataSource = new ArrayDataSource(FLAT_DATA); - - hasChild = (_: number, node: FlatFoodNode) => node.expandable; - - getParentNode(node: FlatFoodNode) { - const nodeIndex = FLAT_DATA.indexOf(node); - - for (let i = nodeIndex - 1; i >= 0; i--) { - if (FLAT_DATA[i].level === node.level - 1) { - return FLAT_DATA[i]; - } - } - - return null; - } - - shouldRender(node: FlatFoodNode) { - let parent = this.getParentNode(node); - while (parent) { - if (!parent.isExpanded) { - return false; - } - parent = this.getParentNode(parent); - } - return true; - } -} diff --git a/src/components-examples/cdk/tree/cdk-tree-redesign/tree-data.ts b/src/components-examples/cdk/tree/cdk-tree-redesign/tree-data.ts deleted file mode 100644 index 780c969d532f..000000000000 --- a/src/components-examples/cdk/tree/cdk-tree-redesign/tree-data.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** Flat node with expandable and level information */ -export interface FlatFoodNode { - expandable: boolean; - name: string; - level: number; - isExpanded?: boolean; -} - -export const FLAT_DATA: FlatFoodNode[] = [ - { - name: 'Fruit', - expandable: true, - level: 0, - }, - { - name: 'Apple', - expandable: false, - level: 1, - }, - { - name: 'Banana', - expandable: false, - level: 1, - }, - { - name: 'Fruit loops', - expandable: false, - level: 1, - }, - { - name: 'Vegetables', - expandable: true, - level: 0, - }, - { - name: 'Green', - expandable: true, - level: 1, - }, - { - name: 'Broccoli', - expandable: false, - level: 2, - }, - { - name: 'Brussels sprouts', - expandable: false, - level: 2, - }, - { - name: 'Orange', - expandable: true, - level: 1, - }, - { - name: 'Pumpkins', - expandable: false, - level: 2, - }, - { - name: 'Carrots', - expandable: false, - level: 2, - }, -]; - -/** - * Food data with nested structure. - * Each node has a name and an optional list of children. - */ -export interface NestedFoodNode { - name: string; - children?: NestedFoodNode[]; -} - -export const NESTED_DATA: NestedFoodNode[] = [ - { - name: 'Fruit', - children: [{name: 'Apple'}, {name: 'Banana'}, {name: 'Fruit loops'}], - }, - { - name: 'Vegetables', - children: [ - { - name: 'Green', - children: [{name: 'Broccoli'}, {name: 'Brussels sprouts'}], - }, - { - name: 'Orange', - children: [{name: 'Pumpkins'}, {name: 'Carrots'}], - }, - ], - }, -]; From ba562c3e726f17c83dd9d6106f966763990750ce Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Mon, 13 Mar 2023 19:18:04 +0000 Subject: [PATCH 07/26] fix(cdk/tree): also update flat level accessor example --- .../cdk-tree-flat-level-accessor-example.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts index 5df6972e12eb..819fb4c707d1 100644 --- a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts @@ -20,6 +20,8 @@ export class CdkTreeFlatLevelAccessorExample { getParentNode(node: FlatFoodNode) { const nodeIndex = FLAT_DATA.indexOf(node); + // Determine the node's parent by finding the first preceding node that's + // one level shallower. for (let i = nodeIndex - 1; i >= 0; i--) { if (FLAT_DATA[i].level === node.level - 1) { return FLAT_DATA[i]; @@ -29,14 +31,9 @@ export class CdkTreeFlatLevelAccessorExample { return null; } - shouldRender(node: FlatFoodNode) { - let parent = this.getParentNode(node); - while (parent) { - if (!parent.isExpanded) { - return false; - } - parent = this.getParentNode(parent); - } - return true; + shouldRender(node: FlatFoodNode): boolean { + // This node should render if it is a root node or if all of its ancestors are expanded. + const parent = this.getParentNode(node); + return !parent || (!!parent.isExpanded && this.shouldRender(parent)); } } From 74ef3a51276ab942c2e4843a6590b7a505de60d2 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Tue, 11 Apr 2023 18:46:14 +0000 Subject: [PATCH 08/26] fix(cdk/tree): update API goldens --- tools/public_api_guard/cdk/tree.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/public_api_guard/cdk/tree.md b/tools/public_api_guard/cdk/tree.md index 5bfcdd4a9269..3d76e5819cb3 100644 --- a/tools/public_api_guard/cdk/tree.md +++ b/tools/public_api_guard/cdk/tree.md @@ -70,7 +70,7 @@ export class CdkNestedTreeNode extends CdkTreeNode implements Af nodeOutlet: QueryList; protected updateChildrenNodes(children?: T[]): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "cdk-nested-tree-node", ["cdkNestedTreeNode"], { "role": "role"; "disabled": "disabled"; "tabIndex": "tabIndex"; }, {}, ["nodeOutlet"], never, false, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "cdk-nested-tree-node", ["cdkNestedTreeNode"], { "role": { "alias": "role"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "tabIndex": { "alias": "tabIndex"; "required": false; }; }, {}, ["nodeOutlet"], never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } @@ -114,7 +114,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, end: number; }>; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration, "cdk-tree", ["cdkTree"], { "dataSource": "dataSource"; "treeControl": "treeControl"; "levelAccessor": "levelAccessor"; "childrenAccessor": "childrenAccessor"; "trackBy": "trackBy"; "expansionKey": "expansionKey"; }, {}, ["_nodeDefs"], never, false, never>; + static ɵcmp: i0.ɵɵComponentDeclaration, "cdk-tree", ["cdkTree"], { "dataSource": { "alias": "dataSource"; "required": false; }; "treeControl": { "alias": "treeControl"; "required": false; }; "levelAccessor": { "alias": "levelAccessor"; "required": false; }; "childrenAccessor": { "alias": "childrenAccessor"; "required": false; }; "trackBy": { "alias": "trackBy"; "required": false; }; "expansionKey": { "alias": "expansionKey"; "required": false; }; }, {}, ["_nodeDefs"], never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } @@ -161,7 +161,7 @@ export class CdkTreeNode implements FocusableOption, OnDestroy, OnInit // (undocumented) protected _tree: CdkTree; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "cdk-tree-node", ["cdkTreeNode"], { "role": "role"; "isExpandable": "isExpandable"; "isExpanded": "isExpanded"; }, {}, never, never, false, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "cdk-tree-node", ["cdkTreeNode"], { "role": { "alias": "role"; "required": false; }; "isExpandable": { "alias": "isExpandable"; "required": false; }; "isExpanded": { "alias": "isExpanded"; "required": false; }; }, {}, never, never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } @@ -173,7 +173,7 @@ export class CdkTreeNodeDef { template: TemplateRef; when: (index: number, nodeData: T) => boolean; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodeDef]", never, { "when": "cdkTreeNodeDefWhen"; }, {}, never, never, false, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodeDef]", never, { "when": { "alias": "cdkTreeNodeDefWhen"; "required": false; }; }, {}, never, never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } @@ -220,7 +220,7 @@ export class CdkTreeNodePadding implements OnDestroy { // (undocumented) _setPadding(forceChange?: boolean): void; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodePadding]", never, { "level": "cdkTreeNodePadding"; "indent": "cdkTreeNodePaddingIndent"; }, {}, never, never, false, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodePadding]", never, { "level": { "alias": "cdkTreeNodePadding"; "required": false; }; "indent": { "alias": "cdkTreeNodePaddingIndent"; "required": false; }; }, {}, never, never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, [null, null, null, { optional: true; }]>; } @@ -239,7 +239,7 @@ export class CdkTreeNodeToggle { // (undocumented) protected _treeNode: CdkTreeNode; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodeToggle]", never, { "recursive": "cdkTreeNodeToggleRecursive"; }, {}, never, never, false, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkTreeNodeToggle]", never, { "recursive": { "alias": "cdkTreeNodeToggleRecursive"; "required": false; }; }, {}, never, never, false, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, never>; } From 841edb2d7de2deb5dc068d13510251b6637b4f31 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:17 -0500 Subject: [PATCH 09/26] feat(cdk/tree): add a translation function to the tree to get children --- src/cdk/tree/tree.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 420826d6f504..16ee5c6b9fdd 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -481,6 +481,43 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, return this.treeControl?.getChildren ?? this.childrenAccessor; } + /** + * Gets the direct children of a node; used for compatibility between the old tree and the + * new tree. + */ + _getDirectChildren(dataNode: T): Observable { + const levelAccessor = this._getLevelAccessor(); + if (levelAccessor) { + // If we have no known nodes, we wouldn't be able to determine descendants + if (!this._dataNodes) { + return observableOf([]); + } + const startIndex = this._dataNodes.indexOf(dataNode); + const results: T[] = [dataNode]; + + // Goes through flattened tree nodes in the `dataNodes` array, and get all direct descendants. + // The level of descendants of a tree node must be equal to the level of the given + // tree node + 1. + // If we reach a node whose level is equal to the level of the tree node, we hit a sibling. + // If we reach a node whose level is greater than the level of the tree node, we hit a + // sibling of an ancestor. + const currentLevel = levelAccessor(dataNode); + for ( + let i = startIndex + 1; + i < this._dataNodes.length && currentLevel + 1 === levelAccessor(this._dataNodes[i]); + i++ + ) { + results.push(this._dataNodes[i]); + } + return observableOf(results); + } + const childrenAccessor = this._getChildrenAccessor(); + if (childrenAccessor) { + return coerceObservable(childrenAccessor(dataNode) ?? []); + } + throw getTreeControlMissingError(); + } + /** * Gets all nodes in the tree, through recursive expansion. * From df29768b1106974217f63f5c9d8603f6dcf375a8 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:17 -0500 Subject: [PATCH 10/26] feat(cdk/tree): use _getDirectChildren method in nested node --- src/cdk/tree/nested-node.ts | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/cdk/tree/nested-node.ts b/src/cdk/tree/nested-node.ts index 55c05c875167..29d6b4b60aac 100644 --- a/src/cdk/tree/nested-node.ts +++ b/src/cdk/tree/nested-node.ts @@ -69,22 +69,13 @@ export class CdkNestedTreeNode ngAfterContentInit() { this._dataDiffer = this._differs.find([]).create(this._tree.trackBy); - const childrenAccessor = this._tree._getChildrenAccessor(); - if (!childrenAccessor && (typeof ngDevMode === 'undefined' || ngDevMode)) { - throw getTreeControlFunctionsMissingError(); - } else if (childrenAccessor) { - const childrenNodes = childrenAccessor(this.data); - if (Array.isArray(childrenNodes)) { - this.updateChildrenNodes(childrenNodes as T[]); - } else if (isObservable(childrenNodes)) { - childrenNodes - .pipe(takeUntil(this._destroyed)) - .subscribe(result => this.updateChildrenNodes(result)); - } - this.nodeOutlet.changes - .pipe(takeUntil(this._destroyed)) - .subscribe(() => this.updateChildrenNodes()); - } + this._tree + ._getDirectChildren(this.data) + .pipe(takeUntil(this._destroyed)) + .subscribe(result => this.updateChildrenNodes(result)); + this.nodeOutlet.changes + .pipe(takeUntil(this._destroyed)) + .subscribe(() => this.updateChildrenNodes()); } // This is a workaround for https://github.com/angular/angular/issues/23091 From 3bbe6dd95f366cabc46aa2d7914f98cad29b31a6 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:17 -0500 Subject: [PATCH 11/26] feat(cdk/tree): add cache of nodes to the tree --- src/cdk/tree/tree.ts | 47 +++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 16ee5c6b9fdd..364330bd2829 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -29,6 +29,7 @@ import { } from '@angular/core'; import { BehaviorSubject, + combineLatest, concat, isObservable, merge, @@ -37,7 +38,7 @@ import { Subject, Subscription, } from 'rxjs'; -import {reduce, switchMap, take, takeUntil} from 'rxjs/operators'; +import {reduce, switchMap, take, map, takeUntil, pairwise, startWith} from 'rxjs/operators'; import {TreeControl} from './control/tree-control'; import {CdkTreeNodeDef, CdkTreeNodeOutletContext} from './node'; import {CdkTreeNodeOutlet} from './outlet'; @@ -57,6 +58,10 @@ function coerceObservable(data: T | Observable): Observable { return data; } +function isNotNullish(val: T | null | undefined): val is T { + return val != null; +} + /** * CDK tree component that connects with a data source to retrieve data of type `T` and renders * dataNodes with hierarchy. Updates the dataNodes when new data is provided by the data source. @@ -170,8 +175,14 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, /** Keep track of which nodes are expanded. */ private _expansionModel?: SelectionModel; + /** Maintain a synchronous cache of the currently known data nodes. */ - private _dataNodes?: readonly T[]; + private _dataNodes: BehaviorSubject = new BehaviorSubject([]); + + /** The mapping between data and the node that is rendered. */ + private _nodes: BehaviorSubject>> = new BehaviorSubject( + new Map>(), + ); constructor(private _differs: IterableDiffers, private _changeDetectorRef: ChangeDetectorRef) {} @@ -279,7 +290,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, viewContainer: ViewContainerRef = this._nodeOutlet.viewContainer, parentData?: T, ) { - this._dataNodes = data; + this._dataNodes.next(data); const changes = dataDiffer.diff(data); if (!changes) { return; @@ -518,6 +529,22 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, throw getTreeControlMissingError(); } + /** + * Adds the specified node component to the tree's internal registry. + * + * This primarily facilitates keyboard navigation. + */ + _registerNode(node: CdkTreeNode) { + this._nodes.value.set(this._trackExpansionKey(node.data), node); + this._nodes.next(this._nodes.value); + } + + /** Removes the specified node component from the tree's internal registry. */ + _unregisterNode(node: CdkTreeNode) { + this._nodes.value.delete(this._trackExpansionKey(node.data)); + this._nodes.next(this._nodes.value); + } + /** * Gets all nodes in the tree, through recursive expansion. * @@ -530,7 +557,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, * the tree. */ private _getAllDescendants(): Observable { - return merge(...(this._dataNodes?.map(dataNode => this._getDescendants(dataNode)) ?? [])); + return merge(...this._dataNodes.value.map(dataNode => this._getDescendants(dataNode))); } private _getDescendants(dataNode: T): Observable { @@ -538,11 +565,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, return observableOf(this.treeControl.getDescendants(dataNode)); } if (this.levelAccessor) { - // If we have no known nodes, we wouldn't be able to determine descendants - if (!this._dataNodes) { - return observableOf([]); - } - const startIndex = this._dataNodes.indexOf(dataNode); + const startIndex = this._dataNodes.value.indexOf(dataNode); const results: T[] = [dataNode]; // Goes through flattened tree nodes in the `dataNodes` array, and get all descendants. @@ -554,10 +577,11 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, const currentLevel = this.levelAccessor(dataNode); for ( let i = startIndex + 1; - i < this._dataNodes.length && currentLevel < this.levelAccessor(this._dataNodes[i]); + i < this._dataNodes.value.length && + currentLevel < this.levelAccessor(this._dataNodes.value[i]); i++ ) { - results.push(this._dataNodes[i]); + results.push(this._dataNodes.value[i]); } return observableOf(results); } @@ -693,6 +717,7 @@ export class CdkTreeNode implements FocusableOption, OnDestroy, OnInit ngOnInit(): void { this._parentNodeAriaLevel = getParentNodeAriaLevel(this._elementRef.nativeElement); this._elementRef.nativeElement.setAttribute('aria-level', `${this.level + 1}`); + this._tree._registerNode(this); } ngOnDestroy() { From c1025ec87ab911743ebbe33fb36d95db35e79629 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:17 -0500 Subject: [PATCH 12/26] fix(cdk/tree): fix cherry-pick errors --- src/cdk/tree/tree.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 364330bd2829..740e1d6683ba 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -503,7 +503,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, if (!this._dataNodes) { return observableOf([]); } - const startIndex = this._dataNodes.indexOf(dataNode); + const startIndex = this._dataNodes.value.indexOf(dataNode); const results: T[] = [dataNode]; // Goes through flattened tree nodes in the `dataNodes` array, and get all direct descendants. @@ -515,10 +515,11 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, const currentLevel = levelAccessor(dataNode); for ( let i = startIndex + 1; - i < this._dataNodes.length && currentLevel + 1 === levelAccessor(this._dataNodes[i]); + i < this._dataNodes.value.length && + currentLevel + 1 === levelAccessor(this._dataNodes.value[i]); i++ ) { - results.push(this._dataNodes[i]); + results.push(this._dataNodes.value[i]); } return observableOf(results); } From 0befab788a219178c2bcbb587dfd528e1d333f43 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:17 -0500 Subject: [PATCH 13/26] feat(cdk/tree): add translation layer for nested nodes using levelAccessor --- src/cdk/tree/nested-node.ts | 4 +- src/cdk/tree/tree-errors.ts | 8 +++ src/cdk/tree/tree.ts | 103 ++++++++++++++++++++++++++---------- 3 files changed, 84 insertions(+), 31 deletions(-) diff --git a/src/cdk/tree/nested-node.ts b/src/cdk/tree/nested-node.ts index 29d6b4b60aac..91a328744e42 100644 --- a/src/cdk/tree/nested-node.ts +++ b/src/cdk/tree/nested-node.ts @@ -16,12 +16,10 @@ import { OnInit, QueryList, } from '@angular/core'; -import {isObservable} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet} from './outlet'; import {CdkTree, CdkTreeNode} from './tree'; -import {getTreeControlFunctionsMissingError} from './tree-errors'; /** * Nested node is a child of ``. It works with nested tree. @@ -97,7 +95,7 @@ export class CdkNestedTreeNode } if (outlet && this._children) { const viewContainer = outlet.viewContainer; - this._tree.renderNodeChanges(this._children, this._dataDiffer, viewContainer, this._data); + this._tree._renderNodeChanges(this._children, this._dataDiffer, viewContainer, this._data); } else { // Reset the data differ if there's no children nodes displayed this._dataDiffer.diff([]); diff --git a/src/cdk/tree/tree-errors.ts b/src/cdk/tree/tree-errors.ts index bb5ae840a31a..59dcd827b5dd 100644 --- a/src/cdk/tree/tree-errors.ts +++ b/src/cdk/tree/tree-errors.ts @@ -54,3 +54,11 @@ export function getMultipleTreeControlsError() { export function getTreeControlFunctionsMissingError() { return Error(`Could not find functions for nested/flat tree in tree control.`); } + +/** + * Returns an error to be thrown when the node type is not specified. + * @docs-private + */ +export function getTreeControlNodeTypeUnspecifiedError() { + return Error(`The nodeType was not specified for the tree.`); +} diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 740e1d6683ba..3cb1a23de146 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -31,6 +31,7 @@ import { BehaviorSubject, combineLatest, concat, + EMPTY, isObservable, merge, Observable, @@ -38,7 +39,7 @@ import { Subject, Subscription, } from 'rxjs'; -import {reduce, switchMap, take, map, takeUntil, pairwise, startWith} from 'rxjs/operators'; +import {reduce, switchMap, take, map, takeUntil, filter, startWith, tap} from 'rxjs/operators'; import {TreeControl} from './control/tree-control'; import {CdkTreeNodeDef, CdkTreeNodeOutletContext} from './node'; import {CdkTreeNodeOutlet} from './outlet'; @@ -48,6 +49,7 @@ import { getTreeMissingMatchingNodeDefError, getTreeMultipleDefaultNodeDefsError, getTreeNoValidDataSourceError, + getTreeControlNodeTypeUnspecifiedError, } from './tree-errors'; import {coerceNumberProperty} from '@angular/cdk/coercion'; @@ -58,10 +60,6 @@ function coerceObservable(data: T | Observable): Observable { return data; } -function isNotNullish(val: T | null | undefined): val is T { - return val != null; -} - /** * CDK tree component that connects with a data source to retrieve data of type `T` and renders * dataNodes with hierarchy. Updates the dataNodes when new data is provided by the data source. @@ -151,6 +149,14 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, */ @Input() expansionKey?: (dataNode: T) => K; + /** + * What type of node is being used in the tree. This must be provided if either of + * `levelAccessor` or `childrenAccessor` are provided. + * + * This controls what selection of data the tree will render. + */ + @Input() nodeType?: 'flat' | 'nested'; + // Outlets within the tree's template where the dataNodes will be inserted. @ViewChild(CdkTreeNodeOutlet, {static: true}) _nodeOutlet: CdkTreeNodeOutlet; @@ -179,6 +185,9 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, /** Maintain a synchronous cache of the currently known data nodes. */ private _dataNodes: BehaviorSubject = new BehaviorSubject([]); + /** Maintain a synchronous cache of the currently rendered nodes. */ + private _renderedNodes: BehaviorSubject> = new BehaviorSubject>(new Set()); + /** The mapping between data and the node that is rendered. */ private _nodes: BehaviorSubject>> = new BehaviorSubject( new Map>(), @@ -197,6 +206,11 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, } else if (provided === 0) { throw getTreeControlMissingError(); } + + // Check that the node type is also provided if treeControl is not. + if (!this.treeControl && !this.nodeType) { + throw getTreeControlNodeTypeUnspecifiedError(); + } } if (!this.treeControl) { @@ -291,6 +305,22 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, parentData?: T, ) { this._dataNodes.next(data); + + this._renderNodeChanges(data, dataDiffer, viewContainer, parentData); + } + + /** Check for changes made in the data and render each change (node added/removed/moved). */ + _renderNodeChanges( + data: readonly T[], + dataDiffer: IterableDiffer, + viewContainer: ViewContainerRef, + parentData?: T, + ) { + const levelAccessor = this._getLevelAccessor(); + if (levelAccessor && this.nodeType === 'nested' && !parentData) { + data = data.filter(data => levelAccessor(data) === 0); + } + const changes = dataDiffer.diff(data); if (!changes) { return; @@ -498,30 +528,47 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, */ _getDirectChildren(dataNode: T): Observable { const levelAccessor = this._getLevelAccessor(); - if (levelAccessor) { - // If we have no known nodes, we wouldn't be able to determine descendants - if (!this._dataNodes) { - return observableOf([]); - } - const startIndex = this._dataNodes.value.indexOf(dataNode); - const results: T[] = [dataNode]; + if (levelAccessor && this._expansionModel) { + const key = this._trackExpansionKey(dataNode); + const isExpanded = this._expansionModel.changed.pipe( + switchMap(changes => { + if (changes.added.includes(key)) { + return observableOf(true); + } else if (changes.removed.includes(key)) { + return observableOf(false); + } + return EMPTY; + }), + startWith(this.isExpanded(dataNode)), + ); - // Goes through flattened tree nodes in the `dataNodes` array, and get all direct descendants. - // The level of descendants of a tree node must be equal to the level of the given - // tree node + 1. - // If we reach a node whose level is equal to the level of the tree node, we hit a sibling. - // If we reach a node whose level is greater than the level of the tree node, we hit a - // sibling of an ancestor. - const currentLevel = levelAccessor(dataNode); - for ( - let i = startIndex + 1; - i < this._dataNodes.value.length && - currentLevel + 1 === levelAccessor(this._dataNodes.value[i]); - i++ - ) { - results.push(this._dataNodes.value[i]); - } - return observableOf(results); + return combineLatest([isExpanded, this._dataNodes]).pipe( + map(([expanded, dataNodes]) => { + if (!expanded) { + return []; + } + const startIndex = dataNodes.indexOf(dataNode); + const level = levelAccessor(dataNode) + 1; + const results: T[] = []; + + // Goes through flattened tree nodes in the `dataNodes` array, and get all direct descendants. + // The level of descendants of a tree node must be equal to the level of the given + // tree node + 1. + // If we reach a node whose level is equal to the level of the tree node, we hit a sibling. + // If we reach a node whose level is greater than the level of the tree node, we hit a + // sibling of an ancestor. + for (let i = startIndex + 1; i < dataNodes.length; i++) { + const currentLevel = levelAccessor(dataNodes[i]); + if (level > currentLevel) { + break; + } + if (level === currentLevel) { + results.push(dataNodes[i]); + } + } + return results; + }), + ); } const childrenAccessor = this._getChildrenAccessor(); if (childrenAccessor) { From 3be6e7c289e8ef5b5c6cf1a53ab0e48f6d695d4b Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:18 -0500 Subject: [PATCH 14/26] feat(cdk/tree): add example with nested nodes & level accessor --- ...cdk-tree-nested-level-accessor-example.css | 17 ++++++++++++---- ...dk-tree-nested-level-accessor-example.html | 6 ++++-- .../cdk-tree-nested-level-accessor-example.ts | 20 ------------------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css index 39f0d83f7741..988fa23745aa 100644 --- a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.css @@ -1,9 +1,18 @@ -.example-tree-node { - display: flex; - align-items: center; +.example-tree-invisible { + display: none; } -.example-tree-node:not(.example-expandable) { +.example-tree ul, +.example-tree li { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} +.example-tree-node { + display: block; line-height: 40px; +} + +.example-tree-node .example-tree-node { padding-left: 40px; } diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html index b17a39873f99..0f25b514e622 100644 --- a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html @@ -1,4 +1,4 @@ - + {{node.name}} @@ -6,7 +6,6 @@ {{node.name}} +
+ +
diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.ts index a8cb614a110b..3a0ea4dd90b4 100644 --- a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.ts +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.ts @@ -16,24 +16,4 @@ export class CdkTreeNestedLevelAccessorExample { dataSource = new ArrayDataSource(FLAT_DATA); hasChild = (_: number, node: FlatFoodNode) => node.expandable; - - getParentNode(node: FlatFoodNode) { - const nodeIndex = FLAT_DATA.indexOf(node); - - // Determine the node's parent by finding the first preceding node that's - // one level shallower. - for (let i = nodeIndex - 1; i >= 0; i--) { - if (FLAT_DATA[i].level === node.level - 1) { - return FLAT_DATA[i]; - } - } - - return null; - } - - shouldRender(node: FlatFoodNode): boolean { - // This node should render if it is a root node or if all of its ancestors are expanded. - const parent = this.getParentNode(node); - return !parent || (!!parent.isExpanded && this.shouldRender(parent)); - } } From 015aef97107b1f65a70c9d52f26c3a6d865e5f01 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:18 -0500 Subject: [PATCH 15/26] feat(cdk/tree): fix examples --- .../cdk-tree-flat-level-accessor-example.html | 5 ++--- .../cdk-tree-nested-level-accessor-example.html | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html index 8f2bc9e6a598..207aae8a6f82 100644 --- a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.html @@ -1,4 +1,4 @@ - + {{node.name}} diff --git a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html index 0f25b514e622..71eef12b5f90 100644 --- a/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html +++ b/src/components-examples/cdk/tree/cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example.html @@ -9,10 +9,9 @@ class="example-tree-node example-expandable"> {{node.name}} From 4d604671fc7569285a6461158b5de95c70fe26b7 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:18 -0500 Subject: [PATCH 16/26] feat(cdk/tree): add example with flat nodes & childrenAccessor --- ...dk-tree-flat-children-accessor-example.css | 4 ++ ...k-tree-flat-children-accessor-example.html | 22 ++++++++ ...cdk-tree-flat-children-accessor-example.ts | 55 +++++++++++++++++++ src/components-examples/cdk/tree/index.ts | 3 + src/dev-app/tree/tree-demo.html | 4 ++ 5 files changed, 88 insertions(+) create mode 100644 src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.css create mode 100644 src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.html create mode 100644 src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.ts diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.css b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.css new file mode 100644 index 000000000000..a88255f0d954 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.css @@ -0,0 +1,4 @@ +.example-tree-node { + display: flex; + align-items: center; +} diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.html b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.html new file mode 100644 index 000000000000..6976db2de744 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.html @@ -0,0 +1,22 @@ + + + + + + {{node.name}} + + + + + {{node.name}} + + diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.ts new file mode 100644 index 000000000000..be1ed4abdcb2 --- /dev/null +++ b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.ts @@ -0,0 +1,55 @@ +import {ArrayDataSource} from '@angular/cdk/collections'; +import {CdkTree} from '@angular/cdk/tree'; +import {Component, ViewChild} from '@angular/core'; +import {timer} from 'rxjs'; +import {mapTo} from 'rxjs/operators'; +import {NestedFoodNode, NESTED_DATA} from '../tree-data'; + +function* allNodes(nodes: NestedFoodNode[]): Iterable { + for (const node of nodes) { + yield node; + if (node.children) { + yield* allNodes(node.children); + } + } +} + +/** + * @title Tree with flat nodes + */ +@Component({ + selector: 'cdk-tree-flat-children-accessor-example', + templateUrl: 'cdk-tree-flat-children-accessor-example.html', + styleUrls: ['cdk-tree-flat-children-accessor-example.css'], +}) +export class CdkTreeFlatChildrenAccessorExample { + @ViewChild(CdkTree) + tree!: CdkTree; + + childrenAccessor = (dataNode: NestedFoodNode) => timer(100).pipe(mapTo(dataNode.children ?? [])); + + dataSource = new ArrayDataSource(NESTED_DATA); + + hasChild = (_: number, node: NestedFoodNode) => !!node.children?.length; + + getParentNode(node: NestedFoodNode) { + for (const parent of allNodes(NESTED_DATA)) { + if (parent.children?.includes(node)) { + return parent; + } + } + + return null; + } + + shouldRender(node: NestedFoodNode) { + let parent = this.getParentNode(node); + while (parent) { + if (!this.tree.isExpanded(parent)) { + return false; + } + parent = this.getParentNode(parent); + } + return true; + } +} diff --git a/src/components-examples/cdk/tree/index.ts b/src/components-examples/cdk/tree/index.ts index b0ddfdf36457..fc8afe4a305d 100644 --- a/src/components-examples/cdk/tree/index.ts +++ b/src/components-examples/cdk/tree/index.ts @@ -2,6 +2,7 @@ import {CdkTreeModule} from '@angular/cdk/tree'; import {NgModule} from '@angular/core'; import {MatButtonModule} from '@angular/material/button'; import {MatIconModule} from '@angular/material/icon'; +import {CdkTreeFlatChildrenAccessorExample} from './cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example'; import {CdkTreeFlatLevelAccessorExample} from './cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example'; import {CdkTreeFlatExample} from './cdk-tree-flat/cdk-tree-flat-example'; import {CdkTreeNestedLevelAccessorExample} from './cdk-tree-nested-level-accessor/cdk-tree-nested-level-accessor-example'; @@ -11,6 +12,7 @@ export { CdkTreeFlatExample, CdkTreeNestedExample, CdkTreeFlatLevelAccessorExample, + CdkTreeFlatChildrenAccessorExample, CdkTreeNestedLevelAccessorExample, }; @@ -18,6 +20,7 @@ const EXAMPLES = [ CdkTreeFlatExample, CdkTreeNestedExample, CdkTreeFlatLevelAccessorExample, + CdkTreeFlatChildrenAccessorExample, CdkTreeNestedLevelAccessorExample, ]; diff --git a/src/dev-app/tree/tree-demo.html b/src/dev-app/tree/tree-demo.html index c07d10b21ec4..21fb0e5d8196 100644 --- a/src/dev-app/tree/tree-demo.html +++ b/src/dev-app/tree/tree-demo.html @@ -11,6 +11,10 @@ CDK Flat tree (levelAccessor)
+ + CDK Flat tree (childrenAccessor) + + Nested tree From 1bb07d1e753b2d0d21e28964947e46c50f23de8b Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:18 -0500 Subject: [PATCH 17/26] feat(cdk/tree): flatten data that uses childrenAccessor --- src/cdk/tree/tree.ts | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 3cb1a23de146..6db36409f920 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import {FocusableOption} from '@angular/cdk/a11y'; +import {coerceNumberProperty} from '@angular/cdk/coercion'; import {CollectionViewer, DataSource, isDataSource, SelectionModel} from '@angular/cdk/collections'; import { AfterContentChecked, @@ -39,19 +40,18 @@ import { Subject, Subscription, } from 'rxjs'; -import {reduce, switchMap, take, map, takeUntil, filter, startWith, tap} from 'rxjs/operators'; +import {concatMap, map, reduce, startWith, switchMap, take, takeUntil} from 'rxjs/operators'; import {TreeControl} from './control/tree-control'; import {CdkTreeNodeDef, CdkTreeNodeOutletContext} from './node'; import {CdkTreeNodeOutlet} from './outlet'; import { getMultipleTreeControlsError, getTreeControlMissingError, + getTreeControlNodeTypeUnspecifiedError, getTreeMissingMatchingNodeDefError, getTreeMultipleDefaultNodeDefsError, getTreeNoValidDataSourceError, - getTreeControlNodeTypeUnspecifiedError, } from './tree-errors'; -import {coerceNumberProperty} from '@angular/cdk/coercion'; function coerceObservable(data: T | Observable): Observable { if (!isObservable(data)) { @@ -290,7 +290,10 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, if (dataStream) { this._dataSubscription = dataStream - .pipe(takeUntil(this._onDestroy)) + .pipe( + switchMap(data => this._flattenChildren(data)), + takeUntil(this._onDestroy), + ) .subscribe(data => this.renderNodeChanges(data)); } else if (typeof ngDevMode === 'undefined' || ngDevMode) { throw getTreeNoValidDataSourceError(); @@ -634,7 +637,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, return observableOf(results); } if (this.childrenAccessor) { - return this._getChildrenRecursively(dataNode).pipe( + return this._getAllChildrenRecursively(dataNode).pipe( reduce( (allChildren: T[], nextChildren) => { allChildren.push(...nextChildren); @@ -653,7 +656,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, * This will emit multiple times, in the order that the children will appear * in the tree, and can be combined with a `reduce` operator. */ - private _getChildrenRecursively(dataNode: T): Observable { + private _getAllChildrenRecursively(dataNode: T): Observable { if (!this.childrenAccessor) { return observableOf([]); } @@ -661,9 +664,8 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, return coerceObservable(this.childrenAccessor(dataNode)).pipe( take(1), switchMap(children => { - return concat( - observableOf(children), - ...children.map(child => this._getChildrenRecursively(child)), + return observableOf(...children).pipe( + concatMap(child => concat(observableOf([child]), this._getAllChildrenRecursively(child))), ); }), ); @@ -679,6 +681,22 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, // - if it's not, then K will be defaulted to T. return this.expansionKey?.(dataNode) ?? (dataNode as unknown as K); } + + private _flattenChildren(nodes: readonly T[]): Observable { + // If we're using TreeControl or levelAccessor, we don't need to manually + // flatten things here. + if (!this.childrenAccessor) { + return observableOf(nodes); + } else { + return observableOf(...nodes).pipe( + concatMap(node => concat(observableOf([node]), this._getAllChildrenRecursively(node))), + reduce((results, nodes) => { + results.push(...nodes); + return results; + }, [] as T[]), + ); + } + } } /** From c00f6978ded748d8402293de0d87bb1c6e2a9214 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:18 -0500 Subject: [PATCH 18/26] fix(cdk/tree): fix padding not showing for `childrenAccessor` trees --- src/cdk/tree/padding.ts | 3 +-- src/cdk/tree/tree.ts | 14 +++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/cdk/tree/padding.ts b/src/cdk/tree/padding.ts index 1953550bf0d3..54c5c51a0774 100644 --- a/src/cdk/tree/padding.ts +++ b/src/cdk/tree/padding.ts @@ -80,8 +80,7 @@ export class CdkTreeNodePadding implements OnDestroy { /** The padding indent value for the tree node. Returns a string with px numbers if not null. */ _paddingIndent(): string | null { - const nodeLevel = - (this._treeNode.data && this._tree._getLevelAccessor()?.(this._treeNode.data)) ?? null; + const nodeLevel = (this._treeNode.data && this._tree._getLevel(this._treeNode.data)) ?? null; const level = this._level == null ? nodeLevel : this._level; return typeof level === 'number' ? `${level * this._indent}${this.indentUnits}` : null; } diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 6db36409f920..186a624d47d3 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -193,6 +193,9 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, new Map>(), ); + /** The mapping between data nodes and the parent node. `null` if no parent. */ + private _parents: Map = new Map(); + constructor(private _differs: IterableDiffers, private _changeDetectorRef: ChangeDetectorRef) {} ngOnInit() { @@ -381,6 +384,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, // Node context that will be provided to created embedded view const context = new CdkTreeNodeOutletContext(nodeData); + parentData ??= this._parents.get(this._trackExpansionKey(nodeData)) ?? undefined; // If the tree is flat tree, then use the `getLevel` function in flat tree control // Otherwise, use the level of parent node. const levelAccessor = this._getLevelAccessor(); @@ -596,6 +600,10 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, this._nodes.next(this._nodes.value); } + _getLevel(node: T) { + return this._levels.get(node); + } + /** * Gets all nodes in the tree, through recursive expansion. * @@ -664,6 +672,10 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, return coerceObservable(this.childrenAccessor(dataNode)).pipe( take(1), switchMap(children => { + // Here, we cache the parents of a particular child so that we can compute the levels. + for (const child of children) { + this._parents.set(this._trackExpansionKey(child), dataNode); + } return observableOf(...children).pipe( concatMap(child => concat(observableOf([child]), this._getAllChildrenRecursively(child))), ); @@ -772,7 +784,7 @@ export class CdkTreeNode implements FocusableOption, OnDestroy, OnInit // If the tree has a levelAccessor, use it to get the level. Otherwise read the // aria-level off the parent node and use it as the level for this node (note aria-level is // 1-indexed, while this property is 0-indexed, so we don't need to increment). - return this._tree._getLevelAccessor()?.(this._data) ?? this._parentNodeAriaLevel; + return this._tree._getLevel(this._data) ?? this._parentNodeAriaLevel; } constructor(protected _elementRef: ElementRef, protected _tree: CdkTree) { From fe30c2b5a3d43216d9751875bda84fc769a55546 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:18 -0500 Subject: [PATCH 19/26] fix(cdk/tree): fix flat tree demo --- src/cdk/tree/tree.ts | 3 --- .../cdk-tree-flat-level-accessor-example.ts | 8 ++++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 186a624d47d3..5f62285e68ae 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -185,9 +185,6 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, /** Maintain a synchronous cache of the currently known data nodes. */ private _dataNodes: BehaviorSubject = new BehaviorSubject([]); - /** Maintain a synchronous cache of the currently rendered nodes. */ - private _renderedNodes: BehaviorSubject> = new BehaviorSubject>(new Set()); - /** The mapping between data and the node that is rendered. */ private _nodes: BehaviorSubject>> = new BehaviorSubject( new Map>(), diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts index 819fb4c707d1..3484953f7abb 100644 --- a/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts +++ b/src/components-examples/cdk/tree/cdk-tree-flat-level-accessor/cdk-tree-flat-level-accessor-example.ts @@ -1,5 +1,6 @@ import {ArrayDataSource} from '@angular/cdk/collections'; -import {Component} from '@angular/core'; +import {CdkTree} from '@angular/cdk/tree'; +import {Component, ViewChild} from '@angular/core'; import {FlatFoodNode, FLAT_DATA} from '../tree-data'; /** @@ -11,6 +12,9 @@ import {FlatFoodNode, FLAT_DATA} from '../tree-data'; styleUrls: ['cdk-tree-flat-level-accessor-example.css'], }) export class CdkTreeFlatLevelAccessorExample { + @ViewChild(CdkTree) + tree: CdkTree; + levelAccessor = (dataNode: FlatFoodNode) => dataNode.level; dataSource = new ArrayDataSource(FLAT_DATA); @@ -34,6 +38,6 @@ export class CdkTreeFlatLevelAccessorExample { shouldRender(node: FlatFoodNode): boolean { // This node should render if it is a root node or if all of its ancestors are expanded. const parent = this.getParentNode(node); - return !parent || (!!parent.isExpanded && this.shouldRender(parent)); + return !parent || (!!this.tree?.isExpanded(parent) && this.shouldRender(parent)); } } From 46cb0a7f6eea2bfbb9aaee8111aaec4ecb95edd1 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:18 -0500 Subject: [PATCH 20/26] fix(cdk/tree): convert generator function to return a regular array in demo --- .../cdk-tree-flat-children-accessor-example.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.ts b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.ts index be1ed4abdcb2..48ed2152d27a 100644 --- a/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.ts +++ b/src/components-examples/cdk/tree/cdk-tree-flat-children-accessor/cdk-tree-flat-children-accessor-example.ts @@ -5,13 +5,15 @@ import {timer} from 'rxjs'; import {mapTo} from 'rxjs/operators'; import {NestedFoodNode, NESTED_DATA} from '../tree-data'; -function* allNodes(nodes: NestedFoodNode[]): Iterable { +function flattenNodes(nodes: NestedFoodNode[]): NestedFoodNode[] { + const flattenedNodes = []; for (const node of nodes) { - yield node; + flattenedNodes.push(node); if (node.children) { - yield* allNodes(node.children); + flattenedNodes.push(...flattenNodes(node.children)); } } + return flattenedNodes; } /** @@ -33,7 +35,7 @@ export class CdkTreeFlatChildrenAccessorExample { hasChild = (_: number, node: NestedFoodNode) => !!node.children?.length; getParentNode(node: NestedFoodNode) { - for (const parent of allNodes(NESTED_DATA)) { + for (const parent of flattenNodes(NESTED_DATA)) { if (parent.children?.includes(node)) { return parent; } From 97ae28d115632ab81c73589b6534a66493205332 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:18 -0500 Subject: [PATCH 21/26] fix(cdk/tree): fix build error --- src/cdk/tree/tree.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 5f62285e68ae..311b487fc53a 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -381,7 +381,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, // Node context that will be provided to created embedded view const context = new CdkTreeNodeOutletContext(nodeData); - parentData ??= this._parents.get(this._trackExpansionKey(nodeData)) ?? undefined; + parentData ??= this._parents.get(this._getExpansionKey(nodeData)) ?? undefined; // If the tree is flat tree, then use the `getLevel` function in flat tree control // Otherwise, use the level of parent node. const levelAccessor = this._getLevelAccessor(); @@ -533,7 +533,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, _getDirectChildren(dataNode: T): Observable { const levelAccessor = this._getLevelAccessor(); if (levelAccessor && this._expansionModel) { - const key = this._trackExpansionKey(dataNode); + const key = this._getExpansionKey(dataNode); const isExpanded = this._expansionModel.changed.pipe( switchMap(changes => { if (changes.added.includes(key)) { @@ -587,13 +587,13 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, * This primarily facilitates keyboard navigation. */ _registerNode(node: CdkTreeNode) { - this._nodes.value.set(this._trackExpansionKey(node.data), node); + this._nodes.value.set(this._getExpansionKey(node.data), node); this._nodes.next(this._nodes.value); } /** Removes the specified node component from the tree's internal registry. */ _unregisterNode(node: CdkTreeNode) { - this._nodes.value.delete(this._trackExpansionKey(node.data)); + this._nodes.value.delete(this._getExpansionKey(node.data)); this._nodes.next(this._nodes.value); } @@ -671,7 +671,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, switchMap(children => { // Here, we cache the parents of a particular child so that we can compute the levels. for (const child of children) { - this._parents.set(this._trackExpansionKey(child), dataNode); + this._parents.set(this._getExpansionKey(child), dataNode); } return observableOf(...children).pipe( concatMap(child => concat(observableOf([child]), this._getAllChildrenRecursively(child))), From 1f056cc03988190295319d51648a2e579e0e0e1c Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:18 -0500 Subject: [PATCH 22/26] feat(cdk/tree): update API goldens --- tools/public_api_guard/cdk/tree.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tools/public_api_guard/cdk/tree.md b/tools/public_api_guard/cdk/tree.md index 3d76e5819cb3..5efaccf26bc7 100644 --- a/tools/public_api_guard/cdk/tree.md +++ b/tools/public_api_guard/cdk/tree.md @@ -89,6 +89,9 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, expandDescendants(dataNode: T): void; expansionKey?: (dataNode: T) => K; _getChildrenAccessor(): ((dataNode: T) => T[] | Observable | null | undefined) | undefined; + _getDirectChildren(dataNode: T): Observable; + // (undocumented) + _getLevel(node: T): number | undefined; _getLevelAccessor(): ((dataNode: T) => number) | undefined; _getNodeDef(data: T, i: number): CdkTreeNodeDef; insertNode(nodeData: T, index: number, viewContainer?: ViewContainerRef, parentData?: T): void; @@ -103,12 +106,16 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, _nodeDefs: QueryList>; // (undocumented) _nodeOutlet: CdkTreeNodeOutlet; + nodeType?: 'flat' | 'nested'; + _registerNode(node: CdkTreeNode): void; renderNodeChanges(data: readonly T[], dataDiffer?: IterableDiffer, viewContainer?: ViewContainerRef, parentData?: T): void; + _renderNodeChanges(data: readonly T[], dataDiffer: IterableDiffer, viewContainer: ViewContainerRef, parentData?: T): void; toggle(dataNode: T): void; toggleDescendants(dataNode: T): void; trackBy: TrackByFunction; // @deprecated treeControl?: TreeControl; + _unregisterNode(node: CdkTreeNode): void; readonly viewChange: BehaviorSubject<{ start: number; end: number; @@ -272,6 +279,9 @@ export function getTreeControlFunctionsMissingError(): Error; // @public export function getTreeControlMissingError(): Error; +// @public +export function getTreeControlNodeTypeUnspecifiedError(): Error; + // @public export function getTreeMissingMatchingNodeDefError(): Error; From dd5c4c298584ab65a7d8428bb91f285993be243b Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:19 -0500 Subject: [PATCH 23/26] fix(cdk/tree): fix some failing tests, one remaining --- src/cdk/tree/tree-errors.ts | 8 --- src/cdk/tree/tree-redesign.spec.ts | 81 +++++++++++------------------- src/cdk/tree/tree.spec.ts | 37 -------------- 3 files changed, 30 insertions(+), 96 deletions(-) diff --git a/src/cdk/tree/tree-errors.ts b/src/cdk/tree/tree-errors.ts index 59dcd827b5dd..d07879aa5e69 100644 --- a/src/cdk/tree/tree-errors.ts +++ b/src/cdk/tree/tree-errors.ts @@ -47,14 +47,6 @@ export function getMultipleTreeControlsError() { return Error(`More than one of tree control, levelAccessor, or childrenAccessor were provided.`); } -/** - * Returns an error to be thrown when tree control did not implement functions for flat/nested node. - * @docs-private - */ -export function getTreeControlFunctionsMissingError() { - return Error(`Could not find functions for nested/flat tree in tree control.`); -} - /** * Returns an error to be thrown when the node type is not specified. * @docs-private diff --git a/src/cdk/tree/tree-redesign.spec.ts b/src/cdk/tree/tree-redesign.spec.ts index 644d6ed03853..07afbd88b284 100644 --- a/src/cdk/tree/tree-redesign.spec.ts +++ b/src/cdk/tree/tree-redesign.spec.ts @@ -24,7 +24,6 @@ import {map} from 'rxjs/operators'; import {CdkTreeModule, CdkTreeNodePadding} from './index'; import {CdkTree, CdkTreeNode} from './tree'; -import {getTreeControlFunctionsMissingError} from './tree-errors'; /** * This is a cloned version of `tree.spec.ts` that contains all the same tests, @@ -1127,20 +1126,6 @@ describe('CdkTree redesign', () => { expect(changedNodes[5].getAttribute('initialIndex')).toBe('2'); }); }); - - it('should throw an error when missing function in nested tree', fakeAsync(() => { - configureCdkTreeTestingModule([NestedCdkErrorTreeApp]); - expect(() => { - try { - TestBed.createComponent(NestedCdkErrorTreeApp).detectChanges(); - flush(); - } catch { - flush(); - } finally { - flush(); - } - }).toThrowError(getTreeControlFunctionsMissingError().message); - })); }); describe('with depth', () => { @@ -1356,7 +1341,8 @@ function expectNestedTreeToMatch(treeElement: Element, ...expectedTree: any[]) { @Component({ template: ` - + + + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} @@ -1417,7 +1405,8 @@ class NestedCdkTreeApp { @Component({ template: ` - + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} @@ -1445,7 +1434,8 @@ class StaticNestedCdkTreeApp { @Component({ template: ` - + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} @@ -1469,7 +1459,8 @@ class WhenNodeNestedCdkTreeApp { @Component({ template: ` - + + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} @@ -1515,7 +1507,8 @@ class NestedCdkTreeAppWithToggle { @Component({ template: ` - + + + + [{{node.pizzaTopping}}] - [{{node.pizzaCheese}}] + [{{node.pizzaBase}}] @@ -1615,7 +1611,8 @@ class ArrayDataSourceNestedCdkTreeApp { @Component({ template: ` - + [{{node.pizzaTopping}}] - [{{node.pizzaCheese}}] + [{{node.pizzaBase}}] @@ -1637,28 +1634,8 @@ class ObservableDataSourceNestedCdkTreeApp { @Component({ template: ` - - - {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} - - - - `, -}) -class NestedCdkErrorTreeApp { - getLevel = (node: TestData) => node.level; - - isExpandable = (node: TestData) => node.children.length > 0; - - dataSource: FakeDataSource | null = new FakeDataSource(); - - @ViewChild(CdkTree) tree: CdkTree; -} - -@Component({ - template: ` - + {{level}} [{{node.pizzaTopping}}] - [{{node.pizzaCheese}}] + [{{node.pizzaBase}}] @@ -1681,7 +1658,8 @@ class DepthNestedCdkTreeApp { @Component({ template: ` - + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} @@ -1712,7 +1690,8 @@ class CdkTreeAppWithTrackBy { @Component({ template: ` - + [{{node.pizzaTopping}}] - [{{node.pizzaCheese}}] + [{{node.pizzaBase}}] diff --git a/src/cdk/tree/tree.spec.ts b/src/cdk/tree/tree.spec.ts index 8c58ee0db1c8..30b979f7702c 100644 --- a/src/cdk/tree/tree.spec.ts +++ b/src/cdk/tree/tree.spec.ts @@ -27,7 +27,6 @@ import {FlatTreeControl} from './control/flat-tree-control'; import {NestedTreeControl} from './control/nested-tree-control'; import {CdkTreeModule, CdkTreeNodePadding} from './index'; import {CdkTree, CdkTreeNode} from './tree'; -import {getTreeControlFunctionsMissingError} from './tree-errors'; describe('CdkTree', () => { /** Represents an indent for expectNestedTreeToMatch */ @@ -1126,20 +1125,6 @@ describe('CdkTree', () => { expect(changedNodes[5].getAttribute('initialIndex')).toBe('2'); }); }); - - it('should throw an error when missing function in nested tree', fakeAsync(() => { - configureCdkTreeTestingModule([NestedCdkErrorTreeApp]); - expect(() => { - try { - TestBed.createComponent(NestedCdkErrorTreeApp).detectChanges(); - flush(); - } catch { - flush(); - } finally { - flush(); - } - }).toThrowError(getTreeControlFunctionsMissingError().message); - })); }); describe('with depth', () => { @@ -1633,28 +1618,6 @@ class ObservableDataSourceNestedCdkTreeApp { @ViewChild(CdkTree) tree: CdkTree; } -@Component({ - template: ` - - - {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} - - - - `, -}) -class NestedCdkErrorTreeApp { - getLevel = (node: TestData) => node.level; - - isExpandable = (node: TestData) => node.children.length > 0; - - treeControl: TreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); - - dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); - - @ViewChild(CdkTree) tree: CdkTree; -} - @Component({ template: ` From 8c4132dd76606462073d244c2027576163bf5d21 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:19 -0500 Subject: [PATCH 24/26] fix(cdk/tree): fix test errors and children conversion; also make `renderNodeChanges` private --- src/cdk/tree/tree.ts | 62 ++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 311b487fc53a..5d7432abb2ed 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -40,7 +40,7 @@ import { Subject, Subscription, } from 'rxjs'; -import {concatMap, map, reduce, startWith, switchMap, take, takeUntil} from 'rxjs/operators'; +import {concatMap, map, reduce, startWith, switchMap, take, takeUntil, tap} from 'rxjs/operators'; import {TreeControl} from './control/tree-control'; import {CdkTreeNodeDef, CdkTreeNodeOutletContext} from './node'; import {CdkTreeNodeOutlet} from './outlet'; @@ -155,6 +155,8 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, * * This controls what selection of data the tree will render. */ + // NB: we're unable to determine this ourselves; Angular's ContentChildren + // unfortunately does not pick up the necessary information. @Input() nodeType?: 'flat' | 'nested'; // Outlets within the tree's template where the dataNodes will be inserted. @@ -182,7 +184,11 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, /** Keep track of which nodes are expanded. */ private _expansionModel?: SelectionModel; - /** Maintain a synchronous cache of the currently known data nodes. */ + /** + * Maintain a synchronous cache of the currently known data nodes. In the + * case of nested nodes (i.e. if `nodeType` is 'nested'), this will + * only contain the root nodes. + */ private _dataNodes: BehaviorSubject = new BehaviorSubject([]); /** The mapping between data and the node that is rendered. */ @@ -291,39 +297,22 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, if (dataStream) { this._dataSubscription = dataStream .pipe( - switchMap(data => this._flattenChildren(data)), + switchMap(data => this._convertChildren(data)), takeUntil(this._onDestroy), ) - .subscribe(data => this.renderNodeChanges(data)); + .subscribe(data => this._renderNodeChanges(data)); } else if (typeof ngDevMode === 'undefined' || ngDevMode) { throw getTreeNoValidDataSourceError(); } } /** Check for changes made in the data and render each change (node added/removed/moved). */ - renderNodeChanges( + _renderNodeChanges( data: readonly T[], dataDiffer: IterableDiffer = this._dataDiffer, viewContainer: ViewContainerRef = this._nodeOutlet.viewContainer, parentData?: T, ) { - this._dataNodes.next(data); - - this._renderNodeChanges(data, dataDiffer, viewContainer, parentData); - } - - /** Check for changes made in the data and render each change (node added/removed/moved). */ - _renderNodeChanges( - data: readonly T[], - dataDiffer: IterableDiffer, - viewContainer: ViewContainerRef, - parentData?: T, - ) { - const levelAccessor = this._getLevelAccessor(); - if (levelAccessor && this.nodeType === 'nested' && !parentData) { - data = data.filter(data => levelAccessor(data) === 0); - } - const changes = dataDiffer.diff(data); if (!changes) { return; @@ -691,19 +680,36 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, return this.expansionKey?.(dataNode) ?? (dataNode as unknown as K); } - private _flattenChildren(nodes: readonly T[]): Observable { - // If we're using TreeControl or levelAccessor, we don't need to manually - // flatten things here. - if (!this.childrenAccessor) { - return observableOf(nodes); - } else { + /** + * Converts children for certain tree configurations. Note also that this + * caches the known nodes for use in other parts of the tree. + */ + private _convertChildren(nodes: readonly T[]): Observable { + // The only situations where we have to convert children types is when + // they're mismatched; i.e. if the tree is using a childrenAccessor and the + // nodes are flat, or if the tree is using a levelAccessor and the nodes are + // nested. + if (this.childrenAccessor && this.nodeType === 'flat') { + // This flattens children into a single array. return observableOf(...nodes).pipe( concatMap(node => concat(observableOf([node]), this._getAllChildrenRecursively(node))), reduce((results, nodes) => { results.push(...nodes); return results; }, [] as T[]), + tap(nodes => { + this._dataNodes.next(nodes); + }), ); + } else if (this.levelAccessor && this.nodeType === 'nested') { + this._dataNodes.next(nodes); + // In the nested case, we only look for root nodes. The CdkNestedNode + // itself will handle rendering each individual node's children. + const levelAccessor = this.levelAccessor; + return observableOf(nodes.filter(node => levelAccessor(node) === 0)); + } else { + this._dataNodes.next(nodes); + return observableOf(nodes); } } } From 94cd73b900e4089fdbbfce0437de0525a3d46454 Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:19 -0500 Subject: [PATCH 25/26] fix(cdk/tree): update api goldens --- tools/public_api_guard/cdk/tree.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tools/public_api_guard/cdk/tree.md b/tools/public_api_guard/cdk/tree.md index 5efaccf26bc7..ec83fed73824 100644 --- a/tools/public_api_guard/cdk/tree.md +++ b/tools/public_api_guard/cdk/tree.md @@ -108,8 +108,7 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, _nodeOutlet: CdkTreeNodeOutlet; nodeType?: 'flat' | 'nested'; _registerNode(node: CdkTreeNode): void; - renderNodeChanges(data: readonly T[], dataDiffer?: IterableDiffer, viewContainer?: ViewContainerRef, parentData?: T): void; - _renderNodeChanges(data: readonly T[], dataDiffer: IterableDiffer, viewContainer: ViewContainerRef, parentData?: T): void; + _renderNodeChanges(data: readonly T[], dataDiffer?: IterableDiffer, viewContainer?: ViewContainerRef, parentData?: T): void; toggle(dataNode: T): void; toggleDescendants(dataNode: T): void; trackBy: TrackByFunction; @@ -273,9 +272,6 @@ export interface FlatTreeControlOptions { // @public export function getMultipleTreeControlsError(): Error; -// @public -export function getTreeControlFunctionsMissingError(): Error; - // @public export function getTreeControlMissingError(): Error; From bdbd7ddb9ae45e9b108ad3b49b0125a1a7a7497c Mon Sep 17 00:00:00 2001 From: Cassandra Choi Date: Fri, 17 Feb 2023 16:47:19 -0500 Subject: [PATCH 26/26] fix(cdk/tree): fix lint errors --- src/cdk/tree/tree-redesign.spec.ts | 2 +- src/cdk/tree/tree.spec.ts | 2 +- src/cdk/tree/tree.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cdk/tree/tree-redesign.spec.ts b/src/cdk/tree/tree-redesign.spec.ts index 07afbd88b284..109d9b32a353 100644 --- a/src/cdk/tree/tree-redesign.spec.ts +++ b/src/cdk/tree/tree-redesign.spec.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; import { Component, ErrorHandler, diff --git a/src/cdk/tree/tree.spec.ts b/src/cdk/tree/tree.spec.ts index 30b979f7702c..3ba2ca114050 100644 --- a/src/cdk/tree/tree.spec.ts +++ b/src/cdk/tree/tree.spec.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; import { Component, ErrorHandler, diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index 5d7432abb2ed..4ce0ce26a8c9 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -693,12 +693,12 @@ export class CdkTree implements AfterContentChecked, CollectionViewer, // This flattens children into a single array. return observableOf(...nodes).pipe( concatMap(node => concat(observableOf([node]), this._getAllChildrenRecursively(node))), - reduce((results, nodes) => { - results.push(...nodes); + reduce((results, children) => { + results.push(...children); return results; }, [] as T[]), - tap(nodes => { - this._dataNodes.next(nodes); + tap(allNodes => { + this._dataNodes.next(allNodes); }), ); } else if (this.levelAccessor && this.nodeType === 'nested') {