Alex Patterson
Posted on April 29, 2019
Original Post: https://ajonp.com/courses/angularmaterial/angular-material-reactive-forms-update-firestore/
Angular Material Reactive Forms Update Firestore
Setup
We can start from the previous lesson and build out our reactive forms.
Previous Lesson: Angular Material Forms Firestore
git clone https://github.com/AJONPLLC/lesson12-angular-material-forms-firestore
This will give us a solid base to start working from, however if you are creating a new firebase project you should change the environment/environment.ts file to match your firebase details. If you have never done this please see Angular Navigation Firestore as this will provide more details on how to update.
Make sure you update your npm packages
npm install
Update Book Model
Navigate to src/app/core/models/book.ts so that we can update more details about the books that we will be adding and editing in the tutorial.
You will notice a big change instead of using this as an interface which only allows for typing, using the class will allow us to create new objects based on our definition of Book. I really enjoy Todd Moto's description of this the most in Classes vs Interfaces in Typescript.
You can see here that we have also provided a constructor that allows for a Partial Book type to be provided that assigns this as a new book, without requiring a full object. You can read more about Partial here. The Object assign will copy the values of all of the enumerable own properties from one or more source objects to a target object and returns the target object, which in our case will return a Book object.
src/app/core/models/book.ts
import { Timestamp } from '@firebase/firestore-types';
export class Book {
ageCategory?: string;
description?: string;
fiction?: boolean;
genre?: string;
hasAudio?: boolean;
hasPhotos?: boolean;
hasVideos?: boolean;
id?: string;
publishDate?: Timestamp | Date;
rating?: number;
status?: string;
title?: string;
public constructor(init?: Partial<Book>) {
Object.assign(this, init);
}
}
Firestore Current Book Value to Form
Subscribing to book from Id
Please note later I have updated the array push method of unsubscribing to a Subject and used
takeUntil(this.unsubscribe$)
.
What we are doing here in the first part of the ngOnInit is subscribing to the router and getting our specified bookId
and setting the global variable to store this off so that we can use this ID to fetch data about the current book.
src/app/modules/books/book-edit/book-edit.component.ts
bookId: string;
...
ngOnInit() {
// Get bookId for book document selection from Firestore
this.subs.push(
this.route.paramMap.subscribe(params => {
this.bookId = params.get('bookId');
this.rebuildForm();
})
);
Building (or rebuilding) Angular Form
We can then use this to call the method rebuildForm()
which will update any of the required bindings on our Angular Form.
If we break down this method we can see that there is a line that sets the blobal book$
variable Observable. Don't be confused by the this.subs.push
you could even leave this out just for sake of the learning exercise (I would leave something to unsubsribe for a production app though).
Next we have this.book$.pipe(map(book
in which we are changing the book.publishDate
which is a Timestamp
over to a Javascript DateTime. This is necessary as our Angular Component is expecting this format.
src/app/modules/books/book-edit/book-edit.component.ts
bookForm: FormGroup;
book$: Observable<Book>;
...
rebuildForm() {
if (this.bookForm) {
this.bookForm.reset();
}
this.book$ = this.fs.getBook(this.bookId);
this.subs.push(
this.book$
.pipe(
map(book => {
console.log(book.publishDate);
if (book.publishDate) {
const timestamp = book.publishDate as Timestamp;
book.publishDate = timestamp.toDate();
}
return book;
})
)
.subscribe(book => {
this.bookForm = this.fb.group({
ageCategory: [book.ageCategory, Validators.required],
description: [
book.description,
[Validators.required, Validators.maxLength(500)]
],
fiction: [book.fiction || false, Validators.required],
genre: [book.genre, Validators.required],
hasAudio: [book.hasAudio],
hasPhotos: [book.hasPhotos],
hasVideos: [book.hasVideos],
id: [book.id],
publishDate: [book.publishDate],
rating: [book.rating, Validators.required],
status: [book.status, Validators.required],
title: [book.title, [Validators.required, Validators.maxLength(50)]]
});
})
);
}
Form Control using Form Builder
We also subscribe to the Observable coming from Firestore using this.book$.subscribe(book
in which we setup the global variable bookForm
with the values coming from Firestore. We use the dependency injected Form Builder private fb: FormBuilder
or fb
to create a form group with all of the necessary form controls.
In our form we can then reference these controls, for instance ageCategory: [book.ageCategory, Validators.required],
ageCategory is now a FormControl that has a default value from Firestore of book.ageCategory
and it is also a required field based on Validators.required
.
You can see here that we then use formControlName="ageCategory"
in order to link that form control based on the name.
<mat-select
placeholder="Age Category"
formControlName="ageCategory"
>
Some of the more interesting use cases for FormControl validation is with something like title: [book.title, [Validators.required, Validators.maxLength(50)]]
which says our title cannot be longer than 50. Just a reminder this is all front end based, so someone could maliciously still add a longer book.title, so you need to make sure if this is a hard requirement that you adjust your firestore.rules accordingly.
<input matInput placeholder="Title" formControlName="title" />
Form Field Errors
Like magic (okay programming), if a fields validation is incorrect you will see an error appear.
This is handled via html with component mat-error
this must be inside of mat-form-field
like all of the Angular Material Form components. In our case we are showing two messages for title it is blank we show required, then if it is in error and not currently required we show that max length of 50.
<mat-form-field style="width: 100%">
<input matInput placeholder="Title" formControlName="title" />
<mat-error *ngIf="!bookForm.get('title').hasError('required')">
Title has a max length of 50.
</mat-error>
<mat-error *ngIf="bookForm.get('title').hasError('required')">
Title is <strong>required</strong>
</mat-error>
</mat-form-field>
Form Submit Only Pristine
Some of the logic here looks a little backwards but because we are disabling the buttons everything is applied in reverse. For cancel we only care if data was changed so we check for pristine
(entered data), for the submit button data must be pristine and also valid. Meaning none of the Validators can be false, like required and length.
Before Data Entered, we only have option to cancel.
After Data Entered if invalid we can only revert.
Finally good data we can save.
Submit and Save data
Once all the data is pristine and valid we can then push SAVE.
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="bookForm.pristine || !bookForm.valid"
aria-label="Save"
>
Save
</button>
This button is within the form component and has this method being called (ngSubmit)="saveBookChanges()"
.
<form
*ngIf="bookForm"
[formGroup]="bookForm"
(ngSubmit)="saveBookChanges()"
fxLayout="column"
>
When this calls the method saveBookChanges
it will call the firestore updateBook await this.fs.updateBook(book);
in which it waits before navigating back to the main books list. This is also where you could throw up a saving dialog before the await statement.
You will notice the first thing that we did was create the Book
class, this is where it becomes hugely valuable. We can directly pass the bookForm.value
and it will create a new Book
Object to make the update!
async saveBookChanges() {
const book = new Book(this.bookForm.value);
await this.fs.updateBook(book);
this.router.navigate(['/books', this.bookId]);
}
Video
I think the video for this lesson is the best guide, don't forget to put those breakpoints in to see what is happening in all the calls, and open up Firestore to watch it auto update.
Posted on April 29, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.