Skip to content
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
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,19 @@ export const appConfig: ApplicationConfig = {
```ts
import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Firestore, collectionData, collection } from '@angular/fire/firestore';
import { Firestore, FirestoreDataConverter, collectionData, collection } from '@angular/fire/firestore';

interface Item {
name: string;
}

// Types the collection and checks every document on its way in
const itemConverter: FirestoreDataConverter<Item> = {
toFirestore: item => item,
fromFirestore: (snapshot, options) => {
const data = snapshot.data(options);
return { name: typeof data['name'] === 'string' ? data['name'] : '' };
},
};

@Component({
Expand All @@ -56,11 +65,13 @@ interface Item {
})
export class App {
firestore = inject(Firestore);
itemCollection = collection(this.firestore, 'items');
items$ = collectionData<Item>(this.itemCollection);
itemCollection = collection(this.firestore, 'items').withConverter(itemConverter);
items$ = collectionData(this.itemCollection); // Observable<Item[]>
}
```

The converter is what gives the read its type. `collection()` alone returns an untyped reference. See [Typed reads](docs/firestore.md#typed-reads).

## Resources

[Quickstart](docs/install-and-setup.md) - Get your first application up and running by following our quickstart guide.
Expand Down
40 changes: 33 additions & 7 deletions docs/firestore.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,27 @@ Let's explore reading data in Angular using the `collection` and `collectionData
In `user-profile.component.ts`:

```typescript
import { Firestore, collection, collectionData} from '@angular/fire/firestore';
import { Firestore, FirestoreDataConverter, collection, collectionData } from '@angular/fire/firestore';
import { Component, inject } from '@angular/core';
import { Observable } from 'rxjs';

export interface UserProfile {
username: string;
}

/* A converter translates between stored documents and your own type.
* fromFirestore runs on every document read, so it is where a missing
* or wrong field gets its default. */
export const userProfileConverter: FirestoreDataConverter<UserProfile> = {
toFirestore: profile => profile,
fromFirestore: (snapshot, options) => {
const data = snapshot.data(options);
return {
username: typeof data['username'] === 'string' ? data['username'] : '',
};
},
};

@Component ({
selector: 'app-user-profile',
standalone: true,
Expand All @@ -119,17 +136,15 @@ export class UserProfileComponent {
users$: Observable<UserProfile[]>;

constructor() {
// get a reference to the user-profile collection
const userProfileCollection = collection(this.firestore, 'users');
// get a reference to the users collection, typed through the converter
const userProfileCollection = collection(this.firestore, 'users').withConverter(userProfileConverter);

// get documents (data) from the collection using collectionData
this.users$ = collectionData(userProfileCollection) as Observable<UserProfile[]>;
this.users$ = collectionData(userProfileCollection); // Observable<UserProfile[]>
}
}
export interface UserProfile {
username: string;
}
```

`collectionData` returns an `observable` that can we can use to display the data in the template. In `user-profile.component.html`:

```html
Expand All @@ -145,6 +160,17 @@ export interface UserProfile {

The `async` pipe handles unsubscribing from observables.

#### Typed reads

`collection()` and `doc()` return references typed as `DocumentData`, and `collectionData()` and `docData()` return whatever type the reference carries (`docData()` adds `undefined` for a document that does not exist). Two tempting shortcuts do not give you a typed read:

* `collectionData<UserProfile>(collection(this.firestore, 'users'))` does not compile (`TS2345`), because the reference says `DocumentData` and the type argument says `UserProfile`.
* `collectionData(...) as Observable<UserProfile[]>` compiles, but it only tells the compiler what you hope is stored. Nothing checks the documents.

`withConverter()` is the Firebase SDK's way to type a reference. Attach a `FirestoreDataConverter<T>` and every read through that reference returns `T`, with no type argument and no cast. Because `fromFirestore` runs on every document, it is also where you decide what happens to a document that does not match your type. The example above substitutes a default, so one malformed document does not stop the list from rendering. Throwing inside `fromFirestore` instead would error the `collectionData` observable at the first bad document, and the subscription would end.

The `idField` option, `collectionData(ref, { idField: 'id' })`, copies each document's id onto the returned object. It does not validate any other field. With a converter you can do the same inside `fromFirestore` with `snapshot.id`.

### Writing data

To write to Cloud Firestore use the `addDoc` function. It will create a new document at the path specified by the collection. In `user-profile.component.ts`, we'll update the code to add a new document on a `<button>` click.
Expand Down
17 changes: 14 additions & 3 deletions docs/install-and-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,21 @@ In `/src/app/app.ts`:
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Observable } from 'rxjs';
import { Firestore, collection, collectionData } from '@angular/fire/firestore';
import { Firestore, FirestoreDataConverter, collection, collectionData } from '@angular/fire/firestore';

interface Item {
name: string;
}

// Types the collection and checks every document on its way in
const itemConverter: FirestoreDataConverter<Item> = {
toFirestore: item => item,
fromFirestore: (snapshot, options) => {
const data = snapshot.data(options);
return { name: typeof data['name'] === 'string' ? data['name'] : '' };
},
};

@Component({
selector: 'app-root',
imports: [AsyncPipe],
Expand All @@ -95,12 +104,14 @@ export class App {
items$: Observable<Item[]>;

constructor() {
const aCollection = collection(this.firestore, 'items')
this.items$ = collectionData<Item>(aCollection);
const aCollection = collection(this.firestore, 'items').withConverter(itemConverter);
this.items$ = collectionData(aCollection);
}
}
```

The converter is what gives the read its type. `collection()` alone returns an untyped reference. The [Firestore guide](firestore.md#typed-reads) explains the pattern.

Open `/src/app/app.html`:

```html
Expand Down
13 changes: 8 additions & 5 deletions docs/version-7-upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ So developing with the new AngularFire is easy, you can use it just like the van

#### Alternatives to v6 APIs

In the modular column, `docRef` is a `DocumentReference<T>` typed through `withConverter(converter)`, where `converter` is a `FirestoreDataConverter<T>`. A reference from a plain `doc()` or `collection()` call is typed `DocumentData`, and the `<T>` calls below do not compile on one. See [Typed reads](firestore.md#typed-reads).

<table>
<thead>
<tr>
Expand Down Expand Up @@ -207,7 +209,7 @@ deleteDoc(docRef) // Promise<void>

```ts
import { collection } from '@angular/fire/firestore';
collection<T>(docRef, 'bar') // CollectionReference<T>
collection(docRef, 'bar').withConverter(converter) // CollectionReference<T>
```
</td>
</tr>
Expand All @@ -229,7 +231,7 @@ docSnapshots<T>(docRef) // Observable<DocumentSnapshot<T>>

```ts
import { docData } from '@angular/fire/firestore';
docData<T>(docRef) // Observable<T>
docData<T>(docRef) // Observable<T | undefined>
```
</td>
</tr>
Expand Down Expand Up @@ -259,14 +261,15 @@ import {
collection,
getFirestore
} from '@angular/fire/firestore';
import { first } from 'rxjs/operators';
import { IFoo } from '../interfaces';
import { concatMap, first } from 'rxjs/operators';
// a FirestoreDataConverter<IFoo>, see firestore.md "Typed reads"
import { fooConverter } from '../foo-converter';

export { getFirestore };

export const fooData = firestoreInstance$.pipe(
first(),
concatMap(firestore => collectionData<IFoo>(collection(firestore, 'foo'))),
concatMap(firestore => collectionData(collection(firestore, 'foo').withConverter(fooConverter))),
);
```

Expand Down
Loading