Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Simple Firebase Pagination With AngularFire2

Episode 18 written by Jeff Delaney
full courses for pro members

Pagination allows users to easily navigate through a large collections, but it is a surprisingly complex challenge to solve with Firebase, especially if you want page numbers for a realtime data steam. In SQL, we can use the OFFSET operator to sort through a table of static data. In Firebase NoSQL, we don’t have this luxury. I’ve seen several blog posts and StackOverflow answers dedicated to this question - all of them wrong - so I wanted to give my take on it

Implementation Strategy

In this lesson, pagination works by pulling the first N+1 items from the database, where N is the number of items on the page and +1 the starting point for the next query. The advantage is that it works with data collections of any size :smile: . The drawback is page numbers are not possible, just next/previous buttons :sad: .

demo of firebase pagination

Possible Alternatives

If you really need numbered pagination, I see as the following two options as potentially viable.

(1) Load the entire collection into memory, then slice it into pages client side. This works fine on small datasets.

(2) Maintain a separate database collection of starting page keys sorted the way you want, which you query before getting the actual data for each page. This could be flexible, but potentially a maintenance nightmare because you would have to update the collection after each create or delete operation.

Building the Firebase Paginator

Let’s get started by generating some resources with the Angular CLI.

ng g service comments
ng g component comment-list

Currently, our database has comments nested under their associated blog post.

comments
postId
commentId
body: string
timestamp: number

Comment Service

The CommentService is going to return a list observable. The offset is the number of comments to show on each page. The startKey is where the query will start (undefined for first page) - the same idea as a SQL OFFSET, just with keys instead of numbers. We add 1 extra item to the offset because we need its key as the starting point for the next page. For example, we pull 3 items, show the user first 2 items, then use the last item’s key as the starting point for the next page.

comments.service.ts

import { Injectable } from '@angular/core';
import { AngularFireDatabase, FirebaseListObservable } from 'angularfire2/database';

@Injectable()
export class CommentsService {

constructor(private db: AngularFireDatabase) {}

getComments(postId, offset, startKey?): FirebaseListObservable<any> {

return this.db.list(`comments/${postId}`, {
query: {
orderByKey: true,
startAt: startKey,
limitToFirst: offset+1
}
});
}

}

Comment List Component

The template simply loops over the comments, then uses a couple buttons for next and previous.

comments-list.component.html

<div *ngFor="let comment of comments">
<h3>{{ comment.body }}</h3>
<p>{{ comment.timestamp | date }}</p>

</div>

<button (click)="prevPage()" [disabled]="!prevKeys?.length">Prev</button>
<button (click)="nextPage()" [disabled]="!nextKey">Next</button>

Lodash is essential for handling the array operations in the component. Most of the functions are self-explanatory, but check out the docs if you’re unsure.

First, subscribe to the observable in the component TypeScript instead of unwrapping it in the template with the | async pipe.

When the observable emits, slice off the first 2 comments, then set the nextKey from the 3rd value. If no 3rd comment is present, the end of the collection has been reached and the button can be disabled.

Cool, that will move forward through the collection. But how do we go backwards?

To go backwards, we need to maintain a running list of prevKeys. Each time the user goes forward, the we push a new key to the array. When the user wants to go backwards, we pop the last element from the array as the starting key, then drop it. When the array is empty, the first page has been reached the prev button can be disabled.

comments-list.component.ts

import { Component, OnInit } from '@angular/core';
import { CommentsService } from '../comments.service';
import * as _ from "lodash";

@Component({
selector: 'comments-list',
templateUrl: './comments-list.component.html',
styleUrls: ['./comments-list.component.scss']
})
export class CommentsListComponent implements OnInit {

comments: any;
offset = 2;
nextKey: any; // for next button
prevKeys: any[] = []; // for prev button
subscription: any;

constructor(private commentsSvc: CommentsService) { }

ngOnInit() {
this.getComments()
}

nextPage() {
this.prevKeys.push(_.first(this.comments)['$key']) // set current key as pointer for previous page
this.getComments(this.nextKey)
}

prevPage() {
const prevKey = _.last(this.prevKeys) // use last key in array
this.prevKeys = _.dropRight(this.prevKeys) // then remove the last key in the array

this.getComments(prevKey)
}

private getComments(key?) {

this.subscription = this.commentsSvc.getComments('samplePost1', this.offset, key)
.subscribe(comments => {

this.comments = _.slice(comments, 0, this.offset)
this.nextKey =_.get(comments[this.offset], '$key')
})
}


}

That’s it for pagination. Stay tuned for an alternative lesson about infinite scroll style pagination.