export class PaginatedListener {
/**
* Creates an object to allow for paginating a listener for table
* read from Firestore. REQUIRES a sorting choice; masks some
* subscribe/unsubscribe action for paging forward/backward
* @param {!string} table - a properly formatted string representing the requested collection
* - always an ODD number of elements
* @param {filterObject} [filterArray] - an (optional) 3xn array of filter(i.e. "where") conditions
* @param {!sortObject} [sortArray] - a 2xn array of sort (i.e. "orderBy") conditions
* @param {?refPath} refPath - (optional) allows "table" parameter to reference a sub-collection
* of an existing document reference (I use a LOT of structured collections)
*
* The array is assumed to be sorted in the correct order -
* i.e. filterArray[0] is added first; filterArray[length-1] last
* returns data as an array of objects (not dissimilar to Redux State objects)
* with both the documentID and documentReference added as fields.
* @param {?number} limit - (optional)
* @param {!callback} dataCallback
* @param {!callback} errCallback
* @category Paginator
*/
constructor(
table,
filterArray = null,
sortArray,
refPath = null,
limit = PAGINATE_DEFAULT,
dataCallback = null,
errCallback = null
) {
/**
* table path at base of listener query, relative to original refPath
* @private
* @type {string}
*/
this.table = table;
/**
* array of filter objects for listener query
* @private
* @type {filterObject}
*/
this.filterArray = filterArray;
/**
* array of sort objects for listener query
* @private
* @type {sortObject}
*/
this.sortArray = sortArray;
/**
* refPath as basis for listener query
* @private
* @type {string}
*/
this.refPath = refPath;
/**
* current limit basis for listener query
* @type {number}
*/
this.limit = limit;
this._setQuery();
/**
* current dataCallback of listener query
* @private
* @type {RecordListener}
*/
this.dataCallback = dataCallback;
/**
* current errCallback of listener query
* @private
* @type {callback}
*/
this.errCallback = errCallback;
/**
* current status of listener
* @type {number}
*/
this.status = PAGINATE_INIT;
}
/**
* reconstructs the basis query
* @private
* @method _setQuery
* @returns {Query}
*/
_setQuery() {
const db = this.refPath ? this.refPath : fdb;
/**
* Query that forms basis for listener query
* @private
* @type {Query}
*/
this.Query = sortQuery(
filterQuery(db.collection(this.table), this.filterArray),
this.sortArray
);
/**
* last QuerySnapshot returned for listener query
* @private
* @type {QuerySnapshot}
*/
this.Snapshot = null;
return this.Query;
}
/**
* resets the listener query to the next page of results.
* Unsubscribes from the current listener, constructs a new query, and sets it
* as the new listener
* @async
* @method
* @returns {unsubscribe} returns the unsubscriber function (for lifecycle events)
*/
PageForward() {
const runQuery =
this.unsubscriber && !this.snapshot.empty
? this.Query.startAfter(last(this.snapshot.docs))
: this.Query;
//IF unsubscribe function is set, run it.
this.unsubscriber && this.unsubscriber();
this.status = PAGINATE_PENDING;
this.unsubscriber = runQuery.limit(Number(this.limit)).onSnapshot(
(QuerySnapshot) => {
this.status = PAGINATE_UPDATED;
//*IF* documents (i.e. haven't gone back ebfore start)
if (!QuerySnapshot.empty) {
//then update document set, and execute callback
this.snapshot = QuerySnapshot;
}
this.dataCallback(RecordsFromSnapshot(this.snapshot));
},
(err) => {
this.errCallback(err);
}
);
return this.unsubscriber;
}
/**
* resets the listener query to the next page of results.
* Unsubscribes from the current listener, constructs a new query, and sets it\
* as the new listener
* @async
* @method
* @returns {unsubscribe} returns the unsubscriber function (for lifecycle events)
*/
PageBack() {
const runQuery =
this.unsubscriber && !this.snapshot.empty
? this.Query.endBefore(this.snapshot.docs[0])
: this.Query;
//IF unsubscribe function is set, run it.
this.unsubscriber && this.unsubscriber();
this.status = PAGINATE_PENDING;
this.unsubscriber = runQuery.limitToLast(Number(this.limit)).onSnapshot(
(QuerySnapshot) => {
//acknowledge complete
this.status = PAGINATE_UPDATED;
//*IF* documents (i.e. haven't gone back ebfore start)
if (!QuerySnapshot.empty) {
//then update document set, and execute callback
this.snapshot = QuerySnapshot;
}
this.dataCallback(RecordsFromSnapshot(this.snapshot));
},
(err) => {
this.errCallback(err);
}
);
return this.unsubscriber;
}
/**
* sets page size limit to new value, and restarts the paged listener
* @async
* @method
* @param {number} newLimit
* @returns {unsubscribe} returns the unsubscriber function (for lifecycle events)
*/
ChangeLimit(newLimit) {
const runQuery = this.Query;
//IF unsubscribe function is set, run it.
this.unsubscriber && this.unsubscriber();
this.limit = newLimit;
this.status = PAGINATE_PENDING;
this.unsubscriber = runQuery.limit(Number(this.limit)).onSnapshot(
(QuerySnapshot) => {
this.status = PAGINATE_UPDATED;
//*IF* documents (i.e. haven't gone back ebfore start)
if (!QuerySnapshot.empty) {
//then update document set, and execute callback
this.snapshot = QuerySnapshot;
}
this.dataCallback(RecordsFromSnapshot(this.snapshot));
},
(err) => {
this.errCallback(err);
}
);
return this.unsubscriber;
}
/**
* changes the filter on the subscription
* This has to unsubscribe the current listener,
* create a new query, then apply it as the listener
* @async
* @method
* @param {filterObject} [filterArray] an array of filter descriptors
* @returns {unsubscribe} returns the unsubscriber function (for lifecycle events)
*/
ChangeFilter(filterArray) {
//IF unsubscribe function is set, run it (and clear it)
this.unsubscriber && this.unsubscriber();
this.filterArray = filterArray; // save the new filter array
const runQuery = this._setQuery(); // re-build the query
this.status = PAGINATE_PENDING;
//fetch the first page of the new filtered query
this.unsubscriber = runQuery.limit(Number(this.limit)).onSnapshot(
(QuerySnapshot) => {
this.status = PAGINATE_UPDATED;
//*IF* documents (i.e. haven't gone back ebfore start)
this.snapshot = QuerySnapshot;
this.dataCallback(RecordsFromSnapshot(this.snapshot));
},
(err) => {
this.errCallback(err);
}
);
return this.unsubscriber;
}
/**
* IF unsubscribe function is set, run it.
* @async
* @method
*/
unsubscribe() {
//IF unsubscribe function is set, run it.
this.unsubscriber && this.unsubscriber();
this.unsubscriber = null;
}
}
/**
* ----------------------------------------------------------------------
* returns an array of internal record structures from a
* firestore Query snapshot
* @private
* @function
* @static
* @param {QuerySnapshot} QuerySnapshot
* @returns {RecordArray}
*/
export const RecordsFromSnapshot = (QuerySnapshot) => {
return QuerySnapshot.docs.map((docSnap) => {
return RecordFromSnapshot(docSnap);
});
};
/**
* ----------------------------------------------------------------------
* returns an internal record structure from a firestore
* Document snapshot
* @private
* @function
* @static
* @param {DocumentSnapshot} DocumentSnapshot
* @returns {Record}
*/
export const RecordFromSnapshot = (DocumentSnapshot) => {
return {
...DocumentSnapshot.data(),
Id: DocumentSnapshot.id,
refPath: DocumentSnapshot.ref.path
};
};
```