Skip to content

Commit e26eddd

Browse files
committed
docs(tree): add more tree examples
1 parent 6405da9 commit e26eddd

File tree

9 files changed

+564
-0
lines changed

9 files changed

+564
-0
lines changed

src/material-examples/tree-checklist/tree-checklist-example.css

Whitespace-only changes.
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>
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import {Component, Injectable} from '@angular/core';
2+
import {SelectionModel} from '@angular/cdk/collections';
3+
import {FlatTreeControl} from '@angular/cdk/tree';
4+
import {MatTreeFlattener, MatTreeFlatDataSource} from '@angular/material/tree';
5+
import {of as ofObservable} from 'rxjs/observable/of';
6+
import {Observable} from 'rxjs/Observable';
7+
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
8+
9+
/**
10+
* Node for to-do item
11+
*/
12+
export class TodoItemNode {
13+
children: TodoItemNode[];
14+
item: string;
15+
}
16+
17+
/** Flat to-do item node with expandable and level information */
18+
export class TodoItemFlatNode {
19+
item: string;
20+
level: number;
21+
expandable: boolean;
22+
}
23+
24+
/**
25+
* The Json object for to-do list data.
26+
*/
27+
const TREE_DATA = {
28+
'Reminders': [
29+
'Cook dinner',
30+
'Read the Material Design spec',
31+
'Upgrade Application to Angular'
32+
],
33+
'Groceries': {
34+
'Organic eggs': null,
35+
'Protein Powder': null,
36+
'Almond Meal flour': null,
37+
'Fruits': {
38+
'Apple': null,
39+
'Orange': null,
40+
'Berries': ['Blueberry', 'Raspberry']
41+
}
42+
}
43+
};
44+
45+
/**
46+
* Checklist database, it can build a tree structured Json object.
47+
* Each node in Json object represents a to-do item or a category.
48+
* If a node is a category, it has children items and new items can be added under the category.
49+
*/
50+
@Injectable()
51+
export class ChecklistDatabase {
52+
dataChange: BehaviorSubject<TodoItemNode[]> = new BehaviorSubject<TodoItemNode[]>([]);
53+
54+
get data(): TodoItemNode[] { return this.dataChange.value; }
55+
56+
constructor() {
57+
this.initialize();
58+
}
59+
60+
initialize() {
61+
// Build the tree nodes from Json object. The result is a list of `TodoItemNode` with nested
62+
// file node as children.
63+
const data = this.buildFileTree(TREE_DATA, 0);
64+
65+
// Notify the change.
66+
this.dataChange.next(data);
67+
}
68+
69+
/**
70+
* Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
71+
* The return value is the list of `TodoItemNode`.
72+
*/
73+
buildFileTree(value: any, level: number) {
74+
let data: any[] = [];
75+
for (let k in value) {
76+
let v = value[k];
77+
let node = new TodoItemNode();
78+
node.item = `${k}`;
79+
if (v === null || v === undefined) {
80+
// no action
81+
} else if (typeof v === 'object') {
82+
node.children = this.buildFileTree(v, level + 1);
83+
} else {
84+
node.item = v;
85+
}
86+
data.push(node);
87+
}
88+
return data;
89+
}
90+
91+
/** Add an item to to-do list */
92+
insertItem(parent: TodoItemNode, name: string) {
93+
const child = <TodoItemNode>{item: name};
94+
if (parent.children) {
95+
parent.children.push(child);
96+
this.dataChange.next(this.data);
97+
}
98+
}
99+
}
100+
101+
/**
102+
* @title Tree with checkboxes
103+
*/
104+
@Component({
105+
moduleId: module.id,
106+
selector: 'tree-checklist-example',
107+
templateUrl: 'tree-checklist-example.html',
108+
styleUrls: ['tree-checklist-example.css'],
109+
providers: [ChecklistDatabase]
110+
})
111+
export class TreeChecklistExample {
112+
/** Map from flat node to nested node. This helps us finding the nested node to be modified */
113+
flatNodeMap: Map<TodoItemFlatNode, TodoItemNode> = new Map<TodoItemFlatNode, TodoItemNode>();
114+
115+
/** Map from nested node to flattened node. This helps us to keep the same object for selection */
116+
nestedNodeMap: Map<TodoItemNode, TodoItemFlatNode> = new Map<TodoItemNode, TodoItemFlatNode>();
117+
118+
/** A selected parent node to be inserted */
119+
selectedParent: TodoItemFlatNode | null = null;
120+
121+
/** The new item's name */
122+
newItemName: string = '';
123+
124+
treeControl: FlatTreeControl<TodoItemFlatNode>;
125+
126+
treeFlattener: MatTreeFlattener<TodoItemNode, TodoItemFlatNode>;
127+
128+
dataSource: MatTreeFlatDataSource<TodoItemNode, TodoItemFlatNode>;
129+
130+
/** The selection for checklist */
131+
checklistSelection = new SelectionModel<TodoItemFlatNode>(true /* multiple */);
132+
133+
constructor(private database: ChecklistDatabase) {
134+
this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel,
135+
this.isExpandable, this.getChildren);
136+
this.treeControl = new FlatTreeControl<TodoItemFlatNode>(this.getLevel, this.isExpandable);
137+
this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
138+
139+
database.dataChange.subscribe(data => {
140+
this.dataSource.data = data;
141+
});
142+
}
143+
144+
getLevel = (node: TodoItemFlatNode) => { return node.level; };
145+
146+
isExpandable = (node: TodoItemFlatNode) => { return node.expandable; };
147+
148+
getChildren = (node: TodoItemNode): Observable<TodoItemNode[]> => {
149+
return ofObservable(node.children);
150+
}
151+
152+
hasChild = (_: number, _nodeData: TodoItemFlatNode) => { return _nodeData.expandable; };
153+
154+
/**
155+
* Transformer to convert nested node to flat node. Record the nodes in maps for later use.
156+
*/
157+
transformer = (node: TodoItemNode, level: number) => {
158+
let flatNode = this.nestedNodeMap.has(node)
159+
? this.nestedNodeMap.get(node)!
160+
: new TodoItemFlatNode();
161+
flatNode.item = node.item;
162+
flatNode.level = level;
163+
flatNode.expandable = !!node.children;
164+
this.flatNodeMap.set(flatNode, node);
165+
this.nestedNodeMap.set(node, flatNode);
166+
return flatNode;
167+
}
168+
169+
/** Whether all the descendants of the node are selected */
170+
descendantsAllSelected(node: TodoItemFlatNode): boolean {
171+
const descendants = this.treeControl.getDescendants(node);
172+
return descendants.every(child => this.checklistSelection.isSelected(child));
173+
}
174+
175+
/** Whether part of the descendants are selected */
176+
descendantsPartiallySelected(node: TodoItemFlatNode): boolean {
177+
const descendants = this.treeControl.getDescendants(node);
178+
const result = descendants.some(child => this.checklistSelection.isSelected(child));
179+
return result && !this.descendantsAllSelected(node);
180+
}
181+
182+
/** Toggle the to-do item selection. Select/deselect all the descendants node */
183+
todoItemSelectionToggle(node: TodoItemFlatNode): void {
184+
this.checklistSelection.toggle(node);
185+
const descendants = this.treeControl.getDescendants(node);
186+
this.checklistSelection.isSelected(node)
187+
? this.checklistSelection.select(...descendants)
188+
: this.checklistSelection.deselect(...descendants);
189+
}
190+
191+
/** Insert a new item with name `newItemName` to be under category `selectedParent`. */
192+
addNode() {
193+
if (this.selectedParent) {
194+
const parentNode = this.flatNodeMap.get(this.selectedParent);
195+
this.database.insertItem(parentNode!, this.newItemName);
196+
}
197+
}
198+
199+
/** Select the category so we can insert the new item. */
200+
setParent(node: TodoItemFlatNode) {
201+
this.selectedParent = node;
202+
}
203+
}

