1216 lines
38 KiB
TypeScript
1216 lines
38 KiB
TypeScript
|
|
/**
|
||
|
|
* @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.dev/license
|
||
|
|
*/
|
||
|
|
import {FlatTreeControl, NestedTreeControl, TreeControl} from '@angular/cdk/tree';
|
||
|
|
import {Component, ViewChild, Type} from '@angular/core';
|
||
|
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||
|
|
import {BehaviorSubject, Observable} from 'rxjs';
|
||
|
|
import {map} from 'rxjs/operators';
|
||
|
|
import {
|
||
|
|
MatTree,
|
||
|
|
MatTreeFlatDataSource,
|
||
|
|
MatTreeFlattener,
|
||
|
|
MatTreeModule,
|
||
|
|
MatTreeNestedDataSource,
|
||
|
|
} from './index';
|
||
|
|
|
||
|
|
describe('MatTree', () => {
|
||
|
|
/** Represents an indent for expectNestedTreeToMatch */
|
||
|
|
const _ = {};
|
||
|
|
|
||
|
|
let treeElement: HTMLElement;
|
||
|
|
let underlyingDataSource: FakeDataSource;
|
||
|
|
|
||
|
|
function configureMatTreeTestingModule(declarations: Type<any>[]) {
|
||
|
|
TestBed.configureTestingModule({
|
||
|
|
imports: [MatTreeModule],
|
||
|
|
declarations: declarations,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('flat tree', () => {
|
||
|
|
describe('should initialize', () => {
|
||
|
|
let fixture: ComponentFixture<SimpleMatTreeApp>;
|
||
|
|
let component: SimpleMatTreeApp;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
configureMatTreeTestingModule([SimpleMatTreeApp]);
|
||
|
|
fixture = TestBed.createComponent(SimpleMatTreeApp);
|
||
|
|
|
||
|
|
component = fixture.componentInstance;
|
||
|
|
underlyingDataSource = component.underlyingDataSource;
|
||
|
|
treeElement = fixture.nativeElement.querySelector('mat-tree');
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('with rendered dataNodes', () => {
|
||
|
|
const nodes = getNodes(treeElement);
|
||
|
|
|
||
|
|
expect(nodes).withContext('Expect nodes to be defined').toBeDefined();
|
||
|
|
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 aria-level attrs', () => {
|
||
|
|
// add a child to the first node
|
||
|
|
const data = underlyingDataSource.data;
|
||
|
|
underlyingDataSource.addChild(data[2]);
|
||
|
|
component.treeControl.expandAll();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
const ariaLevels = getNodes(treeElement).map(n => n.getAttribute('aria-level'));
|
||
|
|
expect(ariaLevels).toEqual(['1', '1', '1', '2']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('with the right aria-expanded attrs', () => {
|
||
|
|
// add a child to the first node
|
||
|
|
const data = underlyingDataSource.data;
|
||
|
|
underlyingDataSource.addChild(data[2]);
|
||
|
|
fixture.detectChanges();
|
||
|
|
let ariaExpandedStates = getNodes(treeElement).map(n => n.getAttribute('aria-expanded'));
|
||
|
|
expect(ariaExpandedStates).toEqual([null, null, 'false']);
|
||
|
|
|
||
|
|
component.treeControl.expandAll();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
ariaExpandedStates = getNodes(treeElement).map(n => n.getAttribute('aria-expanded'));
|
||
|
|
expect(ariaExpandedStates).toEqual([null, null, 'true', null]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('with the right data', () => {
|
||
|
|
expect(underlyingDataSource.data.length).toBe(3);
|
||
|
|
|
||
|
|
const data = underlyingDataSource.data;
|
||
|
|
expectFlatTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
28,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
);
|
||
|
|
|
||
|
|
underlyingDataSource.addChild(data[2]);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expectFlatTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
28,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
[`_, topping_4 - cheese_4 + base_4`],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('with toggle', () => {
|
||
|
|
let fixture: ComponentFixture<MatTreeAppWithToggle>;
|
||
|
|
let component: MatTreeAppWithToggle;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
configureMatTreeTestingModule([MatTreeAppWithToggle]);
|
||
|
|
fixture = TestBed.createComponent(MatTreeAppWithToggle);
|
||
|
|
|
||
|
|
component = fixture.componentInstance;
|
||
|
|
underlyingDataSource = component.underlyingDataSource;
|
||
|
|
treeElement = fixture.nativeElement.querySelector('mat-tree');
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should expand/collapse the node', () => {
|
||
|
|
expect(underlyingDataSource.data.length).toBe(3);
|
||
|
|
|
||
|
|
expect(component.treeControl.expansionModel.selected.length)
|
||
|
|
.withContext(`Expect no expanded node`)
|
||
|
|
.toBe(0);
|
||
|
|
|
||
|
|
component.toggleRecursively = false;
|
||
|
|
const data = underlyingDataSource.data;
|
||
|
|
const child = underlyingDataSource.addChild(data[2]);
|
||
|
|
underlyingDataSource.addChild(child);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expectFlatTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
40,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
);
|
||
|
|
|
||
|
|
(getNodes(treeElement)[2] as HTMLElement).click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(component.treeControl.expansionModel.selected.length)
|
||
|
|
.withContext(`Expect node expanded one level`)
|
||
|
|
.toBe(1);
|
||
|
|
expectFlatTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
40,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
[_, `topping_4 - cheese_4 + base_4`],
|
||
|
|
);
|
||
|
|
|
||
|
|
(getNodes(treeElement)[3] as HTMLElement).click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(component.treeControl.expansionModel.selected.length)
|
||
|
|
.withContext(`Expect node expanded`)
|
||
|
|
.toBe(2);
|
||
|
|
expectFlatTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
40,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
[_, `topping_4 - cheese_4 + base_4`],
|
||
|
|
[_, _, `topping_5 - cheese_5 + base_5`],
|
||
|
|
);
|
||
|
|
|
||
|
|
(getNodes(treeElement)[2] as HTMLElement).click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expectFlatTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
40,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should expand/collapse the node recursively', () => {
|
||
|
|
expect(underlyingDataSource.data.length).toBe(3);
|
||
|
|
|
||
|
|
expect(component.treeControl.expansionModel.selected.length)
|
||
|
|
.withContext(`Expect no expanded node`)
|
||
|
|
.toBe(0);
|
||
|
|
|
||
|
|
const data = underlyingDataSource.data;
|
||
|
|
const child = underlyingDataSource.addChild(data[2]);
|
||
|
|
underlyingDataSource.addChild(child);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expectFlatTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
40,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
);
|
||
|
|
|
||
|
|
(getNodes(treeElement)[2] as HTMLElement).click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(component.treeControl.expansionModel.selected.length)
|
||
|
|
.withContext(`Expect nodes expanded`)
|
||
|
|
.toBe(3);
|
||
|
|
expectFlatTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
40,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
[_, `topping_4 - cheese_4 + base_4`],
|
||
|
|
[_, _, `topping_5 - cheese_5 + base_5`],
|
||
|
|
);
|
||
|
|
|
||
|
|
(getNodes(treeElement)[2] as HTMLElement).click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(component.treeControl.expansionModel.selected.length)
|
||
|
|
.withContext(`Expect node collapsed`)
|
||
|
|
.toBe(0);
|
||
|
|
|
||
|
|
expectFlatTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
40,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('with when node template', () => {
|
||
|
|
let fixture: ComponentFixture<WhenNodeMatTreeApp>;
|
||
|
|
let component: WhenNodeMatTreeApp;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
configureMatTreeTestingModule([WhenNodeMatTreeApp]);
|
||
|
|
fixture = TestBed.createComponent(WhenNodeMatTreeApp);
|
||
|
|
|
||
|
|
component = fixture.componentInstance;
|
||
|
|
underlyingDataSource = component.underlyingDataSource;
|
||
|
|
treeElement = fixture.nativeElement.querySelector('mat-tree');
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('with the right data', () => {
|
||
|
|
expectFlatTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
28,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
[`>>> topping_4 - cheese_4 + base_4`],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('flat tree with undefined or null children', () => {
|
||
|
|
describe('should initialize', () => {
|
||
|
|
let fixture: ComponentFixture<MatTreeWithNullOrUndefinedChild>;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
configureMatTreeTestingModule([MatTreeWithNullOrUndefinedChild]);
|
||
|
|
fixture = TestBed.createComponent(MatTreeWithNullOrUndefinedChild);
|
||
|
|
treeElement = fixture.nativeElement.querySelector('mat-tree');
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('with rendered dataNodes', () => {
|
||
|
|
const nodes = getNodes(treeElement);
|
||
|
|
|
||
|
|
expect(nodes).withContext('Expect nodes to be defined').toBeDefined();
|
||
|
|
expect(nodes[0].classList).toContain('customNodeClass');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('nested tree with undefined or null children', () => {
|
||
|
|
describe('should initialize', () => {
|
||
|
|
let fixture: ComponentFixture<MatNestedTreeWithNullOrUndefinedChild>;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
configureMatTreeTestingModule([MatNestedTreeWithNullOrUndefinedChild]);
|
||
|
|
fixture = TestBed.createComponent(MatNestedTreeWithNullOrUndefinedChild);
|
||
|
|
treeElement = fixture.nativeElement.querySelector('mat-tree');
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('with rendered dataNodes', () => {
|
||
|
|
const nodes = getNodes(treeElement);
|
||
|
|
|
||
|
|
expect(nodes).withContext('Expect nodes to be defined').toBeDefined();
|
||
|
|
expect(nodes[0].classList).toContain('customNodeClass');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
describe('nested tree', () => {
|
||
|
|
describe('should initialize', () => {
|
||
|
|
let fixture: ComponentFixture<NestedMatTreeApp>;
|
||
|
|
let component: NestedMatTreeApp;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
configureMatTreeTestingModule([NestedMatTreeApp]);
|
||
|
|
fixture = TestBed.createComponent(NestedMatTreeApp);
|
||
|
|
|
||
|
|
component = fixture.componentInstance;
|
||
|
|
underlyingDataSource = component.underlyingDataSource;
|
||
|
|
treeElement = fixture.nativeElement.querySelector('mat-tree');
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('with rendered dataNodes', () => {
|
||
|
|
const nodes = getNodes(treeElement);
|
||
|
|
|
||
|
|
expect(nodes).withContext('Expect nodes to be defined').toBeDefined();
|
||
|
|
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(underlyingDataSource.data.length).toBe(3);
|
||
|
|
|
||
|
|
let data = underlyingDataSource.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}`],
|
||
|
|
);
|
||
|
|
|
||
|
|
underlyingDataSource.addChild(data[1]);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
treeElement = fixture.nativeElement.querySelector('mat-tree');
|
||
|
|
data = underlyingDataSource.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(underlyingDataSource.data.length).toBe(3);
|
||
|
|
|
||
|
|
const data = underlyingDataSource.data;
|
||
|
|
const child = underlyingDataSource.addChild(data[1]);
|
||
|
|
underlyingDataSource.addChild(child);
|
||
|
|
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`],
|
||
|
|
);
|
||
|
|
|
||
|
|
underlyingDataSource.addChild(child);
|
||
|
|
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`],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('with correct aria-level on nodes', () => {
|
||
|
|
expect(
|
||
|
|
getNodes(treeElement).every(node => {
|
||
|
|
return node.getAttribute('aria-level') === '1';
|
||
|
|
}),
|
||
|
|
).toBe(true);
|
||
|
|
|
||
|
|
const data = underlyingDataSource.data;
|
||
|
|
const child = underlyingDataSource.addChild(data[1]);
|
||
|
|
underlyingDataSource.addChild(child);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
const ariaLevels = getNodes(treeElement).map(n => n.getAttribute('aria-level'));
|
||
|
|
expect(ariaLevels).toEqual(['1', '1', '2', '3', '1']);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('with when node', () => {
|
||
|
|
let fixture: ComponentFixture<WhenNodeNestedMatTreeApp>;
|
||
|
|
let component: WhenNodeNestedMatTreeApp;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
configureMatTreeTestingModule([WhenNodeNestedMatTreeApp]);
|
||
|
|
fixture = TestBed.createComponent(WhenNodeNestedMatTreeApp);
|
||
|
|
|
||
|
|
component = fixture.componentInstance;
|
||
|
|
underlyingDataSource = component.underlyingDataSource;
|
||
|
|
treeElement = fixture.nativeElement.querySelector('mat-tree');
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('with the right data', () => {
|
||
|
|
expectNestedTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
[`>>> topping_4 - cheese_4 + base_4`],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('with toggle', () => {
|
||
|
|
let fixture: ComponentFixture<NestedMatTreeAppWithToggle>;
|
||
|
|
let component: NestedMatTreeAppWithToggle;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
configureMatTreeTestingModule([NestedMatTreeAppWithToggle]);
|
||
|
|
fixture = TestBed.createComponent(NestedMatTreeAppWithToggle);
|
||
|
|
|
||
|
|
component = fixture.componentInstance;
|
||
|
|
underlyingDataSource = component.underlyingDataSource;
|
||
|
|
treeElement = fixture.nativeElement.querySelector('mat-tree');
|
||
|
|
|
||
|
|
fixture.detectChanges();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('with the right aria-expanded attrs', () => {
|
||
|
|
let ariaExpandedStates = getNodes(treeElement).map(n => n.getAttribute('aria-expanded'));
|
||
|
|
expect(ariaExpandedStates).toEqual([null, null, null]);
|
||
|
|
|
||
|
|
component.toggleRecursively = false;
|
||
|
|
const data = underlyingDataSource.data;
|
||
|
|
const child = underlyingDataSource.addChild(data[1]);
|
||
|
|
underlyingDataSource.addChild(child);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
(getNodes(treeElement)[1] as HTMLElement).click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
// Note: only four elements are present here; children are not present
|
||
|
|
// in DOM unless the parent node is expanded.
|
||
|
|
const ariaExpanded = getNodes(treeElement).map(n => n.getAttribute('aria-expanded'));
|
||
|
|
expect(ariaExpanded).toEqual([null, 'true', 'false', null]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should expand/collapse the node', () => {
|
||
|
|
component.toggleRecursively = false;
|
||
|
|
const data = underlyingDataSource.data;
|
||
|
|
const child = underlyingDataSource.addChild(data[1]);
|
||
|
|
underlyingDataSource.addChild(child);
|
||
|
|
|
||
|
|
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)
|
||
|
|
.withContext(`Expect node expanded`)
|
||
|
|
.toBe(1);
|
||
|
|
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)
|
||
|
|
.withContext(`Expect node collapsed`)
|
||
|
|
.toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should expand/collapse the node recursively', () => {
|
||
|
|
const data = underlyingDataSource.data;
|
||
|
|
const child = underlyingDataSource.addChild(data[1]);
|
||
|
|
underlyingDataSource.addChild(child);
|
||
|
|
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)
|
||
|
|
.withContext(`Expect node expanded`)
|
||
|
|
.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`],
|
||
|
|
);
|
||
|
|
|
||
|
|
(getNodes(treeElement)[1] as HTMLElement).click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(component.treeControl.expansionModel.selected.length)
|
||
|
|
.withContext(`Expect node collapsed`)
|
||
|
|
.toBe(0);
|
||
|
|
expectNestedTreeToMatch(
|
||
|
|
treeElement,
|
||
|
|
[`topping_1 - cheese_1 + base_1`],
|
||
|
|
[`topping_2 - cheese_2 + base_2`],
|
||
|
|
[`topping_3 - cheese_3 + base_3`],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('accessibility', () => {
|
||
|
|
let fixture: ComponentFixture<NestedMatTreeApp>;
|
||
|
|
let component: NestedMatTreeApp;
|
||
|
|
let nodes: HTMLElement[];
|
||
|
|
let tree: MatTree<TestData>;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
configureMatTreeTestingModule([NestedMatTreeApp]);
|
||
|
|
fixture = TestBed.createComponent(NestedMatTreeApp);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
component = fixture.componentInstance;
|
||
|
|
underlyingDataSource = component.underlyingDataSource as FakeDataSource;
|
||
|
|
const data = underlyingDataSource.data;
|
||
|
|
const child = underlyingDataSource.addChild(data[1], false);
|
||
|
|
underlyingDataSource.addChild(child, false);
|
||
|
|
underlyingDataSource.addChild(child, false);
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
tree = component.tree;
|
||
|
|
treeElement = fixture.nativeElement.querySelector('mat-tree');
|
||
|
|
nodes = getNodes(treeElement);
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('focus management', () => {
|
||
|
|
it('sets tabindex on the latest activated item, with all others "-1"', () => {
|
||
|
|
// activate the second child by clicking on it
|
||
|
|
nodes[1].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual(
|
||
|
|
'-1, 0, -1, -1, -1, -1',
|
||
|
|
);
|
||
|
|
|
||
|
|
// activate the first child by clicking on it
|
||
|
|
nodes[0].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual(
|
||
|
|
'0, -1, -1, -1, -1, -1',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('maintains tabindex when component is blurred', () => {
|
||
|
|
// activate the second child by clicking on it
|
||
|
|
nodes[1].click();
|
||
|
|
nodes[1].focus();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(document.activeElement).toBe(nodes[1]);
|
||
|
|
// blur the currently active element (which we just checked is the above node)
|
||
|
|
nodes[1].blur();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual(
|
||
|
|
'-1, 0, -1, -1, -1, -1',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('ignores clicks on disabled items', () => {
|
||
|
|
underlyingDataSource.data[1].isDisabled = true;
|
||
|
|
fixture.changeDetectorRef.markForCheck();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
// attempt to click on the first child
|
||
|
|
nodes[1].click();
|
||
|
|
fixture.detectChanges();
|
||
|
|
|
||
|
|
expect(nodes.map(x => x.getAttribute('tabindex')).join(', ')).toEqual(
|
||
|
|
'0, -1, -1, -1, -1, -1',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('tree role & attributes', () => {
|
||
|
|
it('sets the tree role on the tree element', () => {
|
||
|
|
expect(treeElement.getAttribute('role')).toBe('tree');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('sets the treeitem role on all nodes', () => {
|
||
|
|
expect(nodes.map(x => x.getAttribute('role')).join(', ')).toEqual(
|
||
|
|
'treeitem, treeitem, treeitem, treeitem, treeitem, treeitem',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('sets aria attributes for tree nodes', () => {
|
||
|
|
expect(nodes.map(x => `${x.getAttribute('aria-expanded')}`).join(', '))
|
||
|
|
.withContext('aria-expanded attributes')
|
||
|
|
.toEqual('null, false, false, null, null, null');
|
||
|
|
expect(nodes.map(x => `${x.getAttribute('aria-level')}`).join(', '))
|
||
|
|
.withContext('aria-level attributes')
|
||
|
|
.toEqual('1, 1, 2, 3, 3, 1');
|
||
|
|
expect(nodes.map(x => `${x.getAttribute('aria-posinset')}`).join(', '))
|
||
|
|
.withContext('aria-posinset attributes')
|
||
|
|
.toEqual('1, 2, 1, 1, 2, 3');
|
||
|
|
expect(nodes.map(x => `${x.getAttribute('aria-setsize')}`).join(', '))
|
||
|
|
.withContext('aria-setsize attributes')
|
||
|
|
.toEqual('3, 3, 1, 2, 2, 3');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('changes aria-expanded status when expanded or collapsed', () => {
|
||
|
|
tree.expand(underlyingDataSource.data[1]);
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(nodes.map(x => `${x.getAttribute('aria-expanded')}`).join(', '))
|
||
|
|
.withContext('aria-expanded attributes')
|
||
|
|
.toEqual('null, true, false, null, null, null');
|
||
|
|
|
||
|
|
tree.collapse(underlyingDataSource.data[1]);
|
||
|
|
fixture.detectChanges();
|
||
|
|
expect(nodes.map(x => `${x.getAttribute('aria-expanded')}`).join(', '))
|
||
|
|
.withContext('aria-expanded attributes')
|
||
|
|
.toEqual('null, false, false, null, null, null');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
export class TestData {
|
||
|
|
pizzaTopping: string;
|
||
|
|
pizzaCheese: string;
|
||
|
|
pizzaBase: string;
|
||
|
|
level: number;
|
||
|
|
children: TestData[];
|
||
|
|
observableChildren: BehaviorSubject<TestData[]>;
|
||
|
|
isSpecial: boolean;
|
||
|
|
isDisabled?: boolean;
|
||
|
|
|
||
|
|
constructor(
|
||
|
|
pizzaTopping: string,
|
||
|
|
pizzaCheese: string,
|
||
|
|
pizzaBase: string,
|
||
|
|
children: TestData[] = [],
|
||
|
|
isSpecial: boolean = false,
|
||
|
|
) {
|
||
|
|
this.pizzaTopping = pizzaTopping;
|
||
|
|
this.pizzaCheese = pizzaCheese;
|
||
|
|
this.pizzaBase = pizzaBase;
|
||
|
|
this.isSpecial = isSpecial;
|
||
|
|
this.children = children;
|
||
|
|
this.observableChildren = new BehaviorSubject<TestData[]>(this.children);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class FakeDataSource {
|
||
|
|
dataIndex = 0;
|
||
|
|
_dataChange = new BehaviorSubject<TestData[]>([]);
|
||
|
|
get data() {
|
||
|
|
return this._dataChange.getValue();
|
||
|
|
}
|
||
|
|
set data(data: TestData[]) {
|
||
|
|
this._dataChange.next(data);
|
||
|
|
}
|
||
|
|
|
||
|
|
connect(): Observable<TestData[]> {
|
||
|
|
return this._dataChange;
|
||
|
|
}
|
||
|
|
|
||
|
|
disconnect() {}
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
for (let i = 0; i < 3; i++) {
|
||
|
|
this.addData();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
addChild(parent: TestData, isSpecial: boolean = false) {
|
||
|
|
const nextIndex = ++this.dataIndex;
|
||
|
|
const child = new TestData(`topping_${nextIndex}`, `cheese_${nextIndex}`, `base_${nextIndex}`);
|
||
|
|
|
||
|
|
const index = this.data.indexOf(parent);
|
||
|
|
if (index > -1) {
|
||
|
|
parent = new TestData(
|
||
|
|
parent.pizzaTopping,
|
||
|
|
parent.pizzaCheese,
|
||
|
|
parent.pizzaBase,
|
||
|
|
parent.children,
|
||
|
|
isSpecial,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
parent.children.push(child);
|
||
|
|
parent.observableChildren.next(parent.children);
|
||
|
|
|
||
|
|
let copiedData = this.data.slice();
|
||
|
|
if (index > -1) {
|
||
|
|
copiedData.splice(index, 1, parent);
|
||
|
|
}
|
||
|
|
this.data = copiedData;
|
||
|
|
return child;
|
||
|
|
}
|
||
|
|
|
||
|
|
addData(isSpecial: boolean = false) {
|
||
|
|
const nextIndex = ++this.dataIndex;
|
||
|
|
let copiedData = this.data.slice();
|
||
|
|
copiedData.push(
|
||
|
|
new TestData(
|
||
|
|
`topping_${nextIndex}`,
|
||
|
|
`cheese_${nextIndex}`,
|
||
|
|
`base_${nextIndex}`,
|
||
|
|
[],
|
||
|
|
isSpecial,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
this.data = copiedData;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function getNodes(treeElement: Element): HTMLElement[] {
|
||
|
|
return [].slice.call(treeElement.querySelectorAll('.mat-tree-node, .mat-nested-tree-node'))!;
|
||
|
|
}
|
||
|
|
|
||
|
|
function expectFlatTreeToMatch(
|
||
|
|
treeElement: Element,
|
||
|
|
expectedPaddingIndent: number = 28,
|
||
|
|
...expectedTree: any[]
|
||
|
|
) {
|
||
|
|
const missedExpectations: 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}`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function checkLevel(node: Element, expectedNode: any[]) {
|
||
|
|
const rawLevel = (node as HTMLElement).style.paddingLeft;
|
||
|
|
|
||
|
|
// Some browsers return 0, while others return 0px.
|
||
|
|
const actualLevel = rawLevel === '0' ? '0px' : rawLevel;
|
||
|
|
if (expectedNode.length === 1) {
|
||
|
|
if (actualLevel !== `` && actualLevel !== '0px') {
|
||
|
|
missedExpectations.push(`Expected node level to be 0px but was ${actualLevel}`);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const expectedLevel = `${(expectedNode.length - 1) * 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: `
|
||
|
|
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||
|
|
<mat-tree-node *matTreeNodeDef="let node" class="customNodeClass"
|
||
|
|
matTreeNodePadding [matTreeNodePaddingIndent]="28"
|
||
|
|
matTreeNodeToggle>
|
||
|
|
{{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}}
|
||
|
|
</mat-tree-node>
|
||
|
|
</mat-tree>
|
||
|
|
`,
|
||
|
|
standalone: false,
|
||
|
|
})
|
||
|
|
class SimpleMatTreeApp {
|
||
|
|
getLevel = (node: TestData) => node.level;
|
||
|
|
isExpandable = (node: TestData) => node.children.length > 0;
|
||
|
|
getChildren = (node: TestData) => node.observableChildren;
|
||
|
|
transformer = (node: TestData, level: number) => {
|
||
|
|
node.level = level;
|
||
|
|
return node;
|
||
|
|
};
|
||
|
|
|
||
|
|
treeFlattener = new MatTreeFlattener<TestData, TestData>(
|
||
|
|
this.transformer,
|
||
|
|
this.getLevel,
|
||
|
|
this.isExpandable,
|
||
|
|
this.getChildren,
|
||
|
|
);
|
||
|
|
|
||
|
|
treeControl = new FlatTreeControl(this.getLevel, this.isExpandable);
|
||
|
|
|
||
|
|
dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
|
||
|
|
|
||
|
|
underlyingDataSource = new FakeDataSource();
|
||
|
|
|
||
|
|
@ViewChild(MatTree) tree: MatTree<TestData>;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.underlyingDataSource.connect().subscribe(data => {
|
||
|
|
this.dataSource.data = data;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
interface FoodNode {
|
||
|
|
name: string;
|
||
|
|
children?: FoodNode[] | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Flat node with expandable and level information */
|
||
|
|
interface ExampleFlatNode {
|
||
|
|
expandable: boolean;
|
||
|
|
name: string;
|
||
|
|
level: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Food data with nested structure.
|
||
|
|
* Each node has a name and an optiona list of children.
|
||
|
|
*/
|
||
|
|
const TREE_DATA: FoodNode[] = [
|
||
|
|
{
|
||
|
|
name: 'Fruit',
|
||
|
|
children: [{name: 'Apple'}, {name: 'Banana'}, {name: 'Fruit loops', children: null}],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'Vegetables',
|
||
|
|
children: [
|
||
|
|
{
|
||
|
|
name: 'Green',
|
||
|
|
children: [{name: 'Broccoli'}, {name: 'Brussels sprouts'}],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'Orange',
|
||
|
|
children: [{name: 'Pumpkins'}, {name: 'Carrots'}],
|
||
|
|
},
|
||
|
|
],
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||
|
|
<mat-tree-node *matTreeNodeDef="let node" class="customNodeClass"
|
||
|
|
matTreeNodePadding matTreeNodeToggle>
|
||
|
|
{{node.name}}
|
||
|
|
</mat-tree-node>
|
||
|
|
</mat-tree>
|
||
|
|
`,
|
||
|
|
standalone: false,
|
||
|
|
})
|
||
|
|
class MatTreeWithNullOrUndefinedChild {
|
||
|
|
private _transformer = (node: FoodNode, level: number) => {
|
||
|
|
return {
|
||
|
|
expandable: !!node.children,
|
||
|
|
name: node.name,
|
||
|
|
level: level,
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
treeControl = new FlatTreeControl<ExampleFlatNode>(
|
||
|
|
node => node.level,
|
||
|
|
node => node.expandable,
|
||
|
|
);
|
||
|
|
|
||
|
|
treeFlattener = new MatTreeFlattener(
|
||
|
|
this._transformer,
|
||
|
|
node => node.level,
|
||
|
|
node => node.expandable,
|
||
|
|
node => node.children,
|
||
|
|
);
|
||
|
|
|
||
|
|
dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener, TREE_DATA);
|
||
|
|
|
||
|
|
hasChild = (_: number, node: ExampleFlatNode) => node.expandable;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||
|
|
<mat-nested-tree-node *matTreeNodeDef="let node" class="customNodeClass">
|
||
|
|
{{node.name}}
|
||
|
|
<ng-template matTreeNodeOutlet></ng-template>
|
||
|
|
</mat-nested-tree-node>
|
||
|
|
</mat-tree>
|
||
|
|
`,
|
||
|
|
standalone: false,
|
||
|
|
})
|
||
|
|
class MatNestedTreeWithNullOrUndefinedChild {
|
||
|
|
treeControl: NestedTreeControl<FoodNode>;
|
||
|
|
dataSource: MatTreeNestedDataSource<FoodNode>;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.treeControl = new NestedTreeControl<FoodNode>(this._getChildren);
|
||
|
|
this.dataSource = new MatTreeNestedDataSource();
|
||
|
|
this.dataSource.data = TREE_DATA;
|
||
|
|
}
|
||
|
|
|
||
|
|
private _getChildren = (node: FoodNode) => node.children;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||
|
|
<mat-nested-tree-node *matTreeNodeDef="let node" class="customNodeClass"
|
||
|
|
[isExpandable]="isExpandable(node) | async"
|
||
|
|
[isDisabled]="node.isDisabled">
|
||
|
|
{{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}}
|
||
|
|
<ng-template matTreeNodeOutlet></ng-template>
|
||
|
|
</mat-nested-tree-node>
|
||
|
|
</mat-tree>
|
||
|
|
`,
|
||
|
|
standalone: false,
|
||
|
|
})
|
||
|
|
class NestedMatTreeApp {
|
||
|
|
getChildren = (node: TestData) => node.observableChildren;
|
||
|
|
isExpandable = (node: TestData) =>
|
||
|
|
node.observableChildren.pipe(map(children => children.length > 0));
|
||
|
|
|
||
|
|
treeControl = new NestedTreeControl(this.getChildren);
|
||
|
|
|
||
|
|
dataSource = new MatTreeNestedDataSource();
|
||
|
|
underlyingDataSource = new FakeDataSource();
|
||
|
|
|
||
|
|
@ViewChild(MatTree) tree: MatTree<TestData>;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.underlyingDataSource.connect().subscribe(data => {
|
||
|
|
this.dataSource.data = data;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||
|
|
<mat-nested-tree-node *matTreeNodeDef="let node">
|
||
|
|
{{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}}
|
||
|
|
<ng-template matTreeNodeOutlet></ng-template>
|
||
|
|
</mat-nested-tree-node>
|
||
|
|
<mat-nested-tree-node *matTreeNodeDef="let node; when: isSpecial"
|
||
|
|
matTreeNodeToggle>
|
||
|
|
>>> {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}}
|
||
|
|
<div *ngIf="treeControl.isExpanded(node)">
|
||
|
|
<ng-template matTreeNodeOutlet></ng-template>
|
||
|
|
</div>
|
||
|
|
</mat-nested-tree-node>
|
||
|
|
</mat-tree>
|
||
|
|
`,
|
||
|
|
standalone: false,
|
||
|
|
})
|
||
|
|
class WhenNodeNestedMatTreeApp {
|
||
|
|
isSpecial = (_: number, node: TestData) => node.isSpecial;
|
||
|
|
|
||
|
|
getChildren = (node: TestData) => node.observableChildren;
|
||
|
|
|
||
|
|
treeControl: TreeControl<TestData> = new NestedTreeControl(this.getChildren);
|
||
|
|
|
||
|
|
dataSource = new MatTreeNestedDataSource();
|
||
|
|
underlyingDataSource = new FakeDataSource();
|
||
|
|
|
||
|
|
@ViewChild(MatTree) tree: MatTree<TestData>;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.underlyingDataSource.connect().subscribe(data => {
|
||
|
|
this.dataSource.data = data;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||
|
|
<mat-tree-node *matTreeNodeDef="let node" class="customNodeClass"
|
||
|
|
[isExpandable]="isExpandable(node)"
|
||
|
|
[isDisabled]="node.isDisabled"
|
||
|
|
matTreeNodePadding
|
||
|
|
matTreeNodeToggle [matTreeNodeToggleRecursive]="toggleRecursively">
|
||
|
|
{{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}}
|
||
|
|
</mat-tree-node>
|
||
|
|
</mat-tree>
|
||
|
|
`,
|
||
|
|
standalone: false,
|
||
|
|
})
|
||
|
|
class MatTreeAppWithToggle {
|
||
|
|
toggleRecursively: boolean = true;
|
||
|
|
|
||
|
|
getLevel = (node: TestData) => node.level;
|
||
|
|
isExpandable = (node: TestData) => node.children.length > 0;
|
||
|
|
getChildren = (node: TestData) => node.observableChildren;
|
||
|
|
transformer = (node: TestData, level: number) => {
|
||
|
|
node.level = level;
|
||
|
|
return node;
|
||
|
|
};
|
||
|
|
|
||
|
|
treeFlattener = new MatTreeFlattener<TestData, TestData>(
|
||
|
|
this.transformer,
|
||
|
|
this.getLevel,
|
||
|
|
this.isExpandable,
|
||
|
|
this.getChildren,
|
||
|
|
);
|
||
|
|
|
||
|
|
treeControl = new FlatTreeControl(this.getLevel, this.isExpandable);
|
||
|
|
|
||
|
|
dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
|
||
|
|
underlyingDataSource = new FakeDataSource();
|
||
|
|
|
||
|
|
@ViewChild(MatTree) tree: MatTree<TestData>;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.underlyingDataSource.connect().subscribe(data => {
|
||
|
|
this.dataSource.data = data;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||
|
|
<mat-nested-tree-node *matTreeNodeDef="let node" class="customNodeClass"
|
||
|
|
[isExpandable]="isExpandable(node) | async"
|
||
|
|
matTreeNodeToggle
|
||
|
|
[matTreeNodeToggleRecursive]="toggleRecursively">
|
||
|
|
{{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}}
|
||
|
|
<div *ngIf="treeControl.isExpanded(node)">
|
||
|
|
<ng-template matTreeNodeOutlet></ng-template>
|
||
|
|
</div>
|
||
|
|
</mat-nested-tree-node>
|
||
|
|
</mat-tree>
|
||
|
|
`,
|
||
|
|
standalone: false,
|
||
|
|
})
|
||
|
|
class NestedMatTreeAppWithToggle {
|
||
|
|
toggleRecursively: boolean = true;
|
||
|
|
|
||
|
|
getChildren = (node: TestData) => node.observableChildren;
|
||
|
|
isExpandable = (node: TestData) =>
|
||
|
|
node.observableChildren.pipe(map(children => children.length > 0));
|
||
|
|
|
||
|
|
treeControl = new NestedTreeControl(this.getChildren);
|
||
|
|
dataSource = new MatTreeNestedDataSource();
|
||
|
|
underlyingDataSource = new FakeDataSource();
|
||
|
|
|
||
|
|
@ViewChild(MatTree) tree: MatTree<TestData>;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.underlyingDataSource.connect().subscribe(data => {
|
||
|
|
this.dataSource.data = data;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Component({
|
||
|
|
template: `
|
||
|
|
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||
|
|
<mat-tree-node *matTreeNodeDef="let node" class="customNodeClass"
|
||
|
|
matTreeNodePadding [matTreeNodePaddingIndent]="28"
|
||
|
|
matTreeNodeToggle>
|
||
|
|
{{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}}
|
||
|
|
</mat-tree-node>
|
||
|
|
<mat-tree-node *matTreeNodeDef="let node; when: isSpecial" class="customNodeClass"
|
||
|
|
matTreeNodePadding [matTreeNodePaddingIndent]="28"
|
||
|
|
matTreeNodeToggle>
|
||
|
|
>>> {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}}
|
||
|
|
</mat-tree-node>
|
||
|
|
</mat-tree>
|
||
|
|
`,
|
||
|
|
standalone: false,
|
||
|
|
})
|
||
|
|
class WhenNodeMatTreeApp {
|
||
|
|
isSpecial = (_: number, node: TestData) => node.isSpecial;
|
||
|
|
|
||
|
|
getLevel = (node: TestData) => node.level;
|
||
|
|
isExpandable = (node: TestData) => node.children.length > 0;
|
||
|
|
getChildren = (node: TestData) => node.observableChildren;
|
||
|
|
transformer = (node: TestData, level: number) => {
|
||
|
|
node.level = level;
|
||
|
|
return node;
|
||
|
|
};
|
||
|
|
|
||
|
|
treeFlattener = new MatTreeFlattener<TestData, TestData>(
|
||
|
|
this.transformer,
|
||
|
|
this.getLevel,
|
||
|
|
this.isExpandable,
|
||
|
|
this.getChildren,
|
||
|
|
);
|
||
|
|
|
||
|
|
treeControl = new FlatTreeControl(this.getLevel, this.isExpandable);
|
||
|
|
|
||
|
|
dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
|
||
|
|
underlyingDataSource = new FakeDataSource();
|
||
|
|
|
||
|
|
@ViewChild(MatTree) tree: MatTree<TestData>;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.underlyingDataSource.connect().subscribe(data => {
|
||
|
|
this.dataSource.data = data;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|