Last active
August 1, 2020 00:13
-
-
Save RaschidJFR/ef9f342bc2549211df7c6f00d1a06237 to your computer and use it in GitHub Desktop.
Angular universal pagination service
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Injectable } from '@angular/core'; | |
import { ActivatedRoute } from '@angular/router'; | |
import { map, mergeMap, mapTo } from 'rxjs/operators'; | |
import { from } from 'rxjs'; | |
/** | |
* Check https://gist.github.com/RaschidJFR/ef9f342bc2549211df7c6f00d1a06237 for the latest version of the script | |
* @author Raschid JF Rafaelly <hello@raschidjfr.dev> | |
* @example | |
* | |
* <!-- component.html --> | |
* <div> | |
* <button (click)="pagination.first()">First</button> | |
* <button *ngFor="let page of pagination.getPageArray(10)" | |
* [class.current]="pagination.currentPage === page" | |
* (click)="pagination.gotoPage(page)"> | |
* {{page + 1}} | |
* </button> | |
* <button (click)="pagination.last()">Last</button> | |
* </div> | |
* | |
* // component.ts | |
* @Component({ | |
* providers: [PaginationService] | |
* }) | |
* export class MyComponentWithPagination { | |
* | |
* // ... | |
* | |
* async ngOnInit() { | |
* this.pagination.setConfig({ | |
* limit: 50, | |
* total: await this.countTotalRecords(), | |
* fetch: (skip, limit) => this.fetchRecords(limit, skip) | |
* }); | |
* this.pagination.gotoPage(0); | |
* } | |
*/ | |
export interface PaginationServiceConfig { | |
/** Total record count */ | |
total?: number; | |
/** Record limit per page */ | |
limit?: number; | |
/** Function to obtain the total count of results */ | |
count?: (skip: number, limit: number) => Promise<number>; | |
/** | |
* An async function that performs the data query from the server | |
* and returns the length of the results. | |
*/ | |
fetch: (skip: number, limit: number) => Promise<any[]>; | |
} | |
@Injectable() | |
export class PaginationService { | |
private _skip = 0; | |
private _lastCount = NaN; | |
private _pages = []; | |
private _inProgress = false; | |
private _resolveReady: () => void; | |
private _ready = new Promise<void>(resolve => this._resolveReady = resolve); | |
public config: PaginationServiceConfig = { | |
count: () => Promise.reject(new Error('No `count` function has been set. Call `setConfig()` first')), | |
limit: 100, | |
fetch: () => Promise.reject(new Error('No `fetch` function has been set. Call `setConfig()` first')), | |
total: NaN, | |
}; | |
constructor(private route: ActivatedRoute) { | |
this.route.queryParamMap | |
.pipe( | |
map(paramMap => Number(paramMap.get('page'))), | |
// distinct(), | |
mergeMap(page => from(this._ready).pipe(mapTo(page))) | |
) | |
.subscribe(async page => { | |
if (isNaN(this.total)) { | |
if (this.config && this.config.count) { | |
this.total = await this.config.count(0, this.config.limit); | |
} | |
} | |
page = page > 0 && page <= this.length ? page - 1 : 0; | |
await this.gotoPage(page, true); | |
}); | |
} | |
/** Total record count */ | |
get total() { return this.config.total; } | |
set total(val) { this.config.total = val; } | |
/** Record limit per page */ | |
get limit() { return this.config.limit; } | |
set limit(val) { this.config.limit = val; } | |
/** | |
* `True` if there's still a next page of results (`config.total` must be previously set) | |
*/ | |
get areThereMore() { | |
return !isNaN(this.total) && this.currentPage === this.length - 1; | |
} | |
/** Current display range */ | |
get currentRange() { | |
return { | |
from: this._skip + 1, | |
to: this._skip + Math.min(this.limit, this._lastCount) | |
}; | |
} | |
/** While performing the fetch function */ | |
get loading() { return this._inProgress; } | |
/** An array with all the available page numbers */ | |
get pages(): number[] { | |
return this._pages; | |
} | |
/** zero-based index */ | |
get currentPage(): number { | |
return Math.floor(this._skip / this.limit); | |
} | |
/** The total number of pages (if `total` has been set in `config()`) */ | |
get length(): number { | |
return Math.ceil(this.total / this.limit); | |
} | |
private get isConfigured() { | |
return !!this._fetch; | |
} | |
/** Call this before any other function */ | |
setConfig(params: PaginationServiceConfig) { | |
this.config = Object.assign(this.config, params); | |
this._resolveReady(); | |
} | |
/** | |
* Zero-based sliced array of available pages. | |
* This will only return the selected amount of pages around the current page. | |
* Useful to show a reduced set of pagination buttons when there are too many to show. | |
*/ | |
getPageArray(max = 10): number[] { | |
const m = Math.floor(max / 2); | |
let low = this.currentPage - m; | |
let high = this.currentPage + m + 1; | |
if (this.currentPage <= m || this.length <= max) { | |
low = 0; | |
high = max; | |
} else if (this.currentPage >= this.length - m) { | |
high = this.length; | |
low = this.length - max; | |
} | |
return this._pages.slice(low, high); | |
} | |
/** Go to first page */ | |
first() { | |
return this.gotoPage(0); | |
} | |
/** Go to last page */ | |
last() { | |
if (!isNaN(this.length)) return this.gotoPage(this.length - 1); | |
} | |
/** Next page */ | |
async next() { | |
if ((this.currentPage < this.length - 1) | |
|| this._lastCount >= this.limit | |
|| (isNaN(this._lastCount) && isNaN(this.length))) { | |
this.gotoPage(this.currentPage + 1); | |
} | |
} | |
/** Previous page */ | |
async prev() { | |
if (this.currentPage > 0) { | |
await this.gotoPage(this.currentPage - 1); | |
} | |
} | |
/** | |
* Go to the chosen pagination results | |
* @param page 0-based index | |
*/ | |
async gotoPage(page: number, force = false) { | |
if (!this.isConfigured) throw new Error('You must call setConfig() before using this function'); | |
if (this.loading) { | |
console.warn('Operation already in progress'); | |
return; | |
} | |
if (page !== this.currentPage || isNaN(this._lastCount) || force) { | |
setTimeout(() => { this._inProgress = true; }); | |
this._skip = page * this.limit; | |
try { | |
this._lastCount = await this._fetch(this._skip, this.limit); | |
} finally { | |
setTimeout(() => { | |
this._inProgress = false; | |
this.updatePageArray(); | |
}, 100); | |
} | |
} | |
} | |
private async _fetch(skip: number, limit: number): Promise<number> { | |
if (!this.config.fetch) | |
throw new Error('No fetch function configured. Pass attribute `config` or call `setConfig()`'); | |
this.total = this.config.total || this.total; | |
if (isNaN(this.total) && this.config.count) { | |
this.total = await this.config.count(this._skip, this.limit); | |
} | |
return (await this.config.fetch(skip, limit)).length; | |
} | |
private updatePageArray() { | |
if (isNaN(this.length)) return; | |
this._pages = new Array(this.length) | |
.fill(0) | |
.map((_n, i) => i); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment