|
6 | 6 | * found in the LICENSE file at https://angular.io/license
|
7 | 7 | */
|
8 | 8 | import {ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing';
|
9 |
| -import {Component, ViewChild} from '@angular/core'; |
| 9 | +import {Component, ViewChild, TrackByFunction} from '@angular/core'; |
10 | 10 |
|
11 | 11 | import {CollectionViewer, DataSource} from '@angular/cdk/collections';
|
12 | 12 | import {combineLatest, BehaviorSubject, Observable} from 'rxjs';
|
@@ -296,6 +296,97 @@ describe('CdkTree', () => {
|
296 | 296 | [`[topping_3] - [cheese_3] + [base_3]`]);
|
297 | 297 | });
|
298 | 298 | });
|
| 299 | + |
| 300 | + describe('with trackBy', () => { |
| 301 | + let fixture: ComponentFixture<CdkTreeAppWithTrackBy>; |
| 302 | + let component: CdkTreeAppWithTrackBy; |
| 303 | + |
| 304 | + function createTrackByTestComponent(trackByStrategy: 'reference' | 'property' | 'index') { |
| 305 | + configureCdkTreeTestingModule([CdkTreeAppWithTrackBy]); |
| 306 | + fixture = TestBed.createComponent(CdkTreeAppWithTrackBy); |
| 307 | + component = fixture.componentInstance; |
| 308 | + component.trackByStrategy = trackByStrategy; |
| 309 | + dataSource = component.dataSource as FakeDataSource; |
| 310 | + tree = component.tree; |
| 311 | + treeElement = fixture.nativeElement.querySelector('cdk-tree'); |
| 312 | + |
| 313 | + fixture.detectChanges(); |
| 314 | + |
| 315 | + // Each node receives an attribute 'initialIndex' the element's original place |
| 316 | + getNodes(treeElement).forEach((node: Element, index: number) => { |
| 317 | + node.setAttribute('initialIndex', index.toString()); |
| 318 | + }); |
| 319 | + |
| 320 | + // Prove that the attributes match their indicies |
| 321 | + const initialNodes = getNodes(treeElement); |
| 322 | + expect(initialNodes[0].getAttribute('initialIndex')).toBe('0'); |
| 323 | + expect(initialNodes[1].getAttribute('initialIndex')).toBe('1'); |
| 324 | + expect(initialNodes[2].getAttribute('initialIndex')).toBe('2'); |
| 325 | + } |
| 326 | + |
| 327 | + function mutateData() { |
| 328 | + // Swap first and second data in data array |
| 329 | + const copiedData = component.dataSource.data.slice(); |
| 330 | + const temp = copiedData[0]; |
| 331 | + copiedData[0] = copiedData[1]; |
| 332 | + copiedData[1] = temp; |
| 333 | + |
| 334 | + // Remove the third element |
| 335 | + copiedData.splice(2, 1); |
| 336 | + |
| 337 | + // Add new data |
| 338 | + component.dataSource.data = copiedData; |
| 339 | + component.dataSource.addData(); |
| 340 | + } |
| 341 | + |
| 342 | + it('should add/remove/move nodes with reference-based trackBy', () => { |
| 343 | + createTrackByTestComponent('reference'); |
| 344 | + mutateData(); |
| 345 | + |
| 346 | + // Expect that the first and second nodes were swapped and that the last node is new |
| 347 | + const changedNodes = getNodes(treeElement); |
| 348 | + expect(changedNodes.length).toBe(3); |
| 349 | + expect(changedNodes[0].getAttribute('initialIndex')).toBe('1'); |
| 350 | + expect(changedNodes[1].getAttribute('initialIndex')).toBe('0'); |
| 351 | + expect(changedNodes[2].getAttribute('initialIndex')).toBe(null); |
| 352 | + }); |
| 353 | + |
| 354 | + it('should add/remove/move nodes with property-based trackBy', () => { |
| 355 | + createTrackByTestComponent('property'); |
| 356 | + mutateData(); |
| 357 | + |
| 358 | + // Change each item reference to show that the trackby is checking the item properties. |
| 359 | + // Otherwise this would cause them all to be removed/added. |
| 360 | + component.dataSource.data = component.dataSource.data |
| 361 | + .map(item => new TestData(item.pizzaTopping, item.pizzaCheese, item.pizzaBase, )); |
| 362 | + |
| 363 | + // Expect that the first and second nodes were swapped and that the last node is new |
| 364 | + const changedNodes = getNodes(treeElement); |
| 365 | + expect(changedNodes.length).toBe(3); |
| 366 | + expect(changedNodes[0].getAttribute('initialIndex')).toBe('1'); |
| 367 | + expect(changedNodes[1].getAttribute('initialIndex')).toBe('0'); |
| 368 | + expect(changedNodes[2].getAttribute('initialIndex')).toBe(null); |
| 369 | + }); |
| 370 | + |
| 371 | + it('should add/remove/move nodes with index-based trackBy', () => { |
| 372 | + createTrackByTestComponent('index'); |
| 373 | + mutateData(); |
| 374 | + |
| 375 | + // Change each item reference to show that the trackby is checking the index. |
| 376 | + // Otherwise this would cause them all to be removed/added. |
| 377 | + component.dataSource.data = component.dataSource.data |
| 378 | + .map(item => new TestData(item.pizzaTopping, item.pizzaCheese, item.pizzaBase, )); |
| 379 | + |
| 380 | + // Expect first two to be the same since they were swapped but indicies are consistent. |
| 381 | + // The third element was removed and caught by the tree so it was removed before another |
| 382 | + // item was added, so it is without an initial index. |
| 383 | + const changedNodes = getNodes(treeElement); |
| 384 | + expect(changedNodes.length).toBe(3); |
| 385 | + expect(changedNodes[0].getAttribute('initialIndex')).toBe('0'); |
| 386 | + expect(changedNodes[1].getAttribute('initialIndex')).toBe('1'); |
| 387 | + expect(changedNodes[2].getAttribute('initialIndex')).toBe(null); |
| 388 | + }); |
| 389 | + }); |
299 | 390 | });
|
300 | 391 |
|
301 | 392 | describe('nested tree', () => {
|
@@ -599,6 +690,128 @@ describe('CdkTree', () => {
|
599 | 690 | });
|
600 | 691 | });
|
601 | 692 |
|
| 693 | + describe('with trackBy', () => { |
| 694 | + let fixture: ComponentFixture<NestedCdkTreeAppWithTrackBy>; |
| 695 | + let component: NestedCdkTreeAppWithTrackBy; |
| 696 | + |
| 697 | + function createTrackByTestComponent(trackByStrategy: 'reference' | 'property' | 'index') { |
| 698 | + configureCdkTreeTestingModule([NestedCdkTreeAppWithTrackBy]); |
| 699 | + fixture = TestBed.createComponent(NestedCdkTreeAppWithTrackBy); |
| 700 | + component = fixture.componentInstance; |
| 701 | + component.trackByStrategy = trackByStrategy; |
| 702 | + dataSource = component.dataSource as FakeDataSource; |
| 703 | + |
| 704 | + tree = component.tree; |
| 705 | + treeElement = fixture.nativeElement.querySelector('cdk-tree'); |
| 706 | + |
| 707 | + fixture.detectChanges(); |
| 708 | + |
| 709 | + // Each node receives an attribute 'initialIndex' the element's original place |
| 710 | + getNodes(treeElement).forEach((node: Element, index: number) => { |
| 711 | + node.setAttribute('initialIndex', index.toString()); |
| 712 | + }); |
| 713 | + |
| 714 | + // Prove that the attributes match their indicies |
| 715 | + const initialNodes = getNodes(treeElement); |
| 716 | + expect(initialNodes.length).toBe(3); |
| 717 | + initialNodes.forEach((node, index) => { |
| 718 | + expect(node.getAttribute('initialIndex')).toBe(`${index}`); |
| 719 | + }); |
| 720 | + |
| 721 | + const parent = dataSource.data[0]; |
| 722 | + dataSource.addChild(parent, false); |
| 723 | + dataSource.addChild(parent, false); |
| 724 | + dataSource.addChild(parent, false); |
| 725 | + getNodes(initialNodes[0]).forEach((node: Element, index: number) => { |
| 726 | + node.setAttribute('initialIndex', `c${index}`); |
| 727 | + }); |
| 728 | + getNodes(initialNodes[0]).forEach((node, index) => { |
| 729 | + expect(node.getAttribute('initialIndex')).toBe(`c${index}`); |
| 730 | + }); |
| 731 | + } |
| 732 | + |
| 733 | + function mutateChildren(parent: TestData) { |
| 734 | + // Swap first and second data in data array |
| 735 | + const copiedData = parent.children.slice(); |
| 736 | + const temp = copiedData[0]; |
| 737 | + copiedData[0] = copiedData[1]; |
| 738 | + copiedData[1] = temp; |
| 739 | + |
| 740 | + // Remove the third element |
| 741 | + copiedData.splice(2, 1); |
| 742 | + |
| 743 | + // Add new data |
| 744 | + parent.children = copiedData; |
| 745 | + parent.observableChildren.next(copiedData); |
| 746 | + component.dataSource.addChild(parent, false); |
| 747 | + } |
| 748 | + |
| 749 | + it('should add/remove/move children nodes with reference-based trackBy', () => { |
| 750 | + createTrackByTestComponent('reference'); |
| 751 | + mutateChildren(dataSource.data[0]); |
| 752 | + |
| 753 | + const changedNodes = getNodes(treeElement); |
| 754 | + expect(changedNodes.length).toBe(6); |
| 755 | + expect(changedNodes[0].getAttribute('initialIndex')).toBe('0'); |
| 756 | + |
| 757 | + // Expect that the first and second child nodes were swapped and that the last node is new |
| 758 | + expect(changedNodes[1].getAttribute('initialIndex')).toBe('c1'); |
| 759 | + expect(changedNodes[2].getAttribute('initialIndex')).toBe('c0'); |
| 760 | + expect(changedNodes[3].getAttribute('initialIndex')).toBe(null); |
| 761 | + |
| 762 | + expect(changedNodes[4].getAttribute('initialIndex')).toBe('1'); |
| 763 | + expect(changedNodes[5].getAttribute('initialIndex')).toBe('2'); |
| 764 | + }); |
| 765 | + |
| 766 | + it('should add/remove/move children nodes with property-based trackBy', () => { |
| 767 | + createTrackByTestComponent('property'); |
| 768 | + mutateChildren(dataSource.data[0]); |
| 769 | + |
| 770 | + // Change each item reference to show that the trackby is checking the item properties. |
| 771 | + // Otherwise this would cause them all to be removed/added. |
| 772 | + dataSource.data[0].observableChildren.next(dataSource.data[0].children |
| 773 | + .map(item => new TestData(item.pizzaTopping, item.pizzaCheese, item.pizzaBase))); |
| 774 | + |
| 775 | + // Expect that the first and second nodes were swapped and that the last node is new |
| 776 | + const changedNodes = getNodes(treeElement); |
| 777 | + expect(changedNodes.length).toBe(6); |
| 778 | + expect(changedNodes[0].getAttribute('initialIndex')).toBe('0'); |
| 779 | + |
| 780 | + // Expect that the first and second child nodes were swapped and that the last node is new |
| 781 | + expect(changedNodes[1].getAttribute('initialIndex')).toBe('c1'); |
| 782 | + expect(changedNodes[2].getAttribute('initialIndex')).toBe('c0'); |
| 783 | + expect(changedNodes[3].getAttribute('initialIndex')).toBe(null); |
| 784 | + |
| 785 | + expect(changedNodes[4].getAttribute('initialIndex')).toBe('1'); |
| 786 | + expect(changedNodes[5].getAttribute('initialIndex')).toBe('2'); |
| 787 | + }); |
| 788 | + |
| 789 | + it('should add/remove/move children nodes with index-based trackBy', () => { |
| 790 | + createTrackByTestComponent('index'); |
| 791 | + mutateChildren(dataSource.data[0]); |
| 792 | + |
| 793 | + // Change each item reference to show that the trackby is checking the index. |
| 794 | + // Otherwise this would cause them all to be removed/added. |
| 795 | + dataSource.data[0].observableChildren.next(dataSource.data[0].children |
| 796 | + .map(item => new TestData(item.pizzaTopping, item.pizzaCheese, item.pizzaBase))); |
| 797 | + |
| 798 | + const changedNodes = getNodes(treeElement); |
| 799 | + expect(changedNodes.length).toBe(6); |
| 800 | + expect(changedNodes[0].getAttribute('initialIndex')).toBe('0'); |
| 801 | + |
| 802 | + // Expect first two children to be the same since they were swapped |
| 803 | + // but indicies are consistent. |
| 804 | + // The third element was removed and caught by the tree so it was removed before another |
| 805 | + // item was added, so it is without an initial index. |
| 806 | + expect(changedNodes[1].getAttribute('initialIndex')).toBe('c0'); |
| 807 | + expect(changedNodes[2].getAttribute('initialIndex')).toBe('c1'); |
| 808 | + expect(changedNodes[3].getAttribute('initialIndex')).toBe(null); |
| 809 | + |
| 810 | + expect(changedNodes[4].getAttribute('initialIndex')).toBe('1'); |
| 811 | + expect(changedNodes[5].getAttribute('initialIndex')).toBe('2'); |
| 812 | + }); |
| 813 | + }); |
| 814 | + |
602 | 815 | it('should throw an error when missing function in nested tree', fakeAsync(() => {
|
603 | 816 | configureCdkTreeTestingModule([NestedCdkErrorTreeApp]);
|
604 | 817 | expect(() => {
|
@@ -1144,3 +1357,66 @@ class DepthNestedCdkTreeApp {
|
1144 | 1357 |
|
1145 | 1358 | @ViewChild(CdkTree) tree: CdkTree<TestData>;
|
1146 | 1359 | }
|
| 1360 | + |
| 1361 | +@Component({ |
| 1362 | + template: ` |
| 1363 | + <cdk-tree [dataSource]="dataSource" [treeControl]="treeControl" [trackBy]="trackByFn"> |
| 1364 | + <cdk-tree-node *cdkTreeNodeDef="let node" class="customNodeClass"> |
| 1365 | + {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} |
| 1366 | + </cdk-tree-node> |
| 1367 | + </cdk-tree> |
| 1368 | + ` |
| 1369 | +}) |
| 1370 | +class CdkTreeAppWithTrackBy { |
| 1371 | + trackByStrategy: 'reference' | 'property' | 'index' = 'reference'; |
| 1372 | + |
| 1373 | + trackByFn: TrackByFunction<TestData> = (index, item) => { |
| 1374 | + switch (this.trackByStrategy) { |
| 1375 | + case 'reference': return item; |
| 1376 | + case 'property': return item.pizzaBase; |
| 1377 | + case 'index': return index; |
| 1378 | + } |
| 1379 | + } |
| 1380 | + |
| 1381 | + getLevel = (node: TestData) => node.level; |
| 1382 | + isExpandable = (node: TestData) => node.children.length > 0; |
| 1383 | + |
| 1384 | + treeControl: TreeControl<TestData> = new FlatTreeControl(this.getLevel, this.isExpandable); |
| 1385 | + dataSource: FakeDataSource = new FakeDataSource(this.treeControl); |
| 1386 | + |
| 1387 | + @ViewChild(CdkTree) tree: CdkTree<TestData>; |
| 1388 | +} |
| 1389 | + |
| 1390 | +@Component({ |
| 1391 | + template: ` |
| 1392 | + <cdk-tree [dataSource]="dataArray" [treeControl]="treeControl" [trackBy]="trackByFn"> |
| 1393 | + <cdk-nested-tree-node *cdkTreeNodeDef="let node"> |
| 1394 | + [{{node.pizzaTopping}}] - [{{node.pizzaCheese}}] + [{{node.pizzaBase}}] |
| 1395 | + <ng-template cdkTreeNodeOutlet></ng-template> |
| 1396 | + </cdk-nested-tree-node> |
| 1397 | + </cdk-tree> |
| 1398 | + ` |
| 1399 | +}) |
| 1400 | +class NestedCdkTreeAppWithTrackBy { |
| 1401 | + trackByStrategy: 'reference' | 'property' | 'index' = 'reference'; |
| 1402 | + |
| 1403 | + trackByFn: TrackByFunction<TestData> = (index, item) => { |
| 1404 | + switch (this.trackByStrategy) { |
| 1405 | + case 'reference': return item; |
| 1406 | + case 'property': return item.pizzaBase; |
| 1407 | + case 'index': return index; |
| 1408 | + } |
| 1409 | + } |
| 1410 | + |
| 1411 | + getChildren = (node: TestData) => node.observableChildren; |
| 1412 | + |
| 1413 | + treeControl: TreeControl<TestData> = new NestedTreeControl(this.getChildren); |
| 1414 | + |
| 1415 | + dataSource: FakeDataSource = new FakeDataSource(this.treeControl); |
| 1416 | + |
| 1417 | + get dataArray() { |
| 1418 | + return this.dataSource.data; |
| 1419 | + } |
| 1420 | + |
| 1421 | + @ViewChild(CdkTree) tree: CdkTree<TestData>; |
| 1422 | +} |
0 commit comments