Saturday, September 14, 2024

Infinite Scrolling the Angular 6 and RxJS Way!

Infinite Scrolling the Angular 6 and RxJS Way!

An infinite-scroll-list is one that loads content asynchronously when the user scrolls down to a certain point in the viewport. It’s a lot more fluid than having the user click on a “Load More” button to see more items. Here‘s an example.

You can write a scroll handler yourself if you know what you’re doing, or just employ one of the several specialized infinite scrolling client-side libraries. For modern Angular applications, it’s a slightly more complicated task to get working, due to its server-centric nature. I recently found myself facing this exact challenge. After some experimentation, I came up with a solution that combined Angular’s EventManager with RxJS’s Observable and Subject classes. Here’s what I did.

The ScrollService

It makes sense to implement the scrolling handler as a service. You’ll need to install the RxJS library. We’ll be using a number of its classes and methods to process the Angular EventManager onscroll event. The EventManager is an injectable service that provides event management for Angular through a browser plug-in. All we need to do to use it is, add an argument in our scroll service’s constructor. We can then assign our handler as a global event listener:

@Injectable()
export class ScrollService {

    private _scrollPercent: number = 80;
    private scrollSubject: Subject<Document> = new Subject();

    constructor(private eventManager: EventManager) {
        this.eventManager.addGlobalEventListener('window', 'scroll', this.onScroll.bind(this));
    }
}

A RxJS Subject is a special type of Observable that are like EventEmitters, in that they can multicast values to many Observers. The onScroll method passes along the Document object to those Observers:

private scrollSubject: Subject<Document> = new Subject();

private onScroll = (event: UIEvent) => 
	this.scrollSubject.next(<Document>event.target);

Appending Items to the List

Back in our AppComponent, we can inject our ScrollService into the constructor and subscribe to its onScrolledDown$ method in order to load more list items. To do that, all we need to do is increase the itemCount.

export class AppComponent {
  private ngUnsubscribe = new Subject();
  itemCount = 25;
  title = "Angular 6 + RxJS Infinite Scrolling Demo";

  constructor(
    private scrollService: ScrollService
  ) {}

  public ngOnInit() {
    // Handle scroll event on containing dialog so we can load more results if necessary
   this.scrollService.onScrolledDown$
     .pipe(takeUntil(this.ngUnsubscribe))
     .subscribe(() => this.fetchMoreItems());
  }

  private fetchMoreItems() {
    // add more items
    this.itemCount += 10;
  }

  public ngOnDestroy () {
		// Remove event handlers
		this.ngUnsubscribe.next();
		this.ngUnsubscribe.complete();
	}
}

The template will update its contents automatically!

This is list item {{ i+1 }}</h2>
</ng-container>

The onScrolledDown$ Method

OnScrolledDown$ is a getter method that returns our scroll Subject as an Observable. That way, we can subscribe to it.

The onscroll event is one that fires constantly, whenever the page is being scrolled either up or down. We can do a couple of things to narrow down scrolling events to those that we’re interested in. for instance, we can use throttle() to reduce firing to every half-second. Moreover, we can employ pairwise() two compare successive scroll events to test whether the user is scrolling up or down. We only want to process down scrolling, and only those that occur when the end of the page is in sight.

interface Position {
	scrollHeight: number,
	scrollTop:    number,
	clientHeight: number
}

get onScrolledDown$(): Observable<[Position, Position]> {
	return this.onScroll$
		.pipe(throttle(() => interval(500)))
		.pipe(
			map(doc => {
				return { 
					scrollHeight: doc.documentElement.scrollHeight,
					scrollTop:    doc.documentElement.scrollTop || doc.body.scrollTop,
					clientHeight: doc.documentElement.clientHeight
				};
			}
		))
		.pipe(pairwise())
		.pipe(filter(positions => this.isUserScrollingDown(positions) 
							   && this.isScrollExpectedPercent(positions[1], this._scrollPercent))
		);
}

Here is the full source for the ScrollService. It shows how we determine which direction the page is being scrolled, as well as the percentage scrolled:

import { EventManager } from '@angular/platform-browser';
import { Observable, Subject, interval } from 'rxjs';
import { Injectable } from '@angular/core';
import { map, pairwise, filter, throttle } from 'rxjs/operators';

interface Position {
	scrollHeight: number,
	scrollTop:    number,
	clientHeight: number
}

@Injectable()
export class ScrollService {

    private _scrollPercent: number = 80;
    private scrollSubject: Subject<Document> = new Subject();

    constructor(private eventManager: EventManager) {
        this.eventManager.addGlobalEventListener('window', 'scroll', this.onScroll.bind(this));
    }
    
    private onScroll = (event: UIEvent) => this.scrollSubject.next(<Document>event.target);

	private isUserScrollingDown = (positions:Array<Position>) => positions[0].scrollTop < positions[1].scrollTop;
	
    private isScrollExpectedPercent = (position:Position, percent:number) => 
        ((position.scrollTop + position.clientHeight) / position.scrollHeight) > (percent/100);

    get scrollPercent(): number {
        return this._scrollPercent;
    }

    set scrollPercent(scrollPercent: number) {
        this._scrollPercent = scrollPercent;
    }

    get onScroll$(): Observable<Document> {
        return this.scrollSubject.asObservable();
    }

	get onScrolledDown$(): Observable<[Position, Position]> {
        return this.onScroll$
            .pipe(throttle(() => interval(500)))
            .pipe(
                map(doc => {
                    return { 
                        scrollHeight: doc.documentElement.scrollHeight,
                        scrollTop:    doc.documentElement.scrollTop || doc.body.scrollTop,
                        clientHeight: doc.documentElement.clientHeight
                    };
                }
            ))
            .pipe(pairwise())
            .pipe(filter(positions => this.isUserScrollingDown(positions) 
                                   && this.isScrollExpectedPercent(positions[1], this._scrollPercent))
            );
    }
}

Here’s a screenshot of the list in the browser:

You can explore the source code for this tutorial here.

The working demo can be found here.

Conclusion

Both Angular and RxJS have radically changed how web applications are coded. Long gone are the days when you wrote raw JavaScript to attach event handlers via the on*=”” HTML Element attribute! The RxJS library is immense and includes functionality for just about any reactive programming use cases that you’re likely to encounter. I would suggest that you visit the rxjs.dev site and check out all that it has to offer. There are plenty of tutorials and a very detailed API reference.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured