無限スクロールの実装
Angular2による無限スクロールの実装
1.無限スクロールとは
無限スクロールは、件数の多いリスト表示に便利なユーザーインターフェースです。
例えば、Googleで検索すると、まず10件分の結果が表示され、残りは複数の画面に分割されます。画面の一番下に、下図のようなページ送りアイコンが表示されます。
求めるデータが1ページ目に無いときは、ページ送りを繰り返しています。この操作を面倒に感じませんか。ExcelやWordのように全体を高速でスクロールしたいと思いませんか。
無限スクロールを使うと、件数の多いリスト表示であっても、画面の切り替えなしに、どこまでもスクロールして表示できます。無限スクロールを体験した人で、いままでのページ送り操作に戻りたいと言う人はいません。
[参考]
無限スクロールのサンプルビデオ
操作性の向上だけでなく、大量の書類・図面・写真などの流し見をして、素早くチェックすることも可能になり、業務効率が向上します。無限スクロールは、ブラウザに処理を分散する「モダンWeb」ならではの機能です。幅広い用途に効果があります。
2.無限スクロールのしくみ
無限スクロールは、通信中もユーザーの操作を妨げない非同期通信を使用し、画面表示とデータ受信を同時に行います。
次に表示するデータを次々と事前取得することで、どこまでもスクロールできます。表示済データを廃棄することで、どんなに大量のデータであっても、容量オーバーにはなりません。また、すべてのデータを受信してからの表示ではなく、最小限のデータが準備できた時点で表示が始まりますので、ページ送り方式に比べて大幅に待たされることはありません。
3.無限スクロールの実装
無限スクロール用のJavaScriptライブラリは多数公開されています。いくつか試しましたが例外処理が不十分だったり、ライブラリのファイルサイズが大きかったりしたため、Angular2で新たに実装したところ、無限スクロール実装部分は、300行程度で済みました。
2016年8月12日更新
import {Component, OnInit} from '@angular/core'; import {RequestOptions} from "@angular/http"; import {HttpService} from "../service/http.service" import {DataService} from "../service/data.service"; import {SharedService} from "../service/shared.service"; import {I18nDatePipe} from '../pipe/i18nDate.pipe'; import {Router, ActivatedRoute} from "@angular/router"; import {TransitionService} from "../service/transition.service"; const htmlStr: string = require('component/reporthistory.component.html'); const cssStr: string = require("component/reporthistory.component.css"); const cssNav: string = require("../assets/css/navBar.css"); @Component({ selector: 'report-history', template: htmlStr, styles: [cssStr, cssNav], pipes: [I18nDatePipe] }) export class ReporthistoryComponent implements OnInit { record; nextRecord; prevRecord; startRow = 1; endRow = 1; BLOCK_SIZE = 30; isInitLoadDone = false; HEADER_HEIGHT = 50; BLOCK_HEIGHT; isEnd = false; isNextBufferReady = false; isPrevBufferReady = false; customerId; constructor(private httpService: HttpService, private dataService: DataService, private sharedService: SharedService, private router: Router, private route: ActivatedRoute, private transitionService: TransitionService) { } ngOnInit() { this.initData(); } //初期処理 initData() { //モバイル画面の判定 this.sharedService.onResize(); //UrlからcustomerId取得 let customerId; this.route.params .map(params => params['Id']) .subscribe((Id) => { console.log("@@@@ Id:" + Id); this.customerId = Id; }); //スクロール位置の初期化 setTimeout(()=> { window.scroll(0, 0) }, 1); //位置固定ヘッダで隠れる部分を位置補正 let bodyEl= document.getElementById("myBody") bodyEl.style.paddingTop = this.HEADER_HEIGHT + "px"; this.BLOCK_HEIGHT=bodyEl.clientHeight/3; //初期表示データ取得 this.getRecord(1, this.BLOCK_SIZE * 4) .then((result: any)=> { let data = result.data.data; if (data.length !== this.BLOCK_SIZE * 4) { alert("データ件数が不足しています"); return; } this.record = data.slice(0, this.BLOCK_SIZE * 3);//アクティブなバッファ this.nextRecord = data.slice(-this.BLOCK_SIZE * 3);//下方向の次バッファ this.isNextBufferReady = true;//下方向の次バッファ有効フラグ this.endRow = this.BLOCK_SIZE * 3;//バッファ末尾データの行番号 this.isInitLoadDone = true;//初期処理完了フラグ }); } //ーーーーーーー--------s----------- // メニュー選択処理 //ーーーーーーー------------------- onMenuClick(str, event) { event.preventDefault(); event.stopPropagation(); switch (str) { case "back"://顧客情報画面へ戻る this.transitionService.canTransition = true; this.router.navigate(['/customerDetail', this.customerId]); break; } } //ーーーーーーー------------------- //スクロールイベント処理 //ーーーーーーー------------------- onScroll(event) { //初期処理完了前のスクロールイベントは無視 if (!this.isInitLoadDone) return; //画面のレイアウト情報取得 let html = document.documentElement;//html要素 let pageHeight = html.scrollHeight;//全体の高さ let clientHeight = html.clientHeight; //表示域の高さ let scrollPos = window.pageYOffset;//スクロール位置 let bufferHeight = pageHeight / 3;//Buffer Blockの表示域の高さ let bottom_margine = //下方向スクロール可能サイズ pageHeight - clientHeight - scrollPos; let top_margine = scrollPos; //上方向スクロール可能サイズ //バッファ枯渇の場合はBottomで反転バウンド(下方向) if (!this.isEnd && bottom_margine === 0) { setTimeout(()=> { scrollBy(0, -20) }, 1); return; } //バッファ枯渇の場合はTopで反転バウンド(上方向) if (this.startRow !== 1 && top_margine === 0) { setTimeout(()=> { scrollBy(0, 20) }, 1); return; } //バッファ追加の判定(下方向) if (bottom_margine < bufferHeight * 0.5 && !this.isEnd && this.isNextBufferReady) { this.isNextBufferReady = false; console.log("@@@@ onScrollDown"); this.onScrollDown(); return; } //バッファ追加の判定(上方向) if ((scrollPos < bufferHeight * 0.5) && (this.startRow !== 1) && (this.isPrevBufferReady || this.prevRecord === null)) { this.isPrevBufferReady = false; console.log("@@@@ onScrollUp"); this.onScrollUp(); return; } } //バッファ追加(下方向) onScrollDown() { this.log(); //次の上方向バッファを取得 this.prevRecord = this.record.slice(0, this.BLOCK_SIZE * 3); this.isPrevBufferReady = true; //表示データ書き換え this.record = this.nextRecord; //表示位置カウンタの更新 this.startRow += this.BLOCK_SIZE; this.endRow += this.record.length - this.BLOCK_SIZE * 2; //表示位置補正 let bodyEl= document.getElementById("myBody") this.BLOCK_HEIGHT=(bodyEl.clientHeight+50)/3; let offset = 0 - this.BLOCK_HEIGHT; console.log("@@@@ Scroll offset: " + offset); setTimeout(()=> { window.scrollBy(0, offset) }, 1); //次のデータを準備 setTimeout(()=> { this.getNextBuffer(); }, 1); } //バッファ追加(上方向) onScrollUp() { //次の下方向バッファを取得 this.nextRecord = this.record.slice(0, this.BLOCK_SIZE * 3); this.isNextBufferReady = true; //表示データ書き換え this.record = this.prevRecord; //表示位置カウンタの更新 this.startRow -= this.record.length - this.BLOCK_SIZE * 2; this.endRow -= this.BLOCK_SIZE; //表示位置補正 let offset = this.BLOCK_HEIGHT; console.log("@@@@ Scroll offset: " + offset); setTimeout(()=> { window.scrollBy(0, offset) }, 1); //次のデータを準備 setTimeout(()=> { this.getPrevBuffer(); }, 1); } //次に追加するバッファ作成(下方向) getNextBuffer() { this.getRecord(this.endRow + 1, this.BLOCK_SIZE) .then((result: any)=> { let rec = result.data.data; if (rec.length === this.BLOCK_SIZE) {//ブロックサイズ分の追加データあり this.isEnd = false; this.nextRecord = this.record .slice(-this.BLOCK_SIZE * 2).concat(rec); } else if (rec.length === 0) { //追加データなし this.isEnd = true; } else { //末尾データでブロックサイズ分のデータなし this.isEnd = true; this.record = this.record.concat(rec); } //バッファ準備完了 this.isNextBufferReady = true; }) } //次に追加するバッファ作成(上方向) getPrevBuffer() { this.getRecord(this.startRow - this.BLOCK_SIZE, this.BLOCK_SIZE) .then((result: any)=> { let rec = result.data.data; this.prevRecord = rec.concat (this.record.slice(0, this.BLOCK_SIZE * 2)); //バッファ準備完了 this.isPrevBufferReady = true; this.isEnd = false; }); this.log(); } //サーバーからデータ取得 getRecord(begin: number, size: number): Promise<any> { console.log("@@@@@ getRecord begin=" + begin + " size=" + size + " startRow=" + this.startRow + " endRow=" + this.endRow); let arr = new Array(size); //HTTPリクエスト条件設定 const config = new RequestOptions(); config.url = this.httpService.AJAX_URL + "history"; config.body = ({begin: begin, size: size}); //HTTPリクエスト開始 let promise = this.httpService.send("post", config); promise.then( (result: any)=> { return result; }, (error)=> { alert("通信エラー" + error.message); return "error"; }); return promise; } log() { if (this.record) console.log("@@@@@ current=" + this.record[0].id + "|" + this.record[this.record.length - 1].id); if (this.nextRecord) console.log("@@@@@ next=" + this.nextRecord[0].id + "|" + this.nextRecord[this.nextRecord.length - 1].id); if (this.prevRecord) console.log("@@@@@ prev=" + this.prevRecord[0].id + "|" + this.prevRecord[this.prevRecord.length - 1].id); } }
弊社では、「Angular2とTypeScriptによる モダンWeb開発セミナー」を開催します。
Angular2の実践的な知識とノウハウを習得できます。
/hp/semi/modern/