Skip to content

Commit c811731

Browse files
authored
docs(tree): add more tree demos (#9993)
1 parent 631397c commit c811731

16 files changed

+695
-2
lines changed

src/demo-app/demo-app/demo-module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ import {
5757
} from '../tabs/tabs-demo';
5858
import {ToolbarDemo} from '../toolbar/toolbar-demo';
5959
import {TooltipDemo} from '../tooltip/tooltip-demo';
60-
import {TreeDemo} from '../tree/tree-demo';
6160
import {TypographyDemo} from '../typography/typography-demo';
6261
import {DemoApp, Home} from './demo-app';
6362
import {DEMO_APP_ROUTES} from './routes';
6463
import {TableDemoModule} from '../table/table-demo-module';
6564
import {BadgeDemo} from '../badge/badge-demo';
65+
import {TreeDemoModule} from '../tree/tree-demo-module';
6666

6767
@NgModule({
6868
imports: [
@@ -73,6 +73,7 @@ import {BadgeDemo} from '../badge/badge-demo';
7373
DemoMaterialModule,
7474
LayoutModule,
7575
TableDemoModule,
76+
TreeDemoModule,
7677
],
7778
declarations: [
7879
AutocompleteDemo,
@@ -127,7 +128,6 @@ import {BadgeDemo} from '../badge/badge-demo';
127128
TabsDemo,
128129
ToolbarDemo,
129130
TooltipDemo,
130-
TreeDemo,
131131
TypographyDemo,
132132
ExampleBottomSheet,
133133
],
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {Injectable} from '@angular/core';
9+
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
10+
11+
/**
12+
* Node for to-do item
13+
*/
14+
export class TodoItemNode {
15+
children: TodoItemNode[];
16+
item: string;
17+
}
18+
19+
/** Flat to-do item node with expandable and level information */
20+
export class TodoItemFlatNode {
21+
item: string;
22+
level: number;
23+
expandable: boolean;
24+
}
25+
26+
/**
27+
* The Json object for to-do list data.
28+
*/
29+
const TREE_DATA = {
30+
'Reminders': [
31+
'Cook dinner',
32+
'Read the Material Design spec',
33+
'Upgrade Application to Angular'
34+
],
35+
'Groceries': {
36+
'Organic eggs': null,
37+
'Protein Powder': null,
38+
'Almond Meal flour': null,
39+
'Fruits': {
40+
'Apple': null,
41+
'Orange': null,
42+
'Berries': ['Blueberry', 'Raspberry']
43+
}
44+
}
45+
};
46+
47+
/**
48+
* Checklist database, it can build a tree structured Json object.
49+
* Each node in Json object represents a to-do item or a category.
50+
* If a node is a category, it has children items and new items can be added under the category.
51+
*/
52+
@Injectable()
53+
export class ChecklistDatabase {
54+
dataChange: BehaviorSubject<TodoItemNode[]> = new BehaviorSubject<TodoItemNode[]>([]);
55+
56+
get data(): TodoItemNode[] { return this.dataChange.value; }
57+
58+
constructor() {
59+
this.initialize();
60+
}
61+
62+
initialize() {
63+
// Build the tree nodes from Json object. The result is a list of `TodoItemNode` with nested
64+
// file node as children.
65+
const data = this.buildFileTree(TREE_DATA, 0);
66+
67+
// Notify the change.
68+
this.dataChange.next(data);
69+
}
70+
71+
/**
72+
* Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
73+
* The return value is the list of `TodoItemNode`.
74+
*/
75+
buildFileTree(value: any, level: number) {
76+
let data: any[] = [];
77+
for (let k in value) {
78+
let v = value[k];
79+
let node = new TodoItemNode();
80+
node.item = `${k}`;
81+
if (v === null || v === undefined) {
82+
// no action
83+
} else if (typeof v === 'object') {
84+
node.children = this.buildFileTree(v, level + 1);
85+
} else {
86+
node.item = v;
87+
}
88+
data.push(node);
89+
}
90+
return data;
91+
}
92+
93+
/** Add an item to to-do list */
94+
insertItem(parent: TodoItemNode, name: string) {
95+
const child = <TodoItemNode>{item: name};
96+
if (parent.children) {
97+
parent.children.push(child);
98+
this.dataChange.next(this.data);
99+
}
100+
}
101+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
2+
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding>
3+
<button mat-icon-button disabled></button>
4+
<mat-checkbox class="checklist-leaf-node"
5+
[checked]="checklistSelection.isSelected(node)"
6+
(change)="checklistSelection.toggle(node);">{{node.item}}</mat-checkbox>
7+
</mat-tree-node>
8+
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
9+
<button mat-icon-button matTreeNodeToggle
10+
[attr.aria-label]="'toggle ' + node.filename">
11+
<mat-icon>
12+
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
13+
</mat-icon>
14+
</button>
15+
<mat-checkbox [checked]="descendantsAllSelected(node)"
16+
[indeterminate]="descendantsPartiallySelected(node)"
17+
(change)="todoItemSelectionToggle(node)">{{node.item}}</mat-checkbox>
18+
<button mat-icon-button (click)="setParent(node)"><mat-icon>add</mat-icon></button>
19+
</mat-tree-node>
20+
</mat-tree>
21+
22+
23+
Selected parent: {{selectedParent ? selectedParent.item : 'No selected'}}
24+
<br>
25+
<mat-form-field>
26+
<input matInput [(ngModel)]="newItemName" placeholder="Add an item"/>
27+
</mat-form-field>
28+
<button mat-button (click)="addNode()">Add node</button>

src/demo-app/tree/checklist-tree-demo/checklist-tree-demo.scss

Whitespace-only changes.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Component} from '@angular/core';
10+
import {SelectionModel} from '@angular/cdk/collections';
11+
import {FlatTreeControl} from '@angular/cdk/tree';
12+
import {MatTreeFlattener, MatTreeFlatDataSource} from '@angular/material/tree';
13+
import {of as ofObservable} from 'rxjs/observable/of';
14+
import {TodoItemNode, TodoItemFlatNode, ChecklistDatabase} from './checklist-database';
15+
16+
/**
17+
* A nested checklist example using tree component.
18+
* Each item/category has a checkbox, and user can check/uncheck a to-do item.
19+
*/
20+
@Component({
21+
moduleId: module.id,
22+
selector: 'checklist-tree-demo',
23+
templateUrl: 'checklist-tree-demo.html',
24+
styleUrls: ['checklist-tree-demo.css'],
25+
providers: [ChecklistDatabase]
26+
})
27+
export class ChecklistTreeDemo {
28+
/** Map from flat node to nested node. This helps us finding the nested node to be modified */
29+
flatNodeMap: Map<TodoItemFlatNode, TodoItemNode> = new Map<TodoItemFlatNode, TodoItemNode>();
30+
31+
/** Map from nested node to flattened node. This helps us to keep the same object for selection */
32+
nestedNodeMap: Map<TodoItemNode, TodoItemFlatNode> = new Map<TodoItemNode, TodoItemFlatNode>();
33+
34+
/** A selected parent node to be inserted */
35+
selectedParent: TodoItemFlatNode | null = null;
36+
37+
/** The new item's name */
38+
newItemName: string = '';
39+
40+
treeControl: FlatTreeControl<TodoItemFlatNode>;
41+
42+
treeFlattener: MatTreeFlattener<TodoItemNode, TodoItemFlatNode>;
43+
44+
dataSource: MatTreeFlatDataSource<TodoItemNode, TodoItemFlatNode>;
45+
46+
/** The selection for checklist */
47+
checklistSelection = new SelectionModel<TodoItemFlatNode>(true /* multiple */);
48+
49+
constructor(private database: ChecklistDatabase) {
50+
this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel,
51+
this.isExpandable, this.getChildren);
52+
this.treeControl = new FlatTreeControl<TodoItemFlatNode>(this.getLevel, this.isExpandable);
53+
this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
54+
55+
database.dataChange.subscribe(data => {
56+
this.dataSource.data = data;
57+
});
58+
}
59+
60+
getLevel = (node: TodoItemFlatNode) => { return node.level; };
61+
62+
isExpandable = (node: TodoItemFlatNode) => { return node.expandable; };
63+
64+
getChildren = (node: TodoItemNode) => { return ofObservable(node.children); };
65+
66+
hasChild = (_: number, _nodeData: TodoItemFlatNode) => { return _nodeData.expandable; };
67+
68+
/**
69+
* Transformer to convert nested node to flat node. Record the nodes in maps for later use.
70+
*/
71+
transformer = (node: TodoItemNode, level: number) => {
72+
let flatNode = this.nestedNodeMap.has(node)
73+
? this.nestedNodeMap.get(node)!
74+
: new TodoItemFlatNode();
75+
flatNode.item = node.item;
76+
flatNode.level = level;
77+
flatNode.expandable = !!node.children;
78+
this.flatNodeMap.set(flatNode, node);
79+
this.nestedNodeMap.set(node, flatNode);
80+
return flatNode;
81+
}
82+
83+
/** Whether all the descendants of the node are selected */
84+
descendantsAllSelected(node: TodoItemFlatNode): boolean {
85+
const descendants = this.treeControl.getDescendants(node);
86+
return descendants.every(child => this.checklistSelection.isSelected(child));
87+
}
88+
89+
/** Whether part of the descendants are selected */
90+
descendantsPartiallySelected(node: TodoItemFlatNode): boolean {
91+
const descendants = this.treeControl.getDescendants(node);
92+
const result = descendants.some(child => this.checklistSelection.isSelected(child));
93+
return result && !this.descendantsAllSelected(node);
94+
}
95+
96+
/** Toggle the to-do item selection. Select/deselect all the descendants node */
97+
todoItemSelectionToggle(node: TodoItemFlatNode): void {
98+
this.checklistSelection.toggle(node);
99+
const descendants = this.treeControl.getDescendants(node);
100+
this.checklistSelection.isSelected(node)
101+
? this.checklistSelection.select(...descendants)
102+
: this.checklistSelection.deselect(...descendants);
103+
}
104+
105+
/** Insert a new item with name `newItemName` to be under category `selectedParent`. */
106+
addNode() {
107+
if (this.selectedParent) {
108+
const parentNode = this.flatNodeMap.get(this.selectedParent);
109+
this.database.insertItem(parentNode!, this.newItemName);
110+
}
111+
}
112+
113+
/** Select the category so we can insert the new item. */
114+
setParent(node: TodoItemFlatNode) {
115+
this.selectedParent = node;
116+
}
117+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {Injectable} from '@angular/core';
9+
import {FlatTreeControl} from '@angular/cdk/tree';
10+
import {CollectionViewer, SelectionChange} from '@angular/cdk/collections';
11+
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
12+
import {merge} from 'rxjs/observable/merge';
13+
import {map} from 'rxjs/operators/map';
14+
15+
/** Flat node with expandable and level information */
16+
export class DynamicFlatNode {
17+
constructor(public item: string, public level: number = 1, public expandable: boolean = false) {}
18+
}
19+
20+
21+
/**
22+
* Database for dynamic data. When expanding a node in the tree, the data source will need to fetch
23+
* the descendants data from the database.
24+
*/
25+
export class DynamicDatabase {
26+
dataMap = new Map([
27+
['Fruits', ['Apple', 'Orange', 'Banana']],
28+
['Vegetables', ['Tomato', 'Potato', 'Onion']],
29+
['Apple', ['Fuji', 'Macintosh']],
30+
['Onion', ['Yellow', 'White', 'Purple']]
31+
]);
32+
33+
rootLevelNodes = ['Fruits', 'Vegetables'];
34+
35+
/** Initial data from database */
36+
initialData(): DynamicFlatNode[] {
37+
return this.rootLevelNodes.map(name => new DynamicFlatNode(name, 1, true));
38+
}
39+
40+
41+
getChildren(node: string): string[] | undefined {
42+
return this.dataMap.get(node);
43+
}
44+
45+
isExpandable(node: string): boolean {
46+
return this.dataMap.has(node);
47+
}
48+
}
49+
/**
50+
* File database, it can build a tree structured Json object from string.
51+
* Each node in Json object represents a file or a directory. For a file, it has filename and type.
52+
* For a directory, it has filename and children (a list of files or directories).
53+
* The input will be a json object string, and the output is a list of `FileNode` with nested
54+
* structure.
55+
*/
56+
@Injectable()
57+
export class DynamicDataSource {
58+
59+
dataChange: BehaviorSubject<DynamicFlatNode[]> = new BehaviorSubject<DynamicFlatNode[]>([]);
60+
61+
get data(): DynamicFlatNode[] { return this.dataChange.value; }
62+
set data(value: DynamicFlatNode[]) {
63+
this.treeControl.dataNodes = value;
64+
this.dataChange.next(value);
65+
}
66+
67+
constructor(private treeControl: FlatTreeControl<DynamicFlatNode>,
68+
private database: DynamicDatabase) {}
69+
70+
connect(collectionViewer: CollectionViewer) {
71+
const changes = [
72+
collectionViewer.viewChange,
73+
this.treeControl.expansionModel.onChange!,
74+
];
75+
return merge(...changes).pipe(map((change) => {
76+
if ((change as SelectionChange<DynamicFlatNode>).added ||
77+
(change as SelectionChange<DynamicFlatNode>).removed) {
78+
this.handleTreeControl(change as SelectionChange<DynamicFlatNode>);
79+
}
80+
return this.data;
81+
}));
82+
}
83+
84+
/** Handle expand/collapse behaviors */
85+
handleTreeControl(change: SelectionChange<DynamicFlatNode>) {
86+
if (change.added) {
87+
change.added.forEach((node) => this.toggleNode(node, true));
88+
}
89+
if (change.removed) {
90+
change.removed.forEach((node) => this.toggleNode(node, false));
91+
}
92+
}
93+
94+
/**
95+
* Toggle the node, remove from display list
96+
*/
97+
toggleNode(node: DynamicFlatNode, expand: boolean) {
98+
const children = this.database.getChildren(node.item);
99+
const index = this.data.indexOf(node);
100+
if (!children || index < 0) { // If no children, or cannot find the node, no op
101+
return;
102+
}
103+
104+
if (expand) {
105+
const nodes = children.map(name =>
106+
new DynamicFlatNode(name, node.level + 1, this.database.isExpandable(name)));
107+
this.data.splice(index + 1, 0, ...nodes);
108+
} else {
109+
this.data.splice(index + 1, children.length);
110+
}
111+
112+
// notify the change
113+
this.dataChange.next(this.data);
114+
}
115+
}

0 commit comments

Comments
 (0)