Skip to content

Commit 472cbad

Browse files
committed
docs(afs): collection docs
1 parent 90c8ede commit 472cbad

File tree

4 files changed

+239
-11
lines changed

4 files changed

+239
-11
lines changed

docs/firestore/collections.md

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# Using Collections with AngularFirestore
2+
3+
## Understanding Collections
4+
Every Firestore application starts with a collection. Firestore is structured in a `Collection > Document > Collection > Document` manner. This provides you with a flexible data structure. When you query a collection you only pull back the documents and not their collections. This is different from the Firebase Realtime Database where a query at a top-level location pulls back the entire tree. No more worrying about pulling back all the way down the tree 😎.
5+
6+
## Using an AngularFirestoreCollection
7+
8+
The `AngularFirestoreCollection` service is a wrapper around the native Firestore SDK's `CollectionReference` and `Query` types. It is a generic service that provides you with a strongly typed set of methods for manipulating and streaming data. This service is designed for use as an `@Injectable()`.
9+
10+
```ts
11+
import { Component } from '@angular/core';
12+
import { AngularFirestore, AngularFirestoreCollection } from 'angularfire2/firestore';
13+
import { Observable } from 'rxjs/Observable';
14+
15+
@Component({
16+
selector: 'afs-app',
17+
template: `
18+
<ul>
19+
<li *ngFor="item of items | async">
20+
{{ item.name }}
21+
</li>
22+
</ul>
23+
`
24+
})
25+
export class AppComponent {
26+
private itemsCollection: AngularFirestoreCollection<Item>:
27+
items: Observable<Item[]>;
28+
constructor(private afs: AngularFirestore): {
29+
this.itemsCollection = afs.collection<Item>('items');
30+
this.items = this.itemsCollection.snapshotChanges(['added', 'removed']);
31+
}
32+
addItem(item: Item) {
33+
this.itemsCollection.add(item);
34+
}
35+
}
36+
```
37+
38+
The `AngularFirestoreCollection` is service you use to create streams of the collection and perform data operations on the underyling collection.
39+
40+
### The `DocumentChangeAction` type
41+
42+
With the exception of the `valueChanges()`, each streaming method returns an Observable of `DocumentChangeAction[]`.
43+
44+
A `DocumentChangeAction` gives you the `type` and `payload` properties. The `type` tells when what `DocumentChangeType` operation occured (`added`, `modified`, `removed`). The `payload` property is a `DocumentChange` which provides you important metdata about the change and a `doc` property which is the `DocumentSnapshot`.
45+
46+
```ts
47+
interface DocumentChangeAction {
48+
//'added' | 'modified' | 'removed';
49+
type: DocumentChangeType;
50+
payload: DocumentChange;
51+
}
52+
53+
interface DocumentChange {
54+
type: DocumentChangeType;
55+
doc: DocumentSnapshot;
56+
oldIndex: number;
57+
newIndex: number;
58+
}
59+
60+
interface DocumentSnapshot {
61+
exists: boolean;
62+
ref: DocumentReference;
63+
id: string;
64+
metadata: SnapshotMetadata;
65+
data(): DocumentData;
66+
get(fieldPath: string): any;
67+
}
68+
```
69+
70+
## Streaming collection data
71+
72+
There are multiple ways of streaming collection data from Firestore.
73+
74+
### `valueChanges()`
75+
**What is it?** - Returns an Observable of data as a synchronized array of JSON objects. All Snapshot metadata is stripped and just the method provides only the data.
76+
77+
**Why would you use it?** - When you just need a list of data. No document metadata is attach to the resulting array which makes it simple to render to a view.
78+
79+
**When would you not use it?** - When you need a more complex data structure than an array or you need the `id` of each document to use data manipulation metods. This method assumes you either are saving the `id` to the document data or using a "readonly" approach.
80+
81+
### `snapshotChanges()`
82+
**What is it?** - Returns an Observable of data as a synchronized array of `DocumentChangeAction[]`.
83+
84+
**Why would you use it?** - When you need a list of data but also want to keep around metadata. Metadata provides you the underyling `DocumentReference`, document id, and array index of the single document. Having the document's id around makes it easier to use data manipulation methods. This method gives you more horsepower with other Angular integrations such as ngrx, forms, and animations due to the `type` property. The `type` property on each `DocumentChangeAction` is useful for ngrx reducers, form states, and animation states.
85+
86+
**When would you not use it?** - When you need a more complex data structure than an array or if you need to process changes as they occur. This array is synchronized with the remote and local changes in Firestore.
87+
88+
### `stateChanges()`
89+
**What is it?** - Returns an Observable of the most recent changes as a `DocumentChangeAction[]`.
90+
91+
**Why would you use it?** - The above methods return a synchronized array sorted in query order. `stateChanges()` emits changes as they occur rather than syncing the query order. This works well for ngrx integrations as you can build your own data structure in your reducer methods.
92+
93+
**When would you not use it?** - When you just need a list of data. This is a more advanced usage of AngularFirestore.
94+
95+
### `auditTrail()`
96+
**What is it?** - Returns an Observable of `DocumentChangeAction[]` as they occur. Similar to `stateChanges()`, but instead it keeps around the trail of events as an array.
97+
98+
**Why would you use it?** - This method is like `stateChanges()` except it is not ephemeral. It collects each change in an array as they occur. This is useful for ngrx integrations where you need to replay the entire state of an application. This also works as a great debugging tool for all applications. You can simple write `afs.collection('items').auditTrail().subscribe(console.log)` and check the events in the console as they occur.
99+
100+
**When would you not use it?** - When you just need a list of data. This is a more advanced usage of AngularFirestore.
101+
102+
### Limiting events
103+
104+
There are three `DocumentChangeType`s in Firestore: `added`, `removed`, and `moved`. Each streaming method listens to all three by default. However, your site may only be intrested in one of these events. You can specify which events you'd like to use through the first parameter of each method:
105+
106+
#### Basic smaple
107+
```ts
108+
constructor(private afs: AngularFirestore): {
109+
this.itemsCollection = afs.collection<Item>('items');
110+
this.items = this.itemsCollection.snapshotChanges(['added', 'removed']);
111+
}
112+
```
113+
114+
#### Component Sample
115+
```ts
116+
import { Component } from '@angular/core';
117+
import { AngularFirestore, AngularFirestoreCollection } from 'angularfire2/firestore';
118+
import { Observable } from 'rxjs/Observable';
119+
120+
@Component({
121+
selector: 'afs-app',
122+
template: `
123+
<ul>
124+
<li *ngFor="item of items | async">
125+
{{ item.name }}
126+
</li>
127+
</ul>
128+
`
129+
})
130+
export class AppComponent {
131+
private itemsCollection: AngularFirestoreCollection<Item>:
132+
items: Observable<Item[]>;
133+
constructor(private afs: AngularFirestore): {
134+
this.itemsCollection = afs.collection<Item>('items');
135+
this.items = this.itemsCollection.snapshotChanges(['added', 'removed']);
136+
}
137+
}
138+
```
139+
140+
### Querying
141+
142+
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 it for AngularFirestore ‼
143+
144+
When creating an `AngularFirestoreCollection`, use the optional callback to create a queried reference.
145+
146+
#### Basic Sample
147+
```ts
148+
constructor(private afs: AngularFirestore): {
149+
this.itemsCollection = afs.collection<Item>('items', ref => ref.where('size', '==', 'large'));
150+
this.items = this.itemsCollection.snapshotChanges();
151+
}
152+
```
153+
154+
#### Component Sample
155+
```ts
156+
import { Component } from '@angular/core';
157+
import { AngularFirestore, AngularFirestoreCollection } from 'angularfire2/firestore';
158+
import { Observable } from 'rxjs/Observable';
159+
160+
@Component({
161+
selector: 'afs-app',
162+
template: `
163+
<ul>
164+
<li *ngFor="item of items | async">
165+
{{ item.name }}
166+
</li>
167+
</ul>
168+
`
169+
})
170+
export class AppComponent {
171+
private itemsCollection: AngularFirestoreCollection<Item>:
172+
items: Observable<Item[]>;
173+
constructor(private afs: AngularFirestore): {
174+
this.itemsCollection = afs.collection<Item>('items', ref => {
175+
return ref.where('size', '==', 'large').where('price', '>', 10);
176+
});
177+
this.items = this.itemsCollection.snapshotChanges(['added', 'removed']);
178+
}
179+
}
180+
```
181+
182+
### Dynamic querying
183+
184+
Imagine you're querying a list of T-Shirts. Every facet of the query should be parameterized. Sometimes the user will search small sizes, prices less than $20, or by a specific brand. AngularFirestore intergrates with RxJS to make this easy.
185+
186+
#### Basic Sample
187+
```ts
188+
constructor(private afs: AngularFirestore): {
189+
// import { of } from 'rxjs/observable/of;
190+
// You'll use an Observable source from a ReactiveForm control or even
191+
// an AngularFirestoreDocument
192+
const criteria$ = of({
193+
size: { op: '==', value: 'large' },
194+
price: { op: '>', value: 10 }
195+
});
196+
this.items = size$.switchMap(size => {
197+
return this.afs
198+
.collection('tshirts', ref => ref.where('size', '==', size))
199+
.snapshotChanges();
200+
});
201+
}
202+
```
203+
204+
## Adding documents to a collection
205+
206+
To add a new document to a collection with a generated id use the `add()` method. This method uses the type provided by the generic class to validate it's type structure.
207+
208+
#### Basic Sample
209+
```ts
210+
constructor(private afs: AngularFirestore): {
211+
const shirtsCollection = afs.collection<Item>('tshirts');
212+
shirtsCollection.add({ name: 'item', price: 10 });
213+
}
214+
```
215+
216+
## Manipulating individual documents
217+
218+
To retrieve, update, or delete an individual document you can use the `doc()` method. This method returns an `AngularFirestoreDocument`, which provides methods for streaming, updating, and deleting.
219+
220+
See the Documents page for complete documentation.

