6

There seem to be a lot of examples, tutorials and videos on howto paginate to the next page when using Angular with Cloud Firestore (Firebase).

But after extensive search i cannot find a way to paginate to a previous page. The furthest i got is just returning to the first page.

Here is how my Service looks right now:

import { Injectable } from '@angular/core';
import { AngularFirestore } from 'angularfire2/firestore';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Task } from './task.model';

@Injectable()
export class TaskService {
  private _data: BehaviorSubject<Task[]>;
  public data: Observable<Task[]>;
  firstEntry: any;
  latestEntry: any;

  constructor(private afs: AngularFirestore) { }

  public first() {
    this._data = new BehaviorSubject([]);
    this.data = this._data.asObservable();

    const tasksRef = this.getCollection('tasks', ref => ref.orderBy('createdAt').limit(5))
      .subscribe(data => {
        this.firstEntry = data[0].doc;
        this.latestEntry = data[data.length - 1].doc;
        this._data.next(data);
      });
  }

  public next() {
    const tasksRef = this.getCollection('tasks', ref => ref.orderBy('createdAt').startAfter(this.latestEntry).limit(5))
      .subscribe(data => {
        if (data.length) {
          this.firstEntry = data[0].doc;
          this.latestEntry = data[data.length - 1].doc;
          this._data.next(data);
        }
      });
  }

  public previous() {
    const tasksRef = this.getCollection('tasks', ref => ref.orderBy('createdAt').endBefore(this.firstEntry).limit(5))
      .subscribe(data => {
        if (data.length) {
          this.firstEntry = data[0].doc;
          this.latestEntry = data[data.length - 1].doc;
          this._data.next(data);
        }
      });
  }

  private getCollection(ref, queryFn?): Observable<any[]> {
    return this.afs.collection(ref, queryFn).snapshotChanges().pipe(
      map(actions => {
        return actions.map(a => {
          const data = a.payload.doc.data();
          const id = a.payload.doc.id;
          const doc = a.payload.doc;
          return { id, ...data, doc };
        });
      })
    );
  }
}

The initial loading (first page) works, and also all next pages are working as expected. But it seems like endBefore(this.firstEntry) doesn't hold the correct cursor, and results in the first page again.

What is the proper way to navigate back to the previous page?

And if someone knows of a complete tutorial or example of a working paginator, please share...

ReyAnthonyRenacia
  • 17,219
  • 5
  • 37
  • 56
Jordy Bulten
  • 155
  • 1
  • 1
  • 9

3 Answers3

2
import * as firebase from 'firebase/app';
import { last } from 'lodash';
import { dropRight } from 'lodash';

Install this npm packages ( Lodash, Firebase )

After import firebase, last and dropRight from lodash. Last method returns the last element of array. DropRight method slices the last element of the array.

allEntry: Array<any[]> =  [];
firstEntry = [];
lastEntry;

Create three variables and assign the the firstEntry to an array in order to store the first data in the query. AllEntry stores all the data retrieved from the query. LastEntry to store the last element in the allEntry which would be used to as a cursor to get the data after that.

getMainEntry() {
return this.afs.collection('tasks', ref => ref.orderBy('createdAt').limit(5)).valueChanges().subscribe(data => {
  this.allentry = data;
  firebase.firestore().collection('tasks').doc(data[data.length - 1]['key']).onSnapshot(c => {
    this.lastentry = c;
    firebase.firestore().collection('tasks').doc(data[0]['key']).onSnapshot(da => {
      this.firstEntry.push(da);
    });
  });
});
}

This getMainEntry function gets the data in the collection, orders it by createdAt and limits it to 5. Then gets and assigns the documentsnapshot of the last element in the allEntry array to the lastEntry. After gets and pushes the documentsnapshot of the first element in the allEntry array into the firstEntry array.

Now let's create the next function

getNextData(){
  const entarr = [];
  firebase.firestore().collection('tasks').orderBy('createdAt').startAfter(this.lastEntry).limit(5).get().then((data) => {
    data.docs.map(a => {
      entarr.push(a.data());
      this.allEntry = entarr;
    });
  }).then(() => {
    firebase.firestore().collection('tasks').doc(entarr[entarr.length - 1]['key']).onSnapshot(data => {
      this.lastEntry = data;
    });
  }).then(() => {
    firebase.firestore().collection('tasks').doc(entarr[0]['key']).onSnapshot(da => {
      this.firstEntry.push(da);
    });
  });
}

