diff --git a/src/cdk/tree/nested-node.ts b/src/cdk/tree/nested-node.ts index 72b0241fa796..b06b57e00b00 100644 --- a/src/cdk/tree/nested-node.ts +++ b/src/cdk/tree/nested-node.ts @@ -14,6 +14,7 @@ import { QueryList, } from '@angular/core'; import {takeUntil} from 'rxjs/operators/takeUntil'; + import {CdkTree} from './tree'; import {CdkTreeNodeOutlet} from './outlet'; import {CdkTreeNode} from './node'; @@ -43,9 +44,9 @@ import {CdkTreeNode} from './node'; 'class': 'cdk-tree-node cdk-nested-tree-node', 'tabindex': '0', }, + providers: [{provide: CdkTreeNode, useExisting: CdkNestedTreeNode}] }) export class CdkNestedTreeNode extends CdkTreeNode implements AfterContentInit, OnDestroy { - /** The children data dataNodes of current node. They will be placed in `CdkTreeNodeOutlet`. */ protected _children: T[]; @@ -59,14 +60,16 @@ export class CdkNestedTreeNode extends CdkTreeNode implements AfterContent ngAfterContentInit() { this._tree.treeControl.getChildren(this.data).pipe(takeUntil(this._destroyed)) - .subscribe(result => { - // In case when nodeOutlet is not in the DOM when children changes, save it in the node - // and add to nodeOutlet when it's available. - this._children = result as T[]; - this._addChildrenNodes(); - }); + .subscribe(result => { + if (result && result.length) { + // In case when nodeOutlet is not in the DOM when children changes, save it in the node + // and add to nodeOutlet when it's available. + this._children = result as T[]; + this._addChildrenNodes(); + } + }); this.nodeOutlet.changes.pipe(takeUntil(this._destroyed)) - .subscribe((_) => this._addChildrenNodes()); + .subscribe((_) => this._addChildrenNodes()); } ngOnDestroy() { @@ -78,7 +81,7 @@ export class CdkNestedTreeNode extends CdkTreeNode implements AfterContent /** Add children dataNodes to the NodeOutlet */ protected _addChildrenNodes(): void { this._clear(); - if (this.nodeOutlet.length && this._children) { + if (this.nodeOutlet.length && this._children && this._children.length) { this._children.forEach((child, index) => { this._tree.insertNode(child, index, this.nodeOutlet.first.viewContainer); }); @@ -87,7 +90,7 @@ export class CdkNestedTreeNode extends CdkTreeNode implements AfterContent /** Clear the children dataNodes. */ protected _clear(): void { - if (this.nodeOutlet.first) { + if (this.nodeOutlet && this.nodeOutlet.first) { this.nodeOutlet.first.viewContainer.clear(); } } diff --git a/src/cdk/tree/node.ts b/src/cdk/tree/node.ts index e2311b48189e..75dd0a78c739 100644 --- a/src/cdk/tree/node.ts +++ b/src/cdk/tree/node.ts @@ -119,7 +119,7 @@ export class CdkTreeNode implements FocusableOption, OnDestroy { } this._tree.treeControl.getChildren(this._data).pipe(takeUntil(this._destroyed)) .subscribe(children => { - this.role = children ? 'group' : 'treeitem'; + this.role = children && children.length ? 'group' : 'treeitem'; }); } } diff --git a/src/cdk/tree/tree.spec.ts b/src/cdk/tree/tree.spec.ts index 769a555e04ea..7ce14fa62eb1 100644 --- a/src/cdk/tree/tree.spec.ts +++ b/src/cdk/tree/tree.spec.ts @@ -9,89 +9,425 @@ import {map} from 'rxjs/operators/map'; import {TreeControl} from './control/tree-control'; import {FlatTreeControl} from './control/flat-tree-control'; +import {NestedTreeControl} from './control/nested-tree-control'; import {CdkTreeModule} from './index'; import {CdkTree} from './tree'; describe('CdkTree', () => { - let fixture: ComponentFixture; - - let component: SimpleCdkTreeApp; + /** Represents an indent for expectNestedTreeToMatch */ + const _ = {}; let dataSource: FakeDataSource; - let tree: CdkTree; let treeElement: HTMLElement; + let tree: CdkTree; - beforeEach(async(() => { + function configureCdkTreeTestingModule(declarations) { TestBed.configureTestingModule({ imports: [CdkTreeModule], - declarations: [ - SimpleCdkTreeApp, - // TODO(tinayuangao): Add more test cases with the cdk-tree - // DynamicDataSourceCdkTreeApp, - // NodeContextCdkTreeApp, - // WhenNodeCdkTreeApp - ], + declarations: declarations, }).compileComponents(); - })); + } - beforeEach(() => { - fixture = TestBed.createComponent(SimpleCdkTreeApp); + describe('flat tree', () => { + describe('should initialize', () => { + let fixture: ComponentFixture; + let component: SimpleCdkTreeApp; - component = fixture.componentInstance; - dataSource = component.dataSource as FakeDataSource; - tree = component.tree; - treeElement = fixture.nativeElement.querySelector('cdk-tree'); + beforeEach(() => { + configureCdkTreeTestingModule([SimpleCdkTreeApp]); + fixture = TestBed.createComponent(SimpleCdkTreeApp); - fixture.detectChanges(); - }); + component = fixture.componentInstance; + dataSource = component.dataSource as FakeDataSource; + tree = component.tree; + treeElement = fixture.nativeElement.querySelector('cdk-tree'); + + fixture.detectChanges(); + }); + + it('with a connected data source', () => { + expect(tree.dataSource).toBe(dataSource); + expect(dataSource.isConnected).toBe(true); + }); + + it('with rendered dataNodes', () => { + const nodes = getNodes(treeElement); - describe('should initialize', () => { - it('with a connected data source', () => { - expect(tree.dataSource).toBe(dataSource); - expect(dataSource.isConnected).toBe(true); + expect(nodes).toBeDefined('Expect nodes to be defined'); + expect(nodes[0].classList).toContain('customNodeClass'); + }); + + it('with the right accessibility roles', () => { + expect(treeElement.getAttribute('role')).toBe('tree'); + + getNodes(treeElement).forEach(node => { + expect(node.getAttribute('role')).toBe('treeitem'); + }); + }); + + it('with the right data', () => { + expect(dataSource.data.length).toBe(3); + + let data = dataSource.data; + expectFlatTreeToMatch(treeElement, 28, + [`${data[0].pizzaTopping} - ${data[0].pizzaCheese} + ${data[0].pizzaBase}`], + [`${data[1].pizzaTopping} - ${data[1].pizzaCheese} + ${data[1].pizzaBase}`], + [`${data[2].pizzaTopping} - ${data[2].pizzaCheese} + ${data[2].pizzaBase}`]); + + dataSource.addData(2); + fixture.detectChanges(); + + data = dataSource.data; + expect(data.length).toBe(4); + expectFlatTreeToMatch(treeElement, 28, + [`${data[0].pizzaTopping} - ${data[0].pizzaCheese} + ${data[0].pizzaBase}`], + [`${data[1].pizzaTopping} - ${data[1].pizzaCheese} + ${data[1].pizzaBase}`], + [`${data[2].pizzaTopping} - ${data[2].pizzaCheese} + ${data[2].pizzaBase}`], + [_, `${data[3].pizzaTopping} - ${data[3].pizzaCheese} + ${data[3].pizzaBase}`]); + }); }); - it('with rendered dataNodes', () => { - const nodes = getNodes(treeElement); + describe('with trigger', () => { + let fixture: ComponentFixture; + let component: CdkTreeAppWithTrigger; + + beforeEach(() => { + configureCdkTreeTestingModule([CdkTreeAppWithTrigger]); + fixture = TestBed.createComponent(CdkTreeAppWithTrigger); + + component = fixture.componentInstance; + dataSource = component.dataSource as FakeDataSource; + tree = component.tree; + treeElement = fixture.nativeElement.querySelector('cdk-tree'); + + fixture.detectChanges(); + }); + + it('should expand/collapse the node', () => { + expect(dataSource.data.length).toBe(3); + + expect(component.treeControl.expansionModel.selected.length) + .toBe(0, `Expect no expanded node`); + + component.triggerRecursively = false; + let data = dataSource.data; + dataSource.addChild(data[2]); + fixture.detectChanges(); + + data = dataSource.data; + expect(data.length).toBe(4); + expectFlatTreeToMatch(treeElement, 40, + [`${data[0].pizzaTopping} - ${data[0].pizzaCheese} + ${data[0].pizzaBase}`], + [`${data[1].pizzaTopping} - ${data[1].pizzaCheese} + ${data[1].pizzaBase}`], + [`${data[2].pizzaTopping} - ${data[2].pizzaCheese} + ${data[2].pizzaBase}`], + [_, `${data[3].pizzaTopping} - ${data[3].pizzaCheese} + ${data[3].pizzaBase}`]); + + + (getNodes(treeElement)[2] as HTMLElement).click(); + fixture.detectChanges(); + + expect(component.treeControl.expansionModel.selected.length) + .toBe(1, `Expect node expanded`); + expect(component.treeControl.expansionModel.selected[0]).toBe(data[2]); + + (getNodes(treeElement)[2] as HTMLElement).click(); + fixture.detectChanges(); + + expect(component.treeControl.expansionModel.selected.length) + .toBe(0, `Expect node collapsed`); + }); + + it('should expand/collapse the node recursively', () => { + expect(dataSource.data.length).toBe(3); - expect(nodes).toBeDefined('Expect nodes to be defined'); - expect(nodes[0].classList).toContain('customNodeClass'); + expect(component.treeControl.expansionModel.selected.length) + .toBe(0, `Expect no expanded node`); + + let data = dataSource.data; + dataSource.addChild(data[2]); + fixture.detectChanges(); + + data = dataSource.data; + expect(data.length).toBe(4); + expectFlatTreeToMatch(treeElement, 40, + [`${data[0].pizzaTopping} - ${data[0].pizzaCheese} + ${data[0].pizzaBase}`], + [`${data[1].pizzaTopping} - ${data[1].pizzaCheese} + ${data[1].pizzaBase}`], + [`${data[2].pizzaTopping} - ${data[2].pizzaCheese} + ${data[2].pizzaBase}`], + [_, `${data[3].pizzaTopping} - ${data[3].pizzaCheese} + ${data[3].pizzaBase}`]); + + (getNodes(treeElement)[2] as HTMLElement).click(); + fixture.detectChanges(); + + expect(component.treeControl.expansionModel.selected.length) + .toBe(2, `Expect nodes expanded`); + expect(component.treeControl.expansionModel.selected[0]) + .toBe(data[2], `Expect parent node expanded`); + expect(component.treeControl.expansionModel.selected[1]) + .toBe(data[3], `Expected child node expanded`); + + (getNodes(treeElement)[2] as HTMLElement).click(); + fixture.detectChanges(); + + expect(component.treeControl.expansionModel.selected.length) + .toBe(0, `Expect node collapsed`); + }); }); - it('with the right accessibility roles', () => { - expect(treeElement.getAttribute('role')).toBe('tree'); + describe('with when node template', () => { + let fixture: ComponentFixture; + let component: WhenNodeCdkTreeApp; + + beforeEach(() => { + configureCdkTreeTestingModule([WhenNodeCdkTreeApp]); + fixture = TestBed.createComponent(WhenNodeCdkTreeApp); - getNodes(treeElement).forEach(node => { - expect(node.getAttribute('role')).toBe('treeitem'); + component = fixture.componentInstance; + dataSource = component.dataSource as FakeDataSource; + tree = component.tree; + treeElement = fixture.nativeElement.querySelector('cdk-tree'); + + fixture.detectChanges(); + }); + + it('with the right data', () => { + expect(dataSource.data.length).toBe(3); + + let data = dataSource.data; + expectFlatTreeToMatch(treeElement, 28, + [`[topping_1] - [cheese_1] + [base_1]`], + [`[topping_2] - [cheese_2] + [base_2]`], + [`[topping_3] - [cheese_3] + [base_3]`]); + + dataSource.addChild(data[1]); + fixture.detectChanges(); + + treeElement = fixture.nativeElement.querySelector('cdk-tree'); + data = dataSource.data; + expect(data.length).toBe(4); + expectFlatTreeToMatch(treeElement, 28, + [`[topping_1] - [cheese_1] + [base_1]`], + [`[topping_2] - [cheese_2] + [base_2]`], + [_, `topping_4 - cheese_4 + base_4`], + [`[topping_3] - [cheese_3] + [base_3]`]); }); }); + }); + + describe('nested tree', () => { + describe('should initialize', () => { + let fixture: ComponentFixture; + let component: NestedCdkTreeApp; + + beforeEach(() => { + configureCdkTreeTestingModule([NestedCdkTreeApp]); + fixture = TestBed.createComponent(NestedCdkTreeApp); + + component = fixture.componentInstance; + dataSource = component.dataSource as FakeDataSource; + tree = component.tree; + treeElement = fixture.nativeElement.querySelector('cdk-tree'); - it('with the right data', () => { - expect(dataSource.data.length).toBe(3); - - let data = dataSource.data; - expectFlatTreeToMatchContent(treeElement, - [ - `${data[0].pizzaTopping} - ${data[0].pizzaCheese} + ${data[0].pizzaBase}`, - `${data[1].pizzaTopping} - ${data[1].pizzaCheese} + ${data[1].pizzaBase}`, - `${data[2].pizzaTopping} - ${data[2].pizzaCheese} + ${data[2].pizzaBase}` - ], - [1, 1, 1]); - - dataSource.addData(2); - fixture.detectChanges(); - - data = dataSource.data; - expect(data.length).toBe(4); - expectFlatTreeToMatchContent(treeElement, - [ - `${data[0].pizzaTopping} - ${data[0].pizzaCheese} + ${data[0].pizzaBase}`, - `${data[1].pizzaTopping} - ${data[1].pizzaCheese} + ${data[1].pizzaBase}`, - `${data[2].pizzaTopping} - ${data[2].pizzaCheese} + ${data[2].pizzaBase}`, - `${data[3].pizzaTopping} - ${data[3].pizzaCheese} + ${data[3].pizzaBase}` - ], - [1, 1, 1, 2]); + fixture.detectChanges(); + }); + + it('with a connected data source', () => { + expect(tree.dataSource).toBe(dataSource); + expect(dataSource.isConnected).toBe(true); + }); + + it('with rendered dataNodes', () => { + const nodes = getNodes(treeElement); + + expect(nodes).toBeDefined('Expect nodes to be defined'); + expect(nodes[0].classList).toContain('customNodeClass'); + }); + + it('with the right accessibility roles', () => { + expect(treeElement.getAttribute('role')).toBe('tree'); + + getNodes(treeElement).forEach(node => { + expect(node.getAttribute('role')).toBe('treeitem'); + }); + }); + + it('with the right data', () => { + expect(dataSource.data.length).toBe(3); + + let data = dataSource.data; + expectNestedTreeToMatch(treeElement, + [`${data[0].pizzaTopping} - ${data[0].pizzaCheese} + ${data[0].pizzaBase}`], + [`${data[1].pizzaTopping} - ${data[1].pizzaCheese} + ${data[1].pizzaBase}`], + [`${data[2].pizzaTopping} - ${data[2].pizzaCheese} + ${data[2].pizzaBase}`]); + + dataSource.addChild(data[1], false); + fixture.detectChanges(); + + treeElement = fixture.nativeElement.querySelector('cdk-tree'); + data = dataSource.data; + expect(data.length).toBe(3); + expectNestedTreeToMatch(treeElement, + [`topping_1 - cheese_1 + base_1`], + [`topping_2 - cheese_2 + base_2`], + [_, `topping_4 - cheese_4 + base_4`], + [`topping_3 - cheese_3 + base_3`]) + }); + + it('with nested child data', () => { + expect(dataSource.data.length).toBe(3); + + let data = dataSource.data; + const child = dataSource.addChild(data[1], false); + dataSource.addChild(child, false); + fixture.detectChanges(); + + expect(data.length).toBe(3); + expectNestedTreeToMatch(treeElement, + [`topping_1 - cheese_1 + base_1`], + [`topping_2 - cheese_2 + base_2`], + [_, `topping_4 - cheese_4 + base_4`], + [_, _, `topping_5 - cheese_5 + base_5`], + [`topping_3 - cheese_3 + base_3`]); + + dataSource.addChild(child, false); + fixture.detectChanges(); + + expect(data.length).toBe(3); + expectNestedTreeToMatch(treeElement, + [`topping_1 - cheese_1 + base_1`], + [`topping_2 - cheese_2 + base_2`], + [_, `topping_4 - cheese_4 + base_4`], + [_, _, `topping_5 - cheese_5 + base_5`], + [_, _, `topping_6 - cheese_6 + base_6`], + [`topping_3 - cheese_3 + base_3`]); + }); }); + + describe('with when node', () => { + let fixture: ComponentFixture; + let component: WhenNodeNestedCdkTreeApp; + + beforeEach(() => { + configureCdkTreeTestingModule([WhenNodeNestedCdkTreeApp]); + fixture = TestBed.createComponent(WhenNodeNestedCdkTreeApp); + + component = fixture.componentInstance; + dataSource = component.dataSource as FakeDataSource; + tree = component.tree; + treeElement = fixture.nativeElement.querySelector('cdk-tree'); + + fixture.detectChanges(); + }); + + it('with the right data', () => { + expect(dataSource.data.length).toBe(3); + + let data = dataSource.data; + expectNestedTreeToMatch(treeElement, + [`topping_1 - cheese_1 + base_1`], + [`>> topping_2 - cheese_2 + base_2`], + [`topping_3 - cheese_3 + base_3`]); + + dataSource.addChild(data[1], false); + fixture.detectChanges(); + + treeElement = fixture.nativeElement.querySelector('cdk-tree'); + data = dataSource.data; + expect(data.length).toBe(3); + expectNestedTreeToMatch(treeElement, + [`topping_1 - cheese_1 + base_1`], + [`>> topping_2 - cheese_2 + base_2`], + [_, `topping_4 - cheese_4 + base_4`], + [`topping_3 - cheese_3 + base_3`]); + }); + }); + + describe('with trigger', () => { + let fixture: ComponentFixture; + let component: NestedCdkTreeAppWithTrigger; + + beforeEach(() => { + configureCdkTreeTestingModule([NestedCdkTreeAppWithTrigger]); + fixture = TestBed.createComponent(NestedCdkTreeAppWithTrigger); + + component = fixture.componentInstance; + dataSource = component.dataSource as FakeDataSource; + tree = component.tree; + treeElement = fixture.nativeElement.querySelector('cdk-tree'); + + fixture.detectChanges(); + }); + + it('should expand/collapse the node', () => { + component.triggerRecursively = false; + let data = dataSource.data; + const child = dataSource.addChild(data[1], false); + dataSource.addChild(child, false); + + fixture.detectChanges(); + + expectNestedTreeToMatch(treeElement, + [`topping_1 - cheese_1 + base_1`], + [`topping_2 - cheese_2 + base_2`], + [`topping_3 - cheese_3 + base_3`]); + + fixture.detectChanges(); + + (getNodes(treeElement)[1] as HTMLElement).click(); + fixture.detectChanges(); + + expect(component.treeControl.expansionModel.selected.length) + .toBe(1, `Expect node expanded`); + expectNestedTreeToMatch(treeElement, + [`topping_1 - cheese_1 + base_1`], + [`topping_2 - cheese_2 + base_2`], + [_, `topping_4 - cheese_4 + base_4`], + [`topping_3 - cheese_3 + base_3`]); + + (getNodes(treeElement)[1] as HTMLElement).click(); + fixture.detectChanges(); + + expectNestedTreeToMatch(treeElement, + [`topping_1 - cheese_1 + base_1`], + [`topping_2 - cheese_2 + base_2`], + [`topping_3 - cheese_3 + base_3`]); + expect(component.treeControl.expansionModel.selected.length) + .toBe(0, `Expect node collapsed`); + }); + + it('should expand/collapse the node recursively', () => { + let data = dataSource.data; + const child = dataSource.addChild(data[1], false); + dataSource.addChild(child, false); + fixture.detectChanges(); + + expectNestedTreeToMatch(treeElement, + [`topping_1 - cheese_1 + base_1`], + [`topping_2 - cheese_2 + base_2`], + [`topping_3 - cheese_3 + base_3`]); + + (getNodes(treeElement)[1] as HTMLElement).click(); + fixture.detectChanges(); + + expect(component.treeControl.expansionModel.selected.length) + .toBe(3, `Expect node expanded`); + expectNestedTreeToMatch(treeElement, + [`topping_1 - cheese_1 + base_1`], + [`topping_2 - cheese_2 + base_2`], + [_, `topping_4 - cheese_4 + base_4`], + [_, _, `topping_5 - cheese_5 + base_5`], + [`topping_3 - cheese_3 + base_3`]); + + (getNodes(treeElement)[1] as HTMLElement).click(); + fixture.detectChanges(); + + expect(component.treeControl.expansionModel.selected.length) + .toBe(0, `Expect node collapsed`); + expectNestedTreeToMatch(treeElement, + [`topping_1 - cheese_1 + base_1`], + [`topping_2 - cheese_2 + base_2`], + [`topping_3 - cheese_3 + base_3`]); + }); + }) }); }); @@ -101,6 +437,7 @@ export class TestData { pizzaBase: string; level: number; children: TestData[]; + observableChildren: BehaviorSubject; constructor(pizzaTopping: string, pizzaCheese: string, pizzaBase: string, level: number = 1) { this.pizzaTopping = pizzaTopping; @@ -108,17 +445,19 @@ export class TestData { this.pizzaBase = pizzaBase; this.level = level; this.children = []; + this.observableChildren = new BehaviorSubject(this.children); } } class FakeDataSource extends DataSource { + dataIndex = 0; isConnected = false; _dataChange = new BehaviorSubject([]); set data(data: TestData[]) { this._dataChange.next(data); } get data() { return this._dataChange.getValue(); } - constructor() { + constructor(public treeControl: TreeControl) { super(); for (let i = 0; i < 3; i++) { this.addData(); @@ -128,75 +467,270 @@ class FakeDataSource extends DataSource { connect(collectionViewer: CollectionViewer): Observable { this.isConnected = true; const streams = [this._dataChange, collectionViewer.viewChange]; - return map.call(combineLatest(streams), ([data]) => data); + return combineLatest(streams) + .pipe(map(([data]) => { + this.treeControl.dataNodes = data; + return data; + })); } disconnect() { this.isConnected = false; } + addChild(parent: TestData, isFlat: boolean = true) { + const nextIndex = ++this.dataIndex; + const child = new TestData(`topping_${nextIndex}`, `cheese_${nextIndex}`, `base_${nextIndex}`, + parent.level + 1); + parent.children.push(child); + if (isFlat) { + let copiedData = this.data.slice(); + copiedData.splice(this.data.indexOf(parent) + 1, 0, child); + this.data = copiedData; + } else { + parent.observableChildren.next(parent.children); + } + return child; + } + addData(level: number = 1) { - const nextIndex = this.data.length + 1; + const nextIndex = ++this.dataIndex; let copiedData = this.data.slice(); copiedData.push( - new TestData(`topping_${nextIndex}`, `cheese_${nextIndex}`, `base_${nextIndex}`, level)); + new TestData(`topping_${nextIndex}`, `cheese_${nextIndex}`, `base_${nextIndex}`, level)); this.data = copiedData; } } -@Component({ - template: ` - - - {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} - - - ` -}) -class SimpleCdkTreeApp { - getLevel = (node: TestData) => node.level; - isExpandable = (node: TestData) => node.children.length > 0; - - dataSource: FakeDataSource | null = new FakeDataSource(); - treeControl: TreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); - - @ViewChild(CdkTree) tree: CdkTree; -} - function getNodes(treeElement: Element): Element[] { return [].slice.call(treeElement.querySelectorAll('.cdk-tree-node'))!; } -// TODO(tinayuangao): Add expectedNestedTreeToMatchContent -function expectFlatTreeToMatchContent(treeElement: Element, expectedTreeContent: any[], - expectedLevels: number[]) { - const paddingIndent = 28; +function expectFlatTreeToMatch(treeElement: Element, expectedPaddingIndent: number = 28, + ...expectedTree: any[]) { const missedExpectations: string[] = []; - function checkNodeContent(node: Element, expectedTextContent: string) { + + function checkNode(node: Element, expectedNode: any[]) { const actualTextContent = node.textContent!.trim(); + const expectedTextContent = expectedNode[expectedNode.length - 1]; if (actualTextContent !== expectedTextContent) { missedExpectations.push( `Expected node contents to be ${expectedTextContent} but was ${actualTextContent}`); } } - getNodes(treeElement).forEach((node, index) => { - const expected = expectedTreeContent ? - expectedTreeContent[index] : - null; - checkNodeContent(node, expected); + function checkLevel(node: Element, expectedNode: any[]) { const actualLevel = (node as HTMLElement).style.paddingLeft; - const expectedLevel = `${expectedLevels[index] * paddingIndent}px`; + const expectedLevel = `${(expectedNode.length) * expectedPaddingIndent}px`; if (actualLevel != expectedLevel) { missedExpectations.push( `Expected node level to be ${expectedLevel} but was ${actualLevel}`); } + } + + getNodes(treeElement).forEach((node, index) => { + const expected = expectedTree ? + expectedTree[index] : + null; + + checkLevel(node, expected); + checkNode(node, expected); + }); + + if (missedExpectations.length) { + fail(missedExpectations.join('\n')); + } +} + +function expectNestedTreeToMatch(treeElement: Element, ...expectedTree: any[]) { + const missedExpectations: string[] = []; + function checkNodeContent(node: Element, expectedNode: any[]) { + const expectedTextContent = expectedNode[expectedNode.length - 1]; + const actualTextContent = node.childNodes.item(0).textContent!.trim(); + if (actualTextContent !== expectedTextContent) { + missedExpectations.push( + `Expected node contents to be ${expectedTextContent} but was ${actualTextContent}`); + } + } + + function checkNodeDescendants(node: Element, expectedNode: any[], currentIndex: number) { + let expectedDescendant = 0; + + for (let i = currentIndex + 1; i < expectedTree.length; ++i) { + if (expectedTree[i].length > expectedNode.length) { + ++expectedDescendant; + } else if (expectedTree[i].length === expectedNode.length) { + break; + } + } + + const actualDescendant = getNodes(node).length; + if (actualDescendant !== expectedDescendant) { + missedExpectations.push( + `Expected node descendant num to be ${expectedDescendant} but was ${actualDescendant}`); + } + } + + getNodes(treeElement).forEach((node, index) => { + + const expected = expectedTree ? + expectedTree[index] : + null; + + checkNodeDescendants(node, expected, index); + checkNodeContent(node, expected); }); if (missedExpectations.length) { fail(missedExpectations.join('\n')); } } + +@Component({ + template: ` + + + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} + + + ` +}) +class SimpleCdkTreeApp { + 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: ` + + + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} + + + + ` +}) +class NestedCdkTreeApp { + getChildren = (node: TestData) => node.observableChildren; + + treeControl: TreeControl = new NestedTreeControl(this.getChildren); + + dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); + + @ViewChild(CdkTree) tree: CdkTree; +} + +@Component({ + template: ` + + + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} + + + + >> {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} + + + + ` +}) +class WhenNodeNestedCdkTreeApp { + isSecondNode = (_: number, node: TestData) => node.pizzaBase.indexOf('2') > 0; + + getChildren = (node: TestData) => node.observableChildren; + + treeControl: TreeControl = new NestedTreeControl(this.getChildren); + + dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); + + @ViewChild(CdkTree) tree: CdkTree; +} + + +@Component({ + template: ` + + + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} + + + ` +}) +class CdkTreeAppWithTrigger { + triggerRecursively: boolean = true; + + 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: ` + + + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} +
+ +
+
+
+ ` +}) +class NestedCdkTreeAppWithTrigger { + triggerRecursively: boolean = true; + + getChildren = (node: TestData) => node.observableChildren; + + treeControl: TreeControl = new NestedTreeControl(this.getChildren); + dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); + + @ViewChild(CdkTree) tree: CdkTree; +} + +@Component({ + template: ` + + + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} + + + [{{node.pizzaTopping}}] - [{{node.pizzaCheese}}] + [{{node.pizzaBase}}] + + + ` +}) +class WhenNodeCdkTreeApp { + isOddNode = (_: number, node: TestData) => node.level % 2 === 1; + 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; +} + + diff --git a/src/cdk/tree/tree.ts b/src/cdk/tree/tree.ts index e71638fd41fd..d527903d7980 100644 --- a/src/cdk/tree/tree.ts +++ b/src/cdk/tree/tree.ts @@ -20,7 +20,7 @@ import { QueryList, ViewChild, ViewContainerRef, - ViewEncapsulation, + ViewEncapsulation } from '@angular/core'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {takeUntil} from 'rxjs/operators/takeUntil'; @@ -54,7 +54,7 @@ import { }) export class CdkTree implements CollectionViewer, OnInit, OnDestroy { /** Subject that emits when the component has been destroyed. */ - private _destroyed = new Subject(); + private _onDestroy = new Subject(); /** Latest data provided by the data source through the connect interface. */ private _data: Array = new Array(); @@ -112,8 +112,8 @@ export class CdkTree implements CollectionViewer, OnInit, OnDestroy { ngOnDestroy() { this._nodeOutlet.viewContainer.clear(); - this._destroyed.next(); - this._destroyed.complete(); + this._onDestroy.next(); + this._onDestroy.complete(); if (this.dataSource) { this.dataSource.disconnect(this); @@ -155,7 +155,7 @@ export class CdkTree implements CollectionViewer, OnInit, OnDestroy { /** Set up a subscription for the data provided by the data source. */ private _observeRenderChanges() { - this.dataSource.connect(this).pipe(takeUntil(this._destroyed)) + this.dataSource.connect(this).pipe(takeUntil(this._onDestroy)) .subscribe(data => { this._data = data; this._renderNodeChanges(data); diff --git a/src/lib/tree/tree.spec.ts b/src/lib/tree/tree.spec.ts index d080fd509092..60b263571a2a 100644 --- a/src/lib/tree/tree.spec.ts +++ b/src/lib/tree/tree.spec.ts @@ -5,7 +5,7 @@ import {CollectionViewer, DataSource} from '@angular/cdk/collections'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {Observable} from 'rxjs/Observable'; import {combineLatest} from 'rxjs/observable/combineLatest'; -import {map} from 'rxjs/operators'; +import {map} from 'rxjs/operators/map'; import {TreeControl, FlatTreeControl} from '@angular/cdk/tree'; import {MatTreeModule} from './index'; @@ -127,7 +127,7 @@ class FakeDataSource extends DataSource { connect(collectionViewer: CollectionViewer): Observable { this.isConnected = true; const streams = [this._dataChange, collectionViewer.viewChange]; - return map.call(combineLatest(streams), ([data]) => data); + return combineLatest(streams).pipe(map(([data]) => data)); } disconnect() { diff --git a/test/karma-test-shim.js b/test/karma-test-shim.js index 8a69f544e98b..469ef54639f1 100644 --- a/test/karma-test-shim.js +++ b/test/karma-test-shim.js @@ -69,6 +69,7 @@ System.config({ '@angular/cdk/stepper': 'dist/packages/cdk/stepper/index.js', '@angular/cdk/table': 'dist/packages/cdk/table/index.js', '@angular/cdk/testing': 'dist/packages/cdk/testing/index.js', + '@angular/cdk/tree': 'dist/packages/cdk/tree/index.js', '@angular/material/autocomplete': 'dist/packages/material/autocomplete/index.js', '@angular/material/bottom-sheet': 'dist/packages/material/bottom-sheet/index.js', @@ -103,6 +104,7 @@ System.config({ '@angular/material/tabs': 'dist/packages/material/tabs/index.js', '@angular/material/toolbar': 'dist/packages/material/toolbar/index.js', '@angular/material/tooltip': 'dist/packages/material/tooltip/index.js', + '@angular/material/tree': 'dist/packages/material/tree/index.js', }, packages: { // Thirdparty barrels.