src/material-examples/tree-dynamic/tree-dynamic-example.css

Whitespace-only changes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
2+
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
3+
{{node.item}}
4+
</mat-tree-node>
5+
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
6+
<button mat-icon-button
7+
[attr.aria-label]="'toggle ' + node.filename" matTreeNodeToggle>
8+
<mat-icon>
9+
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
10+
</mat-icon>
11+
</button>
12+
{{node.item}}
13+
</mat-tree-node>
14+
</mat-tree>
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import {Component, Injectable} from '@angular/core';
2+
import {FlatTreeControl} from '@angular/cdk/tree';
3+
import {CollectionViewer, SelectionChange} from '@angular/cdk/collections';
4+
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
5+
import {Observable} from 'rxjs/Observable';
6+
import {merge} from 'rxjs/observable/merge';
7+
import {map} from 'rxjs/operators/map';
8+
9+
10+
/** Flat node with expandable and level information */
11+
export class DynamicFlatNode {
12+
constructor(public item: string, public level: number = 1, public expandable: boolean = false) {}
13+
}
14+
15+
16+
/**
17+
* Database for dynamic data. When expanding a node in the tree, the data source will need to fetch
18+
* the descendants data from the database.
19+
*/
20+
export class DynamicDatabase {
21+
dataMap = new Map([
22+
['Fruits', ['Apple', 'Orange', 'Banana']],
23+
['Vegetables', ['Tomato', 'Potato', 'Onion']],
24+
['Apple', ['Fuji', 'Macintosh']],
25+
['Onion', ['Yellow', 'White', 'Purple']]
26+
]);
27+
28+
rootLevelNodes = ['Fruits', 'Vegetables'];
29+
30+
/** Initial data from database */
31+
initialData(): DynamicFlatNode[] {
32+
return this.rootLevelNodes.map(name => new DynamicFlatNode(name, 1, true));
33+
}
34+
35+
36+
getChildren(node: string): string[] | undefined {
37+
return this.dataMap.get(node);
38+
}
39+
40+
isExpandable(node: string): boolean {
41+
return this.dataMap.has(node);
42+
}
43+
}
44+
/**
45+
* File database, it can build a tree structured Json object from string.
46+
* Each node in Json object represents a file or a directory. For a file, it has filename and type.
47+
* For a directory, it has filename and children (a list of files or directories).
48+
* The input will be a json object string, and the output is a list of `FileNode` with nested
49+
* structure.
50+
*/
51+
@Injectable()
52+
export class DynamicDataSource {
53+
54+
dataChange: BehaviorSubject<DynamicFlatNode[]> = new BehaviorSubject<DynamicFlatNode[]>([]);
55+
56+
get data(): DynamicFlatNode[] { return this.dataChange.value; }
57+
set data(value: DynamicFlatNode[]) {
58+
this.treeControl.dataNodes = value;
59+
this.dataChange.next(value);
60+
}
61+
62+
constructor(private treeControl: FlatTreeControl<DynamicFlatNode>,
63+
private database: DynamicDatabase) {}
64+
65+
connect(collectionViewer: CollectionViewer): Observable<DynamicFlatNode[]> {
66+
return merge(collectionViewer.viewChange, this.treeControl.expansionModel.onChange!)
67+
.pipe(map((change) => {
68+
if ((change as SelectionChange<DynamicFlatNode>).added ||
69+
(change as SelectionChange<DynamicFlatNode>).removed) {
70+
this.handleTreeControl(change as SelectionChange<DynamicFlatNode>);
71+
}
72+
return this.data;
73+
}));
74+
}
75+
76+
/** Handle expand/collapse behaviors */
77+
handleTreeControl(change: SelectionChange<DynamicFlatNode>) {
78+
if (change.added) {
79+
change.added.forEach((node) => this.toggleNode(node, true));
80+
}
81+
if (change.removed) {
82+
change.removed.forEach((node) => this.toggleNode(node, false));
83+
}
84+
}
85+
86+
/**
87+
* Toggle the node, remove from display list
88+
*/
89+
toggleNode(node: DynamicFlatNode, expand: boolean) {
90+
const children = this.database.getChildren(node.item);
91+
const index = this.data.indexOf(node);
92+
if (!children || index < 0) { // If no children, or cannot find the node, no op
93+
return;
94+
}
95+
96+
if (expand) {
97+
const nodes = children.map(name =>
98+
new DynamicFlatNode(name, node.level + 1, this.database.isExpandable(name)));
99+
this.data.splice(index + 1, 0, ...nodes);
100+
} else {
101+
this.data.splice(index + 1, children.length);
102+
}
103+
104+
// notify the change
105+
this.dataChange.next(this.data);
106+
}
107+
}
108+
109+
/**
110+
* @title Tree with dynamic data
111+
*/
112+
@Component({
113+
moduleId: module.id,
114+
selector: 'tree-dynamic-example',
115+
templateUrl: 'tree-dynamic-example.html',
116+
styleUrls: ['tree-dynamic-example.css'],
117+
providers: [DynamicDatabase]
118+
})
119+
export class TreeDynamicExample {
120+
121+
constructor(database: DynamicDatabase) {
122+
this.treeControl = new FlatTreeControl<DynamicFlatNode>(this.getLevel, this.isExpandable);
123+
this.dataSource = new DynamicDataSource(this.treeControl, database);
124+
125+
this.dataSource.data = database.initialData();
126+
}
127+
128+
treeControl: FlatTreeControl<DynamicFlatNode>;
129+
130+
dataSource: DynamicDataSource;
131+
132+
getLevel = (node: DynamicFlatNode) => { return node.level; };
133+
134+
isExpandable = (node: DynamicFlatNode) => { return node.expandable; };
135+
136+
hasChild = (_: number, _nodeData: DynamicFlatNode) => { return _nodeData.expandable; };
137+
}

src/material-examples/tree-loadmore/tree-loadmore-example.css

Whitespace-only changes.

0 commit comments

Comments
 (0)