Torbjorn Zetterlund

Thu 14 2020
Image

Pagination with Angular and Firestore

by bernt & torsten

I have created a Cloud Function that uses the NewsAPI to read the latest news and store the news in a Firestore collection. To read the news links I created an Angular app to read the news. Firestore does not have pagination support so I had to build it myself. This post is all about how I added pagination to my Angular UI.

In this how-to, I’m going to use Angular and Firestore database, and also AngularFire and RxJS. I am not going to describe how to install and configure each of these requirements. If you would face problems setting up these, don’t hesitate to ping me by leaving a comment below.

I created a new news page, searchlinks.component.ts file looks like this:

import { Router } from '@angular/router';
import { QueryLinks } from './searchlinks.model';
import { QueryLinksConfig } from './searchlinks.config';
import { ToastrService } from 'ngx-toastr';
import { Observable} from 'rxjs';
const swal = require('sweetalert');

@Component({
  selector: 'app-searchlinks',
  templateUrl: './searchlinks.component.html',
  styleUrls: ['./searchlinks.component.scss']
})

export class SearchLinksComponent implements OnInit {
  public p: any;
  
  //Save first document in snapshot of items received
  public firstInResponse: any = [];
   
  //Save last document in snapshot of items received
  public lastInResponse: any = [];
   
  //Keep the array of first document of previous pages
  public prev_strt_at: any = [];
   
  //Maintain the count of clicks on Next Prev button
  public pagination_clicked_count = 0;

  //Maintain the count of clicks on Next Prev button
  public pageSize = 20;

//Maintain the count of clicks on Next Prev button
  public itemnumberbypage = 0;
   
  //Disable next and prev buttons
  public disable_next: boolean = false;
  public disable_prev: boolean = false;
  private searchlinkqueriesData: AngularFirestoreDocument<QueryLinks>;

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

  searchlinkqueriesdatanext: Observable<any[]>;

  searchlinkqueriesdataprev: Observable<any[]>;


  constructor(
    public db: AngularFirestore, 
    public router: Router,
    public toasterService: ToastrService
    ) {
  }

  ngOnInit() {
    this.db
    .collection(QueryLinksConfig.collection_endpoint,
      ref => ref
      .limit(this.pageSize)
      .orderBy('date', 'asc')
      ).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.searchlinkqueriesdata = [];
        this.searchlinkqueriesdata = response.map(item => {
          return {
            id: item.payload.doc.id,
            searchlinksdata: item.payload.doc.data()
          }
        })

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

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

  //Show previous set 
  prevPage() {
    this.disable_prev = true;
    this.db.collection(QueryLinksConfig.collection_endpoint,
      ref => ref
        .orderBy('date', 'asc')
        .startAt(this.get_prev_startAt())
        .endBefore(this.firstInResponse)
        .limit(this.pageSize)
      ).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.searchlinkqueriesdata = [];
        this.searchlinkqueriesdata = response.map(item => {
          return {
            id: item.payload.doc.id,
            searchlinksdata: item.payload.doc.data()
          }
        })
 
        //Maintaing page no.
        this.pagination_clicked_count--;
        this.itemnumberbypage/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.db
      .collection('searchlinks', ref => ref
      .limit(this.pageSize)
      .orderBy('date', 'asc')
      .startAfter(this.lastInResponse)
    ).snapshotChanges()
      .subscribe(response => {
        if (!response.length) {
          this.disable_next = true;
          return;
        }
        this.firstInResponse = response[0].payload.doc;
        this.lastInResponse = response[response.length - 1].payload.doc;

        this.searchlinkqueriesdata = [];
        this.searchlinkqueriesdata = response.map(item => {
          return {
            id: item.payload.doc.id,
            searchlinksdata: item.payload.doc.data()
          }
        })
 
        this.pagination_clicked_count++;
        this.itemnumberbypage*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];
    }

  deleteLinks(searchlinksdata) {

    swal({ title: 'Are you sure?',
      text: 'Your will not be able to recover this file!',
      icon: "warning",
      buttons: [
        'No, cancel it!',
        'Yes, I am sure!'
      ],
      dangerMode: true
    }).then(result => {
      console.log(result);
     if (result) {
          //Get the task document
       this.searchlinkqueriesData = this.db.doc<QueryLinks>(`${QueryLinksConfig.collection_endpoint}/${searchlinksdata.id}`);
       //Delete the document
       this.searchlinkqueriesData.delete().then((res) => {
         swal('Deleted!', 'Your file has been deleted.', 'success');
       })
     } else {
        swal('Cancelled', 'Your file is safe :)', 'error');
     }
   });
  }

  updateLinksstatus(linksdata) {
    
    if(linksdata.searchlinksdata.status)
      linksdata.searchlinksdata.status = false;
    else
      linksdata.searchlinksdata.status = true;

    this.db.doc(`${QueryLinksConfig.collection_endpoint}/${linksdata.id}`).update({status: linksdata.searchlinksdata.status})
    .then(res=>{
      this.toasterService.success('Link status change', 'Success!');
    });
}

  updateLinks(searchlinksdata) {
    this.router.navigate(['/querylinks/updateQueryLinks/', searchlinksdata.id]);
  }
}

and my searchlinks.component.html footer look like this:

  <div class="panel-footer">
    <div class="row ">
        <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>
              &nbsp;&nbsp;&nbsp;<b>Page no: {{pagination_clicked_count+1}}</b>&nbsp;&nbsp;&nbsp;
           <button class="btn btn btn-info btn-sm float-right" (click)="nextPage()" [disabled]="disable_next">Next</button>
        </div>
    </div>

I have create two buttons – prev and next. Here is a screenshot of that.

On the first page, Next is only clickable, when you click Next the function nextPage is called, in the next page, we keep track of the previous Firestore collection and can forward back and forth. Each time nextPage or prevPage a query is made to Firestore. We only keep 20 rows of data in the browser as 20 is the current number of rows per page.

nextPage() {
    this.disable_next = true;
    this.db
      .collection('searchlinks', ref => ref
      .limit(this.pageSize)
      .orderBy('date', 'asc')
      .startAfter(this.lastInResponse)
    ).snapshotChanges()
      .subscribe(response => {
        if (!response.length) {
          this.disable_next = true;
          return;
        }
        this.firstInResponse = response[0].payload.doc;
        this.lastInResponse = response[response.length - 1].payload.doc;

        this.searchlinkqueriesdata = [];
        this.searchlinkqueriesdata = response.map(item => {
          return {
            id: item.payload.doc.id,
            searchlinksdata: item.payload.doc.data()
          }
        })
 
        this.pagination_clicked_count++;
        this.itemnumberbypage*this.pagination_clicked_count;
 
        this.push_prev_startAt(this.firstInResponse);
 
        this.disable_next = false;
      }, error => {
        this.disable_next = false;
      });
  }

That is it

I hope this helps if you have questions when you look at the code snippet and if it’s not clear please get in contact and I will help you out by filling in the comment below.

Share: