@@ -25,7 +25,8 @@ describe('CdkTable', () => {
25
25
SimpleCdkTableApp ,
26
26
DynamicDataSourceCdkTableApp ,
27
27
CustomRoleCdkTableApp ,
28
- RowContextCdkTableApp
28
+ TrackByCdkTableApp ,
29
+ RowContextCdkTableApp ,
29
30
] ,
30
31
} ) . compileComponents ( ) ;
31
32
} ) ) ;
@@ -145,6 +146,117 @@ describe('CdkTable', () => {
145
146
expect ( changedRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
146
147
} ) ;
147
148
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
+
148
260
it ( 'should match the right table content with dynamic data' , ( ) => {
149
261
const initialDataLength = dataSource . data . length ;
150
262
expect ( dataSource . data . length ) . toBe ( 3 ) ;
@@ -406,6 +518,41 @@ class DynamicDataSourceCdkTableApp {
406
518
@ViewChild ( CdkTable ) table : CdkTable < TestData > ;
407
519
}
408
520
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
+
409
556
@Component ( {
410
557
template : `
411
558
<cdk-table [dataSource]="dataSource" role="treegrid">
0 commit comments