Skip to content

Commit bdab139

Browse files
mmalerbajosephperrott
authored andcommitted
virtual-scroll: add support for user-provided content wrapper (#12183)
1 parent 1fb1fab commit bdab139

File tree

7 files changed

+127
-11
lines changed

7 files changed

+127
-11
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,13 @@ export type CdkVirtualForOfContext<T> = {
4949
};
5050

5151

52-
/** Helper to extract size from a ClientRect. */
53-
function getSize(orientation: 'horizontal' | 'vertical', rect: ClientRect): number {
52+
/** Helper to extract size from a DOM Node. */
53+
function getSize(orientation: 'horizontal' | 'vertical', node: Node): number {
54+
const el = node as Element;
55+
if (!el.getBoundingClientRect) {
56+
return 0;
57+
}
58+
const rect = el.getBoundingClientRect();
5459
return orientation == 'horizontal' ? rect.width : rect.height;
5560
}
5661

@@ -193,15 +198,14 @@ export class CdkVirtualForOf<T> implements CollectionViewer, DoCheck, OnDestroy
193198
const rangeLen = range.end - range.start;
194199

195200
// Loop over all root nodes for all items in the range and sum up their size.
196-
// TODO(mmalerba): Make this work with non-element nodes.
197201
let totalSize = 0;
198202
let i = rangeLen;
199203
while (i--) {
200204
const view = this._viewContainerRef.get(i + renderedStartIndex) as
201205
EmbeddedViewRef<CdkVirtualForOfContext<T>> | null;
202206
let j = view ? view.rootNodes.length : 0;
203207
while (j--) {
204-
totalSize += getSize(orientation, (view!.rootNodes[j] as Element).getBoundingClientRect());
208+
totalSize += getSize(orientation, view!.rootNodes[j]);
205209
}
206210
}
207211

src/cdk-experimental/scrolling/virtual-scroll-viewport.scss

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
1+
// When elements such as `<tr>` or `<li>` are repeated inside the cdk-virtual-scroll-viewport,
2+
// their container element (e.g. `<table>`, `<ul>`, etc.) needs to be placed in the viewport as
3+
// well. We reset some properties here to prevent these container elements from introducing
4+
// additional space that would throw off the the scrolling calculations.
5+
@mixin _cdk-virtual-scroll-clear-container-space($direction) {
6+
$start: if($direction == horizontal, 'left', 'top');
7+
$end: if($direction == horizontal, 'right', 'bottom');
8+
9+
& > dl:not([cdkVirtualFor]),
10+
& > ol:not([cdkVirtualFor]),
11+
& > table:not([cdkVirtualFor]),
12+
& > ul:not([cdkVirtualFor]) {
13+
padding: {
14+
#{$start}: 0;
15+
#{$end}: 0;
16+
}
17+
margin: {
18+
#{$start}: 0;
19+
#{$end}: 0;
20+
}
21+
border: {
22+
#{$start}-width: 0;
23+
#{$end}-width: 0;
24+
}
25+
outline: none;
26+
}
27+
}
28+
29+
130
// Scrolling container.
231
cdk-virtual-scroll-viewport {
332
display: block;
@@ -16,10 +45,12 @@ cdk-virtual-scroll-viewport {
1645

1746
.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper {
1847
bottom: 0;
48+
@include _cdk-virtual-scroll-clear-container-space(horizontal);
1949
}
2050

2151
.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
2252
right: 0;
53+
@include _cdk-virtual-scroll-clear-container-space(vertical);
2354
}
2455

2556
// Spacer element that whose width or height will be adjusted to match the size of the entire data

src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('CdkVirtualScrollViewport', () => {
2727
finishInit(fixture);
2828

2929
const contentWrapper =
30-
viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper');
30+
viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper')!;
3131
expect(contentWrapper.children.length)
3232
.toBe(4, 'should render 4 50px items to fill 200px space');
3333
}));
@@ -507,7 +507,7 @@ describe('CdkVirtualScrollViewport', () => {
507507
finishInit(fixture);
508508

509509
const contentWrapper =
510-
viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper');
510+
viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper')!;
511511
expect(contentWrapper.children.length)
512512
.toBe(4, 'should render 4 50px items to fill 200px space');
513513
}));
@@ -517,7 +517,7 @@ describe('CdkVirtualScrollViewport', () => {
517517
finishInit(fixture);
518518

519519
const contentWrapper =
520-
viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper');
520+
viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper')!;
521521
expect(contentWrapper.children.length).toBe(4,
522522
'should render 4 items to fill 200px space based on 50px estimate from first item');
523523
}));

src/cdk-experimental/scrolling/virtual-scroll-viewport.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
5757
@Input() orientation: 'horizontal' | 'vertical' = 'vertical';
5858

5959
/** The element that wraps the rendered content. */
60-
@ViewChild('contentWrapper') _contentWrapper: ElementRef;
60+
@ViewChild('contentWrapper') _contentWrapper: ElementRef<HTMLElement>;
6161

6262
/** A stream that emits whenever the rendered range changes. */
6363
renderedRangeStream: Observable<ListRange> = this._renderedRangeSubject.asObservable();
@@ -67,7 +67,10 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
6767
*/
6868
_totalContentSize = 0;
6969

70-
/** The the rendered content transform. */
70+
/**
71+
* The CSS transform applied to the rendered subset of items so that they appear within the bounds
72+
* of the visible viewport.
73+
*/
7174
private _renderedContentTransform: string;
7275

7376
/** The currently rendered range of indices. */
@@ -103,7 +106,7 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
103106
/** A list of functions to run after the next change detection cycle. */
104107
private _runAfterChangeDetection: Function[] = [];
105108

106-
constructor(public elementRef: ElementRef,
109+
constructor(public elementRef: ElementRef<HTMLElement>,
107110
private _changeDetectorRef: ChangeDetectorRef,
108111
private _ngZone: NgZone,
109112
@Inject(VIRTUAL_SCROLL_STRATEGY) private _scrollStrategy: VirtualScrollStrategy) {}
@@ -327,7 +330,7 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
327330
}
328331
}
329332

330-
for (let fn of this._runAfterChangeDetection) {
333+
for (const fn of this._runAfterChangeDetection) {
331334
fn();
332335
}
333336
this._runAfterChangeDetection = [];

src/cdk-experimental/scrolling/virtual-scroll.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,21 @@ content.
142142
...
143143
</cdk-virtual-scroll-viewport>
144144
```
145+
146+
### Elements with parent tag requirements
147+
Some HTML elements such as `<tr>` and `<li>` have limitations on the kinds of parent elements they
148+
can be placed inside. To enable virtual scrolling over these type of elements, place the elements in
149+
their proper parent, and then wrap the whole thing in a `cdk-virtual-scroll-viewport`. Be careful
150+
that the parent does not introduce additional space (e.g. via `margin` or `padding`) as it will
151+
interfere with the scrolling.
152+
153+
```html
154+
<cdk-virtual-scroll-viewport itemSize="50">
155+
<table>
156+
<tr *cdkVirtualFor="let row of rows">
157+
<td>{{row.first}}</td>
158+
<td>{{row.second}}</td>
159+
</tr>
160+
</table>
161+
</cdk-virtual-scroll-viewport>
162+
```

src/demo-app/virtual-scroll/virtual-scroll-demo.html

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,45 @@ <h2>trackBy state name</h2>
9494
<div class="demo-capital">{{state.capital}}</div>
9595
</div>
9696
</cdk-virtual-scroll-viewport>
97+
98+
<h2>Use with <code>&lt;ol&gt;</code></h2>
99+
100+
<cdk-virtual-scroll-viewport class="demo-viewport" autosize #viewport>
101+
<ol class="demo-ol" [start]="viewport.getRenderedRange().start + 1">
102+
<li *cdkVirtualFor="let state of statesObservable | async" class="demo-li">
103+
{{state.name}} - {{state.capital}}
104+
</li>
105+
</ol>
106+
</cdk-virtual-scroll-viewport>
107+
108+
<h2>Use with <code>&lt;ul&gt;</code></h2>
109+
110+
<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
111+
<ul class="demo-ul">
112+
<li *cdkVirtualFor="let state of statesObservable | async" class="demo-li">
113+
{{state.name}} - {{state.capital}}
114+
</li>
115+
</ul>
116+
</cdk-virtual-scroll-viewport>
117+
118+
<h2>Use with <code>&lt;dl&gt;</code></h2>
119+
120+
<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
121+
<dl class="demo-dl">
122+
<ng-container *cdkVirtualFor="let state of statesObservable | async">
123+
<dt class="demo-dt">{{state.name}}</dt>
124+
<dd class="demo-dd">{{state.capital}}</dd>
125+
</ng-container>
126+
</dl>
127+
</cdk-virtual-scroll-viewport>
128+
129+
<h2>Use with <code>&lt;table&gt;</code></h2>
130+
131+
<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
132+
<table class="demo-ol">
133+
<tr *cdkVirtualFor="let state of statesObservable | async" class="demo-tr">
134+
<td class="demo-td">{{state.name}}</td>
135+
<td class="demo-td">{{state.capital}}</td>
136+
</tr>
137+
</table>
138+
</cdk-virtual-scroll-viewport>

src/demo-app/virtual-scroll/virtual-scroll-demo.scss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,21 @@
3737
.demo-capital {
3838
font-size: 14px;
3939
}
40+
41+
.demo-dt {
42+
height: 30px;
43+
font-weight: bold;
44+
}
45+
46+
.demo-dd {
47+
height: 30px;
48+
}
49+
50+
.demo-li,
51+
.demo-td {
52+
height: 50px;
53+
}
54+
55+
.demo-td {
56+
border: 1px solid gray;
57+
}

0 commit comments

Comments
 (0)