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

100% Statements 52/52
100% Branches 24/24
100% Functions 12/12
100% Lines 50/50

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 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 1831x 1x 1x 1x 1x 1x                                           1x       51x   51x     51x                       51x 51x 51x 51x                     504x               504x 503x     504x 504x 504x       504x     504x   36x               9x 9x 8x 8x       8x   1x               505x 505x           505x             9x 9x 8x     1x                 512x 113x                 399x     399x   399x 1806x 1806x 1806x   1806x       399x     399x           399x     399x 23x              
import { Inject, Injectable, NgZone } from '@angular/core';
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { LoadingBarService } from '@ngx-loading-bar/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { scan, take, tap } from 'rxjs/operators';
import { APP_CONFIG, InterfaceAppConfig } from '../app-config';
 
/**
 * 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
     * @param ngZone: NgZone
     * @param loadingBar: LoadingBarService
     * @param appConfig: APP_CONFIG
     */
    constructor(private readonly afs: AngularFirestore,
                private readonly ngZone: NgZone,
                private readonly loadingBar: LoadingBarService,
                @Inject(APP_CONFIG) private readonly appConfig: InterfaceAppConfig) {
    }
 
    /**
     * 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);
        // istanbul ignore next
        if (!this.appConfig.isUnitTest) {
            // I do not want to add 'tick(x)' to all of end of unit test cases just for loading bar
            this.loadingBar.complete();
        }
        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
        // istanbul ignore next
        if (!this.appConfig.isUnitTest) {
            // I do not want to add 'tick(x)' to all of end of unit test cases just for loading bar
            this.loadingBar.start();
        }
        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);
                // istanbul ignore next
                if (!this.appConfig.isUnitTest) {
                    // I do not want to add 'tick(x)' to all of end of unit test cases just for loading bar
                    this.loadingBar.complete();
                }
                this.loading.next(false);
 
                // no more values, mark done
                if (!values.length) {
                    this.done.next(true);
                }
            }))
            .pipe(take(1))
            .subscribe();
    }
}