Tutorial

How To Add Infinite Scrolling within a Virtual Scroll Viewport Using the Angular CDK

Published on September 20, 2019
Default avatar

By Chris Engelsma

How To Add Infinite Scrolling within a Virtual Scroll Viewport Using the Angular CDK

This tutorial is out of date and no longer maintained.

Introduction

When dealing with a large amount of content, you may find it useful to add an infinite scroll feature to your site. When I say infinite scrolling, I’m referring to a page appending new content as the user continues to scroll, giving the page the illusion of scrolling indefinitely. Loads of websites use this feature, and it can be a fluid alternative to something like pagination.

While there are myriad different ways to implement this, let’s explore how we can accomplish this using the Angular Component Dev Kit (CDK).

Setting Up the Project

Let’s start by adding the @angular/cdk package to our project:

  1. npm install @angular/cdk

To leverage the infinite scrolling functionality in this package, import ScrollingModule to your app.module.ts:

app.module.ts
import { ScrollingModule} from '@angular/cdk/scrolling';

Then add it to your imports:

app.module.ts
imports: [
  ScrollingModule
]

You’re now ready to start!

Implementing Infinite Scroll

We’re going to build a component that displays random historical events. When the user reaches the end of the scroll, our application will load more facts.

Animated gif of scrolling through a list of historical events.

For the purpose of this tutorial, I’m going to gloss over the details of building the service.

For the time being, assume that we have a FactService that only provides the following function:

getRandomFact();

We’ll be pulling 10 facts at a time, and each time the user scrolls to the end, we’ll query for 10 more facts.

Building the FactScrollerComponent

Construct a new component that will act as your infinite scroller. I’m calling mine FactScrollerComponent. You can use the Angular CLI to do this:

  1. ng generate component fact-scroller

Ensure that the new component is imported to your app.module.ts:

app.module.ts
import { ScrollingModule } from '@angular/cdk/scrolling';

And added to the declarations:

app.module.ts
declarations: [
  FactScrollerComponent
]

In our fact-scroller.component.html let’s construct the scroller scaffolding:

fact-scroller.component.html
<cdk-virtual-scroll-viewport itemSize="100">
  <li *cdkVirtualFor="let fact of dataSource">
      <!-- Print stuff here -->
  </li>
</cdk-virtual-scroll-viewport>

Here we use a cdk-virtual-scroll-viewport to be our virtual scroller. Within this, we loop over our items using *cdkVirtualFor, which is analogous as using *ngFor.

In order for the component to properly size its internal scroller, we need to tell the scroller how tall each item will be (in pixels). This is done using the itemSize directive. So, itemSize="100" means that item in the list will require 100px of height.

We’ve also told the scroller to pull the data from dataSource, which doesn’t exist yet, so it’s best we create it now.

Building the FactsDataSource

In our fact-scroller.component.ts file, we need to define what our data source looks like. To do this, we’ll extend the DataSource class in @angular/cdk/collections. Here’s what our data source looks like:

fact-scroller.component.ts
import { CollectionViewer, DataSource } from '@angular/cdk/collections';

export interface Fact {
  text?: string;
  date?: string;
}

export class FactsDataSource extends DataSource<Fact | undefined> {
  private cachedFacts = Array.from<Fact>({ length: 0 });
  private dataStream = new BehaviorSubject<(Fact | undefined)[]>(this.cachedFacts);
  private subscription = new Subscription();

  constructor(private factService: FactService) {
    super();
  }

  connect(collectionViewer: CollectionViewer): Observable<(Fact | undefined)[] | ReadonlyArray<Fact | undefined>> {
    this.subscription.add(collectionViewer.viewChange.subscribe(range => {
      // Update the data
    }));
    return this.dataStream;
  }

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

There’s a lot to digest here, so let’s break it down.

We first define our model, Fact, which will define our data structure.

Within FactsDataSource, we need to implement two functions: connect(), and disconnect(). The data source is subscribed to any changes in the collection viewer (e.g., the user scrolls), and will then perform an action and return the data stream. We are going to tell the data source to get more data when we have reached the end of the list.

We also declared three member variables:

  • cachedFacts: our cached results,
  • dataStream: a RxJS BehaviorSubject to propagate changes to our cached results, and
  • subscription: a subscription to listen for view collection changes.

Let’s define a few helpers within this class:

fact-scroller.component.ts
private pageSize = 10;
private lastPage = 0;

private _fetchFactPage(): void {
  for (let i = 0; i < this.pageSize; ++i) {
    this.factService.getRandomFact().subscribe(res => {
      this.cachedFacts = this.cachedFacts.concat(res);
      this.dataStream.next(this.cachedFacts);
    });
  }
}

private _getPageForIndex(i: number): number {
  return Math.floor(i / this.pageSize);
}

I’m setting the page size to 10, meaning I want to grab 10 facts at a time. I’m also going to keep track of the last page loaded.

_fetchFactPage() makes a call to our service to get some facts, which are then appended to the cache.

_getPageForIndex() will convert an line index to a page (or batch) value.

Putting these all together, we can then define how we want the list to update within the subscription callback:

fact-scroller.component.ts
connect(collectionViewer: CollectionViewer): Observable<(Fact | undefined)[] | ReadonlyArray<Fact | undefined>> {
  this.subscription.add(collectionViewer.viewChange.subscribe(range => {

    const currentPage = this._getPageForIndex(range.end);

    if (currentPage > this.lastPage) {
      this.lastPage = currentPage;
      this._fetchFactPage();
    }

  }));
  return this.dataStream;
}

We also want to start with some data, so we can make a call to our fetch function in the constructor:

fact-scroller.component.ts
constructor(private factService: FactService) {
  super();

  // Start with some data.
  this._fetchFactPage();
}

Our custom data source should now get us where we need to go. The final piece to put it all together is to add our new data source to the component.

fact-scroller.component.ts
@Component({
  selector: 'app-fact-scroller',
  templateUrl: './fact-scroller.component.html',
  styleUrls: ['./fact-scroller.component.scss']
})
export class FactScrollerComponent {

  dataSource: FactsDataSource;

  constructor(private factService: FactService) {
    this.dataSource = new FactsDataSource(factService);
  }

}

And we’re done! Everything from here on out is formatting. I’ve rewritten my HTML to display the facts like so:

fact-scroller.component.html
<cdk-virtual-scroll-viewport itemSize="100" class="fact-scroll-viewport">
  <li *cdkVirtualFor="let fact of dataSource">

    <div *ngIf="fact" class="fact-item">
      <div class="fact-date">{{ fact.year }}</div>
      <div class="fact-text">{{ fact.text }}</div>
    </div>
    <div *ngIf="!fact">
      Loading ...
    </div>

  </li>
</cdk-virtual-scroll-viewport>

Happy Scrolling!

Conclusion

Continue your learning with the Angular CDK documentation.

You can find the complete source code for this example project in this repo GitHub.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Chris Engelsma

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
1 Comments


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

nice article, ty :)

but for real, this:

private cachedFacts = Array.from<Fact>({ length: 0 });

instead of this? :)

private cachedFacts: Array<Fact> = []

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel