Skip to content

Instantly share code, notes, and snippets.

@RaschidJFR
Last active August 1, 2020 00:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RaschidJFR/ef9f342bc2549211df7c6f00d1a06237 to your computer and use it in GitHub Desktop.
Save RaschidJFR/ef9f342bc2549211df7c6f00d1a06237 to your computer and use it in GitHub Desktop.
Angular universal pagination service
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&commat;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
* &commat;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