From 0289fd326ba1b1e0d54bbc0d3790ea1a5286dd21 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Tue, 23 Jun 2020 11:23:41 -0400 Subject: [PATCH 01/18] perf(table) Coalesces style updates after style measurements to reduce layout thrashing Exposes the CoalescedStyleScheduler for use by other related components in a table such as column resize. --- src/cdk/table/coalesced-style-scheduler.ts | 26 ++++++ src/cdk/table/public-api.ts | 1 + src/cdk/table/sticky-styler.ts | 99 +++++++++++++--------- src/cdk/table/table.spec.ts | 81 ++++++++++++------ src/cdk/table/table.ts | 28 ++++-- src/material/table/table.spec.ts | 5 +- src/material/table/table.ts | 5 +- tools/public_api_guard/cdk/table.d.ts | 14 ++- 8 files changed, 181 insertions(+), 78 deletions(-) create mode 100644 src/cdk/table/coalesced-style-scheduler.ts diff --git a/src/cdk/table/coalesced-style-scheduler.ts b/src/cdk/table/coalesced-style-scheduler.ts new file mode 100644 index 000000000000..eaa096867634 --- /dev/null +++ b/src/cdk/table/coalesced-style-scheduler.ts @@ -0,0 +1,26 @@ +import {Injectable, NgZone} from '@angular/core'; +import {from, Observable} from 'rxjs'; + +@Injectable() +export class CoalescedStyleScheduler { + private _currentSchedule: Observable|null = null; + + constructor(private readonly _ngZone: NgZone) {} + + schedule(task: () => unknown): void { + this._createScheduleIfNeeded(); + + this._currentSchedule!.subscribe(task); + } + + private _createScheduleIfNeeded() { + if (this._currentSchedule) { return; } + + this._ngZone.runOutsideAngular(() => { + this._currentSchedule = from(new Promise((resolve) => { + this._currentSchedule = null; + resolve(undefined); + })); + }); + } +} diff --git a/src/cdk/table/public-api.ts b/src/cdk/table/public-api.ts index 4095bf4082f5..ff3116bda891 100644 --- a/src/cdk/table/public-api.ts +++ b/src/cdk/table/public-api.ts @@ -8,6 +8,7 @@ export * from './table'; export * from './cell'; +export * from './coalesced-style-scheduler'; export * from './row'; export * from './table-module'; export * from './sticky-styler'; diff --git a/src/cdk/table/sticky-styler.ts b/src/cdk/table/sticky-styler.ts index 73ffced0c888..a333b8021ae3 100644 --- a/src/cdk/table/sticky-styler.ts +++ b/src/cdk/table/sticky-styler.ts @@ -11,6 +11,7 @@ * @docs-private */ import {Direction} from '@angular/cdk/bidi'; +import {CoalescedStyleScheduler} from './coalesced-style-scheduler'; export type StickyDirection = 'top' | 'bottom' | 'left' | 'right'; @@ -37,6 +38,7 @@ export class StickyStyler { constructor(private _isNativeHtmlTable: boolean, private _stickCellCss: string, public direction: Direction, + private _coalescedStyleScheduler: CoalescedStyleScheduler, private _isBrowser = true) { } /** @@ -46,6 +48,7 @@ export class StickyStyler { * @param stickyDirections The directions that should no longer be set as sticky on the rows. */ clearStickyPositioning(rows: HTMLElement[], stickyDirections: StickyDirection[]) { + const elementsToClear: HTMLElement[] = []; for (const row of rows) { // If the row isn't an element (e.g. if it's an `ng-container`), // it won't have inline styles or `children` so we skip it. @@ -53,13 +56,17 @@ export class StickyStyler { continue; } - this._removeStickyStyle(row, stickyDirections); - + elementsToClear.push(row); for (let i = 0; i < row.children.length; i++) { - const cell = row.children[i] as HTMLElement; - this._removeStickyStyle(cell, stickyDirections); + elementsToClear.push(row.children[i] as HTMLElement); } } + + this._coalescedStyleScheduler.schedule(() => { + for (const element of elementsToClear) { + this._removeStickyStyle(element, stickyDirections); + } + }); } /** @@ -73,9 +80,8 @@ export class StickyStyler { */ updateStickyColumns( rows: HTMLElement[], stickyStartStates: boolean[], stickyEndStates: boolean[]) { - const hasStickyColumns = - stickyStartStates.some(state => state) || stickyEndStates.some(state => state); - if (!rows.length || !hasStickyColumns || !this._isBrowser) { + if (!rows.length || !this._isBrowser || !(stickyStartStates.some(state => state) || + stickyEndStates.some(state => state))) { return; } @@ -85,20 +91,25 @@ export class StickyStyler { const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates); const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates); - const isRtl = this.direction === 'rtl'; - - for (const row of rows) { - for (let i = 0; i < numCells; i++) { - const cell = row.children[i] as HTMLElement; - if (stickyStartStates[i]) { - this._addStickyStyle(cell, isRtl ? 'right' : 'left', startPositions[i]); - } - if (stickyEndStates[i]) { - this._addStickyStyle(cell, isRtl ? 'left' : 'right', endPositions[i]); + this._coalescedStyleScheduler.schedule(() => { + const isRtl = this.direction === 'rtl'; + const start = isRtl ? 'right' : 'left'; + const end = isRtl ? 'left' : 'right'; + + for (const row of rows) { + for (let i = 0; i < numCells; i++) { + const cell = row.children[i] as HTMLElement; + if (stickyStartStates[i]) { + this._addStickyStyle(cell, start, startPositions[i]); + } + + if (stickyEndStates[i]) { + this._addStickyStyle(cell, end, endPositions[i]); + } } } - } + }); } /** @@ -124,30 +135,37 @@ export class StickyStyler { const rows = position === 'bottom' ? rowsToStick.slice().reverse() : rowsToStick; const states = position === 'bottom' ? stickyStates.slice().reverse() : stickyStates; - let stickyHeight = 0; - for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { + // Measure row heights all at once before adding sticky styles to reduce layout thrashing. + const stickyHeights: number[] = []; + const elementsToStick: HTMLElement[][] = []; + for (let rowIndex = 0, stickyHeight = 0; rowIndex < rows.length; rowIndex++) { + stickyHeights[rowIndex] = stickyHeight; + if (!states[rowIndex]) { continue; } const row = rows[rowIndex]; - if (this._isNativeHtmlTable) { - for (let j = 0; j < row.children.length; j++) { - const cell = row.children[j] as HTMLElement; - this._addStickyStyle(cell, position, stickyHeight); - } - } else { - // Flex does not respect the stick positioning on the cells, needs to be applied to the row. - // If this is applied on a native table, Safari causes the header to fly in wrong direction. - this._addStickyStyle(row, position, stickyHeight); - } + elementsToStick[rowIndex] = this._isNativeHtmlTable ? + Array.from(row.children) as HTMLElement[] : [row]; - if (rowIndex === rows.length - 1) { - // prevent unnecessary reflow from getBoundingClientRect() - return; + if (rowIndex !== rows.length - 1) { + stickyHeight += row.getBoundingClientRect().height; } - stickyHeight += row.getBoundingClientRect().height; } + + this._coalescedStyleScheduler.schedule(() => { + for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { + if (!states[rowIndex]) { + continue; + } + + const height = stickyHeights[rowIndex]; + for (const element of elementsToStick[rowIndex]) { + this._addStickyStyle(element, position, height); + } + } + }); } /** @@ -162,11 +180,14 @@ export class StickyStyler { } const tfoot = tableElement.querySelector('tfoot')!; - if (stickyStates.some(state => !state)) { - this._removeStickyStyle(tfoot, ['bottom']); - } else { - this._addStickyStyle(tfoot, 'bottom', 0); - } + + this._coalescedStyleScheduler.schedule(() => { + if (stickyStates.some(state => !state)) { + this._removeStickyStyle(tfoot, ['bottom']); + } else { + this._addStickyStyle(tfoot, 'bottom', 0); + } + }); } /** diff --git a/src/cdk/table/table.spec.ts b/src/cdk/table/table.spec.ts index bc5a447ad723..51f331260fa5 100644 --- a/src/cdk/table/table.spec.ts +++ b/src/cdk/table/table.spec.ts @@ -10,7 +10,7 @@ import { ViewChild, AfterViewInit } from '@angular/core'; -import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing'; +import {ComponentFixture, fakeAsync, flush, flushMicrotasks, TestBed} from '@angular/core/testing'; import {BehaviorSubject, combineLatest, Observable, of as observableOf} from 'rxjs'; import {map} from 'rxjs/operators'; import {CdkColumnDef} from './cell'; @@ -870,9 +870,10 @@ describe('CdkTable', () => { dataRows = getRows(tableElement); }); - it('should stick and unstick headers', () => { + it('should stick and unstick headers', fakeAsync(() => { component.stickyHeaders = ['header-1', 'header-3']; fixture.detectChanges(); + flushMicrotasks(); expectStickyStyles(headerRows[0], '100', {top: '0px'}); expectNoStickyStyles([headerRows[1]]); @@ -881,12 +882,14 @@ describe('CdkTable', () => { component.stickyHeaders = []; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles(headerRows); - }); + })); - it('should stick and unstick footers', () => { + it('should stick and unstick footers', fakeAsync(() => { component.stickyFooters = ['footer-1', 'footer-3']; fixture.detectChanges(); + flushMicrotasks(); expectStickyStyles( footerRows[0], '10', {bottom: footerRows[1].getBoundingClientRect().height + 'px'}); @@ -895,20 +898,23 @@ describe('CdkTable', () => { component.stickyFooters = []; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles(footerRows); - }); + })); - it('should stick the correct footer row', () => { + it('should stick the correct footer row', fakeAsync(() => { component.stickyFooters = ['footer-3']; fixture.detectChanges(); + flushMicrotasks(); expectStickyStyles(footerRows[2], '10', {bottom: '0px'}); expectNoStickyStyles([footerRows[0], footerRows[1]]); - }); + })); - it('should stick and unstick left columns', () => { + it('should stick and unstick left columns', fakeAsync(() => { component.stickyStartColumns = ['column-1', 'column-3']; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => { let cells = getHeaderCells(row); @@ -931,14 +937,16 @@ describe('CdkTable', () => { component.stickyStartColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); dataRows.forEach(row => expectNoStickyStyles(getCells(row))); footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row))); - }); + })); - it('should stick and unstick right columns', () => { + it('should stick and unstick right columns', fakeAsync(() => { component.stickyEndColumns = ['column-4', 'column-6']; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => { let cells = getHeaderCells(row); @@ -961,16 +969,18 @@ describe('CdkTable', () => { component.stickyEndColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); dataRows.forEach(row => expectNoStickyStyles(getCells(row))); footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row))); - }); + })); - it('should reverse directions for sticky columns in rtl', () => { + it('should reverse directions for sticky columns in rtl', fakeAsync(() => { component.dir = 'rtl'; component.stickyStartColumns = ['column-1', 'column-2']; component.stickyEndColumns = ['column-5', 'column-6']; fixture.detectChanges(); + flushMicrotasks(); const firstColumnWidth = getHeaderCells(headerRows[0])[0].getBoundingClientRect().width; const lastColumnWidth = getHeaderCells(headerRows[0])[5].getBoundingClientRect().width; @@ -994,14 +1004,16 @@ describe('CdkTable', () => { expectStickyStyles(footerCells[1], '1', {right: `${firstColumnWidth}px`}); expectStickyStyles(footerCells[4], '1', {left: `${lastColumnWidth}px`}); expectStickyStyles(footerCells[5], '1', {left: '0px'}); - }); + })); - it('should stick and unstick combination of sticky header, footer, and columns', () => { + it('should stick and unstick combination of sticky header, footer, and columns', + fakeAsync(() => { component.stickyHeaders = ['header-1']; component.stickyFooters = ['footer-3']; component.stickyStartColumns = ['column-1']; component.stickyEndColumns = ['column-6']; fixture.detectChanges(); + flushMicrotasks(); let headerCells = getHeaderCells(headerRows[0]); expectStickyStyles(headerRows[0], '100', {top: '0px'}); @@ -1029,11 +1041,12 @@ describe('CdkTable', () => { component.stickyStartColumns = []; component.stickyEndColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles([row, ...getHeaderCells(row)])); dataRows.forEach(row => expectNoStickyStyles([row, ...getCells(row)])); footerRows.forEach(row => expectNoStickyStyles([row, ...getFooterCells(row)])); - }); + })); }); describe('on native table layout', () => { @@ -1049,9 +1062,10 @@ describe('CdkTable', () => { dataRows = getRows(tableElement); }); - it('should stick and unstick headers', () => { + it('should stick and unstick headers', fakeAsync(() => { component.stickyHeaders = ['header-1', 'header-3']; fixture.detectChanges(); + flushMicrotasks(); getHeaderCells(headerRows[0]).forEach(cell => { expectStickyStyles(cell, '100', {top: '0px'}); @@ -1065,13 +1079,15 @@ describe('CdkTable', () => { component.stickyHeaders = []; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles(headerRows); // No sticky styles on rows for native table headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); - }); + })); - it('should stick and unstick footers', () => { + it('should stick and unstick footers', fakeAsync(() => { component.stickyFooters = ['footer-1', 'footer-3']; fixture.detectChanges(); + flushMicrotasks(); getFooterCells(footerRows[2]).forEach(cell => { expectStickyStyles(cell, '10', {bottom: '0px'}); @@ -1085,28 +1101,33 @@ describe('CdkTable', () => { component.stickyFooters = []; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles(footerRows); // No sticky styles on rows for native table footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row))); - }); + })); - it('should stick tfoot when all rows are stuck', () => { + it('should stick tfoot when all rows are stuck', fakeAsync(() => { const tfoot = tableElement.querySelector('tfoot'); component.stickyFooters = ['footer-1']; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles([tfoot]); component.stickyFooters = ['footer-1', 'footer-2', 'footer-3']; fixture.detectChanges(); + flushMicrotasks(); expectStickyStyles(tfoot, '10', {bottom: '0px'}); component.stickyFooters = ['footer-1', 'footer-2']; fixture.detectChanges(); + flushMicrotasks(); expectNoStickyStyles([tfoot]); - }); + })); - it('should stick and unstick left columns', () => { + it('should stick and unstick left columns', fakeAsync(() => { component.stickyStartColumns = ['column-1', 'column-3']; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => { let cells = getHeaderCells(row); @@ -1129,14 +1150,16 @@ describe('CdkTable', () => { component.stickyStartColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); dataRows.forEach(row => expectNoStickyStyles(getCells(row))); footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row))); - }); + })); - it('should stick and unstick right columns', () => { + it('should stick and unstick right columns', fakeAsync(() => { component.stickyEndColumns = ['column-4', 'column-6']; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => { let cells = getHeaderCells(row); @@ -1159,17 +1182,20 @@ describe('CdkTable', () => { component.stickyEndColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles(getHeaderCells(row))); dataRows.forEach(row => expectNoStickyStyles(getCells(row))); footerRows.forEach(row => expectNoStickyStyles(getFooterCells(row))); - }); + })); - it('should stick and unstick combination of sticky header, footer, and columns', () => { + it('should stick and unstick combination of sticky header, footer, and columns', + fakeAsync(() => { component.stickyHeaders = ['header-1']; component.stickyFooters = ['footer-3']; component.stickyStartColumns = ['column-1']; component.stickyEndColumns = ['column-6']; fixture.detectChanges(); + flushMicrotasks(); const headerCells = getHeaderCells(headerRows[0]); expectStickyStyles(headerCells[0], '101', {top: '0px', left: '0px'}); @@ -1201,11 +1227,12 @@ describe('CdkTable', () => { component.stickyStartColumns = []; component.stickyEndColumns = []; fixture.detectChanges(); + flushMicrotasks(); headerRows.forEach(row => expectNoStickyStyles([row, ...getHeaderCells(row)])); dataRows.forEach(row => expectNoStickyStyles([row, ...getCells(row)])); footerRows.forEach(row => expectNoStickyStyles([row, ...getFooterCells(row)])); - }); + })); }); }); diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts index 1dabd49ad286..e4ccdc6170c5 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -48,6 +48,7 @@ import { } from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {CdkColumnDef} from './cell'; +import {CoalescedStyleScheduler} from './coalesced-style-scheduler'; import { BaseRowDef, CdkCellOutlet, @@ -186,7 +187,10 @@ export interface RenderRow { // declared elsewhere, they are checked when their declaration points are checked. // tslint:disable-next-line:validate-decorators changeDetection: ChangeDetectionStrategy.Default, - providers: [{provide: CDK_TABLE, useExisting: CdkTable}] + providers: [ + {provide: CDK_TABLE, useExisting: CdkTable}, + CoalescedStyleScheduler, + ] }) export class CdkTable implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit { private _document: Document; @@ -376,6 +380,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes // this setter will be invoked before the row outlet has been defined hence the null check. if (this._rowOutlet && this._rowOutlet.viewContainer.length) { this._forceRenderDataRows(); + this.updateStickyColumnStyles(); } } _multiTemplateDataRows: boolean = false; @@ -422,6 +427,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes constructor( protected readonly _differs: IterableDiffers, protected readonly _changeDetectorRef: ChangeDetectorRef, + protected readonly _coalescedStyleScheduler: CoalescedStyleScheduler, protected readonly _elementRef: ElementRef, @Attribute('role') role: string, @Optional() protected readonly _dir: Directionality, @Inject(DOCUMENT) _document: any, private _platform: Platform) { @@ -461,22 +467,28 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes // Render updates if the list of columns have been changed for the header, row, or footer defs. this._renderUpdatedColumns(); + let forced = false; + // If the header row definition has been changed, trigger a render to the header row. if (this._headerRowDefChanged) { this._forceRenderHeaderRows(); this._headerRowDefChanged = false; + forced = true; } // If the footer row definition has been changed, trigger a render to the footer row. if (this._footerRowDefChanged) { this._forceRenderFooterRows(); this._footerRowDefChanged = false; + forced = true; } // If there is a data source and row definitions, connect to the data source unless a // connection has already been made. if (this.dataSource && this._rowDefs.length > 0 && !this._renderChangeSubscription) { this._observeRenderChanges(); + } else if (forced) { + this.updateStickyColumnStyles(); } this._checkStickyStates(); @@ -785,19 +797,27 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes */ private _renderUpdatedColumns() { const columnsDiffReducer = (acc: boolean, def: BaseRowDef) => acc || !!def.getColumnsDiff(); + let forced = false; // Force re-render data rows if the list of column definitions have changed. if (this._rowDefs.reduce(columnsDiffReducer, false)) { this._forceRenderDataRows(); + forced = true; } // Force re-render header/footer rows if the list of column definitions have changed.. if (this._headerRowDefs.reduce(columnsDiffReducer, false)) { this._forceRenderHeaderRows(); + forced = true; } if (this._footerRowDefs.reduce(columnsDiffReducer, false)) { this._forceRenderFooterRows(); + forced = true; + } + + if (forced) { + this.updateStickyColumnStyles(); } } @@ -868,7 +888,6 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes this._headerRowDefs.forEach((def, i) => this._renderRow(this._headerRowOutlet, def, i)); this.updateStickyHeaderRowStyles(); - this.updateStickyColumnStyles(); } /** * Clears any existing content in the footer row outlet and creates a new embedded view @@ -882,7 +901,6 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes this._footerRowDefs.forEach((def, i) => this._renderRow(this._footerRowOutlet, def, i)); this.updateStickyFooterRowStyles(); - this.updateStickyColumnStyles(); } /** Adds the sticky column styles for the rows according to the columns' stick states. */ @@ -1042,7 +1060,6 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes this._dataDiffer.diff([]); this._rowOutlet.viewContainer.clear(); this.renderRows(); - this.updateStickyColumnStyles(); } /** @@ -1080,7 +1097,8 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes private _setupStickyStyler() { const direction: Direction = this._dir ? this._dir.value : 'ltr'; this._stickyStyler = new StickyStyler( - this._isNativeHtmlTable, this.stickyCssClass, direction, this._platform.isBrowser); + this._isNativeHtmlTable, this.stickyCssClass, direction, this._coalescedStyleScheduler, + this._platform.isBrowser); (this._dir ? this._dir.change : observableOf()) .pipe(takeUntil(this._onDestroy)) .subscribe(value => { diff --git a/src/material/table/table.spec.ts b/src/material/table/table.spec.ts index a64d1b7dc0d0..51a1a3a51555 100644 --- a/src/material/table/table.spec.ts +++ b/src/material/table/table.spec.ts @@ -186,13 +186,14 @@ describe('MatTable', () => { ]); }); - it('should apply custom sticky CSS class to sticky cells', () => { + it('should apply custom sticky CSS class to sticky cells', fakeAsync(() => { let fixture = TestBed.createComponent(StickyTableApp); fixture.detectChanges(); + flushMicrotasks(); const stuckCellElement = fixture.nativeElement.querySelector('.mat-table th')!; expect(stuckCellElement.classList).toContain('mat-table-sticky'); - }); + })); // Note: needs to be fakeAsync so it catches the error. it('should not throw when a row definition is on an ng-container', fakeAsync(() => { diff --git a/src/material/table/table.ts b/src/material/table/table.ts index 921b121f1c68..8bc750dea201 100644 --- a/src/material/table/table.ts +++ b/src/material/table/table.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CDK_TABLE_TEMPLATE, CdkTable, CDK_TABLE} from '@angular/cdk/table'; +import {CDK_TABLE_TEMPLATE, CdkTable, CDK_TABLE, CoalescedStyleScheduler} from '@angular/cdk/table'; import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; /** @@ -22,7 +22,8 @@ import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/co }, providers: [ {provide: CdkTable, useExisting: MatTable}, - {provide: CDK_TABLE, useExisting: MatTable} + {provide: CDK_TABLE, useExisting: MatTable}, + CoalescedStyleScheduler, ], encapsulation: ViewEncapsulation.None, // See note on CdkTable for explanation on why this uses the default change detection strategy. diff --git a/tools/public_api_guard/cdk/table.d.ts b/tools/public_api_guard/cdk/table.d.ts index ab08f3d488ed..d5a0d038a0c3 100644 --- a/tools/public_api_guard/cdk/table.d.ts +++ b/tools/public_api_guard/cdk/table.d.ts @@ -169,6 +169,7 @@ export declare class CdkRowDef extends BaseRowDef { export declare class CdkTable implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit { protected readonly _changeDetectorRef: ChangeDetectorRef; + protected readonly _coalescedStyleScheduler: CoalescedStyleScheduler; _contentColumnDefs: QueryList; _contentFooterRowDefs: QueryList; _contentHeaderRowDefs: QueryList; @@ -194,7 +195,7 @@ export declare class CdkTable implements AfterContentChecked, CollectionViewe start: number; end: number; }>; - constructor(_differs: IterableDiffers, _changeDetectorRef: ChangeDetectorRef, _elementRef: ElementRef, role: string, _dir: Directionality, _document: any, _platform: Platform); + constructor(_differs: IterableDiffers, _changeDetectorRef: ChangeDetectorRef, _coalescedStyleScheduler: CoalescedStyleScheduler, _elementRef: ElementRef, role: string, _dir: Directionality, _document: any, _platform: Platform); _getRenderedRows(rowOutlet: RowOutlet): HTMLElement[]; _getRowDefs(data: T, dataIndex: number): CdkRowDef[]; addColumnDef(columnDef: CdkColumnDef): void; @@ -214,7 +215,7 @@ export declare class CdkTable implements AfterContentChecked, CollectionViewe updateStickyHeaderRowStyles(): void; static ngAcceptInputType_multiTemplateDataRows: BooleanInput; static ɵcmp: i0.ɵɵComponentDefWithMeta, "cdk-table, table[cdk-table]", ["cdkTable"], { "trackBy": "trackBy"; "dataSource": "dataSource"; "multiTemplateDataRows": "multiTemplateDataRows"; }, {}, ["_noDataRow", "_contentColumnDefs", "_contentRowDefs", "_contentHeaderRowDefs", "_contentFooterRowDefs"], ["caption", "colgroup, col"]>; - static ɵfac: i0.ɵɵFactoryDef, [null, null, null, { attribute: "role"; }, { optional: true; }, null, null]>; + static ɵfac: i0.ɵɵFactoryDef, [null, null, null, null, { attribute: "role"; }, { optional: true; }, null, null]>; } export declare class CdkTableModule { @@ -244,6 +245,13 @@ export interface CellDef { template: TemplateRef; } +export declare class CoalescedStyleScheduler { + constructor(_ngZone: NgZone); + schedule(task: () => unknown): void; + static ɵfac: i0.ɵɵFactoryDef; + static ɵprov: i0.ɵɵInjectableDef; +} + export declare type Constructor = new (...args: any[]) => T; export declare class DataRowOutlet implements RowOutlet { @@ -299,7 +307,7 @@ export declare type StickyDirection = 'top' | 'bottom' | 'left' | 'right'; export declare class StickyStyler { direction: Direction; - constructor(_isNativeHtmlTable: boolean, _stickCellCss: string, direction: Direction, _isBrowser?: boolean); + constructor(_isNativeHtmlTable: boolean, _stickCellCss: string, direction: Direction, _coalescedStyleScheduler: CoalescedStyleScheduler, _isBrowser?: boolean); _addStickyStyle(element: HTMLElement, dir: StickyDirection, dirValue: number): void; _getCalculatedZIndex(element: HTMLElement): string; _getCellWidths(row: HTMLElement): number[]; From b79c2b790eb73596ad6caefae30162a871cbabb3 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Thu, 25 Jun 2020 10:20:01 -0400 Subject: [PATCH 02/18] Add license --- src/cdk/table/coalesced-style-scheduler.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/cdk/table/coalesced-style-scheduler.ts b/src/cdk/table/coalesced-style-scheduler.ts index eaa096867634..ca61a3eea576 100644 --- a/src/cdk/table/coalesced-style-scheduler.ts +++ b/src/cdk/table/coalesced-style-scheduler.ts @@ -1,3 +1,11 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + import {Injectable, NgZone} from '@angular/core'; import {from, Observable} from 'rxjs'; From 1161834ae002411a649a3385dd86330d35aeb7d2 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Thu, 25 Jun 2020 10:47:01 -0400 Subject: [PATCH 03/18] Fix column resize tests --- .../column-resize/column-resize.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/material-experimental/column-resize/column-resize.spec.ts b/src/material-experimental/column-resize/column-resize.spec.ts index 0cdbbffbd967..b6538be3d05f 100644 --- a/src/material-experimental/column-resize/column-resize.spec.ts +++ b/src/material-experimental/column-resize/column-resize.spec.ts @@ -6,7 +6,7 @@ import { ViewChild, ChangeDetectionStrategy, } from '@angular/core'; -import {ComponentFixture, TestBed, fakeAsync, inject} from '@angular/core/testing'; +import {ComponentFixture, TestBed, fakeAsync, flushMicrotasks, inject} from '@angular/core/testing'; import {BidiModule} from '@angular/cdk/bidi'; import {DataSource} from '@angular/cdk/collections'; import {dispatchKeyboardEvent} from '@angular/cdk/testing/private'; @@ -347,7 +347,7 @@ describe('Material Popover Edit', () => { let fixture: ComponentFixture; let overlayContainer: OverlayContainer; - beforeEach(() => { + beforeEach(fakeAsync(() => { jasmine.addMatchers(approximateMatcher); TestBed.configureTestingModule({ @@ -360,7 +360,8 @@ describe('Material Popover Edit', () => { fixture = TestBed.createComponent(componentClass); component = fixture.componentInstance; fixture.detectChanges(); - }); + flushMicrotasks(); + })); afterEach(() => { // The overlay container's `ngOnDestroy` won't be called between test runs so we need From a050e413fba8904665b508f5f529142c710b9427 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Thu, 25 Jun 2020 11:06:03 -0400 Subject: [PATCH 04/18] Fixed mdc-table tests --- src/material-experimental/mdc-table/table.spec.ts | 5 +++-- src/material-experimental/mdc-table/table.ts | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/material-experimental/mdc-table/table.spec.ts b/src/material-experimental/mdc-table/table.spec.ts index 8a95ab53574c..d9d617fc703b 100644 --- a/src/material-experimental/mdc-table/table.spec.ts +++ b/src/material-experimental/mdc-table/table.spec.ts @@ -147,13 +147,14 @@ describe('MDC-based MatTable', () => { ]); }); - it('should apply custom sticky CSS class to sticky cells', () => { + it('should apply custom sticky CSS class to sticky cells', fakeAsync(() => { let fixture = TestBed.createComponent(StickyTableApp); fixture.detectChanges(); + flushMicrotasks(); const stuckCellElement = fixture.nativeElement.querySelector('table th')!; expect(stuckCellElement.classList).toContain('mat-mdc-table-sticky'); - }); + })); // Note: needs to be fakeAsync so it catches the error. it('should not throw when a row definition is on an ng-container', fakeAsync(() => { diff --git a/src/material-experimental/mdc-table/table.ts b/src/material-experimental/mdc-table/table.ts index daeb200e8e84..17c9e4fea815 100644 --- a/src/material-experimental/mdc-table/table.ts +++ b/src/material-experimental/mdc-table/table.ts @@ -7,7 +7,7 @@ */ import {ChangeDetectionStrategy, Component, OnInit, ViewEncapsulation} from '@angular/core'; -import {CDK_TABLE_TEMPLATE, CdkTable} from '@angular/cdk/table'; +import {CDK_TABLE_TEMPLATE, CdkTable, CoalescedStyleScheduler} from '@angular/cdk/table'; @Component({ selector: 'table[mat-table]', @@ -17,7 +17,10 @@ import {CDK_TABLE_TEMPLATE, CdkTable} from '@angular/cdk/table'; host: { 'class': 'mat-mdc-table mdc-data-table__table', }, - providers: [{provide: CdkTable, useExisting: MatTable}], + providers: [ + {provide: CdkTable, useExisting: MatTable}, + CoalescedStyleScheduler, + ], encapsulation: ViewEncapsulation.None, // See note on CdkTable for explanation on why this uses the default change detection strategy. // tslint:disable-next-line:validate-decorators From c386bdcca889b6e97b448e883453c9badba61872 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Fri, 26 Jun 2020 16:44:25 -0400 Subject: [PATCH 05/18] jsdoc --- src/cdk/table/coalesced-style-scheduler.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/cdk/table/coalesced-style-scheduler.ts b/src/cdk/table/coalesced-style-scheduler.ts index ca61a3eea576..8e409daff1a1 100644 --- a/src/cdk/table/coalesced-style-scheduler.ts +++ b/src/cdk/table/coalesced-style-scheduler.ts @@ -9,12 +9,22 @@ import {Injectable, NgZone} from '@angular/core'; import {from, Observable} from 'rxjs'; +/** + * Allows grouping up CSSDom mutations after the current execution context. + * This can significantly improve performance when separate consecutive functions are + * reading from the CSSDom and then mutating it. + * + * @docs-private + */ @Injectable() export class CoalescedStyleScheduler { private _currentSchedule: Observable|null = null; constructor(private readonly _ngZone: NgZone) {} + /** + * Schedules the specified task to run after the current microtask. + */ schedule(task: () => unknown): void { this._createScheduleIfNeeded(); From 68f7eae4a64e05f832fef0d68a1b2649b76c3679 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Fri, 26 Jun 2020 16:52:49 -0400 Subject: [PATCH 06/18] more comments --- src/cdk/table/sticky-styler.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cdk/table/sticky-styler.ts b/src/cdk/table/sticky-styler.ts index a333b8021ae3..8ecb8a743260 100644 --- a/src/cdk/table/sticky-styler.ts +++ b/src/cdk/table/sticky-styler.ts @@ -62,6 +62,7 @@ export class StickyStyler { } } + // Coalesce with sticky row/column updates (and potentially other changes like column resize). this._coalescedStyleScheduler.schedule(() => { for (const element of elementsToClear) { this._removeStickyStyle(element, stickyDirections); @@ -92,6 +93,7 @@ export class StickyStyler { const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates); const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates); + // Coalesce with sticky row updates (and potentially other changes like column resize). this._coalescedStyleScheduler.schedule(() => { const isRtl = this.direction === 'rtl'; const start = isRtl ? 'right' : 'left'; @@ -154,6 +156,7 @@ export class StickyStyler { } } + // Coalesce with other sticky row updates (top/bottom), sticky columns updates (and potentially other changes like column resize). this._coalescedStyleScheduler.schedule(() => { for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { if (!states[rowIndex]) { @@ -181,6 +184,7 @@ export class StickyStyler { const tfoot = tableElement.querySelector('tfoot')!; + // Coalesce with other sticky updates (and potentially other changes like column resize). this._coalescedStyleScheduler.schedule(() => { if (stickyStates.some(state => !state)) { this._removeStickyStyle(tfoot, ['bottom']); From b2c05b37ec5b3670355a2763d4a4f87708c8f0a0 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Fri, 26 Jun 2020 16:57:05 -0400 Subject: [PATCH 07/18] lint --- src/cdk/table/sticky-styler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cdk/table/sticky-styler.ts b/src/cdk/table/sticky-styler.ts index 8ecb8a743260..928415a77745 100644 --- a/src/cdk/table/sticky-styler.ts +++ b/src/cdk/table/sticky-styler.ts @@ -156,7 +156,8 @@ export class StickyStyler { } } - // Coalesce with other sticky row updates (top/bottom), sticky columns updates (and potentially other changes like column resize). + // Coalesce with other sticky row updates (top/bottom), sticky columns updates + //(and potentially other changes like column resize). this._coalescedStyleScheduler.schedule(() => { for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { if (!states[rowIndex]) { From af10e1ff7d09819fa79df6a506f5d616a693f646 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Fri, 26 Jun 2020 22:59:14 -0400 Subject: [PATCH 08/18] lint --- src/cdk/table/sticky-styler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cdk/table/sticky-styler.ts b/src/cdk/table/sticky-styler.ts index 928415a77745..09cb9285b32a 100644 --- a/src/cdk/table/sticky-styler.ts +++ b/src/cdk/table/sticky-styler.ts @@ -157,7 +157,7 @@ export class StickyStyler { } // Coalesce with other sticky row updates (top/bottom), sticky columns updates - //(and potentially other changes like column resize). + // (and potentially other changes like column resize). this._coalescedStyleScheduler.schedule(() => { for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { if (!states[rowIndex]) { From 45d856594d5fba35a1ee829c2b064b28efadacd6 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Wed, 1 Jul 2020 10:30:04 -0400 Subject: [PATCH 09/18] Added _ --- src/cdk/table/coalesced-style-scheduler.ts | 3 ++- src/cdk/table/sticky-styler.ts | 4 ++-- src/cdk/table/table.ts | 6 +++--- src/material-experimental/mdc-table/table.ts | 4 ++-- src/material/table/table.ts | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/cdk/table/coalesced-style-scheduler.ts b/src/cdk/table/coalesced-style-scheduler.ts index 8e409daff1a1..cf26c370bb92 100644 --- a/src/cdk/table/coalesced-style-scheduler.ts +++ b/src/cdk/table/coalesced-style-scheduler.ts @@ -17,7 +17,8 @@ import {from, Observable} from 'rxjs'; * @docs-private */ @Injectable() -export class CoalescedStyleScheduler { +// tslint:disable-next-line:class-name +export class _CoalescedStyleScheduler { private _currentSchedule: Observable|null = null; constructor(private readonly _ngZone: NgZone) {} diff --git a/src/cdk/table/sticky-styler.ts b/src/cdk/table/sticky-styler.ts index 09cb9285b32a..afa44787e417 100644 --- a/src/cdk/table/sticky-styler.ts +++ b/src/cdk/table/sticky-styler.ts @@ -11,7 +11,7 @@ * @docs-private */ import {Direction} from '@angular/cdk/bidi'; -import {CoalescedStyleScheduler} from './coalesced-style-scheduler'; +import {_CoalescedStyleScheduler} from './coalesced-style-scheduler'; export type StickyDirection = 'top' | 'bottom' | 'left' | 'right'; @@ -38,7 +38,7 @@ export class StickyStyler { constructor(private _isNativeHtmlTable: boolean, private _stickCellCss: string, public direction: Direction, - private _coalescedStyleScheduler: CoalescedStyleScheduler, + private _coalescedStyleScheduler: _CoalescedStyleScheduler, private _isBrowser = true) { } /** diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts index e4ccdc6170c5..865238305477 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -48,7 +48,7 @@ import { } from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {CdkColumnDef} from './cell'; -import {CoalescedStyleScheduler} from './coalesced-style-scheduler'; +import {_CoalescedStyleScheduler} from './coalesced-style-scheduler'; import { BaseRowDef, CdkCellOutlet, @@ -189,7 +189,7 @@ export interface RenderRow { changeDetection: ChangeDetectionStrategy.Default, providers: [ {provide: CDK_TABLE, useExisting: CdkTable}, - CoalescedStyleScheduler, + _CoalescedStyleScheduler, ] }) export class CdkTable implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit { @@ -427,7 +427,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes constructor( protected readonly _differs: IterableDiffers, protected readonly _changeDetectorRef: ChangeDetectorRef, - protected readonly _coalescedStyleScheduler: CoalescedStyleScheduler, + protected readonly _coalescedStyleScheduler: _CoalescedStyleScheduler, protected readonly _elementRef: ElementRef, @Attribute('role') role: string, @Optional() protected readonly _dir: Directionality, @Inject(DOCUMENT) _document: any, private _platform: Platform) { diff --git a/src/material-experimental/mdc-table/table.ts b/src/material-experimental/mdc-table/table.ts index 17c9e4fea815..1e5ae437a5a8 100644 --- a/src/material-experimental/mdc-table/table.ts +++ b/src/material-experimental/mdc-table/table.ts @@ -7,7 +7,7 @@ */ import {ChangeDetectionStrategy, Component, OnInit, ViewEncapsulation} from '@angular/core'; -import {CDK_TABLE_TEMPLATE, CdkTable, CoalescedStyleScheduler} from '@angular/cdk/table'; +import {CDK_TABLE_TEMPLATE, CdkTable, _CoalescedStyleScheduler} from '@angular/cdk/table'; @Component({ selector: 'table[mat-table]', @@ -19,7 +19,7 @@ import {CDK_TABLE_TEMPLATE, CdkTable, CoalescedStyleScheduler} from '@angular/cd }, providers: [ {provide: CdkTable, useExisting: MatTable}, - CoalescedStyleScheduler, + _CoalescedStyleScheduler, ], encapsulation: ViewEncapsulation.None, // See note on CdkTable for explanation on why this uses the default change detection strategy. diff --git a/src/material/table/table.ts b/src/material/table/table.ts index 8bc750dea201..7092d969d392 100644 --- a/src/material/table/table.ts +++ b/src/material/table/table.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CDK_TABLE_TEMPLATE, CdkTable, CDK_TABLE, CoalescedStyleScheduler} from '@angular/cdk/table'; +import {CDK_TABLE_TEMPLATE, CdkTable, CDK_TABLE, _CoalescedStyleScheduler} from '@angular/cdk/table'; import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; /** @@ -23,7 +23,7 @@ import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/co providers: [ {provide: CdkTable, useExisting: MatTable}, {provide: CDK_TABLE, useExisting: MatTable}, - CoalescedStyleScheduler, + _CoalescedStyleScheduler, ], encapsulation: ViewEncapsulation.None, // See note on CdkTable for explanation on why this uses the default change detection strategy. From 48d6bf5d5c9fc3d0761b176099940e5bea3cfce0 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Wed, 1 Jul 2020 10:31:54 -0400 Subject: [PATCH 10/18] lint --- src/material/table/table.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/material/table/table.ts b/src/material/table/table.ts index 7092d969d392..f86f0eae9ea8 100644 --- a/src/material/table/table.ts +++ b/src/material/table/table.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {CDK_TABLE_TEMPLATE, CdkTable, CDK_TABLE, _CoalescedStyleScheduler} from '@angular/cdk/table'; +import { + CDK_TABLE_TEMPLATE, + CdkTable, + CDK_TABLE, + _CoalescedStyleScheduler +} from '@angular/cdk/table'; import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; /** From 06dd6b255a447904c68499a7eba2315cf6e42a96 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Wed, 1 Jul 2020 14:12:30 -0400 Subject: [PATCH 11/18] -override --- src/cdk/table/coalesced-style-scheduler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cdk/table/coalesced-style-scheduler.ts b/src/cdk/table/coalesced-style-scheduler.ts index cf26c370bb92..720f63d15c21 100644 --- a/src/cdk/table/coalesced-style-scheduler.ts +++ b/src/cdk/table/coalesced-style-scheduler.ts @@ -17,7 +17,6 @@ import {from, Observable} from 'rxjs'; * @docs-private */ @Injectable() -// tslint:disable-next-line:class-name export class _CoalescedStyleScheduler { private _currentSchedule: Observable|null = null; From 498ff4ca01feca9758f40a8cefadbf21aa04520f Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Wed, 1 Jul 2020 14:26:28 -0400 Subject: [PATCH 12/18] update api --- tools/public_api_guard/cdk/table.d.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tools/public_api_guard/cdk/table.d.ts b/tools/public_api_guard/cdk/table.d.ts index d5a0d038a0c3..661e9e0ba3bc 100644 --- a/tools/public_api_guard/cdk/table.d.ts +++ b/tools/public_api_guard/cdk/table.d.ts @@ -1,3 +1,10 @@ +export declare class _CoalescedStyleScheduler { + constructor(_ngZone: NgZone); + schedule(task: () => unknown): void; + static ɵfac: i0.ɵɵFactoryDef<_CoalescedStyleScheduler, never>; + static ɵprov: i0.ɵɵInjectableDef<_CoalescedStyleScheduler>; +} + export declare class BaseCdkCell { constructor(columnDef: CdkColumnDef, elementRef: ElementRef); } @@ -169,7 +176,7 @@ export declare class CdkRowDef extends BaseRowDef { export declare class CdkTable implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit { protected readonly _changeDetectorRef: ChangeDetectorRef; - protected readonly _coalescedStyleScheduler: CoalescedStyleScheduler; + protected readonly _coalescedStyleScheduler: _CoalescedStyleScheduler; _contentColumnDefs: QueryList; _contentFooterRowDefs: QueryList; _contentHeaderRowDefs: QueryList; @@ -195,7 +202,7 @@ export declare class CdkTable implements AfterContentChecked, CollectionViewe start: number; end: number; }>; - constructor(_differs: IterableDiffers, _changeDetectorRef: ChangeDetectorRef, _coalescedStyleScheduler: CoalescedStyleScheduler, _elementRef: ElementRef, role: string, _dir: Directionality, _document: any, _platform: Platform); + constructor(_differs: IterableDiffers, _changeDetectorRef: ChangeDetectorRef, _coalescedStyleScheduler: _CoalescedStyleScheduler, _elementRef: ElementRef, role: string, _dir: Directionality, _document: any, _platform: Platform); _getRenderedRows(rowOutlet: RowOutlet): HTMLElement[]; _getRowDefs(data: T, dataIndex: number): CdkRowDef[]; addColumnDef(columnDef: CdkColumnDef): void; @@ -245,13 +252,6 @@ export interface CellDef { template: TemplateRef; } -export declare class CoalescedStyleScheduler { - constructor(_ngZone: NgZone); - schedule(task: () => unknown): void; - static ɵfac: i0.ɵɵFactoryDef; - static ɵprov: i0.ɵɵInjectableDef; -} - export declare type Constructor = new (...args: any[]) => T; export declare class DataRowOutlet implements RowOutlet { @@ -307,7 +307,7 @@ export declare type StickyDirection = 'top' | 'bottom' | 'left' | 'right'; export declare class StickyStyler { direction: Direction; - constructor(_isNativeHtmlTable: boolean, _stickCellCss: string, direction: Direction, _coalescedStyleScheduler: CoalescedStyleScheduler, _isBrowser?: boolean); + constructor(_isNativeHtmlTable: boolean, _stickCellCss: string, direction: Direction, _coalescedStyleScheduler: _CoalescedStyleScheduler, _isBrowser?: boolean); _addStickyStyle(element: HTMLElement, dir: StickyDirection, dirValue: number): void; _getCalculatedZIndex(element: HTMLElement): string; _getCellWidths(row: HTMLElement): number[]; From 1c6064eea9819c818ce86e5ad4e558429f4ccadd Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Wed, 22 Jul 2020 11:27:12 -0400 Subject: [PATCH 13/18] prevent resource leaks --- src/cdk/table/coalesced-style-scheduler.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/cdk/table/coalesced-style-scheduler.ts b/src/cdk/table/coalesced-style-scheduler.ts index 720f63d15c21..cd1c848055c9 100644 --- a/src/cdk/table/coalesced-style-scheduler.ts +++ b/src/cdk/table/coalesced-style-scheduler.ts @@ -6,8 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, NgZone} from '@angular/core'; -import {from, Observable} from 'rxjs'; +import {Injectable, NgZone, OnDestroy} from '@angular/core'; +import {from, Observable, Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; /** * Allows grouping up CSSDom mutations after the current execution context. @@ -17,8 +18,9 @@ import {from, Observable} from 'rxjs'; * @docs-private */ @Injectable() -export class _CoalescedStyleScheduler { +export class _CoalescedStyleScheduler implements OnDestroy { private _currentSchedule: Observable|null = null; + private _destroyed = new Subject(); constructor(private readonly _ngZone: NgZone) {} @@ -31,6 +33,14 @@ export class _CoalescedStyleScheduler { this._currentSchedule!.subscribe(task); } + /** Cancel and prevent new subscriptions. */ + ngOnDestroy() { + this._destroyed.next(); + this._destroyed.complete(); + + this._currentSchedule = this._destroyed; + } + private _createScheduleIfNeeded() { if (this._currentSchedule) { return; } @@ -38,7 +48,7 @@ export class _CoalescedStyleScheduler { this._currentSchedule = from(new Promise((resolve) => { this._currentSchedule = null; resolve(undefined); - })); + })).pipe(takeUntil(this._destroyed)); }); } } From 3f25d3c6ac7d5d5457b7eb452a4ad03e93135e6f Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Wed, 22 Jul 2020 11:28:37 -0400 Subject: [PATCH 14/18] api --- tools/public_api_guard/cdk/table.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/public_api_guard/cdk/table.d.ts b/tools/public_api_guard/cdk/table.d.ts index 661e9e0ba3bc..15f11535dc79 100644 --- a/tools/public_api_guard/cdk/table.d.ts +++ b/tools/public_api_guard/cdk/table.d.ts @@ -1,5 +1,6 @@ -export declare class _CoalescedStyleScheduler { +export declare class _CoalescedStyleScheduler implements OnDestroy { constructor(_ngZone: NgZone); + ngOnDestroy(): void; schedule(task: () => unknown): void; static ɵfac: i0.ɵɵFactoryDef<_CoalescedStyleScheduler, never>; static ɵprov: i0.ɵɵInjectableDef<_CoalescedStyleScheduler>; From 42478d4d637a40573a7634fa8480ead6febcf369 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Wed, 22 Jul 2020 11:36:48 -0400 Subject: [PATCH 15/18] readonly --- src/cdk/table/coalesced-style-scheduler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cdk/table/coalesced-style-scheduler.ts b/src/cdk/table/coalesced-style-scheduler.ts index cd1c848055c9..091ec02f0260 100644 --- a/src/cdk/table/coalesced-style-scheduler.ts +++ b/src/cdk/table/coalesced-style-scheduler.ts @@ -20,7 +20,7 @@ import {takeUntil} from 'rxjs/operators'; @Injectable() export class _CoalescedStyleScheduler implements OnDestroy { private _currentSchedule: Observable|null = null; - private _destroyed = new Subject(); + private readonly _destroyed = new Subject(); constructor(private readonly _ngZone: NgZone) {} From ae4ab431f0d6f255b115a1ba341dda2e2ac5030a Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Wed, 22 Jul 2020 12:02:32 -0400 Subject: [PATCH 16/18] remove changes that are part of #19739 --- src/cdk/table/table.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts index 865238305477..df10a67bad15 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -467,28 +467,22 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes // Render updates if the list of columns have been changed for the header, row, or footer defs. this._renderUpdatedColumns(); - let forced = false; - // If the header row definition has been changed, trigger a render to the header row. if (this._headerRowDefChanged) { this._forceRenderHeaderRows(); this._headerRowDefChanged = false; - forced = true; } // If the footer row definition has been changed, trigger a render to the footer row. if (this._footerRowDefChanged) { this._forceRenderFooterRows(); this._footerRowDefChanged = false; - forced = true; } // If there is a data source and row definitions, connect to the data source unless a // connection has already been made. if (this.dataSource && this._rowDefs.length > 0 && !this._renderChangeSubscription) { this._observeRenderChanges(); - } else if (forced) { - this.updateStickyColumnStyles(); } this._checkStickyStates(); @@ -797,27 +791,19 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes */ private _renderUpdatedColumns() { const columnsDiffReducer = (acc: boolean, def: BaseRowDef) => acc || !!def.getColumnsDiff(); - let forced = false; // Force re-render data rows if the list of column definitions have changed. if (this._rowDefs.reduce(columnsDiffReducer, false)) { this._forceRenderDataRows(); - forced = true; } // Force re-render header/footer rows if the list of column definitions have changed.. if (this._headerRowDefs.reduce(columnsDiffReducer, false)) { this._forceRenderHeaderRows(); - forced = true; } if (this._footerRowDefs.reduce(columnsDiffReducer, false)) { this._forceRenderFooterRows(); - forced = true; - } - - if (forced) { - this.updateStickyColumnStyles(); } } @@ -888,6 +874,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes this._headerRowDefs.forEach((def, i) => this._renderRow(this._headerRowOutlet, def, i)); this.updateStickyHeaderRowStyles(); + this.updateStickyColumnStyles(); } /** * Clears any existing content in the footer row outlet and creates a new embedded view @@ -901,6 +888,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes this._footerRowDefs.forEach((def, i) => this._renderRow(this._footerRowOutlet, def, i)); this.updateStickyFooterRowStyles(); + this.updateStickyColumnStyles(); } /** Adds the sticky column styles for the rows according to the columns' stick states. */ @@ -1060,6 +1048,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes this._dataDiffer.diff([]); this._rowOutlet.viewContainer.clear(); this.renderRows(); + this.updateStickyColumnStyles(); } /** From 0685b57b5eae003f231602076018a2d19169e71d Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Thu, 23 Jul 2020 17:45:57 -0400 Subject: [PATCH 17/18] Change to onStable to work around downstream test failures --- src/cdk/table/coalesced-style-scheduler.ts | 52 ++++++++++++++++------ 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/src/cdk/table/coalesced-style-scheduler.ts b/src/cdk/table/coalesced-style-scheduler.ts index 091ec02f0260..5e8f985009ac 100644 --- a/src/cdk/table/coalesced-style-scheduler.ts +++ b/src/cdk/table/coalesced-style-scheduler.ts @@ -7,8 +7,16 @@ */ import {Injectable, NgZone, OnDestroy} from '@angular/core'; -import {from, Observable, Subject} from 'rxjs'; -import {takeUntil} from 'rxjs/operators'; +import {Subject} from 'rxjs'; +import {take, takeUntil} from 'rxjs/operators'; + +/** + * @docs-private + */ +export class _Schedule { + tasks: (() => unknown)[] = []; + endTasks: (() => unknown)[] = []; +} /** * Allows grouping up CSSDom mutations after the current execution context. @@ -19,36 +27,54 @@ import {takeUntil} from 'rxjs/operators'; */ @Injectable() export class _CoalescedStyleScheduler implements OnDestroy { - private _currentSchedule: Observable|null = null; + private _currentSchedule: _Schedule|null = null; private readonly _destroyed = new Subject(); constructor(private readonly _ngZone: NgZone) {} /** - * Schedules the specified task to run after the current microtask. + * Schedules the specified task to run at the end of the current VM turn. */ schedule(task: () => unknown): void { this._createScheduleIfNeeded(); - this._currentSchedule!.subscribe(task); + this._currentSchedule!.tasks.push(task); + } + + /** + * Schedules the specified task to run after other scheduled tasks at the end of the current + * VM turn. + */ + scheduleEnd(task: () => unknown): void { + this._createScheduleIfNeeded(); + + this._currentSchedule!.endTasks.push(task); } - /** Cancel and prevent new subscriptions. */ + /** Prevent any further tasks from running. */ ngOnDestroy() { this._destroyed.next(); this._destroyed.complete(); - - this._currentSchedule = this._destroyed; } private _createScheduleIfNeeded() { if (this._currentSchedule) { return; } - this._ngZone.runOutsideAngular(() => { - this._currentSchedule = from(new Promise((resolve) => { - this._currentSchedule = null; - resolve(undefined); - })).pipe(takeUntil(this._destroyed)); + this._currentSchedule = new _Schedule(); + + this._ngZone.onStable.pipe( + take(1), + takeUntil(this._destroyed), + ).subscribe(() => { + const schedule = this._currentSchedule!; + this._currentSchedule = null; + + for (const task of schedule.tasks) { + task(); + } + for (const task of schedule.endTasks) { + task(); + } }); } } From ad091c33bf5facea7ec26b464d0520cc43c2fa3d Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Fri, 24 Jul 2020 10:17:26 -0400 Subject: [PATCH 18/18] api update --- tools/public_api_guard/cdk/table.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/public_api_guard/cdk/table.d.ts b/tools/public_api_guard/cdk/table.d.ts index 15f11535dc79..7d68d6eb4810 100644 --- a/tools/public_api_guard/cdk/table.d.ts +++ b/tools/public_api_guard/cdk/table.d.ts @@ -2,10 +2,16 @@ export declare class _CoalescedStyleScheduler implements OnDestroy { constructor(_ngZone: NgZone); ngOnDestroy(): void; schedule(task: () => unknown): void; + scheduleEnd(task: () => unknown): void; static ɵfac: i0.ɵɵFactoryDef<_CoalescedStyleScheduler, never>; static ɵprov: i0.ɵɵInjectableDef<_CoalescedStyleScheduler>; } +export declare class _Schedule { + endTasks: (() => unknown)[]; + tasks: (() => unknown)[]; +} + export declare class BaseCdkCell { constructor(columnDef: CdkColumnDef, elementRef: ElementRef); }