docs/firestore/documents.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Using Documents with Firestore
2+

docs/firestore/ngrx.md

Whitespace-only changes.

src/firestore/collection/collection.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import 'rxjs/add/operator/map';
88
import 'rxjs/add/operator/filter';
99
import 'rxjs/add/observable/do';
1010

11-
1211
import { Injectable } from '@angular/core';
1312
import { FirebaseApp } from 'angularfire2';
1413

@@ -23,7 +22,6 @@ export function validateEventsArray(events?: DocumentChangeType[]) {
2322
return events;
2423
}
2524

26-
2725
/**
2826
* AngularFirestoreCollection service
2927
*
@@ -45,14 +43,12 @@ export function validateEventsArray(events?: DocumentChangeType[]) {
4543
* await fakeStock.add({ name: 'FAKE', price: 0.01 });
4644
*
4745
* // Subscribe to changes as snapshots. This provides you data updates as well as delta updates.
48-
* fakeStock.valueChanges().map(snap => snap.docs.map(doc => doc.data()).subscribe(value => console.log(value));
49-
* // OR! Avoid unwrapping and transform from an Observable
50-
* Observable.from(fakeStock).subscribe(value => console.log(value));
46+
* fakeStock.valueChanges().subscribe(value => console.log(value));
5147
*/
5248
export class AngularFirestoreCollection<T> {
5349
/**
5450
* The contstuctor takes in a CollectionReference and Query to provide wrapper methods
55-
* for data operations, data streaming, and Symbol.observable.
51+
* for data operations and data streaming.
5652
*
5753
* Note: Data operation methods are done on the reference not the query. This means
5854
* when you update data it is not updating data to the window of your query unless
@@ -65,9 +61,10 @@ export class AngularFirestoreCollection<T> {
6561
private readonly query: Query) { }
6662

6763
/**
68-
* Listen to all documents in the collection and its possible query as an Observable.
69-
* This method returns a stream of DocumentSnapshots which gives the ability to get
70-
* the data set back as array and/or the delta updates in the collection.
64+
* Listen to the latest change in the stream. This method returns changes
65+
* as they occur and they are not sorted by query order. This allows you to construct
66+
* your own data structure.
67+
* @param events
7168
*/
7269
stateChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction[]> {
7370
if(!events || events.length === 0) {
@@ -78,21 +75,30 @@ export class AngularFirestoreCollection<T> {
7875
.filter(changes => changes.length > 0);
7976
}
8077

78+
/**
79+
* Create a stream of changes as they occur it time. This method is similar to stateChanges()
80+
* but it collects each event in an array over time.
81+
* @param events
82+
*/
8183
auditTrail(events?: DocumentChangeType[]): Observable<DocumentChangeAction[]> {
8284
return this.stateChanges(events).scan((current, action) => [...current, ...action], []);
8385
}
8486

87+
/**
88+
* Create a stream of synchronized shanges. This method keeps the local array in sorted
89+
* query order.
90+
* @param events
91+
*/
8592
snapshotChanges(events?: DocumentChangeType[]): Observable<DocumentChangeAction[]> {
8693
events = validateEventsArray(events);
8794
return sortedChanges(this.query, events);
8895
}
8996

9097
/**
9198
* Listen to all documents in the collection and its possible query as an Observable.
92-
* This method returns a stream of unwrapped snapshots.
9399
*/
94100
valueChanges(events?: DocumentChangeType[]): Observable<T[]> {
95-
return this.stateChanges()
101+
return this.snapshotChanges()
96102
.map(actions => actions.map(a => a.payload.doc.data()) as T[]);
97103
}
98104

0 commit comments

Comments
 (0)