Skip to content

Instantly share code, notes, and snippets.

@DawidvanGraan
Last active April 14, 2017 04:14
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 DawidvanGraan/839e0c6ae0262fe3a74d153698a45f6d to your computer and use it in GitHub Desktop.
Save DawidvanGraan/839e0c6ae0262fe3a74d153698a45f6d to your computer and use it in GitHub Desktop.
Angular 2 with Firebase Paging.

Shows an implementation of simple paging (Next and Previous) with Angulare 2 and Firebase.

Setup

Each entry requires a priority field that will allow for sorting the data descending.

entry.priority = 0 - Date.now();

The paging happens in pager.component.ts and the data is emitted to your component. See the below files for example.

How it works

The pager queries an extra entry from Firebase itemsPerPage + 1 and makes use of this entry for the startAt and endAt options.

When next is pressed, the extra entry is used for the startAt option. When previous is pressed the extra entry is used for the endAt option.

Keeping the extra entry allows for simple next and previous paging.

See pager.components.ts for details.

<!-- Replace YOUR-TABLE-NAME with the name of your Firebase table you want to page -->
<app-pager tableName="YOUR-TABLE-NAME"
[itemsPerPage]="3"
(dataChanged)="dataChanged($event)"
(loadingChanged)="loadingChanged($event)"></app-pager>
<div *ngIf="showLoading">
<h1>Loading...</h1>
</div>
<div *ngIf="data && !showLoading">
<div *ngFor="let d of data">
<h2>{{ d.priority }}</h2>
</div>
</div>
import { Component, ViewEncapsulation, OnInit } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-example',
templateUrl: 'example.component.html'
})
export class ExampleComponent implements OnInit {
public data: any[];
showLoading = true;
constructor() {
}
ngOnInit() {
}
dataChanged(event: any): void {
this.data = event.data;
this.showLoading = false;
}
loadingChanged(show: boolean): void {
this.showLoading = show;
}
}
import 'rxjs/add/operator/mergeMap'
import 'rxjs/add/operator/filter'
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { AngularFire } from 'angularfire2';
export interface DataEvent {
data: any[];
}
@Component({
selector: 'app-pager',
template: `
<ul class="pager">
<li [class.disabled]="noPrevious()" [class.previous]="align" [ngClass]="{'pull-right': align}" class="btn btn-link">
<a href (click)="prevPage($event)">« Previous</a>
</li>
<li [class.disabled]="noNext()" [class.next]="align" [ngClass]="{'pull-right': align}" class="btn btn-link">
<a href (click)="nextPage($event)">Next »</a>
</li>
</ul>
`
})
export class PagerComponent implements OnInit {
/** firebase table */
@Input() public tableName: string;
/** items per page */
@Input() public itemsPerPage: number;
/** fired when total pages count changes, $event:number equals to total pages count */
@Output() public loadingChanged: EventEmitter<Boolean> = new EventEmitter<Boolean>();
/** fired when data was changed, $event:{ data[] } */
@Output() public dataChanged: EventEmitter<DataEvent> = new EventEmitter<DataEvent>();
private data: any[];
private firstEntry: any;
private lastEntry: any;
private prevValue: BehaviorSubject<number> = new BehaviorSubject<number>(0);
private nextValue: BehaviorSubject<number> = new BehaviorSubject<number>(0);
private startAtValue: BehaviorSubject<number> = new BehaviorSubject<number>(0);
private endAtValue: BehaviorSubject<number> = new BehaviorSubject<number>(0);
queryableNext = false;
queryablePrev = false;
constructor(public af: AngularFire) {
}
ngOnInit() {
// Get the find the first and last item in the list
this.getFirstEntry(this.tableName)
.flatMap((entry) => {
this.firstEntry = entry;
return this.getLastEntry(this.tableName);
})
.map((entry) => {
this.lastEntry = entry;
return true;
})
.subscribe(() => {
// Next subsribe events
this.startAt().subscribe((result) => {
this.onResult(result);
});
// Prev subsribe events
this.endAt().subscribe((result) => {
this.onResult(result);
});
// Make the first query
if (this.firstEntry != null) {
this.startAtValue.next(this.firstEntry.priority);
}
});
}
private startAt() {
return this.list(this.tableName, {
query: {
orderByChild: 'priority',
limitToFirst: (this.itemsPerPage + 1),
startAt: this.startAtValue
}
})
.filter((ref) => this.startAtValue.getValue() !== 0);
}
private endAt() {
return this.list(this.tableName, {
query: {
orderByChild: 'priority',
limitToLast: (this.itemsPerPage + 1),
endAt: this.endAtValue
}
})
.filter((ref) => this.endAtValue.getValue() !== 0);
}
private onResult(result: any[]) {
if (result != null && result.length > 0) {
let count = result.length;
// Query End at first item
this.prevValue.next(result[0].priority);
// Query Start at the last item (Spare)
this.nextValue.next(result[count - 1].priority);
// Check if we have an spare and remove it
if (count === (this.itemsPerPage + 1)) {
result = result.slice(0, count - 1);
count = result.length;
}
this.data = result;
// Next || Prev Options
this.queryablePrev = !(this.firstEntry.$key === this.data[0].$key);
this.queryableNext = !(this.lastEntry.$key === this.data[count - 1].$key);
} else {
this.queryableNext = false;
this.queryablePrev = false;
this.data = null;
}
this.dataChanged.emit({
data: this.data
});
this.loadingChanged.emit(false);
}
public noPrevious(): boolean {
return !this.queryablePrev;
}
public noNext(): boolean {
return !this.queryableNext;
}
public nextPage(event?: Event): void {
if (event) {
event.preventDefault();
}
if (this.queryableNext) {
if (event && event.target) {
const target: any = event.target;
target.blur();
}
this.loadingChanged.emit(true);
this.startAtValue.next(this.nextValue.getValue());
}
}
public prevPage(event?: Event): void {
if (event) {
event.preventDefault();
}
if (this.queryablePrev) {
if (event && event.target) {
const target: any = event.target;
target.blur();
}
this.loadingChanged.emit(true);
this.endAtValue.next(this.prevValue.getValue());
}
}
list(path: string, options: any) {
return this.af.database.list('/' + path, options);
}
getLastEntry(table: string) {
return this.af.database.list('/' + table, {
query: {
orderByChild: 'priority',
limitToLast: 1
}
})
.map((dataList) => {
if (dataList != null && dataList.length > 0) {
return dataList[0];
}
return null;
});
}
getFirstEntry(table: string) {
return this.af.database.list('/' + table, {
query: {
orderByChild: 'priority',
limitToFirst: 1
}
})
.map((dataList) => {
if (dataList != null && dataList.length > 0) {
return dataList[0];
}
return null;
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment