From 063b755fcdb0361cd349bbec7a109a4bdb06bf69 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 7 Apr 2025 09:02:32 +0200 Subject: [PATCH] fix(cdk/table): error if data is accessed too early Fixes that the table was throwing an error if `renderRows` is called before the data has been loaded for the first time. Fixes #30795. --- goldens/cdk/table/index.api.md | 2 +- src/cdk/table/BUILD.bazel | 1 + src/cdk/table/table.spec.ts | 10 ++++++++++ src/cdk/table/table.ts | 14 +++++++++----- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/goldens/cdk/table/index.api.md b/goldens/cdk/table/index.api.md index 383619d2f929..51e26ef8295b 100644 --- a/goldens/cdk/table/index.api.md +++ b/goldens/cdk/table/index.api.md @@ -313,7 +313,7 @@ export class CdkTable implements AfterContentInit, AfterContentChecked, Colle _contentFooterRowDefs: QueryList; _contentHeaderRowDefs: QueryList; _contentRowDefs: QueryList>; - protected _data: readonly T[]; + protected _data: readonly T[] | undefined; get dataSource(): CdkTableDataSourceInput; set dataSource(dataSource: CdkTableDataSourceInput); // (undocumented) diff --git a/src/cdk/table/BUILD.bazel b/src/cdk/table/BUILD.bazel index 3fe75c792370..818a24085c6b 100644 --- a/src/cdk/table/BUILD.bazel +++ b/src/cdk/table/BUILD.bazel @@ -43,6 +43,7 @@ ts_project( deps = [ ":table", "//:node_modules/@angular/core", + "//:node_modules/@angular/platform-browser", "//:node_modules/rxjs", "//src/cdk/bidi", "//src/cdk/collections", diff --git a/src/cdk/table/table.spec.ts b/src/cdk/table/table.spec.ts index 095d08513354..534e4e146b11 100644 --- a/src/cdk/table/table.spec.ts +++ b/src/cdk/table/table.spec.ts @@ -14,6 +14,7 @@ import { ViewChild, inject, } from '@angular/core'; +import {By} from '@angular/platform-browser'; import {ComponentFixture, TestBed, fakeAsync, flush, waitForAsync} from '@angular/core/testing'; import {BehaviorSubject, Observable, combineLatest, of as observableOf} from 'rxjs'; import {map} from 'rxjs/operators'; @@ -376,6 +377,15 @@ describe('CdkTable', () => { expect(colgroupsAndCols.map(e => e.nodeName.toLowerCase())).toEqual(['colgroup', 'col', 'col']); })); + it('should not throw if `renderRows` is called too early', () => { + // Note that we don't call `detectChanges` here, because we're testing specifically + // what happens when `renderRows` is called before the first change detection run. + const fixture = createComponent(SimpleCdkTableApp); + const table = fixture.debugElement.query(By.directive(CdkTable)) + .componentInstance as CdkTable; + expect(() => table.renderRows()).not.toThrow(); + }); + describe('with different data inputs other than data source', () => { let baseData: TestData[] = [ {a: 'a_1', b: 'b_1', c: 'c_1'}, diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts index e19f32a8c213..4134d2f8a35c 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -307,7 +307,7 @@ export class CdkTable private _document = inject(DOCUMENT); /** Latest data provided by the data source. */ - protected _data: readonly T[]; + protected _data: readonly T[] | undefined; /** Subject that emits when the component has been destroyed. */ private readonly _onDestroy = new Subject(); @@ -621,10 +621,6 @@ export class CdkTable this._isServer = !this._platform.isBrowser; this._isNativeHtmlTable = this._elementRef.nativeElement.nodeName === 'TABLE'; - } - - ngOnInit() { - this._setupStickyStyler(); // Set up the trackBy function so that it uses the `RenderRow` as its identity by default. If // the user has provided a custom trackBy, return the result of that function as evaluated @@ -632,6 +628,10 @@ export class CdkTable this._dataDiffer = this._differs.find([]).create((_i: number, dataRow: RenderRow) => { return this.trackBy ? this.trackBy(dataRow.dataIndex, dataRow.data) : dataRow; }); + } + + ngOnInit() { + this._setupStickyStyler(); this._viewportRuler .change() @@ -981,6 +981,10 @@ export class CdkTable const prevCachedRenderRows = this._cachedRenderRowsMap; this._cachedRenderRowsMap = new Map(); + if (!this._data) { + return renderRows; + } + // For each data object, get the list of rows that should be rendered, represented by the // respective `RenderRow` object which is the pair of `data` and `CdkRowDef`. for (let i = 0; i < this._data.length; i++) {