Skip to content

docs(tree): add more tree demos #9993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/demo-app/demo-app/demo-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ import {
} from '../tabs/tabs-demo';
import {ToolbarDemo} from '../toolbar/toolbar-demo';
import {TooltipDemo} from '../tooltip/tooltip-demo';
import {TreeDemo} from '../tree/tree-demo';
import {TypographyDemo} from '../typography/typography-demo';
import {DemoApp, Home} from './demo-app';
import {DEMO_APP_ROUTES} from './routes';
import {TableDemoModule} from '../table/table-demo-module';
import {BadgeDemo} from '../badge/badge-demo';
import {TreeDemoModule} from '../tree/tree-demo-module';

@NgModule({
imports: [
Expand All @@ -73,6 +73,7 @@ import {BadgeDemo} from '../badge/badge-demo';
DemoMaterialModule,
LayoutModule,
TableDemoModule,
TreeDemoModule,
],
declarations: [
AutocompleteDemo,
Expand Down Expand Up @@ -127,7 +128,6 @@ import {BadgeDemo} from '../badge/badge-demo';
TabsDemo,
ToolbarDemo,
TooltipDemo,
TreeDemo,
TypographyDemo,
ExampleBottomSheet,
],
Expand Down
101 changes: 101 additions & 0 deletions src/demo-app/tree/checklist-tree-demo/checklist-database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* 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 {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';

/**
* Node for to-do item
*/
export class TodoItemNode {
children: TodoItemNode[];
item: string;
}

/** Flat to-do item node with expandable and level information */
export class TodoItemFlatNode {
item: string;
level: number;
expandable: boolean;
}

/**
* The Json object for to-do list data.
*/
const TREE_DATA = {
'Reminders': [
'Cook dinner',
'Read the Material Design spec',
'Upgrade Application to Angular'
],
'Groceries': {
'Organic eggs': null,
'Protein Powder': null,
'Almond Meal flour': null,
'Fruits': {
'Apple': null,
'Orange': null,
'Berries': ['Blueberry', 'Raspberry']
}
}
};

/**
* Checklist database, it can build a tree structured Json object.
* Each node in Json object represents a to-do item or a category.
* If a node is a category, it has children items and new items can be added under the category.
*/
@Injectable()
export class ChecklistDatabase {
dataChange: BehaviorSubject<TodoItemNode[]> = new BehaviorSubject<TodoItemNode[]>([]);

get data(): TodoItemNode[] { return this.dataChange.value; }

constructor() {
this.initialize();
}

initialize() {
// Build the tree nodes from Json object. The result is a list of `TodoItemNode` with nested
// file node as children.
const data = this.buildFileTree(TREE_DATA, 0);

// Notify the change.
this.dataChange.next(data);
}

/**
* Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object.
* The return value is the list of `TodoItemNode`.
*/
buildFileTree(value: any, level: number) {
let data: any[] = [];
for (let k in value) {
let v = value[k];
let node = new TodoItemNode();
node.item = `${k}`;
if (v === null || v === undefined) {
// no action
} else if (typeof v === 'object') {
node.children = this.buildFileTree(v, level + 1);
} else {
node.item = v;
}
data.push(node);
}
return data;
}

/** Add an item to to-do list */
insertItem(parent: TodoItemNode, name: string) {
const child = <TodoItemNode>{item: name};
if (parent.children) {
parent.children.push(child);
this.dataChange.next(this.data);
}
}
}
28 changes: 28 additions & 0 deletions src/demo-app/tree/checklist-tree-demo/checklist-tree-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding>
<button mat-icon-button disabled></button>
<mat-checkbox class="checklist-leaf-node"
[checked]="checklistSelection.isSelected(node)"
(change)="checklistSelection.toggle(node);">{{node.item}}</mat-checkbox>
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
<button mat-icon-button matTreeNodeToggle
[attr.aria-label]="'toggle ' + node.filename">
<mat-icon>
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
<mat-checkbox [checked]="descendantsAllSelected(node)"
[indeterminate]="descendantsPartiallySelected(node)"
(change)="todoItemSelectionToggle(node)">{{node.item}}</mat-checkbox>
<button mat-icon-button (click)="setParent(node)"><mat-icon>add</mat-icon></button>
</mat-tree-node>
</mat-tree>


Selected parent: {{selectedParent ? selectedParent.item : 'No selected'}}
<br>
<mat-form-field>
<input matInput [(ngModel)]="newItemName" placeholder="Add an item"/>
</mat-form-field>
<button mat-button (click)="addNode()">Add node</button>
Empty file.
117 changes: 117 additions & 0 deletions src/demo-app/tree/checklist-tree-demo/checklist-tree-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* 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 {Component} from '@angular/core';
import {SelectionModel} from '@angular/cdk/collections';
import {FlatTreeControl} from '@angular/cdk/tree';
import {MatTreeFlattener, MatTreeFlatDataSource} from '@angular/material/tree';
import {of as ofObservable} from 'rxjs/observable/of';
import {TodoItemNode, TodoItemFlatNode, ChecklistDatabase} from './checklist-database';

/**
* A nested checklist example using tree component.
* Each item/category has a checkbox, and user can check/uncheck a to-do item.
*/
@Component({
moduleId: module.id,
selector: 'checklist-tree-demo',
templateUrl: 'checklist-tree-demo.html',
styleUrls: ['checklist-tree-demo.css'],
providers: [ChecklistDatabase]
})
export class ChecklistTreeDemo {
/** Map from flat node to nested node. This helps us finding the nested node to be modified */
flatNodeMap: Map<TodoItemFlatNode, TodoItemNode> = new Map<TodoItemFlatNode, TodoItemNode>();

/** Map from nested node to flattened node. This helps us to keep the same object for selection */
nestedNodeMap: Map<TodoItemNode, TodoItemFlatNode> = new Map<TodoItemNode, TodoItemFlatNode>();

/** A selected parent node to be inserted */
selectedParent: TodoItemFlatNode | null = null;

/** The new item's name */
newItemName: string = '';

treeControl: FlatTreeControl<TodoItemFlatNode>;

treeFlattener: MatTreeFlattener<TodoItemNode, TodoItemFlatNode>;

dataSource: MatTreeFlatDataSource<TodoItemNode, TodoItemFlatNode>;

/** The selection for checklist */
checklistSelection = new SelectionModel<TodoItemFlatNode>(true /* multiple */);

constructor(private database: ChecklistDatabase) {
this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel,
this.isExpandable, this.getChildren);
this.treeControl = new FlatTreeControl<TodoItemFlatNode>(this.getLevel, this.isExpandable);
this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);

database.dataChange.subscribe(data => {
this.dataSource.data = data;
});
}

getLevel = (node: TodoItemFlatNode) => { return node.level; };

isExpandable = (node: TodoItemFlatNode) => { return node.expandable; };

getChildren = (node: TodoItemNode) => { return ofObservable(node.children); };

hasChild = (_: number, _nodeData: TodoItemFlatNode) => { return _nodeData.expandable; };

/**
* Transformer to convert nested node to flat node. Record the nodes in maps for later use.
*/
transformer = (node: TodoItemNode, level: number) => {
let flatNode = this.nestedNodeMap.has(node)
? this.nestedNodeMap.get(node)!
: new TodoItemFlatNode();
flatNode.item = node.item;
flatNode.level = level;
flatNode.expandable = !!node.children;
this.flatNodeMap.set(flatNode, node);
this.nestedNodeMap.set(node, flatNode);
return flatNode;
}

/** Whether all the descendants of the node are selected */
descendantsAllSelected(node: TodoItemFlatNode): boolean {
const descendants = this.treeControl.getDescendants(node);
return descendants.every(child => this.checklistSelection.isSelected(child));
}

/** Whether part of the descendants are selected */
descendantsPartiallySelected(node: TodoItemFlatNode): boolean {
const descendants = this.treeControl.getDescendants(node);
const result = descendants.some(child => this.checklistSelection.isSelected(child));
return result && !this.descendantsAllSelected(node);
}

/** Toggle the to-do item selection. Select/deselect all the descendants node */
todoItemSelectionToggle(node: TodoItemFlatNode): void {
this.checklistSelection.toggle(node);
const descendants = this.treeControl.getDescendants(node);
this.checklistSelection.isSelected(node)
? this.checklistSelection.select(...descendants)
: this.checklistSelection.deselect(...descendants);
}

/** Insert a new item with name `newItemName` to be under category `selectedParent`. */
addNode() {
if (this.selectedParent) {
const parentNode = this.flatNodeMap.get(this.selectedParent);
this.database.insertItem(parentNode!, this.newItemName);
}
}

/** Select the category so we can insert the new item. */
setParent(node: TodoItemFlatNode) {
this.selectedParent = node;
}
}
115 changes: 115 additions & 0 deletions src/demo-app/tree/dynamic-tree-demo/dynamic-database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* 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 {Injectable} from '@angular/core';
import {FlatTreeControl} from '@angular/cdk/tree';
import {CollectionViewer, SelectionChange} from '@angular/cdk/collections';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {merge} from 'rxjs/observable/merge';
import {map} from 'rxjs/operators/map';

/** Flat node with expandable and level information */
export class DynamicFlatNode {
constructor(public item: string, public level: number = 1, public expandable: boolean = false) {}
}


/**
* Database for dynamic data. When expanding a node in the tree, the data source will need to fetch
* the descendants data from the database.
*/
export class DynamicDatabase {
dataMap = new Map([
['Fruits', ['Apple', 'Orange', 'Banana']],
['Vegetables', ['Tomato', 'Potato', 'Onion']],
['Apple', ['Fuji', 'Macintosh']],
['Onion', ['Yellow', 'White', 'Purple']]
]);

rootLevelNodes = ['Fruits', 'Vegetables'];

/** Initial data from database */
initialData(): DynamicFlatNode[] {
return this.rootLevelNodes.map(name => new DynamicFlatNode(name, 1, true));
}


getChildren(node: string): string[] | undefined {
return this.dataMap.get(node);
}

isExpandable(node: string): boolean {
return this.dataMap.has(node);
}
}
/**
* File database, it can build a tree structured Json object from string.
* Each node in Json object represents a file or a directory. For a file, it has filename and type.
* For a directory, it has filename and children (a list of files or directories).
* The input will be a json object string, and the output is a list of `FileNode` with nested
* structure.
*/
@Injectable()
export class DynamicDataSource {

dataChange: BehaviorSubject<DynamicFlatNode[]> = new BehaviorSubject<DynamicFlatNode[]>([]);

get data(): DynamicFlatNode[] { return this.dataChange.value; }
set data(value: DynamicFlatNode[]) {
this.treeControl.dataNodes = value;
this.dataChange.next(value);
}

constructor(private treeControl: FlatTreeControl<DynamicFlatNode>,
private database: DynamicDatabase) {}

connect(collectionViewer: CollectionViewer) {
const changes = [
collectionViewer.viewChange,
this.treeControl.expansionModel.onChange!,
];
return merge(...changes).pipe(map((change) => {
if ((change as SelectionChange<DynamicFlatNode>).added ||
(change as SelectionChange<DynamicFlatNode>).removed) {
this.handleTreeControl(change as SelectionChange<DynamicFlatNode>);
}
return this.data;
}));
}

/** Handle expand/collapse behaviors */
handleTreeControl(change: SelectionChange<DynamicFlatNode>) {
if (change.added) {
change.added.forEach((node) => this.toggleNode(node, true));
}
if (change.removed) {
change.removed.forEach((node) => this.toggleNode(node, false));
}
}

/**
* Toggle the node, remove from display list
*/
toggleNode(node: DynamicFlatNode, expand: boolean) {
const children = this.database.getChildren(node.item);
const index = this.data.indexOf(node);
if (!children || index < 0) { // If no children, or cannot find the node, no op
return;
}

if (expand) {
const nodes = children.map(name =>
new DynamicFlatNode(name, node.level + 1, this.database.isExpandable(name)));
this.data.splice(index + 1, 0, ...nodes);
} else {
this.data.splice(index + 1, children.length);
}

// notify the change
this.dataChange.next(this.data);
}
}
Loading