diff --git a/docs/auth/AuthServiceDireStructure.png b/docs/auth/AuthServiceDireStructure.png new file mode 100644 index 000000000..dd350f471 Binary files /dev/null and b/docs/auth/AuthServiceDireStructure.png differ diff --git a/docs/auth/getting-started.md b/docs/auth/getting-started.md index 90bca2743..82899ce5f 100644 --- a/docs/auth/getting-started.md +++ b/docs/auth/getting-started.md @@ -1,154 +1,412 @@ # 5. Getting started with Firebase Authentication -`AngularFireAuth.user` provides you an `Observable` to monitor your application's authentication State. +#### This documentation works for Modular Firebase V9+ -`AngularFireAuth` promise proxies an initialized -`firebase.auth.Auth` instance, allowing you to log users in, out, etc. [See -the Firebase docs for more information on what methods are available.](https://firebase.google.com/docs/reference/js/firebase.auth.Auth) +First initialize firebase authentication in `app.module.ts` -**Example app:** - -```ts -import { Component } from '@angular/core'; -import { AngularFireAuth } from '@angular/fire/compat/auth'; -import firebase from 'firebase/compat/app'; +```typescript +@NgModule({ + declarations: [AppComponent], + imports: [provideAuth(() => getAuth())], + providers: [ScreenTrackingService, UserTrackingService], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` +#### A basic authentication demo code. + +```typescript +import { Component } from "@angular/core"; +import { + Auth, + authState, + GoogleAuthProvider, + signInWithPopup, + signOut, + UserCredential, +} from "@angular/fire/auth"; +import { map } from "rxjs"; @Component({ - selector: 'app-root', + selector: "app-root", template: ` -
+

Hello {{ user.displayName }}!

- + +

Please login.

- +
`, }) export class AppComponent { - constructor(public auth: AngularFireAuth) { + title = "AngularFireDevelopment"; + user: any; + public loggedIn: boolean = false; + constructor(private auth: Auth) { + authState(this.auth) + .pipe(map((u) => u)) + .subscribe((userData) => { + this.user = userData; + this.loggedIn = !!userData; + }); } login() { - this.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider()); + signInWithPopup(this.auth, new GoogleAuthProvider()).then( + async (credentials: UserCredential) => { + this.user = credentials.user; + } + ); } logout() { - this.auth.signOut(); + return signOut(this.auth); } } ``` -## Configuration with Dependency Injection +It is a good practice to keep your local code away from component code. So let's create a service file inside our -The AngularFireAuth Module provides several DI tokens to further configure your -authentication process. +`ng generate service services/authentication/authentication` -### Configure +It should create a file like this. This file is known as service. +[More on services here.](https://angular.io/tutorial/toh-pt4) -Using the `SETTINGS` DI Token (*default: null*), we can set the current Auth -instance's settings. This is used to edit/read configuration related options -like app verification mode for phone authentication, which is useful for -[testing](https://cloud.google.com/identity-platform/docs/test-phone-numbers). +![Directory Strucutre](AuthServiceDireStructure.png) -```ts -import { SETTINGS as AUTH_SETTINGS } from '@angular/fire/compat/auth'; +Now add these functions in your services -@NgModule({ - // ... Existing configuration - providers: [ - // ... Existing Providers - { provide: AUTH_SETTINGS, useValue: { appVerificationDisabledForTesting: true } }, - ] +This is for logging in with google. + +```typescript +googleLogin(){ + signInWithPopup(this.auth, new GoogleAuthProvider()).then( + async (credentials: UserCredential) => { + this.user = credentials.user; + } + ); + } +``` + +This is for logging in with email and password it takes two arguments email and password. + +```typescript +emailLogin(email:string,password:string){ + return signInWithEmailAndPassword(this.auth,email,password) + } +``` + +This is for creating account with an email and password + +#### Please verify and check for all password requirements using form validation firebase does not qualifications for secure password and valid emails. + +```typescript +emailSignup(email:string,password:string){ + return createUserWithEmailAndPassword(this.auth,email,password) + } +``` + +This method logs you out of any or all account for current user instance + +```typescript +logout() { + return signOut(this.auth); + } +``` + +This methods signs you in anonymously. + +```typescript +anonymousSignIn(){ + return signInAnonymously(this.auth); + } +``` + +#### The final service file + +`authentication.service.ts` + +```typescript +import { Injectable } from "@angular/core"; +import { + Auth, + GoogleAuthProvider, + signInWithPopup, + signOut, + UserCredential, +} from "@angular/fire/auth"; +import { + createUserWithEmailAndPassword, + signInAnonymously, + signInWithEmailAndPassword, +} from "firebase/auth"; + +@Injectable({ + providedIn: "root", }) -export class AppModule { } +export class AuthenticationService { + user: any; + constructor(private auth: Auth) {} + googleLogin() { + signInWithPopup(this.auth, new GoogleAuthProvider()).then( + async (credentials: UserCredential) => { + this.user = credentials.user; + } + ); + } + emailLogin(email: string, password: string) { + return signInWithEmailAndPassword(this.auth, email, password); + } + emailSignup(email: string, password: string) { + return createUserWithEmailAndPassword(this.auth, email, password); + } + logout() { + return signOut(this.auth); + } + anonymousSignIn() { + return signInAnonymously(this.auth); + } +} ``` -Read more at [Firebase Auth Settings](https://firebase.google.com/docs/reference/js/firebase.auth.AuthSettings). +Now in your `app.component.ts` add this code to immport our `AuthenticationService` file we just created. + +`app.component.ts` + +```typescript +import { Component } from "@angular/core"; +import { + FormControl, + FormControlName, + FormGroup, + Validators, +} from "@angular/forms"; +import { AuthenticationService } from "./services/authentication/authentication.service"; +@Component({ + selector: "app-root", + templateUrl: "./app.component.html", + styleUrls: ["./app.component.css"], +}) +export class AppComponent { + title = "AngularFireDevelopment"; + constructor(public authService: AuthenticationService) {} + signIn: FormGroup = new FormGroup({ + email: new FormControl("", [Validators.required]), + password: new FormControl("", [Validators.required]), + }); + signUp: FormGroup = new FormGroup({ + email: new FormControl("", [Validators.required]), + password: new FormControl("", [Validators.required]), + }); + login() { + if (this.signIn.valid) { + this.authService + .emailLogin(this.signIn.value.email, this.signIn.value.password) + .then(() => alert("Signed In")) + .catch((error) => alert("Invalid Sign In" + error.toString())); + } else { + alert("Invalid Login Form"); + } + } + signup() { + if (this.signUp.valid) { + this.authService + .emailSignup(this.signUp.value.email, this.signUp.value.password) + .then(() => alert("Signed up")) + .catch((error: any) => alert("Invalid Sign Up" + error.toString())); + } else { + alert("Invalid Signup Form"); + } + } +} +``` + +Now add `FormsModule` and `ReactiveFormsModule` to your app module imports. + +```typescript +imports: [FormsModule, ReactiveFormsModule]; +``` + +And your `app.component.html` file which will have only UI code but will be able to access the authentication service using Injection + +`app.component.html` + +```html +
+

Hello {{ authService.user.displayName }}!

+ +
+ +
+

Please login.

+ +
+
+ + + +
+
+
+
+ + + +
+
+
+``` + +### Now you just learned how to do authentication with firebase. + +--- + +[Google Docs Reference](https://firebase.google.com/docs/auth/web/auth-state-persistence) +# Authentication state persistance +Authentication State Persistence + +You can specify how the Authentication state persists when using the Angular Fire SDK. This includes the ability to specify whether a signed in user should be indefinitely persisted until explicit sign out, cleared when the window is closed or cleared on page reload. + +For a web application, the default behavior is to persist a user's session even after the user closes the browser. This is convenient as the user is not required to continuously sign-in every time the web page is visited on the same device. This could require the user having to re-enter their password, send an SMS verification, etc, which could add a lot of friction to the user experience. + +However, there are cases where this behavior may not be ideal: + +* Applications with sensitive data may want to clear the state when the window or tab is closed. This is important in case the user forgets to sign out. +* Applications that are used on a device shared by multiple users. A common example here is an app running in a library computer. + +* An application on a shared device that might be accessed by multiple users. The developer is unable to tell how that application is accessed and may want to provide a user with the ability to choose whether to persist their session or not. This could be done by adding a "Remember me" option during sign-in. + +* In some situations, a developer may want to not persist an anonymous user until that user is upgraded to a non-anonymous account (federated, password, phone, etc.). + +* A developer may want to allow different users to sign in to an application on different tabs. The default behavior is to persist the state across tabs for the same origin. + +As stated above, there are multiple situations where the default permanent persistence may need to be overridden. + +>Note: Do not confuse Auth state persistence with Firestore offline data persistence. Auth state persistence specifies how a user session is persisted on a device. Whereas Firestore enablePersistence enables Cloud Firestore data caching when the device is offline. + +# Overview of persistence behavior +You can specify or modify the existing type of persistence by calling the `setPersistence()` method. +The two parameters of this functions are: +1. authInstance: Instance of the initliazed auth by using `getAuth()` method. +2. persistance: A valid persistance type imported from auth module. + +##### Types of Persistence. +* 'browserSessionPersistence' is used for persistence such as `sessionStorage`. +* 'indexedDBLocalPersistence' or 'browserLocalPersistence' used for long term persistence such as `localStorage` `IndexedDB`. +* 'inMemoryPersistence' is used for in-memory, or persistence. + + +### The following criteria will be applied when determining the current state of persistence. +Initially, the SDK will check if an authenticated user exists. Unless setPersistence is called, that user's current persistence type will be applied for future sign-in attempts. So if that user was persisted in session on a previous web page and a new page was visited, signing in again with a different user will result in that user's state being also saved with session persistence. + +If no user is signed in and no persistence is specified, the default setting will be applied (local in a browser app). +If no user is signed in and a new type of persistence is set, any future sign-in attempt will use that type of persistence. +If the user is signed in and persistence type is modified, that existing signed in user will change persistence to the new one. All future sign-in attempts will use that new persistence. + +When signInWithRedirect is called, the current persistence type is retained and applied at the end of the OAuth flow to the newly signed in user, even if the persistence was none. If the persistence is explicitly specified on that page, it will override the retained auth state persistence from the previous page that started the redirect flow. + +Add this in `app.module.ts` imports array. + +```typescript +imports: [ + provideAuth(() => { + const auth = getAuth(); + setPersistence(auth, inMemoryPersistence); + // setPersistence(auth, inMemoryPersistence); + // setPersistence(auth,browserLocalPersistence) + // setPersistence(auth, indexedDBLocalPersistence); + // setPersistence(auth, browserSessionPersistence); + return auth; + }), +]; +``` + +## Configuration with Dependency Injection and Inbuilt Functions +The AngularFireAuth Module provides several from which you can configure the authentication process. + ### Use Current Browser Language -Using the `USE_DEVICE_LANGUAGE` DI Token (*default: null*), which is a boolean -that allow you to set the current language to the default device/browser +Using the `useDeviceLanguage()` method that allow you to set the current language to the default device/browser preference. This allows to localize emails but be aware that this only applies -if you use the standard template provided by Firebase. +if you use the standard template provided by Firebase. ```ts -import { USE_DEVICE_LANGUAGE } from '@angular/fire/compat/auth'; +import { useDeviceLanguage } from "@angular/fire/auth"; @NgModule({ - // ... Existing configuration - providers: [ - // ... Existing Providers - { provide: USE_DEVICE_LANGUAGE, useValue: true }, - ] + provideAuth(()=>{ + const auth = getAuth(); + useDeviceLanguage(auth); + return auth; + }), }) -export class AppModule { } +export class AppModule {} ``` -If you want to set a different language, you can use `LANGUAGE_CODE` DI Token -(*default: null*). +If you want to set a different language, you can use `languageCode` property of Auth instance. More info at the [firebase auth docs](https://firebase.google.com/docs/reference/js/firebase.auth.Auth#languagecode). ```ts -import { LANGUAGE_CODE } from '@angular/fire/compat/auth'; @NgModule({ - // ... Existing configuration - providers: [ - // ... Existing Providers - { provide: LANGUAGE_CODE, useValue: 'fr' }, - ] + provideAuth(()=>{ + const auth = getAuth(); + auth.languageCode = 'fr'; + return auth; + }), }) -export class AppModule { } +export class AppModule {} ``` +### Tenant -### Persistence - -Firebase Auth default behavior is to persist a user's session even after the -user closes the browser. To change the current type of persistence on the -current Auth instance for the currently saved Auth session and apply this type -of persistence for future sign-in requests, including sign-in with redirect -requests, you can use the `PERSISTENCE` DI Token (*default: null*). +If you need to use multi-tenancy, you can set the current Auth instance's tenant +ID using `tenantId` property from Auth instance. -The possible types are `'local'`, `'session'` or `'none'`. Read more at -[authentication state persistence](https://firebase.google.com/docs/auth/web/auth-state-persistence), +More tutorials regarding this topic are _coming soon_. ```ts -import { PERSISTENCE } from '@angular/fire/compat/auth'; @NgModule({ - // ... Existing configuration - providers: [ - // ... Existing Providers - { provide: PERSISTENCE, useValue: 'session' }, - ] + provideAuth(()=>{ + const auth = getAuth(); + auth.tenantId = "tenant-id-app-one" + return auth; + }), }) -export class AppModule { } +export class AppModule {} ``` -### Tenant +- [Multi-Tenancy Authentication](https://cloud.google.com/identity-platform/docs/multi-tenancy-authentication) +- [Firebase Auth Tenant](https://firebase.google.com/docs/reference/js/firebase.auth.Auth#tenantid) -If you need to use multi-tenancy, you can set the current Auth instance's tenant -ID using `TENANT_ID` DI Token (*default: null*). -More tutorials regarding this topic are _coming soon_. +### Configure + +Using the `SETTINGS` DI Token (_default: null_), we can set the current Auth +instance's settings. This is used to edit/read configuration related options +like app verification mode for phone authentication, which is useful for +[testing](https://cloud.google.com/identity-platform/docs/test-phone-numbers). ```ts -import { TENANT_ID } from '@angular/fire/compat/auth'; +import { SETTINGS as AUTH_SETTINGS } from "@angular/fire/compat/auth"; @NgModule({ // ... Existing configuration providers: [ // ... Existing Providers - { provide: TENANT_ID, useValue: 'tenant-id-app-one' }, - ] + { + provide: AUTH_SETTINGS, + useValue: { appVerificationDisabledForTesting: true }, + }, + ], }) -export class AppModule { } +export class AppModule {} ``` -- [Multi-Tenancy Authentication](https://cloud.google.com/identity-platform/docs/multi-tenancy-authentication) -- [Firebase Auth Tenant](https://firebase.google.com/docs/reference/js/firebase.auth.Auth#tenantid) +Read more at [Firebase Auth Settings](https://firebase.google.com/docs/reference/js/firebase.auth.AuthSettings). ## UI Libraries diff --git a/docs/firestore/collections.md b/docs/firestore/collections.md index 6b17d6ac7..4ad9ceef7 100644 --- a/docs/firestore/collections.md +++ b/docs/firestore/collections.md @@ -1,321 +1,308 @@ # 3. Collections in AngularFirestore -> Cloud Firestore is a NoSQL, document-oriented database. Unlike a SQL database, there are no tables or rows. Instead, you store data in *documents*, which are organized into *collections*. -Each *document* contains a set of key-value pairs. Cloud Firestore is optimized for storing large collections of small documents. +####

This documentation is updated for modular version 9 of angular fire package. For previous or missing features refer to older docs.

+ +> Cloud Firestore is a NoSQL, document-oriented database. Unlike a SQL database, there are no tables or rows. Instead, you store data in _documents_, which are organized into _collections_. +> Each _document_ contains a set of key-value pairs. Cloud Firestore is optimized for storing large collections of small documents. ## Using `AngularFirestoreCollection` The `AngularFirestoreCollection` service is a wrapper around the native Firestore SDK's [`CollectionReference`](https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference) and [`Query`](https://firebase.google.com/docs/reference/js/firebase.firestore.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()`. ```ts -import { Component } from '@angular/core'; -import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/compat/firestore'; -import { Observable } from 'rxjs'; - -export interface Item { name: string; } +import { Component } from "@angular/core"; +import { Subscription } from "rxjs"; +import { collection, getDocs, QuerySnapshot } from "firebase/firestore"; +import { Firestore } from "@angular/fire/firestore"; +export interface Item { + name: string; +} @Component({ - selector: 'app-root', + selector: "app-root", template: `
    -
  • +
  • {{ item.name }}
- ` + `, }) -export class AppComponent { - private itemsCollection: AngularFirestoreCollection; - items: Observable; - constructor(private afs: AngularFirestore) { - this.itemsCollection = afs.collection('items'); - this.items = this.itemsCollection.valueChanges(); - } - addItem(item: Item) { - this.itemsCollection.add(item); +export class AppComponent implements OnInit { + items: Item[] = []; + constructor(private firestore: Firestore) {} + ngOnInit() { + getDocs(collection(this.firestore, "data")).then( + (querySnapshot: QuerySnapshot) => { + this.items = []; + querySnapshot.forEach((doc: any) => { + this.items.push(doc.data()); + }); + } + ); } } ``` -The `AngularFirestoreCollection` is a service you use to create streams of the collection and perform data operations on the underyling collection. - -### The `DocumentChangeAction` type - -With the exception of the `valueChanges()`, each streaming method returns an Observable of `DocumentChangeAction[]`. +## Streaming collection data or get realtime data -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 metadata about the change and a `doc` property which is the `DocumentSnapshot`. +There are multiple ways of streaming collection data from Firestore. -```ts -interface DocumentChangeAction { - //'added' | 'modified' | 'removed'; - type: DocumentChangeType; - payload: DocumentChange; -} +1. `collectionSnapshots()` It is a method which returns a subscription with latest and complete list of documents in a collection. +2. `collectionChanges()` It is a method which returns a subscription with only the changed data in a collection. -interface DocumentChange { - type: DocumentChangeType; - doc: DocumentSnapshot; - oldIndex: number; - newIndex: number; -} +## `collectionSnapshots()` Implementation example -interface DocumentSnapshot { - exists: boolean; - ref: DocumentReference; - id: string; - metadata: SnapshotMetadata; - data(): DocumentData; - get(fieldPath: string): any; +```typescript +import { Component } from "@angular/core"; +import { Subscription } from "rxjs"; +import { collection } from "firebase/firestore"; +import { collectionSnapshots, Firestore } from "@angular/fire/firestore"; +export interface Item { + name: string; } -``` - -## Streaming collection data - -There are multiple ways of streaming collection data from Firestore. - -### `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`. - -**Why would you use it?** - When you just need a list of data. No document metadata is attached to the resulting array which makes it simple to render to a view. - -**When would you not use it?** - When you need a more complex data structure than an array. - -**Best practices** - Use this method to display data on a page. It's simple but effective. Use `.snapshotChanges()` once your needs become more complex. - -#### Example of persisting a Document Id - -```ts -import { Component } from '@angular/core'; -import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/compat/firestore'; -import { Observable } from 'rxjs'; - -export interface Item { id: string; name: string; } @Component({ - selector: 'app-root', + selector: "app-root", template: `
    -
  • +
  • {{ item.name }}
- ` + `, }) -export class AppComponent { - private itemsCollection: AngularFirestoreCollection; - items: Observable; - constructor(private readonly afs: AngularFirestore) { - this.itemsCollection = afs.collection('items'); - this.items = this.itemsCollection.valueChanges({ idField: 'customID' }); +export class AppComponent implements OnInit, OnDestroy { + itemListSubscription: Subscription = Subscription.EMPTY; + items: Item[] = []; + constructor(private firestore: Firestore) {} + ngOnInit() { + this.dataListSubscription = collectionSnapshots( + collection(this.firestore, "data") + ).subscribe((data) => { + this.items = []; + data.forEach((doc: any) => { + this.items.push(doc.data()); + }); + }); } - addItem(name: string) { - // Persist a document id - const id = this.afs.createId(); - const item: Item = { id, name }; - this.itemsCollection.doc(id).set(item); + ngOnDestroy(): void { + this.itemListSubscription.unsubscribe(); } } ``` -### `snapshotChanges()` - -**What is it?** - The current state of your collection. Returns an Observable of data as a synchronized array of `DocumentChangeAction[]`. +## `collectionChanges()` Implementation example -**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. +This method only returns data of a changed document so it can be used for keeping an eye on edits on any dataset. This is good method because you don't have to compare all data on the fly to know which one is new. -**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. +> Cool tip: It has the best use case when showing notifications. But remember -**Best practices** - Use an observable operator to transform your data from `.snapshotChanges()`. Don't return the `DocumentChangeAction[]` to the template. See the example below. +>

WARNING: It outputs every document last change present in the collection when it is first invoked or executed.

-#### Example - -```ts -import { Component } from '@angular/core'; -import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/compat/firestore'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; - -export interface Shirt { name: string; price: number; } -export interface ShirtId extends Shirt { id: string; } +```typescript +import { Component } from "@angular/core"; +import { Subscription } from "rxjs"; +import { collection } from "firebase/firestore"; +import { collectionChanges, Firestore } from "@angular/fire/firestore"; +export interface Item { + name: string; +} @Component({ - selector: 'app-root', + selector: "app-root", template: `
    -
  • - {{ shirt.name }} is {{ shirt.price }} +
  • + {{ item.doc.data().name }} + Type: {{ item.type }} New Index: {{ item.newIndex }} Old Index: {{ item.oldIndex }}
- ` + `, }) -export class AppComponent { - private shirtCollection: AngularFirestoreCollection; - shirts: Observable; - constructor(private readonly afs: AngularFirestore) { - this.shirtCollection = afs.collection('shirts'); - // .snapshotChanges() returns a DocumentChangeAction[], which contains - // a lot of information about "what happened" with each change. If you want to - // get the data and the id use the map operator. - this.shirts = this.shirtCollection.snapshotChanges().pipe( - map(actions => actions.map(a => { - const data = a.payload.doc.data() as Shirt; - const id = a.payload.doc.id; - return { id, ...data }; - })) - ); +export class AppComponent implements OnInit, OnDestroy { + itemListSubscription: Subscription = Subscription.EMPTY; + items: Item[] = []; + constructor(private firestore: Firestore) {} + ngOnInit() { + this.dataListSubscription = collectionChanges( + collection(this.firestore, "data") + ).subscribe((dataChange) => { + this.items = []; + dataChange.forEach((doc: any) => { + this.items.push(doc.data()); + }); + }); + } + ngOnDestroy(): void { + this.itemListSubscription.unsubscribe(); } } ``` -### `stateChanges()` - -**What is it?** - Returns an Observable of the most recent changes as a `DocumentChangeAction[]`. +## Conditional collection querying -**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. +Cloud Firestore provides powerful query functionality for specifying which documents you want to retrieve from a collection or collection group. These queries can also be used with either `getDocs()` or `collectionSnapshots()`, as described in Get Data and Get Realtime Updates. -**When would you not use it?** - When you just need a list of data. This is a more advanced usage of AngularFirestore. +[More on queries refer here](https://firebase.google.com/docs/firestore/query-data/queries) -#### Example - -```ts -import { Component } from '@angular/core'; -import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/compat/firestore'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +#### Simple example demonstrating functioning of queries. -export interface AccountDeposit { description: string; amount: number; } -export interface AccountDepositId extends AccountDeposit { id: string; } +```typescript +import { Component } from "@angular/core"; +import { collection, getDocs, query, where } from "firebase/firestore"; +import { Firestore } from "@angular/fire/firestore"; +export interface Item { + name: string; + salary:number; +} @Component({ - selector: 'app-root', + selector: "app-root", template: `
    -
  • - {{ deposit.description }} for {{ deposit.amount }} +
  • + {{ item.name }}
- ` + `, }) -export class AppComponent { - private depositCollection: AngularFirestoreCollection; - deposits: Observable; - constructor(private readonly afs: AngularFirestore) { - this.depositCollection = afs.collection('deposits'); - this.deposits = this.depositCollection.stateChanges(['added']).pipe( - map(actions => actions.map(a => { - const data = a.payload.doc.data() as AccountDeposit; - const id = a.payload.doc.id; - return { id, ...data }; - })) - ); +export class AppComponent implements OnInit { + items: Item[] = []; + constructor(private firestore: Firestore) {} + ngOnInit() { + getDocs( + query( + collection(this.firestore,'data'), + where('salary', '>=', 400))).then(collections:any)=>{ + collections.forEach((doc:any)=>{ + this.items.push(doc.data()) + }) + }) } } ``` +The where() method takes three parameters: a field to filter on, a comparison operator, and a value. Cloud Firestore supports the following comparison operators: -### `auditTrail()` +* `<` less than +* `<=` less than or equal to +* `==` equal to +* `\>` greater than +* `\>=` greater than or equal to +* `!=` not equal to +* `array-contains` +* `array-contains-any` +* `in` +* `not-in` -**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. -**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 simply write `afs.collection('items').auditTrail().subscribe(console.log)` and check the events in the console as they occur. +[Looking for more advanced queries](https://firebase.google.com/docs/firestore/query-data/queries#query_operators) -**When would you not use it?** - When you just need a list of data. This is a more advanced usage of AngularFirestore. +--- +## Order and limit data with Cloud Firestore -#### Example +Cloud Firestore provides powerful query functionality for specifying which documents you want to retrieve from a collection. These queries can also be used with either `getDocs()` or `collectionChanges()`, as described in Get Data. -```ts -import { Component } from '@angular/core'; -import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/compat/firestore'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +### Order and limit data +By default, a query retrieves all documents that satisfy the query in ascending order by document ID. You can specify the sort order for your data using orderBy(), and you can limit the number of documents retrieved using limit(). + +For example, you could query for the first 3 cities alphabetically with: -export interface AccountLogItem { description: string; amount: number; } -export interface AccountLogItemId extends AccountLogItem { id: string; } +```typescript +import { Component } from "@angular/core"; +import { collection, getDocs, query, limit, where } from "firebase/firestore"; +import { Firestore } from "@angular/fire/firestore"; +export interface City { + name: string; +} @Component({ - selector: 'app-root', + selector: "app-root", template: `
    -
  • - {{ log.description }} for {{ log.amount }} +
  • + {{ city.name }}
- ` + `, }) -export class AppComponent { - private accountLogCollection: AngularFirestoreCollection; - accountLogs: Observable; - constructor(private readonly afs: AngularFirestore) { - this.accountLogCollection = afs.collection('accountLog'); - this.accountLogs = this.accountLogCollection.auditTrail().pipe( - map(actions => actions.map(a => { - const data = a.payload.doc.data() as AccountLogItem; - const id = a.payload.doc.id; - return { id, ...data }; - })) - ); +export class AppComponent implements OnInit { + cities: City[] = []; + constructor(private firestore: Firestore) {} + ngOnInit() { + getDocs( + query( + collection(this.firestore,'cities'), + orderBy('name'), + limit(10))).then((collections:any)=>{ + collections.forEach((doc:any)=>{ + console.log('Conditional',doc.data()) + this.cities.push(doc.data()) + }) + }) } } ``` -### Limiting events +[For more references and extra options visit full docs](https://firebase.google.com/docs/firestore/query-data/order-limit-data) -There are three `DocumentChangeType`s in Firestore: `added`, `removed`, and `modified`. Each streaming method listens to all three by default. However, you may only be interested in one of these events. You can specify which events you'd like to use through the first parameter of each method: +## Paginate data with query cursors +With query cursors in Cloud Firestore, you can split data returned by a query into batches according to the parameters you define in your query. -#### Basic example +Query cursors define the start and end points for a query, allowing you to: -```ts - constructor(private afs: AngularFirestore): { - this.itemsCollection = afs.collection('items'); - this.items = this.itemsCollection.snapshotChanges(['added', 'removed']); - } -``` +* Return a subset of the data. +* Paginate query results. -#### Component example +However, to define a specific range for a query, you should use the `where()` method described in Simple Queries. -```ts -import { Component } from '@angular/core'; -import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/compat/firestore'; -import { Observable } from 'rxjs'; +### Add a simple cursor to a query + +Use the `startAt()` or `startAfter()` methods to define the start point for a query. The `startAt()` method includes the start point, while the `startAfter()` method excludes it. + +For example, if you use `startAt(A)` in a query, it returns the entire alphabet. If you use `startAfter(A)` instead, it returns `B-Z`. + +```typescript +import { Component } from "@angular/core"; +import { collection, getDocs, query, limit, where } from "firebase/firestore"; +import { Firestore } from "@angular/fire/firestore"; +export interface City { + name: string; + population:number; +} @Component({ - selector: 'app-root', + selector: "app-root", template: `
    -
  • - {{ item.name }} +
  • + {{ city.name }}
- ` + `, }) -export class AppComponent { - private itemsCollection: AngularFirestoreCollection; - items: Observable; - constructor(private afs: AngularFirestore) { - this.itemsCollection = afs.collection('items'); - this.items = this.itemsCollection.valueChanges(['added', 'removed']); +export class AppComponent implements OnInit { + cities: City[] = []; + constructor(private firestore: Firestore) {} + ngOnInit() { + getDocs( + query( + collection(this.firestore,'population'), + orderBy("population"), + startAt(1000000))).then((collections:any)=>{ + collections.forEach((doc:any)=>{ + this.cities.push(doc.data()) + }) + }) } } ``` -## State based vs. action based - -Each one of these methods falls into two categories: state based and action based. State based methods return the state of your collection "as-is". Whereas action based methods return "what happened" in your collection. +> Now to paginate this query. You just need to get the length of current cities length(-1) and then use it inside `startAfter` function so that all the results will be after the last document -For example, a user updates the third item in a list. In a state based method like `.valueChanges()` will update the third item in the collection and return an array of JSON data. This is how your state looks. +[Full example on pagination](https://firebase.google.com/docs/firestore/query-data/query-cursors#paginate_a_query) -## Adding documents to a collection - -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. - -#### Basic example - -```ts - constructor(private afs: AngularFirestore): { - const shirtsCollection = afs.collection('tshirts'); - shirtsCollection.add({ name: 'item', price: 10 }); - } -``` ## Manipulating individual documents diff --git a/docs/firestore/documents.md b/docs/firestore/documents.md index fdadc04c9..2516b98d1 100644 --- a/docs/firestore/documents.md +++ b/docs/firestore/documents.md @@ -3,111 +3,195 @@ > Cloud Firestore is a NoSQL, document-oriented database. Unlike a SQL database, there are no tables or rows. Instead, you store data in *documents*, which are organized into *collections*. Each *document* contains a set of key-value pairs. Cloud Firestore is optimized for storing large collections of small documents. -## Using `AngularFirestoreDocument` - -The `AngularFirestoreDocument` service is a wrapper around the native Firestore SDK's [`DocumentReference` type](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference). 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()`. +####

This documentation is updated for modular version 9 of angular fire package. For previous or missing features refer to older docs.

+ +## Features list of document querying +1. Create a document +2. Update a document +3. Delete a document +4. Observer a document for changes +5. Inline querying like + * Incrementing field value + * Adding a data in an array + * Removing a data from an array +6. Offline data persistence + +## Create a document +### Create the document in a fixed document id ```ts -import { Component } from '@angular/core'; -import { AngularFirestore, AngularFirestoreDocument } from '@angular/fire/compat/firestore'; -import { Observable } from 'rxjs'; - -export interface Item { name: string; } +import { Component, OnInit } from '@angular/core'; +import { Firestore } from '@angular/fire/firestore'; +import { doc, getDoc } from 'firebase/firestore'; @Component({ - selector: 'app-root', + selector: "app-root", template: ` -
- {{ (item | async)?.name }} -
- ` +
    +
  • + {{ data.name }} +
  • +
+ `, }) -export class AppComponent { - private itemDoc: AngularFirestoreDocument; - item: Observable; - constructor(private afs: AngularFirestore) { - this.itemDoc = afs.doc('items/1'); - this.item = this.itemDoc.valueChanges(); - } - update(item: Item) { - this.itemDoc.update(item); +export class AppComponent implements OnInit { + data: any = {}; + constructor(private firestore: Firestore) {} + ngOnInit() { + getDoc(doc(this.firestore,'data/Pn5oHxAjpnSEAPJABj22')).then((data)=>{ + this.data = data.data(); + }) } } ``` +### Add the document inside a collection -### The `DocumentChangeAction` type - -With the exception of the `valueChanges()`, each streaming method returns an Observable of `DocumentChangeAction[]`. - -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 metadata about the change and a `doc` property which is the `DocumentSnapshot`. - +Here the document with data `{name:'Rick Roll'}` will be added to collection `data` with unique random id generated by firebase firestore ```ts -interface DocumentChangeAction { - //'added' | 'modified' | 'removed'; - type: DocumentChangeType; - payload: DocumentSnapshot; -} +import { Component, OnInit } from '@angular/core'; +import { Firestore } from '@angular/fire/firestore'; +import { doc, addDoc } from 'firebase/firestore'; -interface DocumentChange { - type: DocumentChangeType; - doc: DocumentSnapshot; - oldIndex: number; - newIndex: number; +@Component({ + selector: "app-root", + template: `

Check console

`, +}) +export class AppComponent implements OnInit { + constructor(private firestore: Firestore) {} + ngOnInit() { + addDoc(collection(this.firestore,'data'),{name:'Rick Roll'}).then((data)=>{ + console.log('Doc added'); + }) + } } +``` +## Update doc with new data +This method changes the data inside the document `mydoc` which is inside the collection `data` with he new data `{name:'Updated name is rick roll'}` +```ts +import { Component, OnInit } from '@angular/core'; +import { Firestore } from '@angular/fire/firestore'; +import { doc, updateDoc } from 'firebase/firestore'; -interface DocumentSnapshot { - exists: boolean; - ref: DocumentReference; - id: string; - metadata: SnapshotMetadata; - data(): DocumentData; - get(fieldPath: string): any; +@Component({ + selector: "app-root", + template: `

Check console

`, +}) +export class AppComponent implements OnInit { + constructor(private firestore: Firestore) {} + ngOnInit() { + updateDoc(doc(this.firestore,'data/mydoc'),{name:'Updated name is rick roll'}).then((data)=>{ + console.log('Doc updated'); + }) + } } ``` +## Delete a doc +This method deletes the document `mydoc` which is inside the collection. +```ts +import { Component, OnInit } from '@angular/core'; +import { Firestore } from '@angular/fire/firestore'; +import { doc, deleteDoc } from 'firebase/firestore'; -## Streaming document data - -There are multiple ways of streaming collection data from Firestore. - -### `valueChanges({ idField?: string })` - -**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`. +@Component({ + selector: "app-root", + template: `

Check console

`, +}) +export class AppComponent implements OnInit { + constructor(private firestore: Firestore) {} + ngOnInit() { + deleteDoc(doc(this.firestore,'data/mydoc')).then((data)=>{ + console.log('Doc deleted'); + }) + } +} +``` +## Get realtime updates on document data -**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. +This methods returns you a subscription from which you can subscribe and get the latest data whenever the document changes it's data. -**When would you not use it?** - When you need document metadata. +```ts +import { Component, OnInit } from '@angular/core'; +import { Firestore } from '@angular/fire/firestore'; +import { doc, docData } from 'firebase/firestore'; +import { Subscription } from 'rxjs'; -### `snapshotChanges()` +@Component({ + selector: "app-root", + template: ` +

This data will change when data inside document changes

+

Data {{this.documentData}}

`, +}) +export class AppComponent implements OnInit, OnDestroy { + constructor(private firestore: Firestore) {} + documentSubscription:Subscription = Subscription.EMPTY; + documentData:any = {} + ngOnInit() { + this.documentSubscription = docData(doc(this.firestore, 'data/Pn5oHxAjpnSEAPJABj22')).subscribe( + (data: any) => { + this.documentData = data.data() + console.log('Doc changed',data.data()) + } + ); + } +} +``` -**What is it?** - Returns an Observable of data as a `DocumentChangeAction`. +## Inline Querying and data manipulation -**Why would you use it?** - When you need the document data but also want to keep around metadata. This metadata provides you the underyling `DocumentReference` and document id. 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. +### Incrementing field value -**When would you not use it?** - When you simply need to render data to a view and don't want to do any extra processing. +```ts +import { Component, OnInit } from '@angular/core'; +import { Firestore } from '@angular/fire/firestore'; +import { doc, docData } from 'firebase/firestore'; +import { Subscription } from 'rxjs'; -## Manipulating documents +@Component({ + selector: "app-root", + template: `

Everytime you refresh the page your score increases by 4

`, +}) +export class AppComponent implements OnInit, OnDestroy { + constructor(private firestore: Firestore) {} + ngOnInit() { + updateDoc(doc(this.firestore, 'data/Pn5oHxAjpnSEAPJABj22'), {score:increment(4)}).then(()=>{ + console.log('incremented'); + }) + } +} +``` +Other field value functions are also accepted like +1. `increment(n)` Increments the specified value by n +2. `arrayRemove(n)` Removes the data n from a specified array +3. `arrayUnion(n)` Adds the data n to specified array -AngularFirestore provides methods for setting, updating, and deleting document data. -- `set(data: T)` - Destructively updates a document's data. -- `update(data: T)` - Non-destructively updates a document's data. -- `delete()` - Deletes an entire document. Does not delete any nested collections. +##Access data offline -## Querying? +Cloud Firestore supports offline data persistence. This feature caches a copy of the Cloud Firestore data that your app is actively using, so your app can access the data when the device is offline. You can write, read, listen to, and query the cached data. When the device comes back online, Cloud Firestore synchronizes any local changes made by your app to the Cloud Firestore backend. -Querying has no effect on documents. Documents are a single object and querying effects a range of multiple documents. If you are looking for querying then you want to use a collection. +To use offline persistence, you don't need to make any changes to the code that you use to access Cloud Firestore data. With offline persistence enabled, the Cloud Firestore client library automatically manages online and offline data access and synchronizes local data when the device is back online. +### Configure offline persistence -## Retrieving nested collections +When you initialize Cloud Firestore, you can enable or disable offline persistence: -Nesting collections is a great way to structure your data. This allows you to group related data structures together. If you are creating a "Task List" site, you can group "tasks" under a user: `user//tasks`. +* For Android and Apple platforms, offline persistence is enabled by default. To disable persistence, set the `PersistenceEnabled` option to `false`. +* For the web, offline persistence is disabled by default. To enable persistence, call the enablePersistence method. Cloud Firestore's cache isn't automatically cleared between sessions. Consequently, if your web app -To retrieve a nested collection use the `collection(path: string)` method. +[For more info refer here](https://firebase.google.com/docs/firestore/manage-data/enable-offline) +This will enable persistence in your firestore database +In your `app.module.ts` file add this when initializing firestore ```ts - constructor(private afs: AngularFirestore) { - this.userDoc = afs.doc('user/david'); - this.tasks = this.userDoc.collection('tasks').valueChanges(); - } +import { enableIndexedDbPersistence } from 'firebase/firestore'; +import { provideFirestore,getFirestore } from '@angular/fire/firestore'; + +@NgModule({ + imports:[ + provideFirestore(()=>{ + const firestore = getFirestore(); + enableIndexedDbPersistence(firestore); + return firestore + }), + ] +}) ``` - -### [Next Step: Collections in AngularFirestore](collections.md)