Skip to content

feat(cdk/table) scrollable table body #20414

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cdk-experimental/config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CDK_EXPERIMENTAL_ENTRYPOINTS = [
"menu",
"listbox",
"popover-edit",
"table",
"scrolling",
"selection",
"table-scroll-container",
Expand Down
35 changes: 35 additions & 0 deletions src/cdk-experimental/table/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")

package(default_visibility = ["//visibility:public"])

ng_module(
name = "table",
srcs = glob(
["**/*.ts"],
exclude = ["**/*.spec.ts"],
),
module_name = "@angular/cdk-experimental/table",
deps = [
"//src/cdk/table",
"@npm//@angular/common",
"@npm//@angular/core",
"@npm//rxjs",
],
)

ng_test_library(
name = "unit_test_sources",
srcs = glob(
["**/*.spec.ts"],
exclude = ["**/*.e2e.spec.ts"],
),
deps = [
":table",
"//src/cdk/table",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_test_sources"],
)
9 changes: 9 additions & 0 deletions src/cdk-experimental/table/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* @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
*/

export * from './public-api';
10 changes: 10 additions & 0 deletions src/cdk-experimental/table/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @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
*/

export * from './scrollable-table-body-layout';
export * from './scrollable-table-body-module';
89 changes: 89 additions & 0 deletions src/cdk-experimental/table/scrollable-table-body-layout.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {Component, Input, ViewChild} from '@angular/core';
import {async, fakeAsync, TestBed} from '@angular/core/testing';
import {CdkTable, CdkTableModule} from '@angular/cdk/table';
import {CdkScrollableTableBodyModule} from './scrollable-table-body-module';


describe('CdkScrollableTableBody', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [CdkScrollableTableBodyModule, CdkTableModule],
declarations: [CdkTableWithScrollableBody],
}).compileComponents();
}));

it('wraps row outlets in container', fakeAsync(() => {
const fixture = TestBed.createComponent(CdkTableWithScrollableBody);
const testComponent = fixture.componentInstance;
fixture.detectChanges();
const table = testComponent.table;
const headerOutletContainer = table._headerRowOutlet.elementRef.nativeElement.parentElement;
const rowOutletContainer = table._rowOutlet.elementRef.nativeElement.parentElement;
const footerOutletContainer = table._footerRowOutlet.elementRef.nativeElement.parentElement;
testComponent.maxHeight = '100px';

expect(headerOutletContainer.classList.contains('cdk-table-scrollable-table-header'))
.toBe(true);
expect(rowOutletContainer.classList.contains('cdk-table-scrollable-table-body'))
.toBe(true);
expect(footerOutletContainer.classList.contains('cdk-table-scrollable-table-footer'))
.toBe(true);
}));

it('updates DOM when max height is changed', fakeAsync(() => {
const fixture = TestBed.createComponent(CdkTableWithScrollableBody);
const testComponent = fixture.componentInstance;
fixture.detectChanges();
const table = testComponent.table;
const rowOutletContainer = table._rowOutlet.elementRef.nativeElement.parentElement;

testComponent.maxHeight = '100px';
fixture.detectChanges();
expect(rowOutletContainer.style.maxHeight).toBe('100px');

testComponent.maxHeight = '200px';
fixture.detectChanges();
expect(rowOutletContainer.style.maxHeight).toBe('200px');
}));
});

interface TestData {
a: string;
b: string;
c: string;
}

@Component({
template: `
<cdk-table [dataSource]="dataSource" [scrollableBody]="maxHeight">
<ng-container cdkColumnDef="column_a">
<cdk-header-cell *cdkHeaderCellDef> Column A </cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.a}} </cdk-cell>
<cdk-footer-cell *cdkFooterCellDef> Footer A </cdk-footer-cell>
</ng-container>

<ng-container cdkColumnDef="column_b">
<cdk-header-cell *cdkHeaderCellDef> Column B </cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.b}} </cdk-cell>
<cdk-footer-cell *cdkFooterCellDef> Footer B </cdk-footer-cell>
</ng-container>

<ng-container cdkColumnDef="column_c">
<cdk-header-cell *cdkHeaderCellDef> Column C </cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.c}} </cdk-cell>
<cdk-footer-cell *cdkFooterCellDef> Footer C </cdk-footer-cell>
</ng-container>

<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
<cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
<cdk-footer-row *cdkFooterRowDef="columnsToRender"></cdk-footer-row>
</cdk-table>
`
})
class CdkTableWithScrollableBody {
dataSource: [];
columnsToRender = ['column_a', 'column_b', 'column_c'];

@Input() maxHeight!: string;
@ViewChild(CdkTable) table: CdkTable<TestData>;
}
115 changes: 115 additions & 0 deletions src/cdk-experimental/table/scrollable-table-body-layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* @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 {Directive, Inject, Injectable, Input} from '@angular/core';
import {CdkTable} from '@angular/cdk/table/table';
import {DOCUMENT} from '@angular/common';
import {
_TABLE_LAYOUT_STRATEGY,
_TableLayoutStrategy,
_StandardTableLayoutStrategy,
} from '@angular/cdk/table/table-layout-strategy';

/**
* A {@link _TableLayoutStrategy} that enables scrollable body content for flex tables.
*/
@Injectable()
export class ScrollableTableBodyLayoutStrategy<T> implements _TableLayoutStrategy<T> {
private readonly _document: Document;
private defaultLayout: _StandardTableLayoutStrategy<T>;
private _pendingMaxHeight = 'none';
private _scrollViewport?: HTMLElement;
readonly headerCssClass = 'cdk-table-scrollable-table-header';
readonly bodyCssClass = 'cdk-table-scrollable-table-body';
readonly footerCssClass = 'cdk-table-scrollable-table-footer';

constructor(@Inject(DOCUMENT) document: any) {
this._document = document;
this.defaultLayout = new _StandardTableLayoutStrategy(this._document);
}

/**
* Returns the DOM structure for a native table. Scrollable body content is not supported for
* native tables. Return `null` to use the default {@link CdkTable} native table layout.
*/
getNativeLayout(table: CdkTable<T>): DocumentFragment {
return this.defaultLayout.getNativeLayout(table);
}

/**
* Returns the DOM structure for a flex table with scrollable body content. Each row outlet
* (header, body, footer) is wrapped in a separate container. The specified max height is applied
* to the body row outlet to make its content scrollable.
*/
getFlexLayout(table: CdkTable<T>): DocumentFragment {
const documentFragment = this._document.createDocumentFragment();
const sections = [
{cssClass: this.headerCssClass, outlets: [table._headerRowOutlet]},
{cssClass: this.bodyCssClass, outlets: [table._rowOutlet, table._noDataRowOutlet]},
{cssClass: this.footerCssClass, outlets: [table._footerRowOutlet]},
];

for (const section of sections) {
const element = this._document.createElement('div');
element.classList.add(section.cssClass);
for (const outlet of section.outlets) {
element.appendChild(outlet.elementRef.nativeElement);
}

documentFragment.appendChild(element);
}

this._scrollViewport = documentFragment.querySelector(`.${this.bodyCssClass}`) as HTMLElement;
this._scrollViewport!.style.overflow = 'auto';
this._applyMaxHeight(this._scrollViewport!, this._pendingMaxHeight);

return documentFragment;
}

/**
* Show a scroll bar if the table's body exceeds this height. The height may be specified with
* any valid CSS unit of measurement.
*/
setMaxHeight(v: string) {
this._pendingMaxHeight = v;
if (this._scrollViewport) {
this._applyMaxHeight(this._scrollViewport, v);
}
}

private _applyMaxHeight(el: HTMLElement, maxHeight: string) {
el.style.maxHeight = maxHeight;
}
}

/** A directive that enables scrollable body content for flex tables. */
@Directive({
selector: 'cdk-table[scrollableBody]',
providers: [
{provide: _TABLE_LAYOUT_STRATEGY, useClass: ScrollableTableBodyLayoutStrategy},
]
})
export class CdkScrollableTableBody<T> {
/**
* Show a scroll bar if the table's body exceeds this height. The height may be specified with
* any valid CSS unit of measurement.
*/
@Input('scrollableBody')
get maxHeight() {
return this._maxHeight;
}
set maxHeight(v: string) {
this._maxHeight = v;
this._layoutStrategy.setMaxHeight(v);
}
private _maxHeight = '';

constructor(@Inject(_TABLE_LAYOUT_STRATEGY)
private readonly _layoutStrategy: ScrollableTableBodyLayoutStrategy<T>) {
}
}
22 changes: 22 additions & 0 deletions src/cdk-experimental/table/scrollable-table-body-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @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 {NgModule} from '@angular/core';
import {CdkScrollableTableBody} from './scrollable-table-body-layout';

export {CdkScrollableTableBody};

const EXPORTED_DECLARATIONS = [
CdkScrollableTableBody,
];

@NgModule({
exports: EXPORTED_DECLARATIONS,
declarations: EXPORTED_DECLARATIONS,
})
export class CdkScrollableTableBodyModule { }
58 changes: 58 additions & 0 deletions src/cdk/table/table-layout-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @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 {Inject, InjectionToken} from '@angular/core';
import {CdkTable} from '@angular/cdk/table/table';
import {DOCUMENT} from '@angular/common';

/** Interface for a service that constructs the DOM structure for a {@link CdkTable}. */
export interface _TableLayoutStrategy<T> {
/** Constructs the DOM structure for a native table. */
getNativeLayout(table: CdkTable<T>): DocumentFragment;
/** Constructs the DOM structure for a flex table. */
getFlexLayout(table: CdkTable<T>): DocumentFragment;
}

/** Injection token for {@link _TableLayoutStrategy}. */
export const _TABLE_LAYOUT_STRATEGY =
new InjectionToken<_TableLayoutStrategy<unknown>>('_TableLayoutStrategy');


export class _StandardTableLayoutStrategy<T> implements _TableLayoutStrategy<T> {
private readonly _document: Document;

constructor(@Inject(DOCUMENT) document: any) {
this._document = document;
}

getNativeLayout(table: CdkTable<any>): DocumentFragment {
const documentFragment = this._document.createDocumentFragment();
const sections = [
{tag: 'thead', outlets: [table._headerRowOutlet]},
{tag: 'tbody', outlets: [table._rowOutlet, table._noDataRowOutlet]},
{tag: 'tfoot', outlets: [table._footerRowOutlet]},
];

for (const section of sections) {
const element = this._document.createElement(section.tag);
element.setAttribute('role', 'rowgroup');

for (const outlet of section.outlets) {
element.appendChild(outlet.elementRef.nativeElement);
}

documentFragment.appendChild(element);
}

return documentFragment;
}

getFlexLayout(table: CdkTable<any>): DocumentFragment {
return this._document.createDocumentFragment();
}
}
Loading