Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Advanced Reactive Forms Techniques With Firestore

Episode 108 written by Jeff Delaney
full courses for pro members

Health Check: This lesson was last reviewed on and tested with these packages:

  • Angular v6.0.3
  • RxJS v6.2
  • AngularFire2 v5.0.0-rc.10

Find an issue? Let's fix it

Source code available to pro members. Learn more

Forms are one of the most critical, yet most cumbersome aspects of app development. They require a ton of HTML markup and require complex validation rules for a good user experience. Almost all forms must be synced to a backend database, leaving us faced a state management conundrum that is not always easy to solve. In this lesson, my goal is to provide you with a reliable solution for syncing your frondend forms to any backend database.

Angular’s ReactiveFormsModule is basically a standalone state management system, while Firebase manages its own state under the hood. We want to combine these tools into a unified system by building a reusable directive that makes working with Firestore and Reactive Forms nearly automatic.

New to Reactive Forms? I highly recommend watching the [basics(lessons/basics-reactive-forms-in-angular/) video] before attempting this lesson.

Syncing Reactive Forms to a Backend

There are several different strategies one could employ to sync their reactive forms a backend database. In our case, we will use Cloud Firestore and AngularFire2. Here’s how the basic process will work from a high level.

  1. Preload existing data from the database into the form.
  2. Listen to changes in the reactive form value.
  3. Debounce for a few seconds.
  4. Write the changes to the database.
  5. Repeat steps 2 through 4.

Auto saving reactive form to firestore database

Firestore Form Directive

An Angular Attribute Directive can be used to abstract away the backend database work, giving us reusable code for multiple forms.

ng g directive fire-form

When we’re done, basic usage will look like this. Drop the directive into a form, point to a Firestore document and the rest is magic.

<form [formGroup]="yourForm" fireForm path="contacts">

</form>

The path attribute represents the firestore document or collection were the data will be saved. When pointing to a collection, it will automatically create a new document. Otherwise, you can point directly to an existing document ID.

<form ... path="contacts/someExistingID">

</form>

fireform.directive.ts

There is quite a bit happening in this directive to manage the state between the reactive form and Firestore database. Let’s break down some of the key points.

The Output/EventEmitter properties are used to emit events about state changes to the parent component. We need to to let the parent know when the form is modified, synced, or throws a backend error. A TypeScript setter is used to emit an event and change the internal state value at the same time.

preloadData() grabs the current data from the form. This is useful when your form can be edited after initial submission.

autoSave() listens to the reactive form value and after a 2000ms debounce will write the form data to the database, as long as all validation rules pass.

@HostListener('ngSubmit') intercepts the form submission and runs the update on the backend. This allows your user to perform a regular save by clicking a button (as opposed to waiting for an auto-save).


@Directive({
selector: '[fireForm]'
})
export class FireFormDirective implements OnInit, OnDestroy {
// ... pro only
}




import { Directive, Input, Output, EventEmitter, HostListener, OnInit, OnDestroy } from '@angular/core';
import { AngularFirestore, AngularFirestoreDocument } from 'angularfire2/firestore';
import { FormControl, FormGroup } from '@angular/forms';
import { tap, map, take, debounceTime } from 'rxjs/operators';
import { Subscription } from 'rxjs';

@Directive({
selector: '[fireForm]'
})
export class FireFormDirective implements OnInit, OnDestroy {

// Inputs
@Input() path: string;
@Input() formGroup: FormGroup;

// Internal state
private _state: 'loading' | 'synced' | 'modified' | 'error';

// Outputs
@Output() stateChange = new EventEmitter<string>();
@Output() formError = new EventEmitter<string>();

// Firestore Document
private docRef: AngularFirestoreDocument;

// Subscriptions
private formSub: Subscription;

constructor(private afs: AngularFirestore) { }


ngOnInit() {
this.preloadData();
this.autoSave();
}

// Loads initial form data from Firestore
preloadData() {
this.state = 'loading';
this.docRef = this.getDocRef(this.path);
this.docRef
.valueChanges()
.pipe(
tap(doc => {
if (doc) {
this.formGroup.patchValue(doc);
this.formGroup.markAsPristine();
this.state = 'synced';
}
}),
take(1)
)
.subscribe();
}


// Autosaves form changes
autoSave() {
this.formSub = this.formGroup.valueChanges
.pipe(
tap(change => {
this.state = 'modified';
}),
debounceTime(2000),
tap(change => {
if (this.formGroup.valid && this._state === 'modified') {
this.setDoc();
}
})
)
.subscribe();
}

// Intercept form submissions to perform the document write
@HostListener('ngSubmit', ['$event'])
onSubmit(e) {
this.setDoc();
}


// Determines if path is a collection or document
getDocRef(path: string): any {
if (path.split('/').length % 2) {
return this.afs.doc(`${path}/${this.afs.createId()}`);
} else {
return this.afs.doc(path);
}
}

// Writes changes to Firestore
async setDoc() {
try {
await this.docRef.set(this.formGroup.value, { merge: true });
this.state = 'synced';
} catch (err) {
console.log(err)
this.formError.emit(err.message)
this.state = 'error';
}
}

// Setter for state changes
set state(val) {
this._state = val;
this.stateChange.emit(val);
}

ngOnDestroy() {
this.formSub.unsubscribe();
}

}


Syncing your reactive forms to Firestore can now be accomplished with a single line of HTML <form [formGroup]="someForm" fireForm path="someCollection/someId">. If you have any questions or feedback please leave a comment or drop me a line on Slack.