Skip to content

feat(firestore): options to include document ID on valueChanges() #2113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions docs/firestore/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ interface DocumentSnapshot {

There are multiple ways of streaming collection data from Firestore.

### `valueChanges({idField?: string})`
### `valueChanges({ idField?: string })`

**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`.

Expand Down Expand Up @@ -107,13 +107,7 @@ export class AppComponent {
items: Observable<Item[]>;
constructor(private readonly afs: AngularFirestore) {
this.itemsCollection = afs.collection<Item>('items');
// .valueChanges() is simple. It just returns the
// JSON data without metadata. If you need the
// doc.id() in the value you must persist it your self
// or use .snapshotChanges() instead. See the addItem()
// method below for how to persist the id with
// valueChanges()
this.items = this.itemsCollection.valueChanges();
this.items = this.itemsCollection.valueChanges({ idField: 'customID' });
}
addItem(name: string) {
// Persist a document id
Expand Down
6 changes: 3 additions & 3 deletions docs/firestore/documents.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ interface DocumentSnapshot {

There are multiple ways of streaming collection data from Firestore.

### `valueChanges()`
### `valueChanges({ idField?: string })`

**What is it?** - Returns an Observable of document data. All Snapshot metadata is stripped. This method provides only the data.
**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`.

**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.

**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.
**When would you not use it?** - When you need document metadata.

### `snapshotChanges()`

Expand Down
80 changes: 58 additions & 22 deletions src/firestore/document/document.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AngularFireModule, FirebaseApp } from '@angular/fire';
import { AngularFirestore, SETTINGS } from '../firestore';
import { AngularFirestoreModule } from '../firestore.module';
import { Subscription } from 'rxjs';
import { AngularFirestoreDocument } from './document';
import { take } from 'rxjs/operators';

Expand Down Expand Up @@ -33,32 +34,67 @@ describe('AngularFirestoreDocument', () => {
app.delete();
});

it('should get action updates', async (done: any) => {
const randomCollectionName = randomName(afs.firestore);
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
await stock.set(FAKE_STOCK_DATA);
const sub = stock
.snapshotChanges()
.subscribe(async a => {
sub.unsubscribe();
if (a.payload.exists) {
expect(a.payload.data()).toEqual(FAKE_STOCK_DATA);
stock.delete().then(done).catch(done.fail);
}
describe('valueChanges()', () => {

it('should get unwrapped snapshot', async (done: any) => {
const randomCollectionName = afs.firestore.collection('a').doc().id;
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
await stock.set(FAKE_STOCK_DATA);
const obs$ = stock.valueChanges();
obs$.pipe(take(1)).subscribe(async data => {
expect(data).toEqual(FAKE_STOCK_DATA);
stock.delete().then(done).catch(done.fail);
});
});

/* TODO(jamesdaniels): test is flaking, look into this
it('should optionally map the doc ID to the emitted data object', async (done: any) => {
const randomCollectionName = afs.firestore.collection('a').doc().id;
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
await stock.set(FAKE_STOCK_DATA);
const idField = 'myCustomID';
const obs$ = stock.valueChanges({ idField });
obs$.pipe(take(1)).subscribe(async data => {
expect(data[idField]).toBeDefined();
expect(data).toEqual(jasmine.objectContaining(FAKE_STOCK_DATA));
stock.delete().then(done).catch(done.fail);
});
});*/

});

it('should get unwrapped snapshot', async (done: any) => {
const randomCollectionName = afs.firestore.collection('a').doc().id;
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
await stock.set(FAKE_STOCK_DATA);
const obs$ = stock.valueChanges();
obs$.pipe(take(1)).subscribe(async data => {
expect(data).toEqual(FAKE_STOCK_DATA);
stock.delete().then(done).catch(done.fail);
describe('snapshotChanges()', () => {

it('should get action updates', async (done: any) => {
const randomCollectionName = randomName(afs.firestore);
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
await stock.set(FAKE_STOCK_DATA);
const sub = stock
.snapshotChanges()
.subscribe(async a => {
sub.unsubscribe();
if (a.payload.exists) {
expect(a.payload.data()).toEqual(FAKE_STOCK_DATA);
stock.delete().then(done).catch(done.fail);
}
});
});

it('should get unwrapped snapshot', async (done: any) => {
const randomCollectionName = afs.firestore.collection('a').doc().id;
const ref = afs.firestore.doc(`${randomCollectionName}/FAKE`);
const stock = new AngularFirestoreDocument<Stock>(ref, afs);
await stock.set(FAKE_STOCK_DATA);
const obs$ = stock.valueChanges();
obs$.pipe(take(1)).subscribe(async data => {
expect(data).toEqual(FAKE_STOCK_DATA);
stock.delete().then(done).catch(done.fail);
});
});

});

});
20 changes: 14 additions & 6 deletions src/firestore/document/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import firebase from 'firebase/app';
* // OR! Transform using Observable.from() and the data is unwrapped for you
* Observable.from(fakeStock).subscribe(value => console.log(value));
*/
export class AngularFirestoreDocument<T= DocumentData> {
export class AngularFirestoreDocument<T = DocumentData> {

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

/**
* Listen to unwrapped snapshot updates from the document.
*
* If the `idField` option is provided, document IDs are included and mapped to the
* provided `idField` property name.
*/
valueChanges(): Observable<T|undefined> {
valueChanges(options?: { }): Observable<T | undefined>;
valueChanges<K extends string>(options: { idField: K }): Observable<(T & { [T in K]: string }) | undefined>;
valueChanges<K extends string>(options: { idField?: K } = {}): Observable<T | undefined> {
return this.snapshotChanges().pipe(
map(action => {
return action.payload.data();
})
map(({ payload }) =>
options.idField ? {
...payload.data(),
...{ [options.idField]: payload.id }
} as T & { [T in K]: string } : payload.data()
)
);
}

Expand Down