DEV Community

Cover image for Angular Material Reactive Forms Update Firestore
Alex Patterson for CodingCatDev

Posted on • Originally published at ajonp.com on

Angular Material Reactive Forms Update Firestore

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
      })
    );
Enter fullscreen mode Exit fullscreen mode

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)]]
          });
        })
    );
  }
Enter fullscreen mode Exit fullscreen mode

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"
>
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

Form Field Errors

Like magic (okay programming), if a fields validation is incorrect you will see an error appear.
Form Field

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>
Enter fullscreen mode Exit fullscreen mode

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.
No Data

After Data Entered if invalid we can only revert.
Bad Data

Finally good data we can save.
Good Data

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>
Enter fullscreen mode Exit fullscreen mode

This button is within the form component and has this method being called (ngSubmit)="saveBookChanges()".

<form
  *ngIf="bookForm"
  [formGroup]="bookForm"
  (ngSubmit)="saveBookChanges()"
  fxLayout="column"
>
Enter fullscreen mode Exit fullscreen mode

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]);
  }
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)