Search code examples
angulargoogle-cloud-firestoreionic4

Ionic AngularFirebase Infinite scroll Pagination service


I am attempting to implement an infinite scroll into my ionic angular app. Being new at this I am needing some expert advice. The problem is the more() function works the first time the scroll event happens but goes into an infinite loop when trying to get the second set of documents. Here is the code, I have included pices of the component and html and the complete pagination service.

Component.ts:

  ngOnInit() {

      this.page.init('Documents', 'Name', { reverse: false, prepend: false })

  scrollHandler(e) {
      if (e === 'bottom') {
        this.page.more()
      }
     }

Template html:

<ion-infinite-scroll threshold="100px" (ionInfinite)="this.page.more($event)">
  <ion-infinite-scroll-content
    loadingSpinner="bubbles"
    loadingText="Loading more data...">
  </ion-infinite-scroll-content>
</ion-infinite-scroll>

pagination-service.ts:

import { Injectable } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from  '@angular/fire/firestore';

import { BehaviorSubject } from 'rxjs';
import { map  } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { tap, scan, take} from 'rxjs/operators';


// Options to reproduce firestore queries consistently
interface QueryConfig {
  path: string, // path to collection
  field: string, // field to orderBy
  limit?: number, // limit per query
  reverse?: boolean, // reverse order?
  prepend?: boolean // prepend to source?
}

@Injectable()
export class PaginationService {

  // Source data
  private _done = new BehaviorSubject(false);
  private _loading = new BehaviorSubject(false);
  private _data = new BehaviorSubject([]);

  private query: QueryConfig;

  // Observable data
  data: Observable<any>;
  done: Observable<boolean> = this._done.asObservable();
  loading: Observable<boolean> = this._loading.asObservable();

  constructor( private afs: AngularFirestore ) { }

  // Initial query sets options and defines the Observable

  init(path, field, opts?) {

    this.query = { 
      path,
      field,
      limit: 10,
      reverse: false,
      prepend: false,
      ...opts
    }

    const first = this.afs.collection(this.query.path, ref => {
      return ref
             .orderBy(this.query.field, this.query.reverse ? 'desc' : 'asc')
              .where('Active', '==', 'Y')
               .limit(this.query.limit)             
    })

    this.mapAndUpdate(first)

    // Create the observable array for consumption in components
    this.data = this._data.asObservable()
        .pipe(scan( (acc, val) => { 
          return this.query.prepend ? val.concat(acc) : acc.concat(val)
        }))

  }

  // more() Retrieves additional data from firestore
// This works the first time but not the second time 

  more() {
    const cursor = this.getCursor()
    const more = this.afs.collection(this.query.path, ref => {
      return ref
              .orderBy(this.query.field, this.query.reverse ? 'desc' : 'asc')
              .limit(this.query.limit)
              .startAfter(cursor)
    })
     this.mapAndUpdate(more)
  }

  // Determines the doc snapshot to paginate query 
  private getCursor() {
    const current = this._data.value
    if (current.length) {
      return this.query.prepend ? current[0].doc : current[current.length - 1].doc 
    }
    return null
  }


  // Maps the snapshot to usable format the updates source
  private mapAndUpdate(col: AngularFirestoreCollection<any>) {

    if (this._done.value || this._loading.value) { return };

    // loading
    this._loading.next(true)

    // Map snapshot with doc ref (needed for cursor)
    return col.snapshotChanges()
      .pipe(tap(arr => {
        let values = arr.map(snap => {
          const data = snap.payload.doc.data()
          const doc = snap.payload.doc
          return { ...data, doc }
        })

        // If prepending, reverse array
        values = this.query.prepend ? values.reverse() : values

        // update source with new values, done loading
        this._data.next(values)
        this._loading.next(false)

        // no more values, mark done
        if (!values.length) {
           this._done.next(true)
        }
    }))
    .pipe(take(1))
    .subscribe()

  }

  // Reset the page
  reset() {
    this._data.next([])
    this._done.next(false)
  }
 }

Solution

  • The expression assigned to the ionInfinite event is called when the user reaches that defined distance. When this expression has finished any and all tasks, it should call the complete() method on the infinite scroll instance.

    In your case, you have not handled the event, just passed...

    this.page.more($event)
    

    change in Template html as below:

    <ion-infinite-scroll threshold="100px" (ionInfinite)="findNext($event)">
    

    Add method in Component.ts:

    async findNext($event) {
        setTimeout(async () => {
          await this.page.more();
          $event.target.complete();
        }, 500);
      }