Firestore has powerful querying syntax and the
AngularFirestoreCollection
provides a thin wrapper around it. This keeps you from having to learn two query syntax systems. If you know the Firestore query API then you know how to query in AngularFirestore.
Queries are created by building on the firebase.firestore.CollectionReference
.
afs.collection('items', ref => ref.where('size', '==', 'large'))
method | purpose |
---|---|
where |
Create a new query. Can be chained to form complex queries. |
orderBy |
Sort by the specified field, in descending or ascending order. |
limit |
Sets the maximum number of items to return. |
startAt |
Results start at the provided document (inclusive). |
startAfter |
Results start after the provided document (exclusive). |
endAt |
Results end at the provided document (inclusive). |
endBefore |
Results end before the provided document (exclusive). |
To learn more about querying and sorting in Firestore, check out the Firebase documentation.
Range filters can only be specified on a single field:
// WARNING: Do not copy and paste. This will not work!
ref.where('state', '>=', 'CA')
.where('population', '>', 100000)
Range filter and orderBy cannot be on different fields:
// WARNING: Do not copy and paste. This will not work!
ref.where('population', '>', 100000).orderBy('country')
Range filters / orderBy cannot be used in conjuction with user-defined data, they require composite indexes be defined on the specific fields.
// WARNING: Do not copy and paste. This will not work!
ref.where('tags.awesome', '==', true).orderBy('population')
To enable dynamic queries one should lean on RxJS Operators like switchMap
.
An RxJS Subject is imported below. A Subject is like an Observable, but can multicast to many Observers. Subjects are like EventEmitters: they maintain a registry of many listeners. See, What is a Subject for more information.
When we call switchMap
on the Subject, we can map each value to a new Observable; in this case a database query.
const size$ = new Subject<string>();
const queryObservable = size$.pipe(
switchMap(size =>
afs.collection('items', ref => ref.where('size', '==', size)).valueChanges()
)
);
// subscribe to changes
queryObservable.subscribe(queriedItems => {
console.log(queriedItems);
});
// trigger the query
size$.next('large');
// re-trigger the query!!!
size$.next('small');
See this example in action on StackBlitz.
import { Component } from '@angular/core';
import { AngularFirestore } from '@angular/fire/compat/firestore';
import { Observable, BehaviorSubject, combineLatest } from 'rxjs';
import { switchMap } from 'rxjs/operators';
export interface Item {
text: string;
color: string;
size: string;
}
@Component({
selector: 'app-root',
template: `
<div *ngIf="items$ | async; let items; else loading">
<ul>
<li *ngFor="let item of items">
{{ item.text }}
</li>
</ul>
<div *ngIf="items.length === 0">No results, try clearing filters</div>
</div>
<ng-template #loading>Loading…</ng-template>
<div>
<h4>Filter by size</h4>
<button (click)="filterBySize('small')">Small</button>
<button (click)="filterBySize('medium')">Medium</button>
<button (click)="filterBySize('large')">Large</button>
<button (click)="filterBySize(null)" *ngIf="this.sizeFilter$.getValue()">
<em>clear filter</em>
</button>
</div>
<div>
<h4>Filter by color</h4>
<button (click)="filterByColor('red')">Red</button>
<button (click)="filterByColor('green')">Blue</button>
<button (click)="filterByColor('blue')">Green</button>
<button (click)="filterByColor(null)" *ngIf="this.colorFilter$.getValue()">
<em>clear filter</em>
</button>
</div>
`,
})
export class AppComponent {
items$: Observable<Item[]>;
sizeFilter$: BehaviorSubject<string|null>;
colorFilter$: BehaviorSubject<string|null>;
constructor(afs: AngularFirestore) {
this.sizeFilter$ = new BehaviorSubject(null);
this.colorFilter$ = new BehaviorSubject(null);
this.items$ = combineLatest(
this.sizeFilter$,
this.colorFilter$
).pipe(
switchMap(([size, color]) =>
afs.collection('items', ref => {
let query : firebase.firestore.CollectionReference | firebase.firestore.Query = ref;
if (size) { query = query.where('size', '==', size) };
if (color) { query = query.where('color', '==', color) };
return query;
}).valueChanges()
)
);
}
filterBySize(size: string|null) {
this.sizeFilter$.next(size);
}
filterByColor(color: string|null) {
this.colorFilter$.next(color);
}
}
To run the above example as is, you need to have sample data in your Firebase database with the following structure:
{
"items": {
"a" : {
"size" : "small",
"text" : "small thing",
"color" : "red"
},
"b" : {
"size" : "medium",
"text" : "medium sample",
"color" : "red"
},
"c" : {
"size" : "large",
"text" : "large widget",
"color" : "green"
}
}
}
To query across collections and sub-collections with the same name anywhere in Firestore, you can use collection group queries.
Collection Group Queries allow you to have a more nested data-structure without sacrificing performance. For example, we could easily query all comments a user posted; even if the comments were stored as a sub-collection under Articles/**
or even nested deeply (Articles/**/Comments/**/Comments/**/...
):
constructor(private afs: AngularFirestore) { }
ngOnInit() {
...
// Get all the user's comments, no matter how deeply nested
this.comments$ = afs.collectionGroup('Comments', ref => ref.where('user', '==', userId))
.valueChanges({ idField: 'docId' });
}
collectionGroup
returns an AngularFirestoreCollectionGroup
which is similar to AngularFirestoreCollection
. The main difference is that AngularFirestoreCollectionGroup
has no data operation methods such as add
because it doesn't have a concrete reference.