Skip to content

Commit

Permalink
feat(cdk/table): fixed table layouts (#20258)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelJamesParsons committed Sep 2, 2020
1 parent 971553d commit 58e0c48
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 17 deletions.
7 changes: 7 additions & 0 deletions src/cdk/table/BUILD.bazel
Expand Up @@ -4,6 +4,7 @@ load(
"ng_module",
"ng_test_library",
"ng_web_test_suite",
"sass_binary",
)

package(default_visibility = ["//visibility:public"])
Expand All @@ -14,6 +15,7 @@ ng_module(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
assets = [":table.css"],
module_name = "@angular/cdk/table",
deps = [
"//src:dev_mode_types",
Expand All @@ -27,6 +29,11 @@ ng_module(
],
)

sass_binary(
name = "table_scss",
src = "table.scss",
)

ng_test_library(
name = "unit_test_sources",
srcs = glob(
Expand Down
16 changes: 13 additions & 3 deletions src/cdk/table/sticky-styler.ts
Expand Up @@ -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 `<table>` element.
Expand Down Expand Up @@ -83,17 +85,20 @@ 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;
}

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);
Expand Down Expand Up @@ -275,14 +280,19 @@ 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++) {
let cell: HTMLElement = firstRowCells[i] as HTMLElement;
cellWidths.push(cell.getBoundingClientRect().width);
}

this._cachedCellWidths = cellWidths;
return cellWidths;
}

Expand Down
3 changes: 3 additions & 0 deletions src/cdk/table/table.scss
@@ -0,0 +1,3 @@
.cdk-table-fixed-layout {
table-layout: fixed;
}
75 changes: 66 additions & 9 deletions src/cdk/table/table.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -187,8 +188,10 @@ export interface RenderRow<T> {
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.
Expand Down Expand Up @@ -290,6 +293,19 @@ export class CdkTable<T> 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
Expand Down Expand Up @@ -401,6 +417,23 @@ export class CdkTable<T> 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);

// Toggling `fixedLayout` may change column widths. Sticky column styles should be recalculated.
this._forceRecalculateCellWidths = true;
this._stickyColumnStylesNeedReset = true;
}
private _fixedLayout: boolean = false;

// TODO(andrewseguin): Remove max value as the end index
// and instead calculate the view on init and scroll.
/**
Expand Down Expand Up @@ -450,7 +483,11 @@ export class CdkTable<T> 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<T, RenderRow<T>, RowContext<T>>) {
protected readonly _viewRepeater: _ViewRepeater<T, RenderRow<T>, RowContext<T>>,
// Optional for backwards compatibility. The viewport ruler is provided in root. Therefore,
// this property will never be null.
// tslint:disable-next-line: lightweight-tokens
@Optional() private readonly _viewportRuler: ViewportRuler) {
if (!role) {
this._elementRef.nativeElement.setAttribute('role', 'grid');
}
Expand All @@ -472,6 +509,12 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
this._dataDiffer = this._differs.find([]).create((_i: number, dataRow: RenderRow<T>) => {
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() {
Expand All @@ -487,8 +530,10 @@ export class CdkTable<T> 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;
// Ensure sticky column styles are reset if set to `true` elsewhere.
this._stickyColumnStylesNeedReset = this._stickyColumnStylesNeedReset || rowDefsChanged;
this._forceRecalculateCellWidths = rowDefsChanged;

// If the header row definition has been changed, trigger a render to the header row.
if (this._headerRowDefChanged) {
Expand All @@ -506,7 +551,7 @@ export class CdkTable<T> 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();
Expand Down Expand Up @@ -688,10 +733,18 @@ export class CdkTable<T> 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) => {
Expand Down Expand Up @@ -936,7 +989,9 @@ export class CdkTable<T> 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. */
Expand Down Expand Up @@ -1115,6 +1170,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
}

if (Array.from(this._columnDefsByName.values()).reduce(stickyCheckReducer, false)) {
this._stickyColumnStylesNeedReset = true;
this.updateStickyColumnStyles();
}
}
Expand Down Expand Up @@ -1156,6 +1212,7 @@ export class CdkTable<T> 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. */
Expand Down
@@ -0,0 +1,7 @@
table {
width: 100%;
}

th {
text-align: left;
}
@@ -0,0 +1,28 @@
<table cdk-table [dataSource]="dataSource" fixedLayout>
<!-- Position Column -->
<ng-container cdkColumnDef="position">
<th cdk-header-cell *cdkHeaderCellDef> No. </th>
<td cdk-cell *cdkCellDef="let element"> {{element.position}} </td>
</ng-container>

<!-- Name Column -->
<ng-container cdkColumnDef="name">
<th cdk-header-cell *cdkHeaderCellDef> Name </th>
<td cdk-cell *cdkCellDef="let element"> {{element.name}} </td>
</ng-container>

<!-- Weight Column -->
<ng-container cdkColumnDef="weight">
<th cdk-header-cell *cdkHeaderCellDef> Weight </th>
<td cdk-cell *cdkCellDef="let element"> {{element.weight}} </td>
</ng-container>

<!-- Symbol Column -->
<ng-container cdkColumnDef="symbol">
<th cdk-header-cell *cdkHeaderCellDef> Symbol </th>
<td cdk-cell *cdkCellDef="let element"> {{element.symbol}} </td>
</ng-container>

<tr cdk-header-row *cdkHeaderRowDef="displayedColumns"></tr>
<tr cdk-row *cdkRowDef="let row; columns: displayedColumns;"></tr>
</table>
@@ -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<PeriodicElement> {
/** Stream of data that is provided to the table. */
data = new BehaviorSubject<PeriodicElement[]>(ELEMENT_DATA);

/** Connect function called by the table to retrieve one stream containing the data to render. */
connect(): Observable<PeriodicElement[]> {
return this.data;
}

disconnect() {}
}
5 changes: 5 additions & 0 deletions src/components-examples/cdk/table/index.ts
Expand Up @@ -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({
Expand Down
3 changes: 3 additions & 0 deletions src/dev-app/table/table-demo.html
@@ -1,6 +1,9 @@
<h3>Cdk table basic</h3>
<cdk-table-basic-example></cdk-table-basic-example>

<h3>Cdk table basic with fixed column widths</h3>
<cdk-table-fixed-layout-example></cdk-table-fixed-layout-example>

<h3>Cdk table basic flex</h3>
<cdk-table-flex-basic-example></cdk-table-flex-basic-example>

Expand Down
1 change: 1 addition & 0 deletions src/material-experimental/mdc-table/table.ts
Expand Up @@ -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.mdc-table-fixed-layout]': 'fixedLayout',
},
providers: [
{provide: CdkTable, useExisting: MatTable},
Expand Down
4 changes: 4 additions & 0 deletions src/material/table/table.scss
Expand Up @@ -122,3 +122,7 @@ th.mat-header-cell, td.mat-cell, td.mat-footer-cell {
.mat-table-sticky {
@include vendor-prefixes.position-sticky;
}

.mat-table-fixed-layout {
table-layout: fixed;
}
1 change: 1 addition & 0 deletions src/material/table/table.ts
Expand Up @@ -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
Expand Down

0 comments on commit 58e0c48

Please sign in to comment.