Skip to content

Commit 58e0c48

Browse files
feat(cdk/table): fixed table layouts (#20258)
1 parent 971553d commit 58e0c48

File tree

13 files changed

+201
-17
lines changed

13 files changed

+201
-17
lines changed

src/cdk/table/BUILD.bazel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ load(
44
"ng_module",
55
"ng_test_library",
66
"ng_web_test_suite",
7+
"sass_binary",
78
)
89

910
package(default_visibility = ["//visibility:public"])
@@ -14,6 +15,7 @@ ng_module(
1415
["**/*.ts"],
1516
exclude = ["**/*.spec.ts"],
1617
),
18+
assets = [":table.css"],
1719
module_name = "@angular/cdk/table",
1820
deps = [
1921
"//src:dev_mode_types",
@@ -27,6 +29,11 @@ ng_module(
2729
],
2830
)
2931

32+
sass_binary(
33+
name = "table_scss",
34+
src = "table.scss",
35+
)
36+
3037
ng_test_library(
3138
name = "unit_test_sources",
3239
srcs = glob(

src/cdk/table/sticky-styler.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const STICKY_DIRECTIONS: StickyDirection[] = ['top', 'bottom', 'left', 'r
2727
* @docs-private
2828
*/
2929
export class StickyStyler {
30+
private _cachedCellWidths: number[] = [];
31+
3032
/**
3133
* @param _isNativeHtmlTable Whether the sticky logic should be based on a table
3234
* that uses the native `<table>` element.
@@ -83,17 +85,20 @@ export class StickyStyler {
8385
* in this index position should be stuck to the start of the row.
8486
* @param stickyEndStates A list of boolean states where each state represents whether the cell
8587
* in this index position should be stuck to the end of the row.
88+
* @param recalculateCellWidths Whether the sticky styler should recalculate the width of each
89+
* column cell. If `false` cached widths will be used instead.
8690
*/
8791
updateStickyColumns(
88-
rows: HTMLElement[], stickyStartStates: boolean[], stickyEndStates: boolean[]) {
92+
rows: HTMLElement[], stickyStartStates: boolean[], stickyEndStates: boolean[],
93+
recalculateCellWidths = true) {
8994
if (!rows.length || !this._isBrowser || !(stickyStartStates.some(state => state) ||
9095
stickyEndStates.some(state => state))) {
9196
return;
9297
}
9398

9499
const firstRow = rows[0];
95100
const numCells = firstRow.children.length;
96-
const cellWidths: number[] = this._getCellWidths(firstRow);
101+
const cellWidths: number[] = this._getCellWidths(firstRow, recalculateCellWidths);
97102

98103
const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates);
99104
const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates);
@@ -275,14 +280,19 @@ export class StickyStyler {
275280
}
276281

277282
/** Gets the widths for each cell in the provided row. */
278-
_getCellWidths(row: HTMLElement): number[] {
283+
_getCellWidths(row: HTMLElement, recalculateCellWidths = true): number[] {
284+
if (!recalculateCellWidths && this._cachedCellWidths.length) {
285+
return this._cachedCellWidths;
286+
}
287+
279288
const cellWidths: number[] = [];
280289
const firstRowCells = row.children;
281290
for (let i = 0; i < firstRowCells.length; i++) {
282291
let cell: HTMLElement = firstRowCells[i] as HTMLElement;
283292
cellWidths.push(cell.getBoundingClientRect().width);
284293
}
285294

295+
this._cachedCellWidths = cellWidths;
286296
return cellWidths;
287297
}
288298

src/cdk/table/table.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.cdk-table-fixed-layout {
2+
table-layout: fixed;
3+
}

src/cdk/table/table.ts

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
_ViewRepeaterOperation,
2121
} from '@angular/cdk/collections';
2222
import {Platform} from '@angular/cdk/platform';
23+
import {ViewportRuler} from '@angular/cdk/scrolling';
2324
import {DOCUMENT} from '@angular/common';
2425
import {
2526
AfterContentChecked,
@@ -187,8 +188,10 @@ export interface RenderRow<T> {
187188
selector: 'cdk-table, table[cdk-table]',
188189
exportAs: 'cdkTable',
189190
template: CDK_TABLE_TEMPLATE,
191+
styleUrls: ['table.css'],
190192
host: {
191193
'class': 'cdk-table',
194+
'[class.cdk-table-fixed-layout]': 'fixedLayout',
192195
},
193196
encapsulation: ViewEncapsulation.None,
194197
// The "OnPush" status for the `MatTable` component is effectively a noop, so we are removing it.
@@ -290,6 +293,19 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
290293
*/
291294
private _footerRowDefChanged = true;
292295

296+
/**
297+
* Whether the sticky column styles need to be updated. Set to `true` when the visible columns
298+
* change.
299+
*/
300+
private _stickyColumnStylesNeedReset = true;
301+
302+
/**
303+
* Whether the sticky styler should recalculate cell widths when applying sticky styles. If
304+
* `false`, cached values will be used instead. This is only applicable to tables with
305+
* {@link fixedLayout} enabled. For other tables, cell widths will always be recalculated.
306+
*/
307+
private _forceRecalculateCellWidths = true;
308+
293309
/**
294310
* Cache of the latest rendered `RenderRow` objects as a map for easy retrieval when constructing
295311
* a new list of `RenderRow` objects for rendering rows. Since the new list is constructed with
@@ -401,6 +417,23 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
401417
}
402418
_multiTemplateDataRows: boolean = false;
403419

420+
/**
421+
* Whether to use a fixed table layout. Enabling this option will enforce consistent column widths
422+
* and optimize rendering sticky styles for native tables. No-op for flex tables.
423+
*/
424+
@Input()
425+
get fixedLayout(): boolean {
426+
return this._fixedLayout;
427+
}
428+
set fixedLayout(v: boolean) {
429+
this._fixedLayout = coerceBooleanProperty(v);
430+
431+
// Toggling `fixedLayout` may change column widths. Sticky column styles should be recalculated.
432+
this._forceRecalculateCellWidths = true;
433+
this._stickyColumnStylesNeedReset = true;
434+
}
435+
private _fixedLayout: boolean = false;
436+
404437
// TODO(andrewseguin): Remove max value as the end index
405438
// and instead calculate the view on init and scroll.
406439
/**
@@ -450,7 +483,11 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
450483
// Optional for backwards compatibility, but a view repeater strategy will always
451484
// be provided.
452485
@Optional() @Inject(_VIEW_REPEATER_STRATEGY)
453-
protected readonly _viewRepeater: _ViewRepeater<T, RenderRow<T>, RowContext<T>>) {
486+
protected readonly _viewRepeater: _ViewRepeater<T, RenderRow<T>, RowContext<T>>,
487+
// Optional for backwards compatibility. The viewport ruler is provided in root. Therefore,
488+
// this property will never be null.
489+
// tslint:disable-next-line: lightweight-tokens
490+
@Optional() private readonly _viewportRuler: ViewportRuler) {
454491
if (!role) {
455492
this._elementRef.nativeElement.setAttribute('role', 'grid');
456493
}
@@ -472,6 +509,12 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
472509
this._dataDiffer = this._differs.find([]).create((_i: number, dataRow: RenderRow<T>) => {
473510
return this.trackBy ? this.trackBy(dataRow.dataIndex, dataRow.data) : dataRow;
474511
});
512+
513+
// Table cell dimensions may change after resizing the window. Signal the sticky styler to
514+
// refresh its cache of cell widths the next time sticky styles are updated.
515+
this._viewportRuler.change().pipe(takeUntil(this._onDestroy)).subscribe(() => {
516+
this._forceRecalculateCellWidths = true;
517+
});
475518
}
476519

477520
ngAfterContentChecked() {
@@ -487,8 +530,10 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
487530

488531
// Render updates if the list of columns have been changed for the header, row, or footer defs.
489532
const columnsChanged = this._renderUpdatedColumns();
490-
const stickyColumnStyleUpdateNeeded =
491-
columnsChanged || this._headerRowDefChanged || this._footerRowDefChanged;
533+
const rowDefsChanged = columnsChanged || this._headerRowDefChanged || this._footerRowDefChanged;
534+
// Ensure sticky column styles are reset if set to `true` elsewhere.
535+
this._stickyColumnStylesNeedReset = this._stickyColumnStylesNeedReset || rowDefsChanged;
536+
this._forceRecalculateCellWidths = rowDefsChanged;
492537

493538
// If the header row definition has been changed, trigger a render to the header row.
494539
if (this._headerRowDefChanged) {
@@ -506,7 +551,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
506551
// connection has already been made.
507552
if (this.dataSource && this._rowDefs.length > 0 && !this._renderChangeSubscription) {
508553
this._observeRenderChanges();
509-
} else if (stickyColumnStyleUpdateNeeded) {
554+
} else if (this._stickyColumnStylesNeedReset) {
510555
// In the above case, _observeRenderChanges will result in updateStickyColumnStyles being
511556
// called when it row data arrives. Otherwise, we need to call it proactively.
512557
this.updateStickyColumnStyles();
@@ -688,10 +733,18 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
688733
const dataRows = this._getRenderedRows(this._rowOutlet);
689734
const footerRows = this._getRenderedRows(this._footerRowOutlet);
690735

691-
// Clear the left and right positioning from all columns in the table across all rows since
692-
// sticky columns span across all table sections (header, data, footer)
693-
this._stickyStyler.clearStickyPositioning(
694-
[...headerRows, ...dataRows, ...footerRows], ['left', 'right']);
736+
// For tables not using a fixed layout, the column widths may change when new rows are rendered.
737+
// In a table using a fixed layout, row content won't affect column width, so sticky styles
738+
// don't need to be cleared unless either the sticky column config changes or one of the row
739+
// defs change.
740+
if ((this._isNativeHtmlTable && !this._fixedLayout)
741+
|| this._stickyColumnStylesNeedReset) {
742+
// Clear the left and right positioning from all columns in the table across all rows since
743+
// sticky columns span across all table sections (header, data, footer)
744+
this._stickyStyler.clearStickyPositioning(
745+
[...headerRows, ...dataRows, ...footerRows], ['left', 'right']);
746+
this._stickyColumnStylesNeedReset = false;
747+
}
695748

696749
// Update the sticky styles for each header row depending on the def's sticky state
697750
headerRows.forEach((headerRow, i) => {
@@ -936,7 +989,9 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
936989
});
937990
const stickyStartStates = columnDefs.map(columnDef => columnDef.sticky);
938991
const stickyEndStates = columnDefs.map(columnDef => columnDef.stickyEnd);
939-
this._stickyStyler.updateStickyColumns(rows, stickyStartStates, stickyEndStates);
992+
this._stickyStyler.updateStickyColumns(
993+
rows, stickyStartStates, stickyEndStates,
994+
!this._fixedLayout || this._forceRecalculateCellWidths);
940995
}
941996

942997
/** Gets the list of rows that have been rendered in the row outlet. */
@@ -1115,6 +1170,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
11151170
}
11161171

11171172
if (Array.from(this._columnDefsByName.values()).reduce(stickyCheckReducer, false)) {
1173+
this._stickyColumnStylesNeedReset = true;
11181174
this.updateStickyColumnStyles();
11191175
}
11201176
}
@@ -1156,6 +1212,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
11561212
}
11571213

11581214
static ngAcceptInputType_multiTemplateDataRows: BooleanInput;
1215+
static ngAcceptInputType_fixedLayout: BooleanInput;
11591216
}
11601217

11611218
/** Utility function that gets a merged list of the entries in an array and values of a Set. */
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
table {
2+
width: 100%;
3+
}
4+
5+
th {
6+
text-align: left;
7+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<table cdk-table [dataSource]="dataSource" fixedLayout>
2+
<!-- Position Column -->
3+
<ng-container cdkColumnDef="position">
4+
<th cdk-header-cell *cdkHeaderCellDef> No. </th>
5+
<td cdk-cell *cdkCellDef="let element"> {{element.position}} </td>
6+
</ng-container>
7+
8+
<!-- Name Column -->
9+
<ng-container cdkColumnDef="name">
10+
<th cdk-header-cell *cdkHeaderCellDef> Name </th>
11+
<td cdk-cell *cdkCellDef="let element"> {{element.name}} </td>
12+
</ng-container>
13+
14+
<!-- Weight Column -->
15+
<ng-container cdkColumnDef="weight">
16+
<th cdk-header-cell *cdkHeaderCellDef> Weight </th>
17+
<td cdk-cell *cdkCellDef="let element"> {{element.weight}} </td>
18+
</ng-container>
19+
20+
<!-- Symbol Column -->
21+
<ng-container cdkColumnDef="symbol">
22+
<th cdk-header-cell *cdkHeaderCellDef> Symbol </th>
23+
<td cdk-cell *cdkCellDef="let element"> {{element.symbol}} </td>
24+
</ng-container>
25+
26+
<tr cdk-header-row *cdkHeaderRowDef="displayedColumns"></tr>
27+
<tr cdk-row *cdkRowDef="let row; columns: displayedColumns;"></tr>
28+
</table>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {DataSource} from '@angular/cdk/collections';
2+
import {Component} from '@angular/core';
3+
import {BehaviorSubject, Observable} from 'rxjs';
4+
5+
export interface PeriodicElement {
6+
name: string;
7+
position: number;
8+
weight: number;
9+
symbol: string;
10+
}
11+
12+
const ELEMENT_DATA: PeriodicElement[] = [
13+
{position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
14+
{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
15+
{position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
16+
{position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
17+
{position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
18+
{position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
19+
{position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
20+
{position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
21+
{position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
22+
{position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
23+
];
24+
25+
/**
26+
* @title CDK table with a fixed layout.
27+
*/
28+
@Component({
29+
selector: 'cdk-table-fixed-layout-example',
30+
styleUrls: ['cdk-table-fixed-layout-example.css'],
31+
templateUrl: 'cdk-table-fixed-layout-example.html',
32+
})
33+
export class CdkTableFixedLayoutExample {
34+
displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
35+
dataSource = new ExampleDataSource();
36+
}
37+
38+
/**
39+
* Data source to provide what data should be rendered in the table. Note that the data source
40+
* can retrieve its data in any way. In this case, the data source is provided a reference
41+
* to a common data base, ExampleDatabase. It is not the data source's responsibility to manage
42+
* the underlying data. Instead, it only needs to take the data and send the table exactly what
43+
* should be rendered.
44+
*/
45+
export class ExampleDataSource extends DataSource<PeriodicElement> {
46+
/** Stream of data that is provided to the table. */
47+
data = new BehaviorSubject<PeriodicElement[]>(ELEMENT_DATA);
48+
49+
/** Connect function called by the table to retrieve one stream containing the data to render. */
50+
connect(): Observable<PeriodicElement[]> {
51+
return this.data;
52+
}
53+
54+
disconnect() {}
55+
}

src/components-examples/cdk/table/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ import {CdkTableModule} from '@angular/cdk/table';
22
import {NgModule} from '@angular/core';
33
import {CdkTableFlexBasicExample} from './cdk-table-flex-basic/cdk-table-flex-basic-example';
44
import {CdkTableBasicExample} from './cdk-table-basic/cdk-table-basic-example';
5+
import {
6+
CdkTableFixedLayoutExample,
7+
} from './cdk-table-fixed-layout/cdk-table-fixed-layout-example';
58

69
export {
710
CdkTableBasicExample,
811
CdkTableFlexBasicExample,
12+
CdkTableFixedLayoutExample,
913
};
1014

1115
const EXAMPLES = [
1216
CdkTableBasicExample,
1317
CdkTableFlexBasicExample,
18+
CdkTableFixedLayoutExample,
1419
];
1520

1621
@NgModule({

src/dev-app/table/table-demo.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<h3>Cdk table basic</h3>
22
<cdk-table-basic-example></cdk-table-basic-example>
33

4+
<h3>Cdk table basic with fixed column widths</h3>
5+
<cdk-table-fixed-layout-example></cdk-table-fixed-layout-example>
6+
47
<h3>Cdk table basic flex</h3>
58
<cdk-table-flex-basic-example></cdk-table-flex-basic-example>
69

src/material-experimental/mdc-table/table.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {_DisposeViewRepeaterStrategy, _VIEW_REPEATER_STRATEGY} from '@angular/cd
1717
styleUrls: ['table.css'],
1818
host: {
1919
'class': 'mat-mdc-table mdc-data-table__table',
20+
'[class.mdc-table-fixed-layout]': 'fixedLayout',
2021
},
2122
providers: [
2223
{provide: CdkTable, useExisting: MatTable},

0 commit comments

Comments
 (0)