Skip to content

Instantly share code, notes, and snippets.

@peavers
Last active February 3, 2023 15:47
Show Gist options
  • Save peavers/0a6d3cd6104b017c0f8476cad5c43f4b to your computer and use it in GitHub Desktop.
Save peavers/0a6d3cd6104b017c0f8476cad5c43f4b to your computer and use it in GitHub Desktop.
Enables infinite/virtual scrolling with ngx-virtual-scroller and Firestore
import {Injectable} from '@angular/core';
import {BehaviorSubject, Observable} from "rxjs";
import {AngularFirestore} from "@angular/fire/firestore";
import {map, mergeMap, scan, tap, throttleTime} from "rxjs/operators";
import {ChangeEvent} from "ngx-virtual-scroller";
/**
* Enables infinite scrolling with ngx-virtual-scroller and Firestore. Most of the hard work goes to
* https://angularfirebase.com/lessons/infinite-virtual-scroll-angular-cdk.
*
* @author Chris Turner (peavers@gmail.com)
*/
@Injectable({
providedIn: 'root'
})
export class FirestoreScrollService {
batch = 10;
theEnd = false;
offset = new BehaviorSubject(null);
items: Observable<any[]>;
constructor(private firestore: AngularFirestore) {
}
/**
* Gets the inital page of data from Firestore.
* @param path String path of collection
* @param field String key to use for ordering
*/
public getFirstPage(path: string, field: string) {
const batchMap = this.offset.pipe(
throttleTime(500),
mergeMap(number => this.getBatch(path, field, number)),
scan((acc, batch) => {
return {...acc, ...batch};
}, {})
);
this.items = batchMap.pipe(map(value => Object.values(value)));
}
/**
* Gets a collection of items from Firebase.
* @param path String path of collection
* @param field String key to use for ordering
* @param offset String the value of the last item
*
* @return Observable holding the mapped value
*/
public getBatch(path: string, field: string, offset: string): Observable<{}> {
return this.firestore
.collection(path, reference => reference
.orderBy(field)
.startAfter(offset)
.limit(this.batch))
.snapshotChanges()
.pipe(
tap(arr => (arr.length ? null : (this.theEnd = true))),
map(arr => {
return arr.reduce((acc, cur) => {
return {...acc, [cur.payload.doc.id]: cur.payload.doc.data()};
}, {});
})
);
}
/**
* Trigger an event to fetch the next batch of items.
* @param event ChangeEvent from ngx-virtual-scroll
* @param offset String value of the last item
* @param totalItems Int count of objects rendered 'scroll.cachedItemsLength' seems to work
*/
public nextBatch(event: ChangeEvent, offset: string, totalItems: number) {
// If we have everything just return
if (this.theEnd) {
return;
}
// If the last item we got from the event is also the end of the rendered view, fetch more
if (event.endIndex === totalItems - 1) {
this.offset.next(offset);
}
}
}
<div class="section">
<div class="container">
<ng-container *ngIf="fss.items | async as items">
<virtual-scroller #scroll class="scroll-window"
[parentScroll]="scroll.window"
[items]="items"
(end)="fss.nextBatch($event, (items[items.length - 1].name), scroll.cachedItemsLength)">
<div class="columns is-multiline item-list" #container>
<item-object*ngFor="let item of scroll.viewPortItems" [item]="item"></item-object>
</div>
</virtual-scroller>
</ng-container>
</div>
</div>
import {Component, OnInit} from '@angular/core';
import {FirestoreScrollService} from "../../services/firestore-scroll.service";
@Component({
selector: 'some-component',
templateUrl: './some-component.html',
})
export class SomeComponent implements OnInit {
constructor(public fss: FirestoreScrollService) {}
ngOnInit(): void {
this.fss.getFirstPage(`/items`, 'name');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment