Skip to content

Commit ad96443

Browse files
feat(cdk/table): Virtual scroll directive for tables
1 parent 30cfd7d commit ad96443

32 files changed

+19569
-34
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
/src/cdk-experimental/menu/** @jelbourn @andy9775
135135
/src/cdk-experimental/popover-edit/** @kseamon @andrewseguin
136136
/src/cdk-experimental/scrolling/** @mmalerba
137+
/src/cdk-experimental/table/** @michaeljamesparsons @andrewseguin
137138
/src/cdk-experimental/table-scroll-container/** @kseamon @andrewseguin
138139
/src/cdk-experimental/listbox/** @nielsr98 @jelbourn
139140
/src/cdk-experimental/selection/** @yifange @jelbourn

package-lock.json

Lines changed: 18826 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cdk-experimental/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CDK_EXPERIMENTAL_ENTRYPOINTS = [
88
"popover-edit",
99
"scrolling",
1010
"selection",
11+
"table",
1112
"table-scroll-container",
1213
]
1314

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
load(
2+
"//tools:defaults.bzl",
3+
"ng_module",
4+
"ng_test_library",
5+
"ng_web_test_suite",
6+
)
7+
8+
package(default_visibility = ["//visibility:public"])
9+
10+
ng_module(
11+
name = "table",
12+
srcs = glob(
13+
["**/*.ts"],
14+
exclude = ["**/*.spec.ts"],
15+
),
16+
module_name = "@angular/cdk-experimental/table",
17+
deps = [
18+
"//src/cdk/bidi",
19+
"//src/cdk/platform",
20+
"//src/cdk/table",
21+
"@npm//@angular/common",
22+
"@npm//@angular/core",
23+
"@npm//rxjs",
24+
],
25+
)
26+
27+
ng_test_library(
28+
name = "unit_test_sources",
29+
srcs = glob(
30+
["**/*.spec.ts"],
31+
exclude = ["**/*.e2e.spec.ts"],
32+
),
33+
deps = [
34+
":table",
35+
"//src/cdk/collections",
36+
"//src/cdk/platform",
37+
"//src/cdk/table",
38+
"@npm//rxjs",
39+
],
40+
)
41+
42+
ng_web_test_suite(
43+
name = "unit_tests",
44+
deps = [":unit_test_sources"],
45+
)

src/cdk-experimental/table/index.ts

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 './table-virtual-scroll';
10+
export * from './table-module';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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 {CdkTableModule as TableModule} from '@angular/cdk/table';
11+
12+
import {CdkTableVirtualScroll} from './table-virtual-scroll';
13+
14+
15+
16+
@NgModule({
17+
declarations: [CdkTableVirtualScroll],
18+
exports: [CdkTableVirtualScroll],
19+
imports: [
20+
TableModule,
21+
],
22+
})
23+
export class CdkTableModule {}
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
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+
import {
9+
Directive,
10+
Inject,
11+
Input,
12+
OnDestroy,
13+
SkipSelf,
14+
} from '@angular/core';
15+
import {
16+
_RecycleViewRepeaterStrategy,
17+
_VIEW_REPEATER_STRATEGY,
18+
ListRange
19+
} from '@angular/cdk/collections';
20+
import {
21+
_TABLE_COLLECTION_VIEWER,
22+
CdkTable,
23+
RenderRow,
24+
RowContext,
25+
STICKY_POSITIONING_LISTENER,
26+
StickyPositioningListener,
27+
StickyUpdate
28+
} from '@angular/cdk/table';
29+
import {
30+
BehaviorSubject,
31+
combineLatest,
32+
Observable,
33+
ReplaySubject,
34+
Subject,
35+
} from 'rxjs';
36+
import {
37+
shareReplay,
38+
takeUntil
39+
} from 'rxjs/operators';
40+
import {
41+
CdkVirtualScrollRepeater,
42+
CdkVirtualScrollViewport,
43+
} from '@angular/cdk/scrolling';
44+
45+
46+
/**
47+
* An implementation of {@link StickyPositioningListener} that forwards sticky updates to another
48+
* listener.
49+
*
50+
* The {@link CdkTableVirtualScroll} directive cannot provide itself a
51+
* {@link StickyPositioningListener} because it introduces a circular dependency. The directive
52+
* instead provides this class and attaches itself as the receiving listener.
53+
*/
54+
export class _PositioningListenerProxy implements StickyPositioningListener {
55+
private _listener?: StickyPositioningListener;
56+
57+
setListener(listener: StickyPositioningListener) {
58+
this._listener = listener;
59+
}
60+
61+
stickyColumnsUpdated(update: StickyUpdate): void {
62+
this._listener?.stickyColumnsUpdated(update);
63+
}
64+
65+
stickyEndColumnsUpdated(update: StickyUpdate): void {
66+
this._listener?.stickyEndColumnsUpdated(update);
67+
}
68+
69+
stickyFooterRowsUpdated(update: StickyUpdate): void {
70+
this._listener?.stickyFooterRowsUpdated(update);
71+
}
72+
73+
stickyHeaderRowsUpdated(update: StickyUpdate): void {
74+
this._listener?.stickyHeaderRowsUpdated(update);
75+
}
76+
}
77+
78+
79+
/**
80+
* A directive that enables virtual scroll for a {@link CdkTable}.
81+
*/
82+
@Directive({
83+
selector: 'cdk-table[virtualScroll], table[cdk-table][virtualScroll]',
84+
exportAs: 'cdkVirtualScroll',
85+
providers: [
86+
{provide: _VIEW_REPEATER_STRATEGY, useClass: _RecycleViewRepeaterStrategy},
87+
// The directive cannot provide itself as the sticky positions listener because it introduces
88+
// a circular dependency. Use an intermediate listener as a proxy.
89+
{provide: STICKY_POSITIONING_LISTENER, useClass: _PositioningListenerProxy},
90+
// Initially emit an empty range. The virtual scroll viewport will update the range after it is
91+
// initialized.
92+
{
93+
provide: _TABLE_COLLECTION_VIEWER,
94+
useFactory: () => new BehaviorSubject<ListRange>({start: 0, end: 0}),
95+
},
96+
],
97+
host: {
98+
'class': 'cdk-table-virtual-scroll',
99+
},
100+
})
101+
export class CdkTableVirtualScroll<T>
102+
implements CdkVirtualScrollRepeater<T>, OnDestroy, StickyPositioningListener {
103+
/** Emits when the component is destroyed. */
104+
private _destroyed = new ReplaySubject<void>(1);
105+
106+
/** Emits when the header rows sticky state changes. */
107+
private readonly _headerRowStickyUpdates = new Subject<StickyUpdate>();
108+
109+
/** Emits when the footer rows sticky state changes. */
110+
private readonly _footerRowStickyUpdates = new Subject<StickyUpdate>();
111+
112+
/**
113+
* Observable that emits the data source's complete data set. This exists to implement
114+
* {@link CdkVirtualScrollRepeater}.
115+
*/
116+
get dataStream(): Observable<readonly T[]> {
117+
return this._dataStream;
118+
}
119+
private _dataStream = this._table._dataStream.pipe(shareReplay(1));
120+
121+
/**
122+
* The size of the cache used to store unused views. Setting the cache size to `0` will disable
123+
* caching.
124+
*/
125+
@Input()
126+
get viewCacheSize(): number {
127+
return this._viewRepeater.viewCacheSize;
128+
}
129+
set viewCacheSize(size: number) {
130+
this._viewRepeater.viewCacheSize = size;
131+
}
132+
133+
constructor(
134+
private readonly _table: CdkTable<T>,
135+
@Inject(_TABLE_COLLECTION_VIEWER) private readonly _viewChange: BehaviorSubject<ListRange>,
136+
@Inject(STICKY_POSITIONING_LISTENER) positioningListener: _PositioningListenerProxy,
137+
@Inject(_VIEW_REPEATER_STRATEGY)
138+
private readonly _viewRepeater: _RecycleViewRepeaterStrategy<T, RenderRow<T>, RowContext<T>>,
139+
@SkipSelf() private readonly _viewport: CdkVirtualScrollViewport) {
140+
positioningListener.setListener(this);
141+
142+
// Force the table to enable `fixedLayout` to prevent column widths from changing as the user
143+
// scrolls. This also enables caching in the table's sticky styler which reduces calls to
144+
// expensive DOM APIs, such as `getBoundingClientRect()`, and improves overall performance.
145+
if (!this._table.fixedLayout && (typeof ngDevMode === 'undefined' || ngDevMode)) {
146+
throw Error('[virtualScroll] expects input `fixedLayout` to be set on the table.');
147+
}
148+
149+
// Update sticky styles for header rows when either the render range or sticky state change.
150+
combineLatest([this._viewport._renderedContentOffsetRendered, this._headerRowStickyUpdates])
151+
.pipe(takeUntil(this._destroyed))
152+
.subscribe(([offset, update]) => {
153+
this._stickHeaderRows(offset, update);
154+
});
155+
156+
// Update sticky styles for footer rows when either the render range or sticky state change.
157+
combineLatest([this._viewport._renderedContentOffsetRendered, this._footerRowStickyUpdates])
158+
.pipe(takeUntil(this._destroyed))
159+
.subscribe(([offset, update]) => {
160+
this._stickFooterRows(offset, update);
161+
});
162+
163+
// Forward the rendered range computed by the virtual scroll viewport to the table.
164+
this._viewport.renderedRangeStream.pipe(takeUntil(this._destroyed)).subscribe(this._viewChange);
165+
this._viewport.attach(this);
166+
}
167+
168+
ngOnDestroy() {
169+
this._destroyed.next();
170+
this._destroyed.complete();
171+
}
172+
173+
/**
174+
* Measures the combined size (width for horizontal orientation, height for vertical) of all items
175+
* in the specified range.
176+
*/
177+
measureRangeSize(range: ListRange, orientation: 'horizontal' | 'vertical'): number {
178+
// TODO(michaeljamesparsons) Implement method so virtual tables can use the `autosize` virtual
179+
// scroll strategy.
180+
console.warn('autoSize is not supported for tables with virtual scroll enabled.');
181+
return 0;
182+
}
183+
184+
stickyColumnsUpdated(update: StickyUpdate): void {
185+
// no-op
186+
}
187+
188+
stickyEndColumnsUpdated(update: StickyUpdate): void {
189+
// no-op
190+
}
191+
192+
stickyHeaderRowsUpdated(update: StickyUpdate): void {
193+
this._headerRowStickyUpdates.next(update);
194+
}
195+
196+
stickyFooterRowsUpdated(update: StickyUpdate): void {
197+
this._footerRowStickyUpdates.next(update);
198+
}
199+
200+
private _stickHeaderRows(offsetFromTop: number, update: StickyUpdate) {
201+
if (!update.sizes || !update.offsets || !update.elements) {
202+
return;
203+
}
204+
205+
for (let i = 0; i < update.elements.length; i++) {
206+
if (!update.elements[i]) {
207+
continue;
208+
}
209+
const offset = offsetFromTop !== 0
210+
? Math.max(offsetFromTop - update.offsets[i]!, update.offsets[i]!)
211+
: -update.offsets[i]!;
212+
this._stickCells(update.elements[i]!, 'top', -offset);
213+
}
214+
}
215+
216+
private _stickFooterRows(offsetFromTop: number, update: StickyUpdate) {
217+
if (!update.sizes || !update.offsets || !update.elements) {
218+
return;
219+
}
220+
221+
for (let i = 0; i < update.elements.length; i++) {
222+
if (!update.elements[i]) {
223+
continue;
224+
}
225+
this._stickCells(update.elements[i]!, 'bottom', offsetFromTop + update.offsets[i]!);
226+
}
227+
}
228+
229+
private _stickCells(cells: HTMLElement[], position: 'bottom'|'top', offset: number) {
230+
for (let j = 0; j < cells.length; j++) {
231+
cells[j].style[position] = `${offset}px`;
232+
}
233+
}
234+
}

src/cdk/collections/recycle-view-repeater-strategy.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ import {
3737
export class _RecycleViewRepeaterStrategy<T, R, C extends _ViewRepeaterItemContext<T>>
3838
implements _ViewRepeater<T, R, C> {
3939
/**
40-
* The size of the cache used to store unused views.
41-
* Setting the cache size to `0` will disable caching. Defaults to 20 views.
40+
* The size of the cache used to store unused views. Setting the cache size to `0` will disable
41+
* caching. Defaults to 100 views.
4242
*/
43-
viewCacheSize: number = 20;
43+
viewCacheSize: number = 100;
4444

4545
/**
4646
* View cache that stores embedded view instances that have been previously stamped out,

src/cdk/scrolling/virtual-for-of.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export class CdkVirtualForOf<T> implements
140140

141141
/**
142142
* The size of the cache used to store templates that are not being used for re-use later.
143-
* Setting the cache size to `0` will disable caching. Defaults to 20 templates.
143+
* Setting the cache size to `0` will disable caching. Defaults to 100 templates.
144144
*/
145145
@Input()
146146
get cdkVirtualForTemplateCacheSize() {

0 commit comments

Comments
 (0)