AngularFire2 + FireStore | Pagination with Next & Previous in Tabular Form Example

FireStore is a Firebase service which provides NoSQL Cloud Database solutions. We can easily use Firebase SDK’s to use a number of services on Android, IOS and even on We Application.

In this tutorial, we will learn how to integrate FireStore service in Angular 8 application and how to store data in the FireStore cloud database using Firebase JavaScript SDK. For Angular, we will use Firebase’s official package to use its services in Angular project known as AngularFire2.

Our tutorial example Application will have a form to submit values in FireStore and a Table to list items from the database. We will implement FireStore Query methods to load data in chunks from database using limit, there will be two buttons to load Next and Previous set of records.

Pagination is used to load limited number rows from database which not only facilitates user by not dumping all data by also proves economical in terms of quota limit to read and write in the database.

Let’s get started!

We will create a new Angular application in latest version 8 but this tutorial is compatible with the previous version as well.

Create a new project

Run following NPM command to quickly create a new Angular app using Ng CLI tool

$ ng new AngularFireStore

Install AngularFire2

To use Firebase services in the project we will install Firebase’s official library AngularFire2.

Run following NPM command to install:

$ npm install firebase @angular/fire --save

How to create a Firebase Project and get credential?

For adding we need Firebase account and a project. If you already have one just skip this part.

Step 1) Sign in Firebase with your Google account then click on “Get started”

Step 2) Click on “+ Add project”, enter a name for your project then hit “Continue”

Step 3) After that it will ask to add Analytics, for now, you can skip it then hit “Create Project”

Step 4) Get project credentials. Click on Web icon looks like this </> and provide you app details, after that copy the credential for your app which we will use in our Angular project.

Looks like this:

<script>
  // Your web app's Firebase configuration
  var firebaseConfig = {
    apiKey: "AIzaSyAMJlBBBBBBBBBBBBBBBBBUQAaRG188",
    authDomain: "just-freaking.firebaseapp.com",
    databaseURL: "https://just-freaking.firebaseio.com",
    projectId: "just-freaking",
    storageBucket: "just-freaking.appspot.com",
    messagingSenderId: "1074214740975",
    appId: "1:1074217777777777:web:5724c07c2try566613"
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);
</script>

Step 5) Final step is to enable Database service in the project. Click on “Database” on the left sidebar. Click on “Create database” as we are going to use this service also known as FireStore.

Make sure to select “Start in test mode” as we are not adding any authentication, but in production, you should handle them.

Integrate Firebase in Angular project

We need to update the Angular project’s environment file with Firebase credentials which we got after following Step 4.

Open “~environments/environment.ts” file and update it with below code:

export const environment = {
  production: false,
  firebase: {
    apiKey: "AIzaSyAMJlBBBBBBBBBBBBBBBBBUQAaRG188",
    authDomain: "just-freaking.firebaseapp.com",
    databaseURL: "https://just-freaking.firebaseio.com",
    projectId: "just-freaking",
    storageBucket: "just-freaking.appspot.com",
    messagingSenderId: "1074214740975",
    appId: "1:1074217777777777:web:5724c07c2try566613"
  }
 };

Update app’s module file to initialize the AngularFire with the above credentials.

Open the app.module.ts file and import  AngularFireModule (Required module) and AngularFirestoreModule (Module to use database services)

Replace below code in the file:

//app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FormsModule } from '@angular/forms';

import { AngularFireModule } from '@angular/fire';
import { AngularFireDatabaseModule } from '@angular/fire/database';
import { environment } from '../environments/environment';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,

    AngularFireModule.initializeApp(environment.firebase),
    AngularFireDatabaseModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

FormsModule is required for submitting values using form.

Use Firestore in Component

To use the Firestore database in component, we will import AngularFirestore class to use its methods for database operations.

import { AngularFirestore } from '@angular/fire/firestore';

Also, add it in the component’s class constructor.

constructor(
    private firestore: AngularFirestore
){}

How data is stored in FireStore?

FireStore is a NoSQL cloud database where data is stored in the form of Collections and Documents. Each Collection act like a table like we have in the Relational Database Mangement System(RDMS) and have Documents in each Collection like table rows.

For example, we will create a Collection “People” that will have each Document with key-value pair of Id, Name, Place, Timestamp 

How to Add a new Document in a Collection?

To add a new document or item in a Collection we use add method as shown below:

  addItem() {
    this.firestore.collection('People').add(
      {
        id: this.firestore.createId(),
        name: this.nameValue,
        place: this.placeValue,
        timestamp: new Date().getTime()
      }
    ).then(response => {
      this.nameValue = this.placeValue = '';
    }).catch(error => {
      console.log(error);
    });
  }

here we added a new Document in People Collection using the add method which returns a promise.

Create a Form and Table to communicate FireStore

For a demonstration of data communication with FireStore, we will create a Form with two Input Fields to take Name and Place value which will be saved in a FireStore collection.

There will be a Table to show a list of items loaded from FireStore using a load method the snapshotChanges() method. This method is an Observable method which works in realtime and returns the Data payload as soon there is a change in Collection.

  loadItems() {
    this.firestore.collection('People', ref => ref
      .limit(5)
      .orderBy('timestamp', 'desc')
    ).snapshotChanges()
      .subscribe(response => {
        ...
        ...
      }, error => {
      });
  }

Here we used to limit and orderBy methods to return only 5 records from Database, ordered by timestamp key in descending order. You can check more details about using Queries here.

Next and Previous Pagination on Data Collections

We will use some of the methods of FireStore to add Next and Previous functionalities on data retuned from Database. For that, we will use the following methods.

startAt(): This method will start looking at documents after a given document.
endBefore(): List documents only before specified document.
startAfter(): This method will be used to list items after a specified document.

Update Component Template and Class

In App component template file we will add a Form and a Table to add and list data with next and previous buttons.

Update the app.component.html file with following template HTML code:

<div class="container">
    <div class="card" style="margin:20px">
        <div class="card-body">
            <form >
                <div class="form-group row">
                    <label for="enterName" class="col-sm-2 col-form-label">Name</label>
                    <div class="col-sm-10">
                        <input id="enterName" name="enterName" class="form-control" placeholder="Type Name" [(ngModel)]="nameValue" required>
                    </div>
                </div>
                <div class="form-group row">
                    <label for="enterPlace" class="col-sm-2 col-form-label">Place</label>
                    <div class="col-sm-10">
                        <input id="enterPlace" name="enterPlace" class="form-control" placeholder="Type Place" [(ngModel)]="placeValue" required>
                    </div>
                </div>
                <div class="form-group row">
                    <div class="col-sm-12">
                        <button type="submit" class="btn btn-primary float-right" (click)="addItem()">Add</button>
                    </div>
                </div>
            </form>
        </div>
    </div>

    <div class="text-center">
        <button class="btn btn btn-info btn-sm float-left" (click)="prevPage()"
            [disabled]="disable_prev || !(pagination_clicked_count>0)">Previous</button> <b>Page no: {{pagination_clicked_count+1}}</b>
        <button class="btn btn btn-info btn-sm float-right" (click)="nextPage()" [disabled]="disable_next">Next</button>
    </div>
    <table class="table">
        <thead>
            <tr>
                <th>Name</th>
                <th>Place</th>
                <th>Time</th>
            </tr>
        </thead>
        <tbody>
            <tr *ngFor="let item of tableData">
                <td>{{item.name}}</td>
                <td>{{item.place}}</td>
                <td>{{readableDate(item.timestamp)}}</td>
            </tr>
        </tbody>
    </table>
</div>

For styling here, we used Bootstrap classes. Also, add bootstrap.css file in index.html file’s head section.

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">

In the App’s component class, we have the following methods:

