Unofficial library for angular & firebase.
Install firebase & firebase-tools:
npm install -D firebase-tools
npm install firebase
Initialize your firebase project:
npx firebase init
Install the lib
npm install ngfire
Add the firebase config
environment.ts
(use emulator) :
import { authEmulator, databaseEmulator, firestoreEmulator, functionsEmulator, storageEmulator } from "ngfire";
export const environment = {
production: false,
firebase: {
options: {
projectId: 'demo-firebase',
apiKey: 'demo',
authDomain: 'demo-firebase.firebaseapp.com',
storageBucket: 'default-bucket',
},
firestore: firestoreEmulator('localhost', 8000),
auth: authEmulator('http://localhost:9099', { disableWarnings: true }),
storage: storageEmulator("localhost", 9199),
functions: functionsEmulator("localhost", 5001),
database: databaseEmulator("localhost", 9000),
},
}
environment.prod.ts
(use your project's config here) :
export const environment = {
production: true,
firebase: {
options: {
apiKey: '******',
authDomain: '******',
projectId: '******',
storageBucket: '******',
messagingSenderId: '******',
appId: '******',
measurementId: '******',
}
},
}
Connect with angular:
app.module.ts
:
...
import { FIREBASE_CONFIG } from 'ngfire';
import { environment } from '../environments/environment';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, ReactiveFormsModule],
providers: [{ provide: FIREBASE_CONFIG, useValue: environment.firebase }],
bootstrap: [AppComponent],
})
export class AppModule {}
You can create a new service per collection of Firestore:
flight.service.ts
:
import { Injectable } from '@angular/core';
import { FireCollection } from 'ngfire';
export interface Flight {
number: string;
info: string;
}
@Injectable({ providedIn: 'root' })
export class FlightService extends FireCollection<Flight> {
// Collection path
readonly path = 'flights';
// Key used to get the id (default: "id")
readonly idKey = 'id';
// Memorize all requests that has been done
memorize = true;
}
And use it in the component
app.component.ts
:
@Component({
selector: 'ngfire-root',
template: `
<ul>
<li *ngFor="let flight of flight$ | async">
{{ flight.number }}: {{ flight.info }}
</li>
</ul>
<form [formGroup]="form" (ngSubmit)="add()">
<input formControlName="number" />
<textarea formControlName="info"></textarea>
<button>Submit</button>
</form>
`,
})
export class AppComponent {
form = new FormGroup({
number: new FormControl(),
info: new FormControl()
});
flight$ = this.service.valueChanges();
constructor(private service: FlightService) {}
add() {
this.service.add(this.form.value);
}
}
The service exposes an API to do most of common tasks:
valueChanges
: returns anObservable
load
: returns aPromise
using the cache if availablegetValue
: returns aPromise
without using the cacheadd
: adds a new documet to the collectionupdate
: update an existing document to the collectionupsert
: add or update a documentremove
: remove a document from the collectionremoveAll
: remove all documents from a collection
service.valueChanges(); // All document of the collection
service.valueChanges('id'); // Documents with id "id"
service.valueChanges([ '1', '2' ]); // Two documents with the ids specified
service.valueChanges([ where('key', '==', value) ]); // All documents where "key" is value
ngfire
will memorize the query, but new Date()
changes each time you trigger the query, so the query won't be memorized as expected.
Examples :
where('createdAt', '<', new Date())
: Query will be trigger each time this is call.where('createdAt', '<', nextMinute(new Date()))
: Query is cached for the next minute.where('createdAt', '<', nextDay(new Date()))
: Query is cached for the current user session.
service.load();
service.load('id');
service.load([ '1', '2' ]);
service.load([ where('key', '==', value) ]);
service.getValue();
service.getValue('id');
service.getValue([ '1', '2' ]);
service.getValue([ where('key', '==', value) ]);
const id = await service.add({ });
const ids = await service.add([{ }, { }]);
// Implicit id: idKey specified in the service should be in the document to update
await service.update({ id: '1', list: arrayUnion(42) });
await service.update([ { id: '1', age: 42 }, { id: '2', age: 24 } ]);
// Explicit id:
await service.update('1', { list: arrayUnion(42) });
await service.update(['1', '2'], { name: 42 }); // each document is updated the same way
// Transaction
await service.update('1', (doc, tx) => ({ age: doc.age + 1 }));
await service.update(['1', '2'], (doc, tx) => ({ age: doc.age + 1 }));
const id = await service.upsert({ id: '1', list: arrayUnion(42) })
const ids = await service.upsert([
{ id: '1', age: 42 }, // If document exist in firestore: fallbacks to `add`
{ age: 24 }, // No id provided: fallbacks to `update`
])
await service.remove('1');
await service.remove(['1', '2']);
await service.removeAll();
Every write operation (add
, upsert
, update
, remove
, removeAll
) support the write
options for batches & transactions.
This snippet will create a document and add the id to the members
map of the other document with status "pending".
If one of the write operation fails, both fail.
const batch = service.batch();
const id = await service.add({ age: 42 }, { write: batch });
await service.update('1', { `members.${id}`: 'pending' }, { write: batch })
await batch.commit();
Regular transaction:
service.runTransaction(async (tx) => {
const ref = service.getRef('1');
const data = await tx.get(ref).then(snap => snap.data());
return service.update('1', { age: data.age + 1 }, { write: tx });
})
Update transaction:
// Simple increment
service.update('1', doc => ({ age: doc.age + 1 }));
// Trigger side effect safely
service.update('id1', (doc, tx) => {
const newAge = doc.age + 1;
if (newAge > 25) {
service.update('id2', { 'members.id1': deleteField() }, { write: tx });
}
return { age: newAge };
});
To access a subcollection you can extend the FireSubcollection
service:
@Injectable({ providedIn: 'root' })
export class TicketService extends FireSubcollection<Ticket> {
readonly path = 'flights/:flightId/tickets';
// Field to be used for the document id (default: id)
override readonly idKey = 'ticketId';
// Field to be used for the full path reference (default: path)
override readonly pathKey = 'refPath';
}
The service extends FireCollection
with additional features:
By default the subcollection will query the collectionGroup
:
// Query all documents inside a collection with name "tickets"
service.valueChanges();
service.valueChanges([where('status', '==', 'cancelled')]);
Add an object with the value of the params as last argument:
service.valueChanges({ flightId: 'AZ301' });
service.valueChanges('001', { flightId: 'AZ301' }); // 001 beeing the ticketId
service.valueChanges([where('status', '==', 'cancelled')], { flightId: 'AZ301' });
Add params as an field of the WriteOption
:
const params = { flightId: 'AZ301' };
service.add(ticket, { params });
service.update('001', ticket, { params }); // 001 beeing the ticketId
service.remove('001', { params });
Example using the batch:
const write = service.batch();
const params = { flightId: 'AZ301' };
await service.add(ticket, { params, write });
await service.update('001', ticket, { params, write });
await service.remove('002', { params, write });
write.commit();