Skip to content

Commit 09ed22a

Browse files
feat(firestore): options to include document ID on valueChanges() (#2113)
Co-authored-by: James Daniels <[email protected]>
1 parent 23bdb2f commit 09ed22a

File tree

4 files changed

+77
-39
lines changed

4 files changed

+77
-39
lines changed

docs/firestore/collections.md

+2-8
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ interface DocumentSnapshot {
7373

7474
There are multiple ways of streaming collection data from Firestore.
7575

76-
### `valueChanges({idField?: string})`
76+
### `valueChanges({ idField?: string })`
7777

7878
**What is it?** - The current state of your collection. Returns an Observable of data as a synchronized array of JSON objects. All Snapshot metadata is stripped and just the document data is included. Optionally, you can pass an options object with an `idField` key containing a string. If provided, the returned JSON objects will include their document ID mapped to a property with the name provided by `idField`.
7979

@@ -107,13 +107,7 @@ export class AppComponent {
107107
items: Observable<Item[]>;
108108
constructor(private readonly afs: AngularFirestore) {
109109
this.itemsCollection = afs.collection<Item>('items');
110-
// .valueChanges() is simple. It just returns the
111-
// JSON data without metadata. If you need the
112-
// doc.id() in the value you must persist it your self
113-
// or use .snapshotChanges() instead. See the addItem()
114-
// method below for how to persist the id with
115-
// valueChanges()
116-
this.items = this.itemsCollection.valueChanges();
110+
this.items = this.itemsCollection.valueChanges({ idField: 'customID' });
117111
}
118112
addItem(name: string) {
119113
// Persist a document id

docs/firestore/documents.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ interface DocumentSnapshot {
6969

7070
There are multiple ways of streaming collection data from Firestore.
7171

72-
### `valueChanges()`
72+
### `valueChanges({ idField?: string })`
7373

74-
**What is it?** - Returns an Observable of document data. All Snapshot metadata is stripped. This method provides only the data.
74+
**What is it?** - Returns an Observable of document data. All Snapshot metadata is stripped. This method provides only the data. Optionally, you can pass an options object with an `idField` key containing a string. If provided, the returned object will include its document ID mapped to a property with the name provided by `idField`.
7575

7676
**Why would you use it?** - When you just need the object data. No document metadata is attached which makes it simple to render to a view.
7777

78-
**When would you not use it?** - When you need the `id` of the document to use data manipulation methods. This method assumes you either are saving the `id` to the document data or using a "readonly" approach.
78+
**When would you not use it?** - When you need document metadata.
7979

8080
### `snapshotChanges()`
8181

src/firestore/document/document.spec.ts

+58-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AngularFireModule, FirebaseApp } from '@angular/fire';
22
import { AngularFirestore, SETTINGS } from '../firestore';
33
import { AngularFirestoreModule } from '../firestore.module';
4+
import { Subscription } from 'rxjs';
45
import { AngularFirestoreDocument } from './document';
56
import { take } from 'rxjs/operators';
67

@@ -33,32 +34,67 @@ describe('AngularFirestoreDocument', () => {
3334
app.delete();
3435
});
3536

36-
it('should get action updates', async (done: any) => {
37-
const randomCollectionName = randomName(afs.firestore);
38-
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
39-
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
40-
await stock.set(FAKE_STOCK_DATA);
41-
const sub = stock
42-
.snapshotChanges()
43-
.subscribe(async a => {
44-
sub.unsubscribe();
45-
if (a.payload.exists) {
46-
expect(a.payload.data()).toEqual(FAKE_STOCK_DATA);
47-
stock.delete().then(done).catch(done.fail);
48-
}
37+
describe('valueChanges()', () => {
38+
39+
it('should get unwrapped snapshot', async (done: any) => {
40+
const randomCollectionName = afs.firestore.collection('a').doc().id;
41+
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
42+
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
43+
await stock.set(FAKE_STOCK_DATA);
44+
const obs$ = stock.valueChanges();
45+
obs$.pipe(take(1)).subscribe(async data => {
46+
expect(data).toEqual(FAKE_STOCK_DATA);
47+
stock.delete().then(done).catch(done.fail);
4948
});
49+
});
50+
51+
/* TODO(jamesdaniels): test is flaking, look into this
52+
it('should optionally map the doc ID to the emitted data object', async (done: any) => {
53+
const randomCollectionName = afs.firestore.collection('a').doc().id;
54+
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
55+
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
56+
await stock.set(FAKE_STOCK_DATA);
57+
const idField = 'myCustomID';
58+
const obs$ = stock.valueChanges({ idField });
59+
obs$.pipe(take(1)).subscribe(async data => {
60+
expect(data[idField]).toBeDefined();
61+
expect(data).toEqual(jasmine.objectContaining(FAKE_STOCK_DATA));
62+
stock.delete().then(done).catch(done.fail);
63+
});
64+
});*/
65+
5066
});
5167

52-
it('should get unwrapped snapshot', async (done: any) => {
53-
const randomCollectionName = afs.firestore.collection('a').doc().id;
54-
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
55-
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
56-
await stock.set(FAKE_STOCK_DATA);
57-
const obs$ = stock.valueChanges();
58-
obs$.pipe(take(1)).subscribe(async data => {
59-
expect(data).toEqual(FAKE_STOCK_DATA);
60-
stock.delete().then(done).catch(done.fail);
68+
describe('snapshotChanges()', () => {
69+
70+
it('should get action updates', async (done: any) => {
71+
const randomCollectionName = randomName(afs.firestore);
72+
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
73+
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
74+
await stock.set(FAKE_STOCK_DATA);
75+
const sub = stock
76+
.snapshotChanges()
77+
.subscribe(async a => {
78+
sub.unsubscribe();
79+
if (a.payload.exists) {
80+
expect(a.payload.data()).toEqual(FAKE_STOCK_DATA);
81+
stock.delete().then(done).catch(done.fail);
82+
}
83+
});
6184
});
85+
86+
it('should get unwrapped snapshot', async (done: any) => {
87+
const randomCollectionName = afs.firestore.collection('a').doc().id;
88+
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
89+
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
90+
await stock.set(FAKE_STOCK_DATA);
91+
const obs$ = stock.valueChanges();
92+
obs$.pipe(take(1)).subscribe(async data => {
93+
expect(data).toEqual(FAKE_STOCK_DATA);
94+
stock.delete().then(done).catch(done.fail);
95+
});
96+
});
97+
6298
});
6399

64100
});

src/firestore/document/document.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import firebase from 'firebase/app';
2828
* // OR! Transform using Observable.from() and the data is unwrapped for you
2929
* Observable.from(fakeStock).subscribe(value => console.log(value));
3030
*/
31-
export class AngularFirestoreDocument<T= DocumentData> {
31+
export class AngularFirestoreDocument<T = DocumentData> {
3232

3333
/**
3434
* The contstuctor takes in a DocumentReference to provide wrapper methods
@@ -61,7 +61,7 @@ export class AngularFirestoreDocument<T= DocumentData> {
6161
* Create a reference to a sub-collection given a path and an optional query
6262
* function.
6363
*/
64-
collection<R= DocumentData>(path: string, queryFn?: QueryFn): AngularFirestoreCollection<R> {
64+
collection<R = DocumentData>(path: string, queryFn?: QueryFn): AngularFirestoreCollection<R> {
6565
const collectionRef = this.ref.collection(path);
6666
const { ref, query } = associateQuery(collectionRef, queryFn);
6767
return new AngularFirestoreCollection<R>(ref, query, this.afs);
@@ -79,12 +79,20 @@ export class AngularFirestoreDocument<T= DocumentData> {
7979

8080
/**
8181
* Listen to unwrapped snapshot updates from the document.
82+
*
83+
* If the `idField` option is provided, document IDs are included and mapped to the
84+
* provided `idField` property name.
8285
*/
83-
valueChanges(): Observable<T|undefined> {
86+
valueChanges(options?: { }): Observable<T | undefined>;
87+
valueChanges<K extends string>(options: { idField: K }): Observable<(T & { [T in K]: string }) | undefined>;
88+
valueChanges<K extends string>(options: { idField?: K } = {}): Observable<T | undefined> {
8489
return this.snapshotChanges().pipe(
85-
map(action => {
86-
return action.payload.data();
87-
})
90+
map(({ payload }) =>
91+
options.idField ? {
92+
...payload.data(),
93+
...{ [options.idField]: payload.id }
94+
} as T & { [T in K]: string } : payload.data()
95+
)
8896
);
8997
}
9098

0 commit comments

Comments
 (0)