diff --git a/src/cdk/table/BUILD.bazel b/src/cdk/table/BUILD.bazel
index 5284718aee7e..5a9e4a15741f 100644
--- a/src/cdk/table/BUILD.bazel
+++ b/src/cdk/table/BUILD.bazel
@@ -4,6 +4,7 @@ load(
"ng_module",
"ng_test_library",
"ng_web_test_suite",
+ "sass_binary",
)
package(default_visibility = ["//visibility:public"])
@@ -14,6 +15,7 @@ ng_module(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
+ assets = [":table.css"],
module_name = "@angular/cdk/table",
deps = [
"//src/cdk/bidi",
@@ -26,6 +28,11 @@ ng_module(
],
)
+sass_binary(
+ name = "table_scss",
+ src = "table.scss",
+)
+
ng_test_library(
name = "unit_test_sources",
srcs = glob(
diff --git a/src/cdk/table/sticky-styler.ts b/src/cdk/table/sticky-styler.ts
index 1cb73461f1af..cdde20a97834 100644
--- a/src/cdk/table/sticky-styler.ts
+++ b/src/cdk/table/sticky-styler.ts
@@ -27,6 +27,8 @@ export const STICKY_DIRECTIONS: StickyDirection[] = ['top', 'bottom', 'left', 'r
* @docs-private
*/
export class StickyStyler {
+ private _cachedCellWidths: number[] = [];
+
/**
* @param _isNativeHtmlTable Whether the sticky logic should be based on a table
* that uses the native `
` element.
@@ -83,9 +85,12 @@ export class StickyStyler {
* in this index position should be stuck to the start of the row.
* @param stickyEndStates A list of boolean states where each state represents whether the cell
* in this index position should be stuck to the end of the row.
+ * @param recalculateCellWidths Whether the sticky styler should recalculate the width of each
+ * column cell. If `false` cached widths will be used instead.
*/
updateStickyColumns(
- rows: HTMLElement[], stickyStartStates: boolean[], stickyEndStates: boolean[]) {
+ rows: HTMLElement[], stickyStartStates: boolean[], stickyEndStates: boolean[],
+ recalculateCellWidths = true) {
if (!rows.length || !this._isBrowser || !(stickyStartStates.some(state => state) ||
stickyEndStates.some(state => state))) {
return;
@@ -93,7 +98,7 @@ export class StickyStyler {
const firstRow = rows[0];
const numCells = firstRow.children.length;
- const cellWidths: number[] = this._getCellWidths(firstRow);
+ const cellWidths: number[] = this._getCellWidths(firstRow, recalculateCellWidths);
const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates);
const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates);
@@ -275,7 +280,11 @@ export class StickyStyler {
}
/** Gets the widths for each cell in the provided row. */
- _getCellWidths(row: HTMLElement): number[] {
+ _getCellWidths(row: HTMLElement, recalculateCellWidths = true): number[] {
+ if (!recalculateCellWidths && this._cachedCellWidths.length) {
+ return this._cachedCellWidths;
+ }
+
const cellWidths: number[] = [];
const firstRowCells = row.children;
for (let i = 0; i < firstRowCells.length; i++) {
@@ -283,6 +292,7 @@ export class StickyStyler {
cellWidths.push(cell.getBoundingClientRect().width);
}
+ this._cachedCellWidths = cellWidths;
return cellWidths;
}
diff --git a/src/cdk/table/table.scss b/src/cdk/table/table.scss
new file mode 100644
index 000000000000..556226aa82e5
--- /dev/null
+++ b/src/cdk/table/table.scss
@@ -0,0 +1,3 @@
+.cdk-table-fixed-layout {
+ table-layout: fixed;
+}
diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts
index 1670b9bcdff5..f62734435c90 100644
--- a/src/cdk/table/table.ts
+++ b/src/cdk/table/table.ts
@@ -20,6 +20,7 @@ import {
_ViewRepeaterOperation,
} from '@angular/cdk/collections';
import {Platform} from '@angular/cdk/platform';
+import {ViewportRuler} from '@angular/cdk/scrolling';
import {DOCUMENT} from '@angular/common';
import {
AfterContentChecked,
@@ -188,8 +189,10 @@ export interface RenderRow {
selector: 'cdk-table, table[cdk-table]',
exportAs: 'cdkTable',
template: CDK_TABLE_TEMPLATE,
+ styleUrls: ['table.css'],
host: {
'class': 'cdk-table',
+ '[class.cdk-table-fixed-layout]': 'fixedLayout',
},
encapsulation: ViewEncapsulation.None,
// The "OnPush" status for the `MatTable` component is effectively a noop, so we are removing it.
@@ -291,6 +294,19 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes
*/
private _footerRowDefChanged = true;
+ /**
+ * Whether the sticky column styles need to be updated. Set to `true` when the visible columns
+ * change.
+ */
+ private _stickyColumnStylesNeedReset = true;
+
+ /**
+ * Whether the sticky styler should recalculate cell widths when applying sticky styles. If
+ * `false`, cached values will be used instead. This is only applicable to tables with
+ * {@link fixedLayout} enabled. For other tables, cell widths will always be recalculated.
+ */
+ private _forceRecalculateCellWidths = true;
+
/**
* Cache of the latest rendered `RenderRow` objects as a map for easy retrieval when constructing
* a new list of `RenderRow` objects for rendering rows. Since the new list is constructed with
@@ -403,6 +419,19 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes
}
_multiTemplateDataRows: boolean = false;
+ /**
+ * Whether to use a fixed table layout. Enabling this option will enforce consistent column widths
+ * and optimize rendering sticky styles for native tables. No-op for flex tables.
+ */
+ @Input()
+ get fixedLayout(): boolean {
+ return this._fixedLayout;
+ }
+ set fixedLayout(v: boolean) {
+ this._fixedLayout = coerceBooleanProperty(v);
+ }
+ private _fixedLayout: boolean = false;
+
// TODO(andrewseguin): Remove max value as the end index
// and instead calculate the view on init and scroll.
/**
@@ -452,7 +481,9 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes
// Optional for backwards compatibility, but a view repeater strategy will always
// be provided.
@Optional() @Inject(_VIEW_REPEATER_STRATEGY)
- protected readonly _viewRepeater: _ViewRepeater, RowContext>) {
+ protected readonly _viewRepeater: _ViewRepeater, RowContext>,
+ // Optional for backwards compatibility, but a view ruler will always be provided.
+ @Optional() private readonly _viewportRuler: ViewportRuler) {
if (!role) {
this._elementRef.nativeElement.setAttribute('role', 'grid');
}
@@ -474,6 +505,12 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes
this._dataDiffer = this._differs.find([]).create((_i: number, dataRow: RenderRow) => {
return this.trackBy ? this.trackBy(dataRow.dataIndex, dataRow.data) : dataRow;
});
+
+ // Table cell dimensions may change after resizing the window. Signal the sticky styler to
+ // refresh its cache of cell widths the next time sticky styles are updated.
+ this._viewportRuler.change().pipe(takeUntil(this._onDestroy)).subscribe(() => {
+ this._forceRecalculateCellWidths = true;
+ });
}
ngAfterContentChecked() {
@@ -488,8 +525,9 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes
// Render updates if the list of columns have been changed for the header, row, or footer defs.
const columnsChanged = this._renderUpdatedColumns();
- const stickyColumnStyleUpdateNeeded =
- columnsChanged || this._headerRowDefChanged || this._footerRowDefChanged;
+ const rowDefsChanged = columnsChanged || this._headerRowDefChanged || this._footerRowDefChanged;
+ this._stickyColumnStylesNeedReset = rowDefsChanged;
+ this._forceRecalculateCellWidths = rowDefsChanged;
// If the header row definition has been changed, trigger a render to the header row.
if (this._headerRowDefChanged) {
@@ -507,7 +545,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes
// connection has already been made.
if (this.dataSource && this._rowDefs.length > 0 && !this._renderChangeSubscription) {
this._observeRenderChanges();
- } else if (stickyColumnStyleUpdateNeeded) {
+ } else if (this._stickyColumnStylesNeedReset) {
// In the above case, _observeRenderChanges will result in updateStickyColumnStyles being
// called when it row data arrives. Otherwise, we need to call it proactively.
this.updateStickyColumnStyles();
@@ -689,10 +727,18 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes
const dataRows = this._getRenderedRows(this._rowOutlet);
const footerRows = this._getRenderedRows(this._footerRowOutlet);
- // Clear the left and right positioning from all columns in the table across all rows since
- // sticky columns span across all table sections (header, data, footer)
- this._stickyStyler.clearStickyPositioning(
- [...headerRows, ...dataRows, ...footerRows], ['left', 'right']);
+ // For tables not using a fixed layout, the column widths may change when new rows are rendered.
+ // In a table using a fixed layout, row content won't affect column width, so sticky styles
+ // don't need to be cleared unless either the sticky column config changes or one of the row
+ // defs change.
+ if ((this._isNativeHtmlTable && !this._fixedLayout)
+ || this._stickyColumnStylesNeedReset) {
+ // Clear the left and right positioning from all columns in the table across all rows since
+ // sticky columns span across all table sections (header, data, footer)
+ this._stickyStyler.clearStickyPositioning(
+ [...headerRows, ...dataRows, ...footerRows], ['left', 'right']);
+ this._stickyColumnStylesNeedReset = false;
+ }
// Update the sticky styles for each header row depending on the def's sticky state
headerRows.forEach((headerRow, i) => {
@@ -934,7 +980,9 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes
});
const stickyStartStates = columnDefs.map(columnDef => columnDef.sticky);
const stickyEndStates = columnDefs.map(columnDef => columnDef.stickyEnd);
- this._stickyStyler.updateStickyColumns(rows, stickyStartStates, stickyEndStates);
+ this._stickyStyler.updateStickyColumns(
+ rows, stickyStartStates, stickyEndStates,
+ !this._fixedLayout || this._forceRecalculateCellWidths);
}
/** Gets the list of rows that have been rendered in the row outlet. */
@@ -1113,6 +1161,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes
}
if (Array.from(this._columnDefsByName.values()).reduce(stickyCheckReducer, false)) {
+ this._stickyColumnStylesNeedReset = true;
this.updateStickyColumnStyles();
}
}
@@ -1154,6 +1203,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes
}
static ngAcceptInputType_multiTemplateDataRows: BooleanInput;
+ static ngAcceptInputType_fixedLayout: BooleanInput;
}
/** Utility function that gets a merged list of the entries in an array and values of a Set. */
diff --git a/src/components-examples/cdk/table/cdk-table-fixed-layout/cdk-table-fixed-layout-example.css b/src/components-examples/cdk/table/cdk-table-fixed-layout/cdk-table-fixed-layout-example.css
new file mode 100644
index 000000000000..edaff91eb1a2
--- /dev/null
+++ b/src/components-examples/cdk/table/cdk-table-fixed-layout/cdk-table-fixed-layout-example.css
@@ -0,0 +1,7 @@
+table {
+ width: 100%;
+}
+
+th {
+ text-align: left;
+}
diff --git a/src/components-examples/cdk/table/cdk-table-fixed-layout/cdk-table-fixed-layout-example.html b/src/components-examples/cdk/table/cdk-table-fixed-layout/cdk-table-fixed-layout-example.html
new file mode 100644
index 000000000000..f981320a6b51
--- /dev/null
+++ b/src/components-examples/cdk/table/cdk-table-fixed-layout/cdk-table-fixed-layout-example.html
@@ -0,0 +1,28 @@
+
+
+
+ No. |
+ {{element.position}} |
+
+
+
+
+ Name |
+ {{element.name}} |
+
+
+
+
+ Weight |
+ {{element.weight}} |
+
+
+
+
+ Symbol |
+ {{element.symbol}} |
+
+
+
+
+
diff --git a/src/components-examples/cdk/table/cdk-table-fixed-layout/cdk-table-fixed-layout-example.ts b/src/components-examples/cdk/table/cdk-table-fixed-layout/cdk-table-fixed-layout-example.ts
new file mode 100644
index 000000000000..c3cb10998f08
--- /dev/null
+++ b/src/components-examples/cdk/table/cdk-table-fixed-layout/cdk-table-fixed-layout-example.ts
@@ -0,0 +1,55 @@
+import {DataSource} from '@angular/cdk/collections';
+import {Component} from '@angular/core';
+import {BehaviorSubject, Observable} from 'rxjs';
+
+export interface PeriodicElement {
+ name: string;
+ position: number;
+ weight: number;
+ symbol: string;
+}
+
+const ELEMENT_DATA: PeriodicElement[] = [
+ {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
+ {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
+ {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
+ {position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
+ {position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
+ {position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
+ {position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
+ {position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
+ {position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
+ {position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
+];
+
+/**
+ * @title CDK table with a fixed layout.
+ */
+@Component({
+ selector: 'cdk-table-fixed-layout-example',
+ styleUrls: ['cdk-table-fixed-layout-example.css'],
+ templateUrl: 'cdk-table-fixed-layout-example.html',
+})
+export class CdkTableFixedLayoutExample {
+ displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
+ dataSource = new ExampleDataSource();
+}
+
+/**
+ * Data source to provide what data should be rendered in the table. Note that the data source
+ * can retrieve its data in any way. In this case, the data source is provided a reference
+ * to a common data base, ExampleDatabase. It is not the data source's responsibility to manage
+ * the underlying data. Instead, it only needs to take the data and send the table exactly what
+ * should be rendered.
+ */
+export class ExampleDataSource extends DataSource {
+ /** Stream of data that is provided to the table. */
+ data = new BehaviorSubject(ELEMENT_DATA);
+
+ /** Connect function called by the table to retrieve one stream containing the data to render. */
+ connect(): Observable {
+ return this.data;
+ }
+
+ disconnect() {}
+}
diff --git a/src/components-examples/cdk/table/index.ts b/src/components-examples/cdk/table/index.ts
index 5992abba0cfb..6944295d4ede 100644
--- a/src/components-examples/cdk/table/index.ts
+++ b/src/components-examples/cdk/table/index.ts
@@ -2,15 +2,20 @@ import {CdkTableModule} from '@angular/cdk/table';
import {NgModule} from '@angular/core';
import {CdkTableFlexBasicExample} from './cdk-table-flex-basic/cdk-table-flex-basic-example';
import {CdkTableBasicExample} from './cdk-table-basic/cdk-table-basic-example';
+import {
+ CdkTableFixedLayoutExample,
+} from './cdk-table-fixed-layout/cdk-table-fixed-layout-example';
export {
CdkTableBasicExample,
CdkTableFlexBasicExample,
+ CdkTableFixedLayoutExample,
};
const EXAMPLES = [
CdkTableBasicExample,
CdkTableFlexBasicExample,
+ CdkTableFixedLayoutExample,
];
@NgModule({
diff --git a/src/dev-app/table/table-demo.html b/src/dev-app/table/table-demo.html
index 4793e15e5484..c4c07b9e4783 100644
--- a/src/dev-app/table/table-demo.html
+++ b/src/dev-app/table/table-demo.html
@@ -1,6 +1,9 @@
Cdk table basic
+Cdk table basic with fixed column widths
+
+
Cdk table basic flex
diff --git a/src/material-experimental/mdc-table/table.ts b/src/material-experimental/mdc-table/table.ts
index 0e67b5d97f5a..2a79367bcffb 100644
--- a/src/material-experimental/mdc-table/table.ts
+++ b/src/material-experimental/mdc-table/table.ts
@@ -17,6 +17,7 @@ import {_DisposeViewRepeaterStrategy, _VIEW_REPEATER_STRATEGY} from '@angular/cd
styleUrls: ['table.css'],
host: {
'class': 'mat-mdc-table mdc-data-table__table',
+ '[class.mat-table-fixed-layout]': 'fixedLayout',
},
providers: [
{provide: CdkTable, useExisting: MatTable},
diff --git a/src/material/table/table.scss b/src/material/table/table.scss
index 8ee7994267e3..09c7bcc3ee75 100644
--- a/src/material/table/table.scss
+++ b/src/material/table/table.scss
@@ -120,3 +120,7 @@ th.mat-header-cell:last-of-type, td.mat-cell:last-of-type, td.mat-footer-cell:la
.mat-table-sticky {
@include vendor-prefixes.position-sticky;
}
+
+.mat-table-fixed-layout {
+ table-layout: fixed;
+}
diff --git a/src/material/table/table.ts b/src/material/table/table.ts
index be4a12d6ed95..468dfc2a69fc 100644
--- a/src/material/table/table.ts
+++ b/src/material/table/table.ts
@@ -25,6 +25,7 @@ import {_DisposeViewRepeaterStrategy, _VIEW_REPEATER_STRATEGY} from '@angular/cd
styleUrls: ['table.css'],
host: {
'class': 'mat-table',
+ '[class.mat-table-fixed-layout]': 'fixedLayout',
},
providers: [
// TODO(michaeljamesparsons) Abstract the view repeater strategy to a directive API so this code
diff --git a/tools/public_api_guard/cdk/table.d.ts b/tools/public_api_guard/cdk/table.d.ts
index f4936087c8f9..17fba7c49c02 100644
--- a/tools/public_api_guard/cdk/table.d.ts
+++ b/tools/public_api_guard/cdk/table.d.ts
@@ -203,6 +203,8 @@ export declare class CdkTable implements AfterContentChecked, CollectionViewe
protected readonly _viewRepeater: _ViewRepeater, RowContext>;
get dataSource(): CdkTableDataSourceInput;
set dataSource(dataSource: CdkTableDataSourceInput);
+ get fixedLayout(): boolean;
+ set fixedLayout(v: boolean);
get multiTemplateDataRows(): boolean;
set multiTemplateDataRows(v: boolean);
protected needsPositionStickyOnElement: boolean;
@@ -213,7 +215,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, _viewRepeater: _ViewRepeater, RowContext>);
+ constructor(_differs: IterableDiffers, _changeDetectorRef: ChangeDetectorRef, _coalescedStyleScheduler: _CoalescedStyleScheduler, _elementRef: ElementRef, role: string, _dir: Directionality, _document: any, _platform: Platform, _viewRepeater: _ViewRepeater, RowContext>, _viewportRuler: ViewportRuler);
_getRenderedRows(rowOutlet: RowOutlet): HTMLElement[];
_getRowDefs(data: T, dataIndex: number): CdkRowDef[];
addColumnDef(columnDef: CdkColumnDef): void;
@@ -231,9 +233,10 @@ export declare class CdkTable implements AfterContentChecked, CollectionViewe
updateStickyColumnStyles(): void;
updateStickyFooterRowStyles(): void;
updateStickyHeaderRowStyles(): void;
+ static ngAcceptInputType_fixedLayout: BooleanInput;
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, null, { attribute: "role"; }, { optional: true; }, null, null, { optional: true; }]>;
+ static ɵcmp: i0.ɵɵComponentDefWithMeta, "cdk-table, table[cdk-table]", ["cdkTable"], { "trackBy": "trackBy"; "dataSource": "dataSource"; "multiTemplateDataRows": "multiTemplateDataRows"; "fixedLayout": "fixedLayout"; }, {}, ["_noDataRow", "_contentColumnDefs", "_contentRowDefs", "_contentHeaderRowDefs", "_contentFooterRowDefs"], ["caption", "colgroup, col"]>;
+ static ɵfac: i0.ɵɵFactoryDef, [null, null, null, null, { attribute: "role"; }, { optional: true; }, null, null, { optional: true; }, { optional: true; }]>;
}
export declare class CdkTableModule {
@@ -321,13 +324,13 @@ export declare class StickyStyler {
constructor(_isNativeHtmlTable: boolean, _stickCellCss: string, direction: Direction, _coalescedStyleScheduler: _CoalescedStyleScheduler, _isBrowser?: boolean, _needsPositionStickyOnElement?: boolean);
_addStickyStyle(element: HTMLElement, dir: StickyDirection, dirValue: number): void;
_getCalculatedZIndex(element: HTMLElement): string;
- _getCellWidths(row: HTMLElement): number[];
+ _getCellWidths(row: HTMLElement, recalculateCellWidths?: boolean): number[];
_getStickyEndColumnPositions(widths: number[], stickyStates: boolean[]): number[];
_getStickyStartColumnPositions(widths: number[], stickyStates: boolean[]): number[];
_removeStickyStyle(element: HTMLElement, stickyDirections: StickyDirection[]): void;
clearStickyPositioning(rows: HTMLElement[], stickyDirections: StickyDirection[]): void;
stickRows(rowsToStick: HTMLElement[], stickyStates: boolean[], position: 'top' | 'bottom'): void;
- updateStickyColumns(rows: HTMLElement[], stickyStartStates: boolean[], stickyEndStates: boolean[]): void;
+ updateStickyColumns(rows: HTMLElement[], stickyStartStates: boolean[], stickyEndStates: boolean[], recalculateCellWidths?: boolean): void;
updateStickyFooterContainer(tableElement: Element, stickyStates: boolean[]): void;
}