Angular 7 Virtual Scroll example - Angular Material CDK
Tags: angular, cdk, virtualscroll
Published: 2021-03-10 15:38:03

https://grokonez.com/frontend/angular/angular-7/angular-7-virtual-scroll-example-angular-material-cdk

The <code>@angular/cdk/scrolling</code> module with a technique called <strong>Virtual Scrolling</strong> helps us display a big list of elements efficiently by only rendering the items in view. <strong>Virtual Scrolling</strong> is different from infinite scroll - where it renders batches of elements and then when user goes to bottom of the list, it renders the rest. In this tutorial, we create many simple examples that show you how to work with Angular 7 Material CDK – Virtual Scrolling.

Related Post: <a href="https://grokonez.com/frontend/angular-7-drag-and-drop-example-angular-material-cdk">Angular 7 Drag and Drop example – Angular Material CDK</a>

<!--more-->
<h2 id="setup-angular-7-material-cdk">Setup Angular 7 Material CDK</h2>
- Run cmd:
<code>npm install @angular/cdk</code>

- Import <code>ScrollingModule</code> into <code>NgModule</code>:
<pre><code class="languag-java">...
import { ScrollingModule } from '@angular/cdk/scrolling';

@NgModule({
  declarations: [...],
  imports: [
    ...
    ScrollingModule
  ],
  ...
})
export class AppModule { }</code></pre>
<h2 id="angular-7-virtual-scroll">Angular 7 Virtual Scroll</h2>
Generate a list of 1000 random number items:
<pre><code class="language-java">items = Array(1000).fill(0).map(() => Math.round(Math.random() * 100));</code></pre>
<h2 id="create-viewport">Create Viewport</h2>
We use <code>&#60;cdk-virtual-scroll-viewport&#62;</code> directive with <code>*cdkVirtualFor</code> inside (loop same as <code>*ngFor</code>).
To indicate the size of each item (it maybe height or width depending on the orientation of the Viewport), we use <code>itemSize</code> input property.
<pre><code class="language-html">&lt;cdk-virtual-scroll-viewport class=&quot;gkz-viewport&quot; [itemSize]=&quot;20&quot;&gt;
  &lt;div *cdkVirtualFor=&quot;let item of items; let i = index&quot; class=&quot;item&quot;&gt;
    Item # { { i}}:  { { item}}
  &lt;/div&gt;
&lt;/cdk-virtual-scroll-viewport&gt;</code></pre>
Style with <code>.cdk-virtual-scroll-content-wrapper</code> (wrapper element that contains the rendered content):
<pre><code class="language-css">.gkz-viewport {
    height: 200px;
    width: 200px;
    border: 1px solid black;
    .cdk-virtual-scroll-content-wrapper {
        display: flex;
        flex-direction: column;
    }
    .item {
        height: 19px;
        border-bottom: 1px dashed #aaa;
    }
}</code></pre>
<img src="https://grokonez.com/wp-content/uploads/2018/11/angular-virtual-scrolling-create-viewport.gif" alt="angular-virtual-scroll-example-create-viewport" width="596" height="500" class="alignnone size-full wp-image-15690" />
<h2 id="use-context-variables">Use Context Variables</h2>
With <code>*cdkVirtualFor</code>, we have some context variables: <code>index</code>, <code>count</code>, <code>first</code>, <code>last</code>, <code>even</code>, <code>odd</code>.
<pre><code class="language-html">&lt;cdk-virtual-scroll-viewport class=&quot;gkz-viewport&quot; [itemSize]=&quot;20*6&quot;&gt;
  &lt;div [class.item-alternate]=&quot;odd&quot; *cdkVirtualFor=&quot;
    let item of items;
    let i = index;
    let count = count;
    let first = first;
    let last = last;
    let even = even;
    let odd = odd;&quot;&gt;
    &lt;div class=&quot;item&quot;&gt;Item # { { i}}:  { { item}}&lt;/div&gt;
    &lt;div class=&quot;item&quot;&gt;count:  { { count}}&lt;/div&gt;
    &lt;div class=&quot;item&quot;&gt;first:  { { first}}&lt;/div&gt;
    &lt;div class=&quot;item&quot;&gt;last:  { { last}}&lt;/div&gt;
    &lt;div class=&quot;item&quot;&gt;even:  { { even ? 'Yes' : 'No'}}&lt;/div&gt;
    &lt;div class=&quot;item&quot;&gt;odd:  { { odd ? 'Yes' : 'No'}}&lt;/div&gt;
  &lt;/div&gt;
