@@ -21,7 +21,12 @@ describe('CdkTable', () => {
21
21
22
22
TestBed . configureTestingModule ( {
23
23
imports : [ CdkDataTableModule ] ,
24
- declarations : [ SimpleCdkTableApp , DynamicDataSourceCdkTableApp , CustomRoleCdkTableApp ] ,
24
+ declarations : [
25
+ SimpleCdkTableApp ,
26
+ DynamicDataSourceCdkTableApp ,
27
+ CustomRoleCdkTableApp ,
28
+ TrackByCdkTableApp ,
29
+ ] ,
25
30
} ) . compileComponents ( ) ;
26
31
27
32
fixture = TestBed . createComponent ( SimpleCdkTableApp ) ;
@@ -138,6 +143,119 @@ describe('CdkTable', () => {
138
143
expect ( changedRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
139
144
} ) ;
140
145
146
+ describe ( 'should properly use trackBy when diffing to add/remove/move rows' , ( ) => {
147
+
148
+ afterEach ( ( ) => {
149
+ // Resetting the static value of the trackby function for TrackByCdkTableApp
150
+ TrackByCdkTableApp . TRACK_BY = 'reference' ;
151
+ } ) ;
152
+
153
+ function createComponent ( ) {
154
+ fixture = TestBed . createComponent ( TrackByCdkTableApp ) ;
155
+
156
+ component = fixture . componentInstance ;
157
+ dataSource = component . dataSource as FakeDataSource ;
158
+ table = component . table ;
159
+ tableElement = fixture . nativeElement . querySelector ( 'cdk-table' ) ;
160
+
161
+ fixture . detectChanges ( ) ; // Let the component and table create embedded views
162
+ fixture . detectChanges ( ) ; // Let the cells render
163
+
164
+ // Each row receives an attribute 'initialIndex' the element's original place
165
+ getRows ( tableElement ) . forEach ( ( row : Element , index : number ) => {
166
+ row . setAttribute ( 'initialIndex' , index . toString ( ) ) ;
167
+ } ) ;
168
+
169
+ // Prove that the attributes match their indicies
170
+ const initialRows = getRows ( tableElement ) ;
171
+ expect ( initialRows [ 0 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '0' ) ;
172
+ expect ( initialRows [ 1 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '1' ) ;
173
+ expect ( initialRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '2' ) ;
174
+ }
175
+
176
+ // Swap first two elements, remove the third, add new data
177
+ function changeData ( ) {
178
+ // Swap first and second data in data array
179
+ const copiedData = component . dataSource . data . slice ( ) ;
180
+ const temp = copiedData [ 0 ] ;
181
+ copiedData [ 0 ] = copiedData [ 1 ] ;
182
+ copiedData [ 1 ] = temp ;
183
+
184
+ // Remove the third element
185
+ copiedData . splice ( 2 , 1 ) ;
186
+
187
+ // Add new data
188
+ component . dataSource . data = copiedData ;
189
+ component . dataSource . addData ( ) ;
190
+ }
191
+
192
+ it ( 'with reference-based trackBy' , ( ) => {
193
+ createComponent ( ) ;
194
+ changeData ( ) ;
195
+
196
+ // Expect that the first and second rows were swapped and that the last row is new
197
+ const changedRows = getRows ( tableElement ) ;
198
+ expect ( changedRows . length ) . toBe ( 3 ) ;
199
+ expect ( changedRows [ 0 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '1' ) ;
200
+ expect ( changedRows [ 1 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '0' ) ;
201
+ expect ( changedRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
202
+ } ) ;
203
+
204
+ it ( 'with changed references without property-based trackBy' , ( ) => {
205
+ createComponent ( ) ;
206
+ changeData ( ) ;
207
+
208
+ // Change each item reference to show that the trackby is not checking the item properties.
209
+ component . dataSource . data = component . dataSource . data
210
+ . map ( item => { return { a : item . a , b : item . b , c : item . c } ; } ) ;
211
+
212
+ // Expect that all the rows are considered new since their references are all different
213
+ const changedRows = getRows ( tableElement ) ;
214
+ expect ( changedRows . length ) . toBe ( 3 ) ;
215
+ expect ( changedRows [ 0 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
216
+ expect ( changedRows [ 1 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
217
+ expect ( changedRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
218
+ } ) ;
219
+
220
+ it ( 'with changed references with property-based trackBy' , ( ) => {
221
+ TrackByCdkTableApp . TRACK_BY = 'propertyA' ;
222
+ createComponent ( ) ;
223
+ changeData ( ) ;
224
+
225
+ // Change each item reference to show that the trackby is checking the item properties.
226
+ // Otherwise this would cause them all to be removed/added.
227
+ component . dataSource . data = component . dataSource . data
228
+ . map ( item => { return { a : item . a , b : item . b , c : item . c } ; } ) ;
229
+
230
+ // Expect that the first and second rows were swapped and that the last row is new
231
+ const changedRows = getRows ( tableElement ) ;
232
+ expect ( changedRows . length ) . toBe ( 3 ) ;
233
+ expect ( changedRows [ 0 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '1' ) ;
234
+ expect ( changedRows [ 1 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '0' ) ;
235
+ expect ( changedRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
236
+ } ) ;
237
+
238
+ it ( 'with changed references with index-based trackBy' , ( ) => {
239
+ TrackByCdkTableApp . TRACK_BY = 'index' ;
240
+ createComponent ( ) ;
241
+ changeData ( ) ;
242
+
243
+ // Change each item reference to show that the trackby is checking the index.
244
+ // Otherwise this would cause them all to be removed/added.
245
+ component . dataSource . data = component . dataSource . data
246
+ . map ( item => { return { a : item . a , b : item . b , c : item . c } ; } ) ;
247
+
248
+ // Expect first two to be the same since they were swapped but indicies are consistent.
249
+ // The third element was removed and caught by the table so it was removed before another
250
+ // item was added, so it is without an initial index.
251
+ const changedRows = getRows ( tableElement ) ;
252
+ expect ( changedRows . length ) . toBe ( 3 ) ;
253
+ expect ( changedRows [ 0 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '0' ) ;
254
+ expect ( changedRows [ 1 ] . getAttribute ( 'initialIndex' ) ) . toBe ( '1' ) ;
255
+ expect ( changedRows [ 2 ] . getAttribute ( 'initialIndex' ) ) . toBe ( null ) ;
256
+ } ) ;
257
+ } ) ;
258
+
141
259
it ( 'should match the right table content with dynamic data' , ( ) => {
142
260
const initialDataLength = dataSource . data . length ;
143
261
expect ( dataSource . data . length ) . toBe ( 3 ) ;
@@ -316,6 +434,41 @@ class DynamicDataSourceCdkTableApp {
316
434
@ViewChild ( CdkTable ) table : CdkTable < TestData > ;
317
435
}
318
436
437
+ @Component ( {
438
+ template : `
439
+ <cdk-table [dataSource]="dataSource" [trackBy]="trackBy">
440
+ <ng-container cdkColumnDef="column_a">
441
+ <cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
442
+ <cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
443
+ </ng-container>
444
+
445
+ <ng-container cdkColumnDef="column_b">
446
+ <cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
447
+ <cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
448
+ </ng-container>
449
+
450
+ <cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
451
+ <cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
452
+ </cdk-table>
453
+ `
454
+ } )
455
+ class TrackByCdkTableApp {
456
+ static TRACK_BY : 'reference' | 'propertyA' | 'index' = 'reference' ;
457
+
458
+ dataSource : FakeDataSource = new FakeDataSource ( ) ;
459
+ columnsToRender = [ 'column_a' , 'column_b' ] ;
460
+
461
+ @ViewChild ( CdkTable ) table : CdkTable < TestData > ;
462
+
463
+ trackBy ( index : number , item : TestData ) {
464
+ switch ( TrackByCdkTableApp . TRACK_BY ) {
465
+ case 'reference' : return item ;
466
+ case 'propertyA' : return item . a ;
467
+ case 'index' : return index ;
468
+ }
469
+ }
470
+ }
471
+
319
472
@Component ( {
320
473
template : `
321
474
<cdk-table [dataSource]="dataSource" role="treegrid">
0 commit comments