原文連接: codeburst.io/angular-2-s…css
本文爲 RxJS 中文社區 翻譯文章,如需轉載,請註明出處,謝謝合做!html
若是你也想和咱們一塊兒,翻譯更多優質的 RxJS 文章以奉獻給你們,請點擊【這裏】node
這篇文章是我上篇文章 使用響應式編程來實現簡易版的無限滾動加載 的延續。在本文中,咱們將建立一個 Angular 指令來實現無限滾動加載功能。咱們還將繼續使用 HackerNews 的非官方 API 來獲取數據以填充到頁面中。git
我使用 angular-cli
來搭建項目。github
ng new infinite-scroller-poc --style=scss
複製代碼
項目生成好後,進入 infinite-scroller-poc
目錄下。shell
Angular CLI 提供了一堆命令用來生成組件、指令、服務和模塊。編程
咱們來生成一個服務和一個指令。json
ng g service hacker-news
ng g directive infinite-scroller
複製代碼
注意: Angular CLI 會自動在 app.module.ts
裏註冊指令,但不會將服務添加到 providers
數組中。你須要手動添加。app.module.ts
以下所示。bootstrap
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { InfiniteScrollerDirective } from './infinite-scroller.directive';
import { HackerNewsService } from './hacker-news.service';
@NgModule({
declarations: [
AppComponent,
InfiniteScrollerDirective
],
imports: [
BrowserModule,
HttpModule
],
providers: [HackerNewsService],
bootstrap: [AppComponent]
})
export class AppModule { }
複製代碼
接下來,咱們在服務中添加 HackerNews 的 API 調用。下面是 hacker-news.service.ts
,它只有一個函數 getLatestStories
。api
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
const BASE_URL = 'http://node-hnapi.herokuapp.com';
@Injectable()
export class HackerNewsService {
constructor(private http: Http) { }
getLatestStories(page: number = 1) {
return this.http.get(`${BASE_URL}/news?page=${page}`);
}
}
複製代碼
如今來構建咱們的無限滾動加載指令。下面是指令的完整代碼,別擔憂代碼太長,咱們會分解來看。
import { Directive, AfterViewInit, ElementRef, Input } from '@angular/core';
import { Observable, Subscription } from 'rxjs/Rx';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/pairwise';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/exhaustMap';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/startWith';
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$ = Observable.fromEvent(this.elm.nativeElement, 'scroll');
}
private streamScrollEvents() {
this.userScrolledDown$ = this.scrollEvent$
.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$
.startWith([DEFAULT_SCROLL_POSITION, DEFAULT_SCROLL_POSITION]);
}
this.requestOnScroll$
.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);
}
}
複製代碼
指令接收3個輸入值:
scrollPercent
- 用戶須要滾動到容器的百分比,達到後方可調用 scrollCallback
。scrollCallback
- 返回 observable 的回調函數。immediateCallback
- 布爾值,若是爲 true 則指令初始化後會當即調用 scrollCallback
。Angular 爲組件和指令提供了4個生命週期鉤子。
對於這個指令,咱們想要進入 ngAfterViewInit
生命週期鉤子以註冊和處理滾動事件。在 constructor
中,咱們注入了 ElementRef
,它可讓咱們引用應用了指令的元素,即滾動容器。
constructor(private elm: ElementRef) { }
ngAfterViewInit() {
this.registerScrollEvent();
this.streamScrollEvents();
this.requestCallbackOnScroll();
}
複製代碼
在 ngAfterViewInit
生命週期鉤子中,咱們執行了3個函數:
registerScrollEvent
- 使用 Observable.fromEvent
來監聽元素的滾動事件。streamScrollEvents
- 根據咱們的需求來處理傳入的滾動事件流,當滾動到給定的容器高度百分比時發起 API 請求。requestCallbackOnScroll
- 一旦達到咱們設定的條件後,調用 scrollCallback
來發起 API 請求。還有一個可選的輸入條件 immediateCallback
,若是設置爲 true 的話,咱們會將 DEFAULT_SCROLL_POSITION
做爲流的起始數據,它會觸發 scrollCallback
而無需用戶滾動頁面。這樣的話會調用一次 API 以獲取初始數據展現在頁面中。上述全部函數的做用都與個人上篇文章中是同樣的,上篇文章中已經詳細地解釋了 RxJS Observable 各個操做符的用法,這裏就不贅述了。
接下來將無限滾動指令添加到 AppComponent
中。下面是 app.component.ts
的完整代碼。
import { Component } from '@angular/core';
import { HackerNewsService } from './hacker-news.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
currentPage: number = 1;
news: Array<any> = [];
scrollCallback;
constructor(private hackerNewsSerivce: HackerNewsService) {
this.scrollCallback = this.getStories.bind(this);
}
getStories() {
return this.hackerNewsSerivce.getLatestStories(this.currentPage).do(this.processData);
}
private processData = (news) => {
this.currentPage++;
this.news = this.news.concat(news.json());
}
}
複製代碼
getStories
- 調用 hackerNewsService
並處理返回數據。
注意 constructor 中的 this.scrollCallback
寫法
this.scrollCallback = this.getStories.bind(this);
複製代碼
咱們將 this.getStories
函數賦值給 scrollCallback
並將其上下文綁定爲 this
。這樣能夠確保當回調函數在無限滾動指令裏執行時,它的上下文是 AppComponent
而不是 InfiniteScrollerDirective
。更多關於 .bind
的用法,能夠參考這裏。
<ul id="infinite-scroller" appInfiniteScroller scrollPerecnt="70" immediateCallback="true" [scrollCallback]="scrollCallback" >
<li *ngFor="let item of news">{{item.title}}</li>
</ul>
複製代碼
html 想當簡單,ul
做爲 appInfiniteScroller
指令的容器,同時還傳入了參數 scrollPercent
、immediateCallback
和 scrollCallback
。每一個 li
表示一條新聞,並只顯示新聞的標題。
爲容器設置基礎樣式。
#infinite-scroller {
height: 500px;
width: 700px;
border: 1px solid #f5ad7c;
overflow: scroll;
padding: 0;
list-style: none;
li {
padding : 10px 5px;
line-height: 1.5;
&:nth-child(odd) {
background : #ffe8d8;
}
&:nth-child(even) {
background : #f5ad7c;
}
}
}
複製代碼
下面的示例是使用了 Angular 指令的無限滾動加載,注意觀察右邊的滾動條。
在線示例: ashwin-sureshkumar.github.io/angular-inf…
我沒法將 gif 圖片上傳到此處。這是 gif 圖片的連接: giphy.com/gifs/xTiN0F… 。
若是你喜歡本文的話,歡迎分享、評論及點 💚 。