&lt;/cdk-virtual-scroll-viewport&gt;</code></pre>
With <code>item-alternate</code> style:
<pre><code class="language-css">
.gkz-viewport {
    ...
    .item-alternate {
        background: #ddd;
    }
}</code></pre>

<img src="https://grokonez.com/wp-content/uploads/2018/11/angular-virtual-scrolling-example-custom-variables.gif" alt="angular-virtual-scroll-example-custom-variables" width="240" height="300" class="alignnone size-full wp-image-15692" />
<h2 id="reduce-memory-on-caching">Reduce memory on Caching</h2>
By default, <code>*cdkVirtualFor</code> caches created items to improve rendering performance.
We can adjusted size of the view cache using <code>templateCacheSize</code> property (0 means disabling caching).
<pre><code class="language-html">&lt;cdk-virtual-scroll-viewport class=&quot;gkz-viewport&quot; [itemSize]=&quot;20&quot;&gt;
  &lt;div *cdkVirtualFor=&quot;let item of items; let i = index; templateCacheSize: 0&quot; class=&quot;item&quot;&gt;
    Item # { { i}}:  { { item}}
  &lt;/div&gt;
&lt;/cdk-virtual-scroll-viewport&gt;</code></pre>
<h2 id="horizontal-viewport">Horizontal Viewport</h2>
By default, the orientation of Viewport is vertical, we can change it by setting <code>orientation="horizontal"</code>.
Notice that, instead of height, <code>itemSize</code> property in horizontal orienttation specifies <em>width</em>.
<pre><code class="language-html">&lt;cdk-virtual-scroll-viewport orientation=&quot;horizontal&quot; itemSize=&quot;50&quot; class=&quot;gkz-horizontal-viewport&quot;&gt;
  &lt;div *cdkVirtualFor=&quot;let item of items; let i = index&quot; class=&quot;item&quot;&gt;
    Item # { { i}}:  { { item}}
  &lt;/div&gt;
&lt;/cdk-virtual-scroll-viewport&gt;</code></pre>
We also need to target CSS at <code>.cdk-virtual-scroll-content-wrapper</code> (the wrapper element contains the rendered content).
<pre><code class="language-css">.gkz-horizontal-viewport {
    height: 120px;
    width: 200px;
    border: 1px solid black;
    .cdk-virtual-scroll-content-wrapper {
        display: flex;
        flex-direction: row;
    }
    .item {
        width: 48px;
        height: 100px;
        border: 1px dashed #aaa;
        display: inline-flex;
        justify-content: center;
        align-items: center;
        writing-mode: vertical-lr;
    }
}</code></pre>

<img src="https://grokonez.com/wp-content/uploads/2018/11/angular-virtual-scrolling-example-horizontal-viewport.gif" alt="angular-virtual-scroll-example-horizontal-viewport" width="336" height="452" class="alignnone size-full wp-image-15693" />
<h2 id="custom-buffer-parameters">Custom Buffer Parameters</h2>
When using <code>itemSize</code> directive for fixed size strategy, we can set 2 more buffer parameters: <code>minBufferPx</code> & <code>maxBufferPx</code> that calculate the extra content to be rendered beyond visible items in the Viewport:
- if the Viewport detects that there is less buffered content than <code>minBufferPx</code> it will immediately render more.
- the Viewport will render at least enough buffer to get back to <code>maxBufferPx</code>.

