DEV Community

Cover image for Angular Material Forms from Firestore
Alex Patterson for CodingCatDev

Posted on • Originally published at ajonp.com on

Angular Material Forms from Firestore

Original Post: https://ajonp.com/courses/angularmaterial/angular-material-forms-from-firestore/

Angular Material Forms from Firestore

🌎 Demo: https://ajonp-lesson-12.firebaseapp.com/books/FirstBook/edit

This lesson will cover how to create all of the Angular Material Form Components, the data behind many of them will be coming from Cloud Firestore.

Setup

We will start this lesson from where we left off on Angular Navigation Firestore

Source from Prior lesson

Clone

git clone https://github.com/AJONPLLC/lesson11-angular-navigation-firestore.git lesson12-angular-material-forms-firestore
Enter fullscreen mode Exit fullscreen mode

Remove Origin, just always easier up front if you want to add this to your own repo (or again you can fork and just clone.)

git remote rm origin
Enter fullscreen mode Exit fullscreen mode

Install Dependencies

Make sure you are in the correct directory cd lesson12-angular-material-forms-firestore.

npm install
Enter fullscreen mode Exit fullscreen mode

Book Edit Module

Create

Using the Angular CLI create the module with routing and corresponding component.

ng g m modules/books/book-edit --routing && ng g c modules/books/book-edit
Enter fullscreen mode Exit fullscreen mode

Router updates

books-routing.module.ts

Add new lazy loaded path so that our main books-routing knows where to send requests for edit.

...
  {
    path: ':bookId/edit',
    loadChildren: './book-edit/book-edit.module#BookEditModule'
  }
  ...
Enter fullscreen mode Exit fullscreen mode

book-edit-routing.module.ts

Now that we are in the book-edit module make sure it has a path to the book-edit Component.

...
const routes: Routes = [
  {
    path: '',
    component: BookEditComponent
  }
];
...
Enter fullscreen mode Exit fullscreen mode

Serve the edit path

Startup the server

ng serve
Enter fullscreen mode Exit fullscreen mode

Now that our router is all setup we should start to see the book-edit.component.html. Because we don't have a way to navigate to this path yet just type it in the url bar manually localhost:4200/books/FirstBook/edit.

You should see

book-edit works!

Update Book Edit

Structure

To give our form some structure we can now add Flex Layout and Material Card centered at 75% to give a nice look to our form.

book-edit.component.html

<div fxLayout="column" fxLayoutAlign="space-around center">
  <mat-card style="width: 75%; margin-bottom: 100px;">
    <mat-card-content>
    </mat-card-content>
    <mat-card-actions> <button mat-button>Ok</button> </mat-card-actions>
  </mat-card>
</div>
Enter fullscreen mode Exit fullscreen mode

Because these are new Elements we need to import them into our Book Edit module.
book-edit.module.ts

import { FlexLayoutModule } from '@angular/flex-layout';
import {MatCardModule} from '@angular/material';

...

@NgModule({
  declarations: [BookEditComponent],
  imports: [
    CommonModule,
    BookEditRoutingModule,
    FlexLayoutModule,
    MatCardModule,
  ]
})
...
Enter fullscreen mode Exit fullscreen mode

Getting Firestore Data for Book Edit

Because we are now navigating to an area that uses Angular router and part of the path contains a specified paramter id :bookId/edit we can get this bookId from the currently Activated Route. In order to do this we need to use dependency injection and provide this in our constructor. To then fetch that data from our FirestoreService we can then inject this service as well.

  subs: Subscription[] = [];
  book$: Observable<Book>;
  constructor(private router: ActivatedRoute, private fs: FirestoreService) {}

  ngOnInit() {
    // Get bookId for book document selection from Firestore
    this.subs.push(
      this.router.paramMap.subscribe(params => {
        const bookId = params.get('bookId');
        this.book$ = this.fs.getBook(bookId);
      })
    );
Enter fullscreen mode Exit fullscreen mode

By calling the firestore getBook function and passing in the current parameter bookId we now have an Observable reference to the Firestore data.

firestore.service.ts

getBook(bookId: string): Observable<Book> {
  // Start Using AngularFirebase Service!!
  return this.afb.doc$<Book>(`books/${bookId}`);
}
Enter fullscreen mode Exit fullscreen mode

This is a cool wrapper that Jeff over at fireship.io created.
Feel free to copy this service and use it as a nice wrapper for all of your projects, I won't include the two calls as we move forward
angularfirebase.service.ts

  doc$<T>(ref: DocPredicate<T>): Observable<T> {
    return this.doc(ref)
      .snapshotChanges()
      .pipe(
        map(
          (
            doc: Action<
              DocumentSnapshotDoesNotExist | DocumentSnapshotExists<T>
            >
          ) => {
            return doc.payload.data() as T;
          }
        )
      );
  }
Enter fullscreen mode Exit fullscreen mode

Example of Book Data in Firestore Console:

Book Data

In the snippet above we are also pushing our RxJs Subscription into an array so that we can then loop through any subscriptions during the destroy method and unsubscribe. This is a pattern I often use when I cannot use pipe(take(1)) or | async.

  ngOnDestroy() {
    this.subs.forEach(sub => {
      sub.unsubscribe();
    });
  }
Enter fullscreen mode Exit fullscreen mode

Using Firestore Data Inside of Template

We can not use our new book$ Observable in our template to show any of the current data. We can update our card to show only when the book data is available, otherwise show a Material Spinner. You can read more on how the NgIf directive works in the Angular docs if you are unfamiliar.

  <mat-card *ngIf="(book$ | async); let book; else: spin" style="width: 75%; margin-bottom: 100px;">
  <mat-card-title>{{book.title}}</mat-card-title>
  ...
<ng-template #spin><mat-spinner></mat-spinner></ng-template>
Enter fullscreen mode Exit fullscreen mode

Current title:
Title View

Adding Form Input

Just a warning here, if you want to see detailed Angular Reactive Form usage this will be done in the next lesson.

Now that we know our Observable is working successfully we can now change the card title out and start switching our card into several form inputs.

For this we will need to include FormsModule, ReactiveFormsModule, MatFormFieldModule, and MatInputModule into our Book Edit Module.

book-edit.module.ts

  imports: [
    CommonModule,
    BookEditRoutingModule,
    FlexLayoutModule,
    MatCardModule,
    FormsModule,
    ReactiveFormsModule,
    MatFormFieldModule,
    MatInputModule,
Enter fullscreen mode Exit fullscreen mode

Title Input

Title Input

The entire Material Card should now look like below, the new div surrounding the card is just to allow each section to flow in its own row of the column. If you have not used Flex Layout check out the details.

book-edit.component.html

  <mat-card
    *ngIf="(book$ | async); let book; else: spin"
    style="width: 75%; margin-bottom: 100px;"
  >
    <mat-card-content>
      <div fxLayout="column" fxLayoutAlign="space-around stretch">
        <section>
          <mat-form-field style="width: 100%">
            <input matInput placeholder="Title" [(ngModel)]="book.title" />
          </mat-form-field>
        </section>
      </div>
    </mat-card-content>
    <mat-card-actions> <button mat-button>Ok</button> </mat-card-actions>
  </mat-card>
Enter fullscreen mode Exit fullscreen mode

Above we have our first two components

-Form Field which you can think of as a wrapper to all of our components allowing for styling of the other form fields.

-Input the key with this is the directive matInput, this will allow you to use a native <input> or <textarea> correctly styled within <mat-form-field>.

If you see any issues at this point, make sure you have imported all the modules into book-edit.module.ts

For an extra you can checkout the textarea example too.

  <section>
    <mat-form-field style="width: 100%">
      <textarea
        matInput
        placeholder="Description"
        [(ngModel)]="book.description"
        rows="5"
      ></textarea>
    </mat-form-field>
  </section>
Enter fullscreen mode Exit fullscreen mode

Slide Toggle

Slide Toggle

-Slide Toggle is a very simple comoponent that is either on or off (binary).

book-edit.module.ts

  imports: [
    ...
       MatSlideToggleModule,
Enter fullscreen mode Exit fullscreen mode

For our requirement we are going to use this to determine whether a book is Fiction or non-fiction.

We will set the default fictionSelected = true; so that Fiction is set first.

book-edit.component.ts

export class BookEditComponent implements OnInit, OnDestroy {
  subs: Subscription[] = [];
  book$: Observable<Book>;
  fictionSelected = true;
Enter fullscreen mode Exit fullscreen mode

book-edit.component.html

<section>
  <mat-slide-toggle
    [checked]="fictionSelected"
    (change)="fictionChange($event)"
    #ficToggle
  >
    <p *ngIf="ficToggle.checked; else nonFic">Fiction</p>
  </mat-slide-toggle>
</section>
Enter fullscreen mode Exit fullscreen mode

You can see that our input directive checked (denoted by []), will now take the value of fictionSelected and every time the toggle is changed we will use the output directive change (denoted by ()) to trigger function fictionChange passing in the current components instance of the event $event. You can read more about DOM event payloads.

book-edit.component.ts

  fictionChange(e) {
    this.fictionSelected = e.checked;
    this.genreControl.reset();
  }
Enter fullscreen mode Exit fullscreen mode

Autocomplete

  • Autocomplete is just another input type in our case for text. However it also has a Panel that is associated to provide a list of options. For our use case it will list out two different lists based on our Slide Toggle. So we will either filter the Genere Fiction list of values, or a Non-Fiction list of values.

book-edit.module.ts

  imports: [
    ...
       MatAutocompleteModule,
Enter fullscreen mode Exit fullscreen mode

These two lists will be contained in a new collection at the base of our Firestore Database called config. Within our config collection we will create a document called book, which will hold many of our different configurations. For these two specifically they will be arrays that are on the book object.

Firestore Fiction/Non-Fiction

First we will create our Observable to the Config Book object.
book-edit.component.ts

  bookConfig$: Observable<ConfigBook>;
  ...
    // Set Book Config
    this.bookConfig$ = this.fs.getConfigBook();
Enter fullscreen mode Exit fullscreen mode

Create the ConfigBook Interface for our type.

ng g i core/models/config-book
Enter fullscreen mode Exit fullscreen mode

config-book.ts

export interface ConfigBook {
  ageCategory?: Array<string>;
  fiction?: Array<string>;
  nonFiction?: Array<string>;
}
Enter fullscreen mode Exit fullscreen mode

Then we can will grab the first set of values emitted from Firestore and send those out as a BehaviorSubject with type ConfigBook. Our toggle has set this.fictionSelected so we can determine what list should be emitted in this.genereList$.

book-edit.component.ts

export class BookEditComponent implements OnInit, OnDestroy {
  ...
  genreControl = new FormControl();
  ...
    // Set default Genere
    this.bookConfig$.pipe(take(1)).subscribe(bookConfig => {
      this.subs.push(
        this.genreControl.valueChanges.pipe(startWith('')).subscribe(value => {
          const filterValue = value ? value.toLowerCase() : '';
          if (this.fictionSelected) {
            this.genreList$.next(
              bookConfig.fiction.filter(option =>
                option.toLowerCase().includes(filterValue)
              )
            );
          } else {
            this.genreList$.next(
              bookConfig.nonFiction.filter(option =>
                option.toLowerCase().includes(filterValue)
              )
            );
          }
        })
      );
    });
Enter fullscreen mode Exit fullscreen mode

You will also notice above the we have subscribed to any of the valueChanges that are happening on our new genreControl. Below you will see that formControl input directive is passed our class parameter genreControl which is an instance of FormControl. We will dive into all of the @angular/forms in more detail in the next lesson. For our sake here just know that this allows us to check all of the changing values as you type. When we start to type it uses the arrays that we have passed in from Firestore and filters them based on the string we are inputing using either bookConfig.fiction.filter or bookConfig.nonFiction.filter.

  <section>
    <mat-form-field style="width: 100%">
      <input
        name="genre"
        type="text"
        matInput
        [formControl]="genreControl"
        [matAutocomplete]="auto"
        placeholder="Genre"
        aria-label="Genre"
      />
    </mat-form-field>
    <mat-autocomplete #auto="matAutocomplete">
      <mat-option
        *ngFor="let genre of (genreList$ | async)"
        [value]="genre"
      >
        {{ genre }}
      </mat-option>
    </mat-autocomplete>
  </section>
Enter fullscreen mode Exit fullscreen mode

Above we are listening to the updates from genreList$ BehaviorSubject to create our <mat-option> list of values. Our input has an input directive [matAutocomplete]="auto" to attach this <mat-autocomplete> by assigning the instance variable #auto to the input using matAutocomplete.

Checkbox

Checkbox

Take a deep breath there was a lot going on with the Autocomplete, the rest get a lot easier 😺

  • Checkbox is again providing the same functionality as the native <input type="checkbox"> enhanced with Material Design.

book-edit.module.ts

  imports: [
    ...
       MatCheckboxModule,
Enter fullscreen mode Exit fullscreen mode

Firestore Config

For us this contains some more of the configuration items from our book, because we already have an Observable created in bookConfig$ we can just tell Angular to listen for this and assign it to our local template variable bookConfig. We then are just assigning these to the directive NgModel. This is a two way binding, for our example doesn't mean much, but again we will drive these things home further in the next lesson.

<section *ngIf="(bookConfig$ | async); let bookConfig">
  <h3>Options</h3>
  <div fxLayout="column">
    <mat-checkbox [(ngModel)]="bookConfig.options.hasAudio">
      Audio
    </mat-checkbox>
    <mat-checkbox [(ngModel)]="bookConfig.options.hasPhotos">
      Photos
    </mat-checkbox>
    <mat-checkbox [(ngModel)]="bookConfig.options.hasVideos">
      Videos
    </mat-checkbox>
  </div>
</section>
Enter fullscreen mode Exit fullscreen mode

Datepicker

Date Picker

  • Datepicker The datepicker allows users to enter a date either through text input, or by choosing a date from the calendar. It is made up of several components and directives that work together.

Special note here you need MatNativeDateModule in addition to the MatDatepickerModule.

book-edit.module.ts

  imports: [
    ...
       MatDatepickerModule,
       MatNativeDateModule
Enter fullscreen mode Exit fullscreen mode

This is just creating the pre canned datepicker. We don't have any data that will will bring in currently to update this field. We will cover this in the next lesson.

book-edit.component.html

  <section>
    <mat-form-field>
      <input
        matInput
        [matDatepicker]="picker"
        placeholder="Publish Date"
      />
      <mat-datepicker-toggle
        matSuffix
        [for]="picker"
      ></mat-datepicker-toggle>
      <mat-datepicker #picker></mat-datepicker>
    </mat-form-field>
  </section>
Enter fullscreen mode Exit fullscreen mode

Select

Select

  • Select you can use either the Material Select, or the native select within the <mat-form-field>. The native control has several performance advantages...but I really like the style of using the Material Design.

book-edit.module.ts

  imports: [
    ...
       MatSelectModule,
Enter fullscreen mode Exit fullscreen mode

It would probably be better to unwrap our bookConfig$ once in our template, but I wanted to keep each of these as seperate units.

For our book component we are once again going to get all of the age categories from our Config Book in Firestore and use those values for our select.

Mat Select

book-edit.component.html

  <section>
    <mat-form-field style="width: 100%">
      <mat-select placeholder="Age Category">
        <mat-option
          *ngFor="let ageCategory of (bookConfig$ | async)?.ageCategory"
          [value]="ageCategory"
        >
          {{ ageCategory }}
        </mat-option>
      </mat-select>
    </mat-form-field>
  </section>
Enter fullscreen mode Exit fullscreen mode

Slider

Slider

  • Slider <mat-slider> allows for the selection of a value from a range via mouse, touch, or keyboard, similar to <input type="range">.

book-edit.module.ts

  imports: [
    ...
       MatSliderModule,
Enter fullscreen mode Exit fullscreen mode

We once again will not be doing anything with this value, but I did want to show you how to default the value on creation. Later we will tie this directly to the Firestore value for our book.

book-edit.component.ts

  bookRating = 3;
Enter fullscreen mode Exit fullscreen mode

book-edit.component.html

  <section>
    <h3>Rating</h3>
    <mat-slider
      min="1"
      max="5"
      step="0.5"
      value="1.5"
      [(ngModel)]="bookRating"
    ></mat-slider>
  </section>
Enter fullscreen mode Exit fullscreen mode

Radio button

Radio Button

  • Radio button <mat-radio-button> provides the same functionality as a native <input type="radio"> enhanced with Material Design styling and animations.

book-edit.module.ts

  imports: [
    ...
       MatRadioModule,
Enter fullscreen mode Exit fullscreen mode

For us this will again in the future refer directly to a status on our Book, we could create these from a Firestore config, but I don't see us changing these options very often. If you want you could do the same loop as we did with the select option and add the config. In the next lesson we will add that config and show how to do a validation of sorts.

book-edit-component.html

  <section>
    <h3>Status</h3>
    <mat-radio-group [(ngModel)]="bookStatus" fxLayout="column">
      <mat-radio-button value="Draft">Draft</mat-radio-button>
      <mat-radio-button value="Published">Published</mat-radio-button>
    </mat-radio-group>
  </section>
Enter fullscreen mode Exit fullscreen mode

Wrap Up

Here we have created all of the Angular Material Form Components, many of them with data being pulled from Firestore. Next is to make our form more Reactive.

Kitchen Sink

Latest comments (0)