First look at the runtime effect of my Angular application that supports infinite scroll:
https://jerry-infinite-scroller.stackblitz.io/
Scroll the middle mouse button and scroll down to trigger the list to continuously initiate requests to the background to load new data:
The following are the specific development steps.
(1) The source code of app.component.html:
<div>
<h2>{{ title }}</h2>
<ul
id="infinite-scroller"
appInfiniteScroller
scrollPerecnt="70"
[immediateCallback]="true"
[scrollCallback]="scrollCallback"
>
<li *ngFor="let item of news">{{ item.title }}</li>
</ul>
</div>
Here we apply a custom directive appInfiniteScroller to the list element ul, thus giving it the ability to support infinite scroll.
[scrollCallback]="scrollCallback" This line statement, the former is the input attribute of custom execution, the latter is a function defined by the app Component, which is used to specify what kind of business logic should be executed when the scroll event of the list occurs.
The app component has a property news of type collection, which is expanded by the structure instruction ngFor and displayed as a list line item.
(2) Implementation of app Component:
import { Component } from '@angular/core';
import { HackerNewsService } from './hacker-news.service';
import { tap } from 'rxjs/operators';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
currentPage: number = 1;
title = '';
news: Array<any> = [];
scrollCallback;
constructor(private hackerNewsSerivce: HackerNewsService) {
this.scrollCallback = this.getStories.bind(this);
}
getStories() {
return this.hackerNewsSerivce
.getLatestStories(this.currentPage)
.pipe(tap(this.processData));
// .do(this.processData);
}
private processData = (news) => {
this.currentPage++;
this.news = this.news.concat(news);
};
}
Bind the function getStories to the attribute scrollCallback, so that when the list scroll event occurs, the getStories function is called to read the stories data of a new page, and the result is merged into the array attribute this.news. The logic for reading Stories is done in hackerNewsService.
(3) hackerNewsService is consumed by app Component through dependency injection.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
const BASE_URL = 'https://node-hnapi.herokuapp.com';
@Injectable()
export class HackerNewsService {
constructor(private http: HttpClient) {}
getLatestStories(page: number = 1) {
return this.http.get(`${BASE_URL}/news?page=${page}`);
}
}
(4) The core part is the custom command.
import { Directive, AfterViewInit, ElementRef, Input } from '@angular/core';
import { fromEvent } from 'rxjs';
import { pairwise, map, exhaustMap, filter, startWith } from 'rxjs/operators';
interface ScrollPosition {
sH: number;
sT: number;
cH: number;
}
const DEFAULT_SCROLL_POSITION: ScrollPosition = {
sH: 0,
sT: 0,
cH: 0,
};
@Directive({
selector: '[appInfiniteScroller]',
})
export class InfiniteScrollerDirective implements AfterViewInit {
private scrollEvent$;
private userScrolledDown$;
// private requestStream$;
private requestOnScroll$;
@Input()
scrollCallback;
@Input()
immediateCallback;
@Input()
scrollPercent = 70;
constructor(private elm: ElementRef) {}
ngAfterViewInit() {
this.registerScrollEvent();
this.streamScrollEvents();
this.requestCallbackOnScroll();
}
private registerScrollEvent() {
this.scrollEvent$ = fromEvent(this.elm.nativeElement, 'scroll');
}
private streamScrollEvents() {
this.userScrolledDown$ = this.scrollEvent$.pipe(
map(
(e: any): ScrollPosition => ({
sH: e.target.scrollHeight,
sT: e.target.scrollTop,
cH: e.target.clientHeight,
})
),
pairwise(),
filter(
(positions) =>
this.isUserScrollingDown(positions) &&
this.isScrollExpectedPercent(positions[1])
)
);
}
private requestCallbackOnScroll() {
this.requestOnScroll$ = this.userScrolledDown$;
if (this.immediateCallback) {
this.requestOnScroll$ = this.requestOnScroll$.pipe(
startWith([DEFAULT_SCROLL_POSITION, DEFAULT_SCROLL_POSITION])
);
}
this.requestOnScroll$
.pipe(
exhaustMap(() => {
return this.scrollCallback();
})
)
.subscribe(() => {});
}
private isUserScrollingDown = (positions) => {
return positions[0].sT < positions[1].sT;
};
private isScrollExpectedPercent = (position) => {
return (position.sT + position.cH) / position.sH > this.scrollPercent / 100;
};
}
First define a ScrollPosition interface, containing three fields sH
, sT and cH, respectively maintain three fields of the scroll event object: scrollHeight, scrollTop and clientHeight.
We construct a scrollEvent$Observable object from the scroll event of the dom element to which the custom directive is applied. In this way, when the scroll event occurs, scrollEvent$ will automatically emit the event object.
Because most of the property information of this event object is not of interest to us, we use map to map the scroll event object to the three fields we are only interested in: scrollHeight, scrollTop and clientHeight:
But only with the data of these three points, we can't determine the scroll direction of the current list.
So use the operator provided by rxjs pairwise, put the coordinates generated by every two clicks into an array, and then use the function this.isUserScrollingDown to judge the scrolling direction of the current user.
If the scrollTop of the next element is larger than the previous element, it means scrolling down:
private isUserScrollingDown = (positions) => {
return positions[0].sT < positions[1].sT;
};
Instead of detecting that the current user scrolls down, we immediately trigger an HTTP request to load the next page of data, but we have to exceed a threshold.
The implementation logic of this threshold is as follows:
private isScrollExpectedPercent = (position) => {
console.log('Jerry position: ', position);
const reachThreshold =
(position.sT + position.cH) / position.sH > this.scrollPercent / 100;
const percent = ((position.sT + position.cH) * 100) / position.sH;
console.log('reach threshold: ', reachThreshold, ' percent: ', percent);
return reachThreshold;
};
As shown in the following figure: When the threshold reaches 70, return true:
More Jerry's original articles, all in: "Wang Zixi":
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。