For example, we set: <code>itemSize = 20</code>, <code>minBufferPx = 50</code>, <code>maxBufferPx = 200</code>.
The viewport detects that buffer remaining is only <code>40px</code> (2 items).
- <code>40px</code> < <code>minBufferPx</code>: render more buffer.
- render an additional <code>160px</code> (8 items) to bring the total buffer size to <code>40px + 160px = 200px</code> >= <code>maxBufferPx</code>.

<img src="https://grokonez.com/wp-content/uploads/2018/11/angular-virtual-scrolling-example-custom-buffer-params.gif" alt="angular-virtual-scroll-example-custom-buffer-params" width="624" height="444" class="alignnone size-full wp-image-15695" />
<h2 id="angular-7-virtual-scroll-with-specific-data">Angular 7 Virtual Scroll with Specific Data</h2>
As well as <code>Array</code>, <code>*cdkVirtualFor</code> can work with <code>DataSource</code> or <code>Observable&#60;Array&#62;</code>.
<h2 id="datasource">DataSource</h2>
A <code>DataSource</code> is an abstract class with two methods:
- <code>connect()</code>: is called by the Viewport to receive a stream that emits the data array.
- <code>disconnect()</code>: will be invoked when the Viewport is destroyed.

In this example, we will create a <code>DataSource</code> with <code>1000</code> items, and simulate fetching data from server using <code>setTimeout()</code> function.
<pre><code class="language-javascript">import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { DataSource, CollectionViewer } from '@angular/cdk/collections';
...
export class SpecDataComponent {
  dataSource = new MyDataSource();
}

export class MyDataSource extends DataSource&lt;string | undefined&gt; {
  private PAGE_SIZE = 10;
  private fetchedPages = new Set&lt;number&gt;();

  private cachedData = Array.from&lt;string&gt;({ length: 1000 });
  private dataStream = new BehaviorSubject&lt;(string | undefined)[]&gt;(this.cachedData);

  private subscription = new Subscription();

  connect(collectionViewer: CollectionViewer): Observable&lt;(string | undefined)[]&gt; {
    this.subscription.add(collectionViewer.viewChange.subscribe(range =&gt; {
      const startPage = this.getPageForIndex(range.start);
      const endPage = this.getPageForIndex(range.end);
      for (let i = startPage; i &lt;= endPage; i++) {
        this.fetchPage(i);
      }
    }));

    return this.dataStream;
  }

  disconnect(): void {
    this.subscription.unsubscribe();
  }

  private getPageForIndex(index: number): number {
    return Math.floor(index / this.PAGE_SIZE);
  }

  private fetchPage(page: number) {
    if (this.fetchedPages.has(page)) {
      return;
    }
    this.fetchedPages.add(page);

    // simulate fetching data from server
    setTimeout(() =&gt; {
      this.cachedData.splice(page * this.PAGE_SIZE, this.PAGE_SIZE,
        ...Array.from({ length: this.PAGE_SIZE })
          .map((_, i) =&gt; `Item #${page * this.PAGE_SIZE + i}`));

      this.dataStream.next(this.cachedData);
    }, 2000);
  }
}</code></pre>
Iterating <code>DataSource</code> by <code>*cdkVirtualFor</code> is just like working with an <code>Array</code>:
<pre><code class="language-html">&lt;cdk-virtual-scroll-viewport itemSize=&quot;20&quot; class=&quot;gkz-viewport&quot;&gt;
  &lt;div *cdkVirtualFor=&quot;let item of dataSource&quot; class=&quot;item&quot;&gt; { { item || 'Loading...'}}&lt;/div&gt;
&lt;/cdk-virtual-scroll-viewport&gt;</code></pre>

<img src="https://grokonez.com/wp-content/uploads/2018/11/angular-virtual-scrolling-example-datasource.gif" alt="angular-virtual-scroll-example-datasource" width="244" height="296" class="alignnone size-full wp-image-15703" />
<h2 id="observable">Observable</h2>
We create an <code>BehaviorSubject</code> that emit an <code>Array</code> everytime user clicks on Button:
<pre><code class="language-javascript">observableData = new BehaviorSubject&lt;number[]&gt;([]);

emitData() {
  const items = Array(3).fill(0).map(() =&gt; Math.round(Math.random() * 100));
  const data = this.observableData.value.concat(items);
  this.observableData.next(data);
}</code></pre>
Iterating Observable Data by <code>*cdkVirtualFor</code> is just like working with an <code>Array</code>:
<pre><code class="language-html">&lt;button (click)=&quot;emitData()&quot;&gt;Add item&lt;/button&gt;
&lt;cdk-virtual-scroll-viewport class=&quot;gkz-viewport&quot; [itemSize]=&quot;20&quot;&gt;
  &lt;div *cdkVirtualFor=&quot;let item of observableData | async; let i = index&quot; class=&quot;item&quot;&gt;
    Item # { { i}}:  { { item}}
  &lt;/div&gt;
&lt;/cdk-virtual-scroll-viewport&gt;</code></pre>

<img src="https://grokonez.com/wp-content/uploads/2018/11/angular-virtual-scrolling-example-observable-data.gif" alt="angular-virtual-scroll-example-observable-data" width="252" height="308" class="alignnone size-full wp-image-15709" />
<h2 id="angular-7-virtual-scroll-trackby">Angular 7 Virtual Scroll trackBy</h2>
We create an Observable data that will be changed (sorted):
<pre><code class="language-javascript">import { BehaviorSubject } from 'rxjs';

interface Customer {
  id: number;
  name: string;
}

export class TrackbyComponent {
  customers = [
    { id: 1, name: 'John Bailey' },
    { id: 2, name: 'Amelia Kerr' },
    { id: 3, name: 'Julian Wallace' },
    { id: 4, name: 'Pippa Sutherland' },
    { id: 5, name: 'Stephanie Simpson' },
    ...
  ];

  customersObservable = new BehaviorSubject(this.customers);

  sortBy(prop: 'id' | 'name') {
    this.customersObservable.next(this.customers.map(s =&gt; ({ ...s })).sort((a, b) =&gt; {
      const aProp = a[prop], bProp = b[prop];
      if (aProp &lt; bProp) {
        return -1;
      } else if (aProp &gt; bProp) {
        return 1;
      }
      return 0;
    }));
  }
}</code></pre>
To check <code>trackBy</code>, we will test 3 cases:
- no <code>trackBy</code> function
- <code>trackBy</code> index
- <code>trackBy</code> <em>name</em> field
<h2 id="no-trackby-function">No trackBy function</h2>
<pre><code class="language-html">
&lt;button (click)=&quot;sortBy('id')&quot;&gt;Sort by id&lt;/button&gt;
&lt;button (click)=&quot;sortBy('name')&quot;&gt;Sort by name&lt;/button&gt;
&lt;cdk-virtual-scroll-viewport class=&quot;gkz-viewport&quot; [itemSize]=&quot;20&quot;&gt;
  &lt;div *cdkVirtualFor=&quot;let customer of customersObservable | async&quot; class=&quot;customer-item&quot;&gt;
     { { customer.id}} -  { { customer.name}}
  &lt;/div&gt;
&lt;/cdk-virtual-scroll-viewport&gt;</code></pre>

<img src="https://grokonez.com/wp-content/uploads/2018/11/angular-virtual-scrolling-example-no-trackby.gif" alt="angular-virtual-scroll-example-no-trackby" width="780" height="404" class="alignnone size-full wp-image-15710" />
<h2 id="with-trackby-function">With trackBy function</h2>
<code>trackBy</code> function works the same as the <code>*ngFor trackBy</code>.
<pre><code class="language-javascript">indexTrackFn = (index: number) =&gt; index;
nameTrackFn = (_: number, item: Customer) =&gt; item.name;</code></pre>
<h2 id="trackby-index">trackBy index</h2>
<pre><code class="language-html">&lt;button (click)=&quot;sortBy('id')&quot;&gt;Sort by id&lt;/button&gt;
&lt;button (click)=&quot;sortBy('name')&quot;&gt;Sort by name&lt;/button&gt;
&lt;cdk-virtual-scroll-viewport class=&quot;gkz-viewport&quot; [itemSize]=&quot;20&quot;&gt;
  &lt;div *cdkVirtualFor=&quot;let customer of customersObservable | async; trackBy: indexTrackFn&quot; class=&quot;customer-item&quot;&gt;
     { { customer.id}} -  { { customer.name}}
  &lt;/div&gt;