loadItems(): This method will load items on Component initialization and also updated snapshot of items as soon they are updated in FireStore.
addItem(): Add new item in FireStore collection.
nextPage(): Load next set of items.
prevPage(): Load a previous set of items.
To handle the previous set we have used startAt and endBefore methods. In endBefore we take the last document of items so that items will be before from current.
But there is tweak we need to use by saving the previous first document in an array prev_first_doc
Why we are doing this?
Suppose we have items with value1 to 20. Page 1 will have 1-5, then next 6-10,10-15,15-20 Right?
But in the case to get previous 5 we call endBefore where we pass the last document of current 5. so if we show 10-15 where last is 15, endBefore will fetch 1-15 and limit(5) will return only 1-5. That will always return 1-5.
So to resolve this we are maintaining the first item of the last page to use it in startAt method.
Replace app.component.ts file with below code:
//app.component.ts
import { Component, OnInit } from '@angular/core';
import { AngularFirestore } from '@angular/fire/firestore';


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  //Models for Input fields
  nameValue: string;
  placeValue: string;

  //Data object for listing items
  tableData: any[] = [];

  //Save first document in snapshot of items received
  firstInResponse: any = [];

  //Save last document in snapshot of items received
  lastInResponse: any = [];

  //Keep the array of first document of previous pages
  prev_strt_at: any = [];

  //Maintain the count of clicks on Next Prev button
  pagination_clicked_count = 0;

  //Disable next and prev buttons
  disable_next: boolean = false;
  disable_prev: boolean = false;

  constructor(
    private firestore: AngularFirestore
  ) {
    this.loadItems();
  }

  ngOnInit() {
  }


  loadItems() {
    this.firestore.collection('People', ref => ref
      .limit(5)
      .orderBy('timestamp', 'desc')
    ).snapshotChanges()
      .subscribe(response => {
        if (!response.length) {
          console.log("No Data Available");
          return false;
        }
        this.firstInResponse = response[0].payload.doc;
        this.lastInResponse = response[response.length - 1].payload.doc;

        this.tableData = [];
        for (let item of response) {
          this.tableData.push(item.payload.doc.data());
        }

        //Initialize values
        this.prev_strt_at = [];
        this.pagination_clicked_count = 0;
        this.disable_next = false;
        this.disable_prev = false;

        //Push first item to use for Previous action
        this.push_prev_startAt(this.firstInResponse);
      }, error => {
      });
  }

  // Add item in Collection
  addItem() {
    this.firestore.collection('People').add(
      {
        id: this.firestore.createId(),
        name: this.nameValue,
        place: this.placeValue,
        timestamp: new Date().getTime()
      }
    ).then(response => {
      this.nameValue = this.placeValue = '';
    }).catch(error => {
      console.log(error);
    });
  }

  //Show previous set 
  prevPage() {
    this.disable_prev = true;
    this.firestore.collection('People', ref => ref
      .orderBy('timestamp', 'desc')
      .startAt(this.get_prev_startAt())
      .endBefore(this.firstInResponse)
      .limit(5)
    ).get()
      .subscribe(response => {
        this.firstInResponse = response.docs[0];
        this.lastInResponse = response.docs[response.docs.length - 1];
        
        this.tableData = [];
        for (let item of response.docs) {
          this.tableData.push(item.data());
        }

        //Maintaing page no.
        this.pagination_clicked_count--;

        //Pop not required value in array
        this.pop_prev_startAt(this.firstInResponse);

        //Enable buttons again
        this.disable_prev = false;
        this.disable_next = false;
      }, error => {
        this.disable_prev = false;
      });
  }

  nextPage() {
    this.disable_next = true;
    this.firestore.collection('People', ref => ref
      .limit(5)
      .orderBy('timestamp', 'desc')
      .startAfter(this.lastInResponse)
    ).get()
      .subscribe(response => {

        if (!response.docs.length) {
          this.disable_next = true;
          return;
        }

        this.firstInResponse = response.docs[0];

        this.lastInResponse = response.docs[response.docs.length - 1];
        this.tableData = [];
        for (let item of response.docs) {
          this.tableData.push(item.data());
        }

        this.pagination_clicked_count++;

        this.push_prev_startAt(this.firstInResponse);

        this.disable_next = false;
      }, error => {
        this.disable_next = false;
      });
  }

  //Add document
  push_prev_startAt(prev_first_doc) {
    this.prev_strt_at.push(prev_first_doc);
  }

  //Remove not required document 
  pop_prev_startAt(prev_first_doc) {
    this.prev_strt_at.forEach(element => {
      if (prev_first_doc.data().id == element.data().id) {
        element = null;
      }
    });
  }

  //Return the Doc rem where previous page will startAt
  get_prev_startAt() {
    if (this.prev_strt_at.length > (this.pagination_clicked_count + 1))
      this.prev_strt_at.splice(this.prev_strt_at.length - 2, this.prev_strt_at.length - 1);
    return this.prev_strt_at[this.pagination_clicked_count - 1];
  }

  //Date formate
  readableDate(time) {
    var d = new Date(time);
    return d.getDate() + "/" + d.getMonth() + "/" + d.getFullYear();
  }

}

That’s it Now you have FireStore listing with Add new item Form. We used queries to limit data set to 5 records per snapshot. Next and previous pages can be accessed using FireStore methods like endBefore, startAt and startAfter.

 

6 thoughts on “AngularFire2 + FireStore | Pagination with Next & Previous in Tabular Form Example”

  1. It’s work fine previous and next functionality had done.But we need also add number based pagination using firebase with Angular 8

  2. This does not work ….. results in
    ERROR NullInjectorError: StaticInjectorError(AppModule)[AppComponent -> AngularFirestore]:
    No Provider

Leave a Comment

Your email address will not be published. Required fields are marked *