Skip to content

virtual-scroll: add support for user-provided content wrapper #12183

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

Merged
merged 3 commits into from
Jul 18, 2018
Merged
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
12 changes: 8 additions & 4 deletions src/cdk-experimental/scrolling/virtual-for-of.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@ export type CdkVirtualForOfContext<T> = {
};


/** Helper to extract size from a ClientRect. */
function getSize(orientation: 'horizontal' | 'vertical', rect: ClientRect): number {
/** Helper to extract size from a DOM Node. */
function getSize(orientation: 'horizontal' | 'vertical', node: Node): number {
const el = node as Element;
if (!el.getBoundingClientRect) {
return 0;
}
const rect = el.getBoundingClientRect();
return orientation == 'horizontal' ? rect.width : rect.height;
}

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

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

Expand Down
31 changes: 31 additions & 0 deletions src/cdk-experimental/scrolling/virtual-scroll-viewport.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
// When elements such as `<tr>` or `<li>` are repeated inside the cdk-virtual-scroll-viewport,
// their container element (e.g. `<table>`, `<ul>`, etc.) needs to be placed in the viewport as
// well. We reset some properties here to prevent these container elements from introducing
// additional space that would throw off the the scrolling calculations.
@mixin _cdk-virtual-scroll-clear-container-space($direction) {
$start: if($direction == horizontal, 'left', 'top');
$end: if($direction == horizontal, 'right', 'bottom');

& > dl:not([cdkVirtualFor]),
& > ol:not([cdkVirtualFor]),
& > table:not([cdkVirtualFor]),
& > ul:not([cdkVirtualFor]) {
padding: {
#{$start}: 0;
#{$end}: 0;
}
margin: {
#{$start}: 0;
#{$end}: 0;
}
border: {
#{$start}-width: 0;
#{$end}-width: 0;
}
outline: none;
}
}


// Scrolling container.
cdk-virtual-scroll-viewport {
display: block;
Expand All @@ -16,10 +45,12 @@ cdk-virtual-scroll-viewport {

.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper {
bottom: 0;
@include _cdk-virtual-scroll-clear-container-space(horizontal);
}

.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
right: 0;
@include _cdk-virtual-scroll-clear-container-space(vertical);
}

// Spacer element that whose width or height will be adjusted to match the size of the entire data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('CdkVirtualScrollViewport', () => {
finishInit(fixture);

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

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

const contentWrapper =
viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper');
viewport.elementRef.nativeElement.querySelector('.cdk-virtual-scroll-content-wrapper')!;
expect(contentWrapper.children.length).toBe(4,
'should render 4 items to fill 200px space based on 50px estimate from first item');
}));
Expand Down
11 changes: 7 additions & 4 deletions src/cdk-experimental/scrolling/virtual-scroll-viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
@Input() orientation: 'horizontal' | 'vertical' = 'vertical';

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

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

/** The the rendered content transform. */
/**
* The CSS transform applied to the rendered subset of items so that they appear within the bounds
* of the visible viewport.
*/
private _renderedContentTransform: string;

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

constructor(public elementRef: ElementRef,
constructor(public elementRef: ElementRef<HTMLElement>,
private _changeDetectorRef: ChangeDetectorRef,
private _ngZone: NgZone,
@Inject(VIRTUAL_SCROLL_STRATEGY) private _scrollStrategy: VirtualScrollStrategy) {}
Expand Down Expand Up @@ -327,7 +330,7 @@ export class CdkVirtualScrollViewport implements OnInit, OnDestroy {
}
}

for (let fn of this._runAfterChangeDetection) {
for (const fn of this._runAfterChangeDetection) {
fn();
}
this._runAfterChangeDetection = [];
Expand Down
18 changes: 18 additions & 0 deletions src/cdk-experimental/scrolling/virtual-scroll.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,21 @@ content.
...
</cdk-virtual-scroll-viewport>
```

### Elements with parent tag requirements
Some HTML elements such as `<tr>` and `<li>` have limitations on the kinds of parent elements they
can be placed inside. To enable virtual scrolling over these type of elements, place the elements in
their proper parent, and then wrap the whole thing in a `cdk-virtual-scroll-viewport`. Be careful
that the parent does not introduce additional space (e.g. via `margin` or `padding`) as it will
interfere with the scrolling.

```html
<cdk-virtual-scroll-viewport itemSize="50">
<table>
<tr *cdkVirtualFor="let row of rows">
<td>{{row.first}}</td>
<td>{{row.second}}</td>
</tr>
</table>
</cdk-virtual-scroll-viewport>
```
42 changes: 42 additions & 0 deletions src/demo-app/virtual-scroll/virtual-scroll-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,45 @@ <h2>trackBy state name</h2>
<div class="demo-capital">{{state.capital}}</div>
</div>
</cdk-virtual-scroll-viewport>

<h2>Use with <code>&lt;ol&gt;</code></h2>

<cdk-virtual-scroll-viewport class="demo-viewport" autosize #viewport>
<ol class="demo-ol" [start]="viewport.getRenderedRange().start + 1">
<li *cdkVirtualFor="let state of statesObservable | async" class="demo-li">
{{state.name}} - {{state.capital}}
</li>
</ol>
</cdk-virtual-scroll-viewport>

<h2>Use with <code>&lt;ul&gt;</code></h2>

<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
<ul class="demo-ul">
<li *cdkVirtualFor="let state of statesObservable | async" class="demo-li">
{{state.name}} - {{state.capital}}
</li>
</ul>
</cdk-virtual-scroll-viewport>

<h2>Use with <code>&lt;dl&gt;</code></h2>

<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
<dl class="demo-dl">
<ng-container *cdkVirtualFor="let state of statesObservable | async">
<dt class="demo-dt">{{state.name}}</dt>
<dd class="demo-dd">{{state.capital}}</dd>
</ng-container>
</dl>
</cdk-virtual-scroll-viewport>

<h2>Use with <code>&lt;table&gt;</code></h2>

<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
<table class="demo-ol">
<tr *cdkVirtualFor="let state of statesObservable | async" class="demo-tr">
<td class="demo-td">{{state.name}}</td>
<td class="demo-td">{{state.capital}}</td>
</tr>
</table>
</cdk-virtual-scroll-viewport>
18 changes: 18 additions & 0 deletions src/demo-app/virtual-scroll/virtual-scroll-demo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,21 @@
.demo-capital {
font-size: 14px;
}

.demo-dt {
height: 30px;
font-weight: bold;
}

.demo-dd {
height: 30px;
}

.demo-li,
.demo-td {
height: 50px;
}

.demo-td {
border: 1px solid gray;
}