diff --git a/src/cdk/table/table.spec.ts b/src/cdk/table/table.spec.ts index d618b79523e6..4c3e3a326000 100644 --- a/src/cdk/table/table.spec.ts +++ b/src/cdk/table/table.spec.ts @@ -801,6 +801,25 @@ describe('CdkTable', () => { ]); }); + it('should replace only the row whose row definition changed', () => { + setupTableTestApp(WhenRowChangeDetectionCdkTableApp); + const initialRows = getRows(tableElement); + + initialRows.forEach(row => expect(row.classList).toContain('default-row')); + + component.showAlternate = true; + component.table.renderRows(); + fixture.detectChanges(); + + const updatedRows = getRows(tableElement); + expect(updatedRows[0]).toBe(initialRows[0]); + expect(updatedRows[1]).not.toBe(initialRows[1]); + expect(updatedRows[2]).toBe(initialRows[2]); + expect(updatedRows[0].classList).toContain('default-row'); + expect(updatedRows[1].classList).toContain('alternate-row'); + expect(updatedRows[2].classList).toContain('default-row'); + }); + it('should error if there is row data that does not have a matching row template', fakeAsync(() => { const whenRowWithoutDefaultFixture = TestBed.createComponent( WhenRowWithoutDefaultCdkTableApp, @@ -2405,6 +2424,31 @@ class WhenRowCdkTableApp { } } +@Component({ + template: ` + + + {{row.a}} + + + + + + `, + imports: [CdkTableModule], + changeDetection: ChangeDetectionStrategy.Eager, +}) +class WhenRowChangeDetectionCdkTableApp { + dataSource = new FakeDataSource(); + showAlternate = false; + + @ViewChild(CdkTable) table!: CdkTable; + + trackByIndex = (index: number, _item: TestData) => index; + isAlternateRow = (index: number, _item: TestData) => this.showAlternate && index === 1; +} + @Component({ template: ` diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts index 0818d1ef981b..f09107661b44 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -423,6 +423,12 @@ export class CdkTable */ private _cachedRenderRowsMap = new Map, RenderRow[]>>(); + /** + * Row definition used to create each rendered data-row view. Keying by the view keeps the + * association intact when the differ moves rows. + */ + private _rowDefsByView = new WeakMap>, CdkRowDef>(); + /** Whether the table is applied to a native ``. */ protected _isNativeHtmlTable: boolean; @@ -750,20 +756,32 @@ export class CdkTable (change: _ViewRepeaterItemChange, RowContext>) => { if (change.operation === _ViewRepeaterOperation.INSERTED && change.context) { this._renderCellTemplateForItem(change.record.item.rowDef, change.context); + const rowView = viewContainer.get(change.record.currentIndex!) as RowViewRef; + this._rowDefsByView.set(rowView, change.record.item.rowDef); } }, ); - // Update the meta context of a row's context data (index, count, first, last, ...) - this._updateRowIndexContext(); - // Update rows that did not get added/removed/moved but may have had their identity changed, // e.g. if trackBy matched data on some property but the actual data reference changed. changes.forEachIdentityChange((record: IterableChangeRecord>) => { - const rowView = >viewContainer.get(record.currentIndex!); - rowView.context.$implicit = record.item.data; + const currentIndex = record.currentIndex!; + const rowView = viewContainer.get(currentIndex) as RowViewRef; + + if (this._rowDefsByView.get(rowView) !== record.item.rowDef) { + viewContainer.remove(currentIndex); + const newRowView = this._renderRow(this._rowOutlet, record.item.rowDef, currentIndex, { + $implicit: record.item.data, + }); + this._rowDefsByView.set(newRowView, record.item.rowDef); + } else { + rowView.context.$implicit = record.item.data; + } }); + // Update the meta context of a row's context data (index, count, first, last, ...) + this._updateRowIndexContext(); + this._updateNoDataRow(); this.contentChanged.next();