The getNextData function gets the next data in the collection, orders it by createdAt and limits it to the next 5 data after the lastEntry. Then gets and assigns the documentsnapshot of the last element in the entarr array to the lastEntry. After gets and pushes the documentsnapshot of the first element in the allEntry array into the firstEntry array.

Previous Function

getPrevData() {
const entarr = [];
  this.firstEntry = dropRight(this.firstEntry);
const firstEntry = last(this.firstEntry);
firebase.firestore().collection('tasks').orderBy('createdAt').startAt(firstEntry).limit(5).get().then((d) => {
  d.docs.map(a => {
    entarr.push(a.data());
    this.allEntry = entarr;
    });
  }).then(() => {
    firebase.firestore().collection('tasks').doc(entarr[entarr.length - 1]['key']).onSnapshot(da => {
      this.lastEntry = da;
    });
  });
}

Hope this would be useful.

Code Mickey
  • 947
  • 6
  • 9
0

Reduced Code

paginate(navigation) { 

    switch (navigation) {
      case 'first':
        this.dataQuery = this.itemDoc.collection('photos')
          .ref.orderBy('modifiedDate', 'desc').limit(2);
        break;
      case 'next':
        this.dataQuery = this.itemDoc.collection('photos')
          .ref.orderBy('modifiedDate', 'desc').startAfter(this.lastVisible).limit(2);
        break;
      case 'prev':
        this.firstVisible = dropRight(this.firstVisible);
        const firstVisible = last(this.firstVisible);
        this.dataQuery = this.itemDoc.collection('photos')
          .ref.orderBy('modifiedDate', 'desc').startAt(firstVisible).limit(2);
        break;
    }
      
    this.dataSource = this.dataQuery.get().then((documentSnapshots:QuerySnapshot<any>)=>{

      if(navigation != 'prev') {
        this.firstVisible.push(documentSnapshots.docs[0]);
      }
      this.lastVisible = documentSnapshots.docs[documentSnapshots.docs.length -1];
            
      return documentSnapshots.docs.map((element:QueryDocumentSnapshot<any>)=>{
        const data = element.data();
        const id = element.id;
        return { id, ...data };
      })
    });
  }
  paginatorOnChange(paginatorObj) {
    //console.log(paginatorObj);
    this.pageIndex = paginatorObj.pageIndex;
    this.paginate(paginatorObj.previousPageIndex<paginatorObj.pageIndex? 'next': 'prev');
  }
0

With one minor update, your service seems to work, however i am left with questions, noted below and would love to hear what you, or anyone with knowledge on this thinks...

First, the update:

Version 7.3.0 of the Firebase SDK, published recently, provides a new query method: limitToLast(n:number).

And, i have tested your service to work correctly simply by using that query method in your previous() method.

I changed this:

const tasksRef = this.getCollection('tasks', ref => ref.orderBy('createdAt').endBefore(this.firstEntry)
.limit(5))
.subscribe(...)

To this:

const tasksRef = this.getCollection('tasks', ref => ref.orderBy('createdAt').endBefore(this.firstEntry)
.limitToLast(5)) // <-- using the new query method here
.subscribe(...)

However, as i said above i am left with some questions:

Isn't it true that each call to first(), next() and previous() instantiate new subscriptions with each request? And if so, shouldn't you be unsubscribing prior to subsequent requests?

I see that you are storing each of the subscriptions into a tasksRef constant in each of your methods, and it is not clear to me why, unless there was intent to unsubscribe elsewhere in code not yet written in your sample code?

With that said though, I am trying to achieve pagination in the same way you have while still maintaining a listener for realtime updates. In my particular case, i know that none of the documents will change in the collection i need to paginate, nor will the order, but rather just that new documents could be added to the collection, and id like to listen for the additional documents realtime. It seems though that unsubscribing would complicate how to achieve this.

Anyone have insight into whether or not we should be worried about unsubscribing prior to paginating specific to the sample implementation posted?

DevMike
  • 1,630
  • 2
  • 19
  • 33