@@ -25,7 +25,8 @@ describe('CdkTable', () => {
2525 SimpleCdkTableApp ,
2626 DynamicDataSourceCdkTableApp ,
2727 CustomRoleCdkTableApp ,
28- RowContextCdkTableApp
28+ TrackByCdkTableApp ,
29+ RowContextCdkTableApp ,
2930 ] ,
3031 } ) . compileComponents ( ) ;
3132 } ) ) ;
@@ -145,6 +146,117 @@ describe('CdkTable', () => {
145146 expect ( changedRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
146147 } ) ;
147148
149+ describe ( 'with trackBy' , ( ) => {
150+
151+ let trackByComponent : TrackByCdkTableApp ;
152+ let trackByFixture : ComponentFixture < TrackByCdkTableApp > ;
153+
154+ function createTestComponentWithTrackyByTable ( trackByStrategy ) {
155+ trackByFixture = TestBed . createComponent ( TrackByCdkTableApp ) ;
156+
157+ trackByComponent = trackByFixture . componentInstance ;
158+ trackByComponent . trackByStrategy = trackByStrategy ;
159+
160+ dataSource = trackByComponent . dataSource as FakeDataSource ;
161+ table = trackByComponent . table ;
162+ tableElement = trackByFixture . nativeElement . querySelector ( 'cdk-table' ) ;
163+
164+ trackByFixture . detectChanges ( ) ; // Let the component and table create embedded views
165+ trackByFixture . detectChanges ( ) ; // Let the cells render
166+
167+ // Each row receives an attribute 'initialIndex' the element's original place
168+ getRows ( tableElement ) . forEach ( ( row : Element , index : number ) => {
169+ row . setAttribute ( 'initialIndex' , index . toString ( ) ) ;
170+ } ) ;
171+
172+ // Prove that the attributes match their indicies
173+ const initialRows = getRows ( tableElement ) ;
174+ expect ( initialRows [ 0 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '0' ) ;
175+ expect ( initialRows [ 1 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '1' ) ;
176+ expect ( initialRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '2' ) ;
177+ }
178+
179+ // Swap first two elements, remove the third, add new data
180+ function mutateData ( ) {
181+ // Swap first and second data in data array
182+ const copiedData = trackByComponent . dataSource . data . slice ( ) ;
183+ const temp = copiedData [ 0 ] ;
184+ copiedData [ 0 ] = copiedData [ 1 ] ;
185+ copiedData [ 1 ] = temp ;
186+
187+ // Remove the third element
188+ copiedData . splice ( 2 , 1 ) ;
189+
190+ // Add new data
191+ trackByComponent . dataSource . data = copiedData ;
192+ trackByComponent . dataSource . addData ( ) ;
193+ }
194+
195+ it ( 'should add/remove/move rows with reference-based trackBy' , ( ) => {
196+ createTestComponentWithTrackyByTable ( 'reference' ) ;
197+ mutateData ( ) ;
198+
199+ // Expect that the first and second rows were swapped and that the last row is new
200+ const changedRows = getRows ( tableElement ) ;
201+ expect ( changedRows . length ) . toBe ( 3 ) ;
202+ expect ( changedRows [ 0 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '1' ) ;
203+ expect ( changedRows [ 1 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '0' ) ;
204+ expect ( changedRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
205+ } ) ;
206+
207+ it ( 'should add/remove/move rows with changed references without property-based trackBy' , ( ) => {
208+ createTestComponentWithTrackyByTable ( 'reference' ) ;
209+ mutateData ( ) ;
210+
211+ // Change each item reference to show that the trackby is not checking the item properties.
212+ trackByComponent . dataSource . data = trackByComponent . dataSource . data
213+ . map ( item => { return { a : item . a , b : item . b , c : item . c } ; } ) ;
214+
215+ // Expect that all the rows are considered new since their references are all different
216+ const changedRows = getRows ( tableElement ) ;
217+ expect ( changedRows . length ) . toBe ( 3 ) ;
218+ expect ( changedRows [ 0 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
219+ expect ( changedRows [ 1 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
220+ expect ( changedRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
221+ } ) ;
222+
223+ it ( 'should add/remove/move rows with changed references with property-based trackBy' , ( ) => {
224+ createTestComponentWithTrackyByTable ( 'propertyA' ) ;
225+ mutateData ( ) ;
226+
227+ // Change each item reference to show that the trackby is checking the item properties.
228+ // Otherwise this would cause them all to be removed/added.
229+ trackByComponent . dataSource . data = trackByComponent . dataSource . data
230+ . map ( item => { return { a : item . a , b : item . b , c : item . c } ; } ) ;
231+
232+ // Expect that the first and second rows were swapped and that the last row is new
233+ const changedRows = getRows ( tableElement ) ;
234+ expect ( changedRows . length ) . toBe ( 3 ) ;
235+ expect ( changedRows [ 0 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '1' ) ;
236+ expect ( changedRows [ 1 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '0' ) ;
237+ expect ( changedRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
238+ } ) ;
239+
240+ it ( 'should add/remove/move rows with changed references with index-based trackBy' , ( ) => {
241+ createTestComponentWithTrackyByTable ( 'index' ) ;
242+ mutateData ( ) ;
243+
244+ // Change each item reference to show that the trackby is checking the index.
245+ // Otherwise this would cause them all to be removed/added.
246+ trackByComponent . dataSource . data = trackByComponent . dataSource . data
247+ . map ( item => { return { a : item . a , b : item . b , c : item . c } ; } ) ;
248+
249+ // Expect first two to be the same since they were swapped but indicies are consistent.
250+ // The third element was removed and caught by the table so it was removed before another
251+ // item was added, so it is without an initial index.
252+ const changedRows = getRows ( tableElement ) ;
253+ expect ( changedRows . length ) . toBe ( 3 ) ;
254+ expect ( changedRows [ 0 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '0' ) ;
255+ expect ( changedRows [ 1 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '1' ) ;
256+ expect ( changedRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
257+ } ) ;
258+ } ) ;
259+
148260 it ( 'should match the right table content with dynamic data' , ( ) => {
149261 const initialDataLength = dataSource . data . length ;
150262 expect ( dataSource . data . length ) . toBe ( 3 ) ;
@@ -406,6 +518,41 @@ class DynamicDataSourceCdkTableApp {
406518 @ViewChild ( CdkTable ) table : CdkTable < TestData > ;
407519}
408520
521+ @Component ( {
522+ template : `
523+ <cdk-table [dataSource]="dataSource" [trackBy]="trackBy">
524+ <ng-container cdkColumnDef="column_a">
525+ <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
526+ <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
527+ </ng-container>
528+
529+ <ng-container cdkColumnDef="column_b">
530+ <cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
531+ <cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
532+ </ng-container>
533+
534+ <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
535+ <cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
536+ </cdk-table>
537+ `
538+ } )
539+ class TrackByCdkTableApp {
540+ trackByStrategy : 'reference' | 'propertyA' | 'index' = 'reference' ;
541+
542+ dataSource : FakeDataSource = new FakeDataSource ( ) ;
543+ columnsToRender = [ 'column_a' , 'column_b' ] ;
544+
545+ @ViewChild ( CdkTable ) table : CdkTable < TestData > ;
546+
547+ trackBy = ( index : number , item : TestData ) => {
548+ switch ( this . trackByStrategy ) {
549+ case 'reference' : return item ;
550+ case 'propertyA' : return item . a ;
551+ case 'index' : return index ;
552+ }
553+ }
554+ }
555+
409556@Component ( {
410557 template : `
411558 <cdk-table [dataSource]="dataSource" role="treegrid">
0 commit comments