&lt;/cdk-virtual-scroll-viewport&gt;</code></pre>

<img src="https://grokonez.com/wp-content/uploads/2018/11/angular-virtual-scrolling-example-trackby-index.gif" alt="angular-virtual-scroll-example-trackby-index" width="780" height="404" class="alignnone size-full wp-image-15712" />
<h2 id="trackby-a-field">trackBy a field</h2>
<pre><code class="language-html">&lt;button (click)=&quot;sortBy('id')&quot;&gt;Sort by id&lt;/button&gt;
&lt;button (click)=&quot;sortBy('name')&quot;&gt;Sort by name&lt;/button&gt;
&lt;cdk-virtual-scroll-viewport class=&quot;gkz-viewport&quot; [itemSize]=&quot;20&quot;&gt;
  &lt;div *cdkVirtualFor=&quot;let customer of customersObservable | async; trackBy: nameTrackFn&quot; class=&quot;customer-item&quot;&gt;
     { { customer.id}} -  { { customer.name}}
  &lt;/div&gt;
&lt;/cdk-virtual-scroll-viewport&gt;</code></pre>

<img src="https://grokonez.com/wp-content/uploads/2018/11/angular-virtual-scrolling-example-trackby-field.gif" alt="angular-virtual-scroll-example-trackby-field" width="780" height="404" class="alignnone size-full wp-image-15713" />
<h2 id="angular-7-virtual-scroll-strategy">Angular 7 Virtual Scroll Strategy</h2>
Instead of using the <code>itemSize</code> directive on the Viewport, we can provide a custom strategy by creating a class that implements the <code>VirtualScrollStrategy</code> interface and providing it as the <code>VIRTUAL_SCROLL_STRATEGY</code> on the <code>@Component</code>.
<pre><code class="language-javascript">import { Component, ChangeDetectionStrategy } from '@angular/core';
import { FixedSizeVirtualScrollStrategy, VIRTUAL_SCROLL_STRATEGY } from '@angular/cdk/scrolling';

export class CustomVirtualScrollStrategy extends FixedSizeVirtualScrollStrategy {
  constructor() {
    super(20, 50, 200); // (itemSize, minBufferPx, maxBufferPx)
  }
}

@Component({
  selector: 'app-scrolling-strategy',
  templateUrl: './scrolling-strategy.component.html',
  styleUrls: ['./scrolling-strategy.component.scss'],
  providers: [{ provide: VIRTUAL_SCROLL_STRATEGY, useClass: CustomVirtualScrollStrategy }]
})
export class ScrollingStrategyComponent {
  items = Array(1000).fill(0).map(() =&gt; Math.round(Math.random() * 100));
}</code></pre>
In the example, our custom strategy class extends <code>FixedSizeVirtualScrollStrategy</code> that implements <code>VirtualScrollStrategy</code>.
<pre><code class="language-html">&lt;cdk-virtual-scroll-viewport class=&quot;gkz-viewport&quot;&gt;
  &lt;div *cdkVirtualFor=&quot;let item of items; let i = index&quot; class=&quot;item&quot;&gt;
    Item # { { i}}:  { { item}}
  &lt;/div&gt;
&lt;/cdk-virtual-scroll-viewport&gt;</code></pre>

<img src="https://grokonez.com/wp-content/uploads/2018/11/angular-virtual-scrolling-example-scroll-strategy.gif" alt="angular-virtual-scroll-example-scroll-strategy" width="676" height="416" class="alignnone size-full wp-image-15715" />
<h2 id="sourcecode">Source Code</h2>

<a href="https://grokonez.com/wp-content/uploads/2018/11/AngularMaterialScrolling.zip">AngularMaterialScrolling</a>