Skip to content

Commit

Permalink
feat(tree): merge tree branch to master (#9796)
Browse files Browse the repository at this point in the history
  • Loading branch information
tinayuangao authored and josephperrott committed Feb 22, 2018
1 parent 4a36cf6 commit c975ca8
Show file tree
Hide file tree
Showing 69 changed files with 5,271 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
/src/lib/toolbar/** @devversion
/src/lib/tooltip/** @andrewseguin
/src/lib/badge/** @amcdnl
/src/lib/tree/** @tinayuangao

# Angular Material core
/src/lib/core/* @jelbourn
Expand Down Expand Up @@ -71,6 +72,7 @@
/src/cdk/stepper/** @mmalerba
/src/cdk/table/** @andrewseguin
/src/cdk/testing/** @devversion
/src/cdk/tree/** @tinayuangao

# Moment adapter package
/src/material-moment-adapter/** @mmalerba
Expand Down Expand Up @@ -129,6 +131,7 @@
/src/demo-app/tabs/** @andrewseguin
/src/demo-app/toolbar/** @devversion
/src/demo-app/tooltip/** @andrewseguin
/src/demo-app/tree/** @tinayuangao
/src/demo-app/typography/** @crisbeto
/src/demo-app/badge/** @amcdnl

Expand Down
1 change: 1 addition & 0 deletions src/cdk/collections/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export {
UniqueSelectionDispatcherListener,
UNIQUE_SELECTION_DISPATCHER_PROVIDER,
} from './unique-selection-dispatcher';
export * from './tree-adapter';
31 changes: 31 additions & 0 deletions src/cdk/collections/tree-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @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 {SelectionModel} from './selection';


/**
* Interface for a class that can flatten hierarchical structured data and re-expand the flattened
* data back into its original structure. Should be used in conjunction with the cdk-tree.
*/
export interface TreeDataNodeFlattener<T> {
/** Transforms a set of hierarchical structured data into a flattened data array. */
flattenNodes(structuredData: any[]): T[];

/**
* Expands a flattened array of data into its hierarchical form using the provided expansion
* model.
*/
expandFlattenedNodes(nodes: T[], expansionModel: SelectionModel<T>): T[];

/**
* Put node descendants of node in array.
* If `onlyExpandable` is true, then only process expandable descendants.
*/
nodeDescendents(node: T, nodes: T[], onlyExpandable: boolean);
}
84 changes: 84 additions & 0 deletions src/cdk/tree/control/base-tree-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @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 {SelectionModel} from '@angular/cdk/collections';
import {Observable} from 'rxjs/Observable';
import {TreeControl} from './tree-control';

/** Base tree control. It has basic toggle/expand/collapse operations on a single data node. */
export abstract class BaseTreeControl<T> implements TreeControl<T> {

/** Gets a list of descendent data nodes of a subtree rooted at given data node recursively. */
abstract getDescendants(dataNode: T): T[];

/** Expands all data nodes in the tree. */
abstract expandAll(): void;

/** Saved data node for `expandAll` action. */
dataNodes: T[];

/** A selection model with multi-selection to track expansion status. */
expansionModel: SelectionModel<T> = new SelectionModel<T>(true);

/** Get depth of a given data node, return the level number. This is for flat tree node. */
getLevel: (dataNode: T) => number;

/**
* Whether the data node is expandable. Returns true if expandable.
* This is for flat tree node.
*/
isExpandable: (dataNode: T) => boolean;

/** Gets a stream that emits whenever the given data node's children change. */
getChildren: (dataNode: T) => Observable<T[]>;

/** Toggles one single data node's expanded/collapsed state. */
toggle(dataNode: T): void {
this.expansionModel.toggle(dataNode);
}

/** Expands one single data node. */
expand(dataNode: T): void {
this.expansionModel.select(dataNode);
}

/** Collapses one single data node. */
collapse(dataNode: T): void {
this.expansionModel.deselect(dataNode);
}

/** Whether a given data node is expanded or not. Returns true if the data node is expanded. */
isExpanded(dataNode: T): boolean {
return this.expansionModel.isSelected(dataNode);
}

/** Toggles a subtree rooted at `node` recursively. */
toggleDescendants(dataNode: T): void {
this.expansionModel.isSelected(dataNode)
? this.collapseDescendants(dataNode)
: this.expandDescendants(dataNode);
}

/** Collapse all dataNodes in the tree. */
collapseAll(): void {
this.expansionModel.clear();
}

/** Expands a subtree rooted at given data node recursively. */
expandDescendants(dataNode: T): void {
let toBeProcessed = [dataNode];
toBeProcessed.push(...this.getDescendants(dataNode));
this.expansionModel.select(...toBeProcessed);
}

/** Collapses a subtree rooted at given data node recursively. */
collapseDescendants(dataNode: T): void {
let toBeProcessed = [dataNode];
toBeProcessed.push(...this.getDescendants(dataNode));
this.expansionModel.deselect(...toBeProcessed);
}
}
188 changes: 188 additions & 0 deletions src/cdk/tree/control/flat-tree-control.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import {FlatTreeControl} from './flat-tree-control';

describe('CdkFlatTreeControl', () => {
let treeControl: FlatTreeControl<TestData>;
let getLevel = (node: TestData) => node.level;
let isExpandable = (node: TestData) => node.children && node.children.length > 0;

beforeEach(() => {
treeControl = new FlatTreeControl<TestData>(getLevel, isExpandable);
});

describe('base tree control actions', () => {
it('should be able to expand and collapse dataNodes', () => {
const nodes = generateData(10, 4);
const secondNode = nodes[1];
const sixthNode = nodes[5];
treeControl.dataNodes = nodes;

treeControl.expand(secondNode);


expect(treeControl.isExpanded(secondNode))
.toBeTruthy('Expect second node to be expanded');
expect(treeControl.expansionModel.selected)
.toContain(secondNode, 'Expect second node in expansionModel');
expect(treeControl.expansionModel.selected.length)
.toBe(1, 'Expect only second node in expansionModel');

treeControl.toggle(sixthNode);

expect(treeControl.isExpanded(secondNode))
.toBeTruthy('Expect second node to stay expanded');
expect(treeControl.isExpanded(sixthNode))
.toBeTruthy('Expect sixth node to be expanded');
expect(treeControl.expansionModel.selected)
.toContain(sixthNode, 'Expect sixth node in expansionModel');
expect(treeControl.expansionModel.selected)
.toContain(secondNode, 'Expect second node in expansionModel');
expect(treeControl.expansionModel.selected.length)
.toBe(2, 'Expect two dataNodes in expansionModel');

treeControl.collapse(secondNode);

expect(treeControl.isExpanded(secondNode))
.toBeFalsy('Expect second node to be collapsed');
expect(treeControl.expansionModel.selected.length)
.toBe(1, 'Expect one node in expansionModel');
expect(treeControl.isExpanded(sixthNode)).toBeTruthy('Expect sixth node to stay expanded');
expect(treeControl.expansionModel.selected)
.toContain(sixthNode, 'Expect sixth node in expansionModel');
});

it('should return correct expandable values', () => {
const nodes = generateData(10, 4);
treeControl.dataNodes = nodes;

for (let i = 0; i < 10; i++) {
expect(treeControl.isExpandable(nodes[i]))
.toBeTruthy(`Expect node[${i}] to be expandable`);

for (let j = 0; j < 4; j++) {
expect(treeControl.isExpandable(nodes[i].children[j]))
.toBeFalsy(`Expect node[${i}]'s child[${j}] to be not expandable`);
}
}
});

it('should return correct levels', () => {
const numNodes = 10;
const numChildren = 4;
const numGrandChildren = 2;
const nodes = generateData(numNodes, numChildren, numGrandChildren);
treeControl.dataNodes = nodes;

for (let i = 0; i < numNodes; i++) {
expect(treeControl.getLevel(nodes[i]))
.toBe(1, `Expec node[${i}]'s level to be 1`);

for (let j = 0; j < numChildren; j++) {
expect(treeControl.getLevel(nodes[i].children[j]))
.toBe(2, `Expect node[${i}]'s child[${j}] to be not expandable`);

for (let k = 0; k < numGrandChildren; k++) {
expect(treeControl.getLevel(nodes[i].children[j].children[k]))
.toBe(3, `Expect node[${i}]'s child[${j}] to be not expandable`);
}
}
}
});

it('should toggle descendants correctly', () => {
const numNodes = 10;
const numChildren = 4;
const numGrandChildren = 2;
const nodes = generateData(numNodes, numChildren, numGrandChildren);

let data = [];
flatten(nodes, data);
treeControl.dataNodes = data;

treeControl.expandDescendants(nodes[1]);

const expandedNodesNum = 1 + numChildren + numChildren * numGrandChildren;
expect(treeControl.expansionModel.selected.length)
.toBe(expandedNodesNum, `Expect expanded ${expandedNodesNum} nodes`);

expect(treeControl.isExpanded(nodes[1])).toBeTruthy('Expect second node to be expanded');
for (let i = 0; i < numChildren; i++) {

expect(treeControl.isExpanded(nodes[1].children[i]))
.toBeTruthy(`Expect second node's children to be expanded`);
for (let j = 0; j < numGrandChildren; j++) {
expect(treeControl.isExpanded(nodes[1].children[i].children[j]))
.toBeTruthy(`Expect second node grand children to be not expanded`);
}
}

});

it('should be able to expand/collapse all the dataNodes', () => {
const numNodes = 10;
const numChildren = 4;
const numGrandChildren = 2;
const nodes = generateData(numNodes, numChildren, numGrandChildren);
let data = [];
flatten(nodes, data);
treeControl.dataNodes = data;

treeControl.expandDescendants(nodes[1]);

treeControl.collapseAll();

expect(treeControl.expansionModel.selected.length).toBe(0, `Expect no expanded nodes`);

treeControl.expandAll();

const totalNumber = numNodes + numNodes * numChildren
+ numNodes * numChildren * numGrandChildren;
expect(treeControl.expansionModel.selected.length)
.toBe(totalNumber, `Expect ${totalNumber} expanded nodes`);
});
});
});

export class TestData {
a: string;
b: string;
c: string;
level: number;
children: TestData[];

constructor(a: string, b: string, c: string, level: number = 1, children: TestData[] = []) {
this.a = a;
this.b = b;
this.c = c;
this.level = level;
this.children = children;
}
}

function generateData(dataLength: number, childLength: number, grandChildLength: number = 0)
: TestData[] {
let data = <any>[];
let nextIndex = 0;
for (let i = 0; i < dataLength; i++) {
let children = <any>[];
for (let j = 0; j < childLength; j++) {
let grandChildren = <any>[];
for (let k = 0; k < grandChildLength; k++) {
grandChildren.push(new TestData(`a_${nextIndex}`, `b_${nextIndex}`, `c_${nextIndex++}`, 3));
}
children.push(
new TestData(`a_${nextIndex}`, `b_${nextIndex}`, `c_${nextIndex++}`, 2, grandChildren));
}
data.push(new TestData(`a_${nextIndex}`, `b_${nextIndex}`, `c_${nextIndex++}`, 1, children));
}
return data;
}

function flatten(nodes: TestData[], data: TestData[]) {
for (let node of nodes) {
data.push(node);

if (node.children && node.children.length > 0) {
flatten(node.children, data);
}
}
}
53 changes: 53 additions & 0 deletions src/cdk/tree/control/flat-tree-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @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 {BaseTreeControl} from './base-tree-control';

/** Flat tree control. Able to expand/collapse a subtree recursively for flattened tree. */
export class FlatTreeControl<T> extends BaseTreeControl<T> {

/** Construct with flat tree data node functions getLevel and isExpandable. */
constructor(public getLevel: (dataNode: T) => number,
public isExpandable: (dataNode: T) => boolean) {
super();
}

/**
* Gets a list of the data node's subtree of descendent data nodes.
*
* To make this working, the `dataNodes` of the TreeControl must be flattened tree nodes
* with correct levels.
*/
getDescendants(dataNode: T): T[] {
const startIndex = this.dataNodes.indexOf(dataNode);
const results: T[] = [];

// Goes through flattened tree nodes in the `dataNodes` array, and get all descendants.
// The level of descendants of a tree node must be greater than the level of the given
// tree node.
// 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 < this.dataNodes.length && this.getLevel(dataNode) < this.getLevel(this.dataNodes[i]);
i++) {
results.push(this.dataNodes[i]);
}
return results;
}

/**
* Expands all data nodes in the tree.
*
* To make this working, the `dataNodes` variable of the TreeControl must be set to all flattened
* data nodes of the tree.
*/
expandAll(): void {
this.expansionModel.select(...this.dataNodes);
}
}
Loading

0 comments on commit c975ca8

Please sign in to comment.