All files / src/app/services pagination.service.ts

100% Statements 56/56
100% Branches 24/24
100% Functions 13/13
100% Lines 51/51

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 1601x 1x 1x 1x                                           1x       50x   50x     50x                 50x                   504x 504x               504x 503x     504x 504x 504x       504x     504x   42x             9x 9x 9x 8x 8x       8x   1x             1x 505x 505x 505x           1x 9x 9x 8x     1x             512x   512x 115x       397x     397x   397x 1776x 1776x 1776x   1776x       397x     397x 397x     397x 27x           1x  
import { Injectable } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { BehaviorSubject, Observable } from 'rxjs';
import { scan, take, tap } from 'rxjs/operators';
 
/**
 * options to reproduce firestore queries consistently
 */
interface QueryConfig {
    /** path to collection */
    path: string;
    /** field to orderBy */
    field: string;
    /** limit per query */
    limit?: number;
    /** reverse order? */
    reverse?: boolean;
    /** prepend to source? */
    prepend?: boolean;
}
 
/**
 * Pagination Service
 */
@Injectable()
export class PaginationService {
    /** observable data */
    data: Observable<any>;
    /** is done? */
    done = new BehaviorSubject<boolean>(false);
    /** is loading? */
    loading = new BehaviorSubject<boolean>(false);
 
    /** private source data */
    private readonly _data = new BehaviorSubject([]);
 
    /** QueryConfig */
    private query: QueryConfig;
 
    /**
     * constructor of PaginationService
     * @param afs: AngularFirestore
     */
    constructor(private readonly afs: AngularFirestore) {
    }
 
    /**
     * initial query sets options and defines the Observable
     * @param path: path to collection
     * @param field: field to orderBy
     * @param opts: options
     * @param isReset: do you want to reset before init?
     */
    init(path, field, opts?, isReset?): void {
        this.query = {
            path,
            field,
            limit: 2,
            reverse: false,
            prepend: false,
            ...opts
        };
        if (isReset || isReset === undefined) {
            this.reset();
        }
 
        setTimeout(() => {
            const first = this.afs.collection(this.query.path, ref =>
                ref
                    .orderBy(this.query.field, this.query.reverse ? 'desc' : 'asc')
                    .limit(this.query.limit));
 
            this.mapAndUpdate(first);
 
            // create the observable array for consumption in components
            this.data = this._data.asObservable()
                .pipe(scan((acc, val) =>
                    this.query.prepend ? val.concat(acc) : acc.concat(val)));
        }, 0);
    }
 
    /**
     * Retrieves additional data from firestore
     */
    more(): any {
        const cursor = this.getCursor();
        if (cursor) {
            const more = this.afs.collection(this.query.path, ref =>
                ref
                    .orderBy(this.query.field, this.query.reverse ? 'desc' : 'asc')
                    .limit(this.query.limit)
                    .startAfter(cursor));
            this.mapAndUpdate(more);
        } else {
            this.done.next(true);
        }
    }
 
    /**
     * Reset the pagination
     */
    reset(): void {
        this._data.next([]);
        this.done.next(false);
        this.loading.next(false);
    }
 
    /**
     * Determines the doc snapshot to paginate query
     */
    private getCursor(): any {
        const current = this._data.value;
        if (current.length) {
            return this.query.prepend ? current[0].doc : current[current.length - 1].doc;
        }
 
        return;
    }
 
    /**
     * Maps the snapshot to usable format the updates source
     * @param col: AngularFirestoreCollection
     */
    private mapAndUpdate(col: AngularFirestoreCollection<any>): 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 id = snap.payload.doc.id;
                    const data = snap.payload.doc.data();
                    const doc = snap.payload.doc;
 
                    return {id, ...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();
    }
}