Skip to content

Commit 6200db9

Browse files
feat(cdk/table) scrollable table body
1 parent 23d3c21 commit 6200db9

26 files changed

+786
-25
lines changed

src/cdk-experimental/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CDK_EXPERIMENTAL_ENTRYPOINTS = [
66
"menu",
77
"listbox",
88
"popover-edit",
9+
"scrollable-table-body",
910
"scrolling",
1011
"selection",
1112
]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
load("//tools:defaults.bzl", "ng_module", "ng_test_library", "ng_web_test_suite")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ng_module(
6+
name = "scrollable-table-body",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
module_name = "@angular/cdk-experimental/scrollable-table-body",
12+
deps = [
13+
"//src/cdk/table",
14+
"@npm//@angular/common",
15+
"@npm//@angular/core",
16+
"@npm//rxjs",
17+
],
18+
)
19+
20+
ng_test_library(
21+
name = "unit_test_sources",
22+
srcs = glob(
23+
["**/*.spec.ts"],
24+
exclude = ["**/*.e2e.spec.ts"],
25+
),
26+
deps = [
27+
":scrollable-table-body",
28+
"//src/cdk/table",
29+
],
30+
)
31+
32+
ng_web_test_suite(
33+
name = "unit_tests",
34+
deps = [":unit_test_sources"],
35+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export * from './public-api';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export * from './scrollable-table-body-layout';
10+
export * from './scrollable-table-body-module';
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {Component, Input, ViewChild} from '@angular/core';
2+
import {async, fakeAsync, TestBed} from '@angular/core/testing';
3+
import {CdkTable, CdkTableModule} from '@angular/cdk/table';
4+
import {CdkScrollableTableBodyModule} from './scrollable-table-body-module';
5+
6+
7+
describe('CdkScrollableTableBody', () => {
8+
beforeEach(async(() => {
9+
TestBed.configureTestingModule({
10+
imports: [CdkScrollableTableBodyModule, CdkTableModule],
11+
declarations: [CdkTableWithScrollableBody],
12+
}).compileComponents();
13+
}));
14+
15+
it('wraps row outlets in container', fakeAsync(() => {
16+
const fixture = TestBed.createComponent(CdkTableWithScrollableBody);
17+
const testComponent = fixture.componentInstance;
18+
fixture.detectChanges();
19+
const table = testComponent.table;
20+
const headerOutletContainer = table._headerRowOutlet.elementRef.nativeElement.parentElement;
21+
const rowOutletContainer = table._rowOutlet.elementRef.nativeElement.parentElement;
22+
const footerOutletContainer = table._footerRowOutlet.elementRef.nativeElement.parentElement;
23+
testComponent.maxHeight = '100px';
24+
25+
expect(headerOutletContainer.classList.contains('cdk-table-scrollable-table-header'))
26+
.toBe(true);
27+
expect(rowOutletContainer.classList.contains('cdk-table-scrollable-table-body'))
28+
.toBe(true);
29+
expect(footerOutletContainer.classList.contains('cdk-table-scrollable-table-footer'))
30+
.toBe(true);
31+
}));
32+
33+
it('updates DOM when max height is changed', fakeAsync(() => {
34+
const fixture = TestBed.createComponent(CdkTableWithScrollableBody);
35+
const testComponent = fixture.componentInstance;
36+
fixture.detectChanges();
37+
const table = testComponent.table;
38+
const rowOutletContainer = table._rowOutlet.elementRef.nativeElement.parentElement;
39+
40+
testComponent.maxHeight = '100px';
41+
fixture.detectChanges();
42+
expect(rowOutletContainer.style.maxHeight).toBe('100px');
43+
44+
testComponent.maxHeight = '200px';
45+
fixture.detectChanges();
46+
expect(rowOutletContainer.style.maxHeight).toBe('200px');
47+
}));
48+
});
49+
50+
interface TestData {
51+
a: string;
52+
b: string;
53+
c: string;
54+
}
55+
56+
@Component({
57+
template: `
58+
<cdk-table [dataSource]="dataSource" [scrollableBody]="maxHeight">
59+
<ng-container cdkColumnDef="column_a">
60+
<cdk-header-cell *cdkHeaderCellDef> Column A </cdk-header-cell>
61+
<cdk-cell *cdkCellDef="let row"> {{row.a}} </cdk-cell>
62+
<cdk-footer-cell *cdkFooterCellDef> Footer A </cdk-footer-cell>
63+
</ng-container>
64+
65+
<ng-container cdkColumnDef="column_b">
66+
<cdk-header-cell *cdkHeaderCellDef> Column B </cdk-header-cell>
67+
<cdk-cell *cdkCellDef="let row"> {{row.b}} </cdk-cell>
68+
<cdk-footer-cell *cdkFooterCellDef> Footer B </cdk-footer-cell>
69+
</ng-container>
70+
71+
<ng-container cdkColumnDef="column_c">
72+
<cdk-header-cell *cdkHeaderCellDef> Column C </cdk-header-cell>
73+
<cdk-cell *cdkCellDef="let row"> {{row.c}} </cdk-cell>
74+
<cdk-footer-cell *cdkFooterCellDef> Footer C </cdk-footer-cell>
75+
</ng-container>
76+
77+
<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
78+
<cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
79+
<cdk-footer-row *cdkFooterRowDef="columnsToRender"></cdk-footer-row>
80+
</cdk-table>
81+
`
82+
})
83+
class CdkTableWithScrollableBody {
84+
dataSource: [];
85+
columnsToRender = ['column_a', 'column_b', 'column_c'];
86+
87+
@Input() maxHeight!: string;
88+
@ViewChild(CdkTable) table: CdkTable<TestData>;
89+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Directive, Inject, Injectable, Input} from '@angular/core';
10+
import {CdkTable} from '@angular/cdk/table/table';
11+
import {DOCUMENT} from '@angular/common';
12+
import {
13+
_TABLE_LAYOUT_STRATEGY,
14+
_TableLayoutStrategy,
15+
_DefaultTableLayoutStrategy,
16+
} from '@angular/cdk/table/table-layout-strategy';
17+
18+
/**
19+
* A {@link _TableLayoutStrategy} that enables scrollable body content for flex tables.
20+
*/
21+
@Injectable()
22+
class ScrollableTableBodyLayoutStrategy implements _TableLayoutStrategy {
23+
private defaultLayout: _DefaultTableLayoutStrategy;
24+
private _pendingMaxHeight = 'none';
25+
private _scrollViewport?: HTMLElement;
26+
readonly headerCssClass = 'cdk-table-scrollable-table-header';
27+
readonly bodyCssClass = 'cdk-table-scrollable-table-body';
28+
readonly footerCssClass = 'cdk-table-scrollable-table-footer';
29+
30+
constructor(@Inject(DOCUMENT) private readonly _document: any) {
31+
this.defaultLayout = new _DefaultTableLayoutStrategy(this._document);
32+
}
33+
34+
/**
35+
* Returns the DOM structure for a native table. Scrollable body content is not supported for
36+
* native tables. Return `null` to use the default {@link CdkTable} native table layout.
37+
*/
38+
getNativeLayout(table: CdkTable<unknown>): DocumentFragment {
39+
return this.defaultLayout.getNativeLayout(table);
40+
}
41+
42+
/**
43+
* Returns the DOM structure for a flex table with scrollable body content. Each row outlet
44+
* (header, body, footer) is wrapped in a separate container. The specified max height is applied
45+
* to the body row outlet to make its content scrollable.
46+
*/
47+
getFlexLayout(table: CdkTable<unknown>): DocumentFragment {
48+
const documentFragment = this._document.createDocumentFragment();
49+
const sections = [
50+
{selector: this.headerCssClass, outlets: [table._headerRowOutlet]},
51+
{selector: this.bodyCssClass, outlets: [table._rowOutlet, table._noDataRowOutlet]},
52+
{selector: this.footerCssClass, outlets: [table._footerRowOutlet]},
53+
];
54+
55+
for (const section of sections) {
56+
const element = this._document.createElement('div');
57+
element.classList.add(section.selector);
58+
for (const outlet of section.outlets) {
59+
element.appendChild(outlet.elementRef.nativeElement);
60+
}
61+
62+
documentFragment.appendChild(element);
63+
}
64+
65+
this._scrollViewport = documentFragment.querySelector(`.${this.bodyCssClass}`);
66+
this._scrollViewport!.style.overflow = 'auto';
67+
this._applyMaxHeight(this._scrollViewport!, this._pendingMaxHeight);
68+
69+
return documentFragment;
70+
}
71+
72+
/**
73+
* Show a scroll bar if the table's body exceeds this height. The height may be specified with
74+
* any valid CSS unit of measurement.
75+
*/
76+
setMaxHeight(v: string) {
77+
this._pendingMaxHeight = v;
78+
if (this._scrollViewport) {
79+
this._applyMaxHeight(this._scrollViewport, v);
80+
}
81+
}
82+
83+
private _applyMaxHeight(el: HTMLElement, maxHeight: string) {
84+
el.style.maxHeight = maxHeight;
85+
}
86+
}
87+
88+
/** A directive that enables scrollable body content for flex tables. */
89+
@Directive({
90+
selector: 'cdk-table[scrollableBody], mat-table[scrollableBody]',
91+
providers: [
92+
{provide: _TABLE_LAYOUT_STRATEGY, useClass: ScrollableTableBodyLayoutStrategy},
93+
]
94+
})
95+
export class CdkScrollableTableBody {
96+
/**
97+
* Show a scroll bar if the table's body exceeds this height. The height may be specified with
98+
* any valid CSS unit of measurement.
99+
*/
100+
@Input('scrollableBody')
101+
get maxHeight() {
102+
return this._maxHeight;
103+
}
104+
set maxHeight(v: string) {
105+
this._maxHeight = v;
106+
this._layoutStrategy.setMaxHeight(v);
107+
}
108+
private _maxHeight = '';
109+
110+
constructor(@Inject(_TABLE_LAYOUT_STRATEGY)
111+
private readonly _layoutStrategy: ScrollableTableBodyLayoutStrategy) {
112+
}
113+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {NgModule} from '@angular/core';
10+
import {CdkScrollableTableBody} from './scrollable-table-body-layout';
11+
12+
export {CdkScrollableTableBody};
13+
14+
const EXPORTED_DECLARATIONS = [
15+
CdkScrollableTableBody,
16+
];
17+
18+
@NgModule({
19+
exports: EXPORTED_DECLARATIONS,
20+
declarations: EXPORTED_DECLARATIONS,
21+
})
22+
export class CdkScrollableTableBodyModule { }
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Inject, InjectionToken} from '@angular/core';
10+
import {CdkTable} from '@angular/cdk/table/table';
11+
import {DOCUMENT} from '@angular/common';
12+
13+
/** Interface for a service that constructs the DOM structure for a {@link CdkTable}. */
14+
export interface _TableLayoutStrategy {
15+
/** Constructs the DOM structure for a native table. */
16+
getNativeLayout(table: CdkTable<any>): DocumentFragment;
17+
/** Constructs the DOM structure for a flex table. */
18+
getFlexLayout(table: CdkTable<any>): DocumentFragment;
19+
}
20+
21+
/** Injection token for {@link _TableLayoutStrategy}. */
22+
export const _TABLE_LAYOUT_STRATEGY =
23+
new InjectionToken<_TableLayoutStrategy>('_TableLayoutStrategy');
24+
25+
26+
export class _DefaultTableLayoutStrategy implements _TableLayoutStrategy {
27+
private readonly _document: Document;
28+
29+
constructor(@Inject(DOCUMENT) document: any) {
30+
this._document = document;
31+
}
32+
33+
getNativeLayout(table: CdkTable<any>): DocumentFragment {
34+
const documentFragment = this._document.createDocumentFragment();
35+
const sections = [
36+
{tag: 'thead', outlets: [table._headerRowOutlet]},
37+
{tag: 'tbody', outlets: [table._rowOutlet, table._noDataRowOutlet]},
38+
{tag: 'tfoot', outlets: [table._footerRowOutlet]},
39+
];
40+
41+
for (const section of sections) {
42+
const element = this._document.createElement(section.tag);
43+
element.setAttribute('role', 'rowgroup');
44+
45+
for (const outlet of section.outlets) {
46+
element.appendChild(outlet.elementRef.nativeElement);
47+
}
48+
49+
documentFragment.appendChild(element);
50+
}
51+
52+
return documentFragment;
53+
}
54+
55+
getFlexLayout(table: CdkTable<any>): DocumentFragment {
56+
return this._document.createDocumentFragment();
57+
}
58+
}

0 commit comments

Comments
 (0)