How to Create Angular Material 16 Firebase SPA CRUD App

Last updated on: by Digamber
In this Angular tutorial, we are going to build a simple Angular CRUD (Create, Read, Update and Delete) web app with Angular Material and Firebase real-time NoSQL database.

We’ll take the help of Angular Material—for creating a little bit eye-catching UI components for our demo application.

We’ll be storing data on cloud with the help of Firebase NoSQL database. We’ll learn how to connect Firebase with Angular web app.

For the demo purpose, I will be creating a book store inventory CRUD web application. In this Book management app user will be able to do the following things:

Add, Read, Edit and Delete book from Firebase db using AngularFire2 library<

  • Add book name
  • Add author name
  • Add 10 digit Book ISBN
  • Add bookbinding type using Angular Material dropdown
  • Include book publication date
  • Add multiple languages
  • Manage stock using Angular material radio buttons

01. Prerequisite

Setup Node JS
Before we move further I assume you are already having Node JS set up in your machine. If not theN follow the given below link:

Follow this link How to Set up Node JS Development Environment?

Install Latest Angular CLI
Use the below command to install the Angular CLI. Avoid, if you’ve already installed the Angular CLI.

npm install -g @angular/cli

02. Project Set Up

Then, set up a new Angular project for creating CRUD app using given below command in your terminal.

ng new angular-material-firebase-crud

Your Angular project is installed, it’s time to enter into the project directory.

cd angular-material-firebase-crud

No Property Access From Index Signature

To resolve error:

Property ‘xxxName’ comes from an index signature, so it must be accessed with [‘xxxName’]

This setting makes sure profound consistency between accessing a field via the “dot” (obj.key) syntax, and “indexed” (obj["key"]) and the way which the property is declared in the type.

Without this flag, TypeScript will allow you to use the dot syntax to access fields which are not defined:

Make sure to set noPropertyAccessFromIndexSignature and strictTemplates properties to false under compilerOptions and angularCompilerOptions respectively in tsconfig.json file:

"compilerOptions": {
 ...
   "strict": false,
   "noPropertyAccessFromIndexSignature": false,
 ...
}
"angularCompilerOptions": {
    "strictTemplates": false
  }

03. Create and Setup Angular Routes

Angular CLI had already included the routes when we were creating the application in the earlier step. Before we enable the routes we need to create components in the Angular app so that we can use them while creating the routes.

ng g component components/add-book --module app
ng g component components/edit-book --module app
ng g component components/book-list --module app

We are using --module app parameter with Angular CLI command because we have 2 module files in the app folder. Now with the –module app parameter We are narrating Angular CLI that app.module.ts is our main app module file.

Go to app > app-routing.module.ts file and add the below code.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AddBookComponent } from './components/add-book/add-book.component';
import { BookListComponent } from './components/book-list/book-list.component';
import { EditBookComponent } from './components/edit-book/edit-book.component';
const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'add-book' },
  { path: 'add-book', component: AddBookComponent },
  { path: 'edit-book/:id', component: EditBookComponent },
  { path: 'books-list', component: BookListComponent }
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

04. Add Angular Material UI Library in Angular

In next step we are going to set up Angular material UI library in our Angular CRUD web app. This UI library follows Google’s Material design guidelines to create user interfaces.

Let us build a basic book inventory CRUD web application with Angular Material. While implementing the Angular Material, we’ll be taking help of Angular Material official documentation.

Run the command to install Angualr material.

ng add @angular/material

Angualr CLI will ask as to choose the Angular Material theme, i will select `Indigo/Pink`. However you may choose whatever material theme you like.

? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink
❯ Indigo/Pink        [ Preview: https://material.angular.io?theme=indigo-pink ] 
  Deep Purple/Amber  [ Preview: https://material.angular.io?theme=deeppurple-amber ] 
  Pink/Blue Grey     [ Preview: https://material.angular.io?theme=pink-bluegrey ] 
  Purple/Green       [ Preview: https://material.angular.io?theme=purple-green ]

Then go to src > styles.scss file and import the Angular material theme and CRUD web project css like given below.

@import "~@angular/material/prebuilt-themes/indigo-pink.css";html,body{height:100%;}
body{margin:0;font-family:'Roboto', sans-serif;}
.header{justify-content:space-between;}
.user-profile{margin-left:15px;}
.mat-sidenav-container{height:100%;display:flex;flex:1 1 auto;}
.mat-nav-list .mat-list-item{font-size:15px;}
.nav-tool-items{display:inline-block;margin-right:13px;}
.user-profile{margin-left:15px;cursor:pointer;}
.hamburger{visibility:hidden !important;}
.mat-sidenav,.mat-sidenav-content{padding:15px;}
.mat-list-item.active{background:rgba(0, 0, 0, .04);}
.mat-sidenav-content{padding:25px 40px 0;}
.mat-sidenav{background-color:#F2F2F2;width:250px;}
.header{position:sticky;position:-webkit-sticky;top:0;z-index:1000;}
mat-sidenav mat-icon{margin-right:12px;}
.hamburger{margin-top:5px;cursor:pointer;}
.mat-radio-button,.mat-radio-group{margin-right:25px;}
.controlers-wrapper>*{width:100%;padding:0;}
.misc-bottom-padding{margin:8px 0 10px;}
.misc-bottom-padding mat-label{margin-right:15px;}
mat-radio-group mat-radio-button{margin-left:5px;}
.button-wrapper button{margin-right:5px;}
table.mat-table,table{width:100%;}
.inner-wrapper{padding:15px 0 130px;width:100%;}
.inner-wrapper mat-card{display:inline-block;margin:0 6% 0 0;vertical-align:top;width:44%;}
.full-wrapper{width:100%;}
.multiple-items{position:relative;}
.multiple-items .tooltip-info{right:0;top:7px;cursor:pointer;color:#a1a7c7;position:absolute;font-size:20px;}
body .push-right{margin-right:10px;}
.no-data{text-align:center;padding-top:30px;color:#6c75a9;}
.button-wrapper{margin:20px 0 0 0;}
@media (max-width:1024px){.inner-wrapper mat-card{width:100%;}
 .mat-sidenav-content{padding:20px 20px 0;}
 .misc-bottom-padding mat-label{display:block;padding-bottom:10px;}
 .mat-sidenav{width:230px;}
 .mat-nav-list .mat-list-item{font-size:14px;}
}
@media (max-width:767px){.nav-tool-items{margin-right:0;}
 .hamburger{visibility:visible !important;}
}
html, body{height:100%;}
body{margin:0;font-family:Roboto, "Helvetica Neue", sans-serif;}

Now you are ready to use Angular Material UI components in your Angular Firebase CRUD web app.

Create a Custom Angular Material Module

Create app > material.module.ts file and add the following code into it.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatNativeDateModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatTableModule } from '@angular/material/table';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
@NgModule({
   imports: [
      CommonModule,
      MatButtonModule,
      MatToolbarModule,
      MatIconModule,
      MatSidenavModule,
      MatBadgeModule,
      MatListModule,
      MatGridListModule,
      MatFormFieldModule,
      MatInputModule,
      MatSelectModule,
      MatRadioModule,
      MatDatepickerModule,
      MatNativeDateModule,
      MatChipsModule,
      MatTooltipModule,
      MatTableModule,
      MatPaginatorModule
   ],
   exports: [
      MatButtonModule,
      MatToolbarModule,
      MatIconModule,
      MatSidenavModule,
      MatBadgeModule,
      MatListModule,
      MatGridListModule,
      MatInputModule,
      MatFormFieldModule,
      MatSelectModule,
      MatRadioModule,
      MatDatepickerModule,
      MatChipsModule,
      MatTooltipModule,
      MatTableModule,
      MatPaginatorModule
   ],
   providers: [
      MatDatepickerModule,
   ]
})
export class AngularMaterialModule { }

Then go to app.module.ts file and define AngularMaterialModule and CUSTOM_ELEMENTS_SCHEMA.

import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AddBookComponent } from './components/add-book/add-book.component';
import { EditBookComponent } from './components/edit-book/edit-book.component';
import { BookListComponent } from './components/book-list/book-list.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AngularMaterialModule } from './material.module';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
  declarations: [
    AppComponent,
    AddBookComponent,
    EditBookComponent,
    BookListComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    AngularMaterialModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}

Now you can import any Angular Material UI component in your custom Angular material module.

Create Angular Material Basic Responsive Layout

Now we have to create a basic Angular app with Angular Material. I’ll take the help of Angular Material’s APIs to create basic responsive layout.

Go to app.component.html file and add the below code. It contains the basic layout created with Angular Material ui library.

<!-- Toolbar -->
<mat-toolbar color="primary" class="header">
  <div>Book Store</div>
  <span class="nav-tool-items">
    <mat-icon (click)="sidenav.toggle()" class="hamburger">menu</mat-icon>
  </span>
</mat-toolbar>
<mat-sidenav-container>
  <!-- Sidenav -->
  <mat-sidenav #sidenav [mode]="isBiggerScreen() ? 'over' : 'side'" [(opened)]="opened" [fixedInViewport]="true"
    [fixedTopGap]>
    <mat-nav-list>
      <a mat-list-item routerLinkActive="active" routerLink="/add-book">
        <mat-icon>add</mat-icon> Add Book
      </a>
      <a mat-list-item routerLinkActive="active" routerLink="/books-list">
        <mat-icon>format_list_bulleted</mat-icon> View Books
      </a>
    </mat-nav-list>
  </mat-sidenav>
  <!-- Main content -->
  <mat-sidenav-content>
    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>

After that, go to app.component.ts file and paste the following code.

import { Component, ViewChild, HostListener, OnInit } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  opened = true;
  @ViewChild('sidenav', { static: true }) sidenav: MatSidenav;
  ngOnInit() {
    if (window.innerWidth < 768) {
      this.sidenav.fixedTopGap = 55;
      this.opened = false;
    } else {
      this.sidenav.fixedTopGap = 55;
      this.opened = true;
    }
  }
  @HostListener('window:resize', ['$event'])
  onResize(event) {
    if (event.target.innerWidth < 768) {
      this.sidenav.fixedTopGap = 55;
      this.opened = false;
    } else {
      this.sidenav.fixedTopGap = 55;
      this.opened = true;
    }
  }
  isBiggerScreen() {
    const width =
      window.innerWidth ||
      document.documentElement.clientWidth ||
      document.body.clientWidth;
    if (width < 768) {
      return true;
    } else {
      return false;
    }
  }
}

05. Set up AngularFire2 Library

In order to use Firebase real-time NoSQL database in your Angular project, you need to install AngularFire library from NPM.

I assume you already have account set up in Firebase database, if not then follow this tutorial: Setup Account in Firebase Database and Connect with Angular App.

Enter the below command in the terminal.

npm install @angular/fire firebase@9.16.0 --legacy-peer-deps

To make connection between your Firebase real-time NoSQL database and Angular app.

Go to src > environments folder and add your Firebase config details in the environment file.

Update code in environment.ts file.

export const environment = {
  production: false,
  firebaseConfig: {
    apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    authDomain: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    databaseURL: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    projectId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    storageBucket: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
};

Head over to Firebase console of your app:

Select Database From Side bar menu —> Select Rule From Tabs —–> Update your rule similar to this:

{
    "rules": {    
        ".read": true,
        ".write": true
    }
}

If you are seeing notice on the Firebase console:

“Your security rules are defined as public, so anyone can steal, modify, or delete data in your database”.

Make sure to change the rules to prior stage of the rules from the security purpose.

Import AngularFirebase 2 services in app.module.ts file.

/* Firebase */
import { AngularFireModule } from '@angular/fire/compat';
import { AngularFireDatabaseModule } from '@angular/fire/compat/database';
import { environment } from '../environments/environment';
@NgModule({
  declarations: [...],
  imports: [
    AngularFireModule.initializeApp(environment.firebaseConfig),
    AngularFireDatabaseModule,
  ],
  providers: [...],
  bootstrap: [...]
})
export class AppModule { }

06. Create Service to Manage CRUD operations via RESTful APIs using AngularFire2 Library

Now create a folder by the name of shared, in this folder we’ll keep our service file and book interface class.

This service & interface file will help us to manage CRUD operations for our Angular Material and Angular Firebase web app.

Run the command:

Enter the below command to create the book interface class for setting up data types for a book inventory app.

ng g i shared/book

app > shared > book.ts

export interface Book {
   $key: string;
   book_name: string;
   isbn_10: number;
   author_name: string
   publication_date: Date;
   binding_type: string;
   in_stock: string;
   languages: Array<string>;
}

Its time to create book service, run the following command.

ng g s shared/book

Our book.service.ts file holds the core logic for our Angular material web app. We are using AngularFire2 library to build create, read, update & delete operations.

import { Injectable } from '@angular/core';
import { Book } from './book';

import {
  AngularFireDatabase,
  AngularFireList,
  AngularFireObject,
} from '@angular/fire/compat/database';

@Injectable({
  providedIn: 'root',
})
export class BookService {
  booksRef: AngularFireList<any>;
  bookRef: AngularFireObject<any>;
  constructor(private db: AngularFireDatabase) {}
  /* Create book */
  AddBook(book: Book) {
    this.booksRef
      .push({
        book_name: book.book_name,
        isbn_10: book.isbn_10,
        author_name: book.author_name,
        publication_date: book.publication_date,
        binding_type: book.binding_type,
        in_stock: book.in_stock,
        languages: book.languages,
      })
      .catch((error) => {
        this.errorMgmt(error);
      });
  }
  /* Get book */
  GetBook(id: string) {
    this.bookRef = this.db.object('books-list/' + id);
    return this.bookRef;
  }
  /* Get book list */
  GetBookList() {
    this.booksRef = this.db.list('books-list');
    return this.booksRef;
  }
  /* Update book */
  UpdateBook(id, book: Book) {
    this.bookRef
      .update({
        book_name: book.book_name,
        isbn_10: book.isbn_10,
        author_name: book.author_name,
        publication_date: book.publication_date,
        binding_type: book.binding_type,
        in_stock: book.in_stock,
        languages: book.languages,
      })
      .catch((error) => {
        this.errorMgmt(error);
      });
  }
  /* Delete book */
  DeleteBook(id: string) {
    this.bookRef = this.db.object('books-list/' + id);
    this.bookRef.remove().catch((error) => {
      this.errorMgmt(error);
    });
  }
  // Error management
  private errorMgmt(error) {
    console.log(error);
  }
}

Go to app.module.ts file and import the Angular service and also import into the providers array like given below.

/* Angular CRUD services */
import { BookService } from './shared/book.service';
@NgModule({
  providers: [BookService],
})
export class AppModule { }

07. Add Book using Angular Material 13 and AngularFire2

In this part of the tutorial, we are going to cover following topics using given below logic.

  • Creating Reactive Forms with Angular Material.
  • Validating Reactive Forms with Angular Material form elements.
  • Creating Radio buttons with Angular Material
  • Creating Reactive Nested Form in Angular Firebase and Angular Material.
  • Saving data in Firebase Real-time database using AngularFire2 RESTful APIs.
  • Save and store date to Firebase using Angular Material date-picker element.
  • Create custom form reset functionality to reset Reactive forms FormGroup.
  • Working with Angular Material Chips Input with Reactive Forms.
  • Angular Material Datepicker in Angular and save in Firebase.

We need to import FormsModule and ReactiveFormsModule in app.module.ts file.

/* Reactive form services in Angular */
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
  imports: [
    FormsModule,
    ReactiveFormsModule
  ],
})
export class AppModule { }

Go to add-book.component.html and paste the following code.

<!-- Title group  -->
<div class="title-group">
  <h1 class="mat-h1">Add Book</h1>
  <mat-divider fxFlex="1 0"></mat-divider>
</div>
<!-- Book form -->
<div class="inner-wrapper">
  <form
    [formGroup]="bookForm"
    (ngSubmit)="submitBook()"
    #resetBookForm="ngForm"
    novalidate
  >
    <!-- Left block -->
    <mat-card>
      <div class="controlers-wrapper">
        <!-- Book name -->
        <mat-form-field class="example-full-width">
          <input matInput placeholder="Book name" formControlName="book_name" />
          <mat-error *ngIf="handleError('book_name', 'required')">
            You must provide a<strong>book name</strong>
          </mat-error>
        </mat-form-field>
        <!-- ISBN -->
        <mat-form-field class="example-full-width">
          <input
            matInput
            placeholder="ISBN-10"
            formControlName="isbn_10"
            pattern="[0-9]*"
            minlength="10"
            maxlength="10"
          />
          <mat-error *ngIf="handleError('isbn_10', 'required')">
            You must provide a <strong>10 digit ISBN</strong>
          </mat-error>
          <mat-error *ngIf="handleError('isbn_10', 'pattern')">
            Only numbers are allowed
          </mat-error>
          <mat-error *ngIf="handleError('isbn_10', 'minlength')">
            Your <strong>ISBN</strong> must be 10 digit
          </mat-error>
        </mat-form-field>
        <!-- Author name -->
        <mat-form-field class="example-full-width">
          <input
            matInput
            placeholder="Author name"
            formControlName="author_name"
          />
          <mat-error *ngIf="handleError('author_name', 'required')">
            You must provide an <strong>author name</strong>
          </mat-error>
        </mat-form-field>
        <!-- Publication date -->
        <mat-form-field>
          <input
            matInput
            readonly
            [matDatepicker]="picker"
            placeholder="Publication date"
            formControlName="publication_date"
            (dateChange)="formatDate($event)"
          />
          <mat-datepicker-toggle
            matSuffix
            [for]="picker"
          ></mat-datepicker-toggle>
          <mat-datepicker #picker></mat-datepicker>
          <mat-error *ngIf="handleError('publication_date', 'required')">
            Publication date is required
          </mat-error>
        </mat-form-field>
      </div>
    </mat-card>
    <!-- Right block -->
    <mat-card>
      <div class="controlers-wrapper">
        <!-- Book binding -->
        <mat-form-field>
          <mat-label>Binding type</mat-label>
          <mat-select formControlName="binding_type">
            <mat-option
              [value]="bindingType"
              *ngFor="let bindingType of BindingType"
              >{{ bindingType }}</mat-option
            >
          </mat-select>
          <mat-error *ngIf="handleError('binding_type', 'required')">
            Binding type is required
          </mat-error>
        </mat-form-field>
        <!-- Book stock -->
        <div class="misc-bottom-padding">
          <mat-label>Available in stock: </mat-label>
          <mat-radio-group
            aria-label="Select an option"
            formControlName="in_stock"
          >
            <mat-radio-button value="Yes">Yes</mat-radio-button>
            <mat-radio-button value="No">No</mat-radio-button>
          </mat-radio-group>
        </div>
        <!-- Add languages -->
        <mat-form-field class="multiple-items">
          <mat-chip-grid #chipGrid>
            <mat-chip
              *ngFor="let lang of languageArray"
              [selectable]="selectable"
              [removable]="removable"
              (removed)="remove(lang)"
            >
              {{ lang.name }}
              <mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
            </mat-chip>
            <input
              placeholder="Add languages"
              [matChipInputFor]="chipGrid"
              [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
              [matChipInputAddOnBlur]="addOnBlur"
              (matChipInputTokenEnd)="add($event)"
            />
          </mat-chip-grid>
          <i
            class="material-icons tooltip-info"
            matTooltip="Enter item name and press enter to add multiple items"
          >
            info
          </i>
        </mat-form-field>
      </div>
    </mat-card>
    <!-- Submit & Reset -->
    <mat-card>
      <div class="full-wrapper button-wrapper">
        <div class="button-wrapper">
          <button mat-flat-button color="warn">Submit</button>
          <button mat-flat-button color="war" (click)="resetForm()">
            Clear
          </button>
        </div>
      </div>
    </mat-card>
  </form>
</div>

To add a book into the inventory, go to components > add-book.component.ts file and include the following imports.

import { Component, OnInit, ViewChild } from '@angular/core';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
import { BookService } from './../../shared/book.service';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
export interface Language {
  name: string;
}
@Component({
  selector: 'app-add-book',
  templateUrl: './add-book.component.html',
  styleUrls: ['./add-book.component.scss'],
})
export class AddBookComponent implements OnInit {
  visible = true;
  selectable = true;
  removable = true;
  addOnBlur = true;
  languageArray: Language[] = [];
  @ViewChild('chipList') chipList;
  @ViewChild('resetBookForm') myNgForm;
  readonly separatorKeysCodes: number[] = [ENTER, COMMA];
  selectedBindingType: string;
  bookForm: FormGroup;
  BindingType: any = [
    'Paperback',
    'Case binding',
    'Perfect binding',
    'Saddle stitch binding',
    'Spiral binding',
  ];
  ngOnInit() {
    this.bookApi.GetBookList();
    this.submitBookForm();
  }
  constructor(public fb: FormBuilder, private bookApi: BookService) {}
  /* Remove dynamic languages */
  remove(language: Language): void {
    const index = this.languageArray.indexOf(language);
    if (index >= 0) {
      this.languageArray.splice(index, 1);
    }
  }
  /* Reactive book form */
  submitBookForm() {
    this.bookForm = this.fb.group({
      book_name: ['', [Validators.required]],
      isbn_10: ['', [Validators.required]],
      author_name: ['', [Validators.required]],
      publication_date: ['', [Validators.required]],
      binding_type: ['', [Validators.required]],
      in_stock: ['Yes'],
      languages: [this.languageArray],
    });
  }
  /* Get errors */
  public handleError = (controlName: string, errorName: string) => {
    return this.bookForm.controls[controlName].hasError(errorName);
  };
  /* Add dynamic languages */
  add(event: MatChipInputEvent): void {
    let input = (event.value || '').trim();
    let value = event.value;
    // Add language
    if ((value || '').trim() && this.languageArray.length < 5) {
      this.languageArray.push({ name: value.trim() });
    }
    // Reset the input value
    event.chipInput!.clear();
  }
  /* Date */
  formatDate(e) {
    var convertDate = new Date(e.target.value).toISOString().substring(0, 10);
    this.bookForm.get('publication_date').setValue(convertDate, {
      onlyself: true,
    });
  }
  /* Reset form */
  resetForm() {
    this.languageArray = [];
    this.bookForm.reset();
    Object.keys(this.bookForm.controls).forEach((key) => {
      this.bookForm.controls[key].setErrors(null);
    });
  }
  /* Submit book */
  submitBook() {
    if (this.bookForm.valid) {
      this.bookApi.AddBook(this.bookForm.value);
      this.resetForm();
    }
  }
}

08. Show Books Data and Delete Book

We will fetch the book data from Firebase database and show it to front-end using Angular Material data table. By using the given below code we’ll complete the following tasks.

  • How to use Angular Material data table in Angular
  • How to implement Angular material pagination in Angular material table?
  • How to delete angular material table’s specific row?
  • How to Delete a single object from Firebase database?

Go to book-list.component.ts file and import the following services.

import { Book } from './../../shared/book';
import { Component, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { BookService } from './../../shared/book.service';
@Component({
  selector: 'app-book-list',
  templateUrl: './book-list.component.html',
  styleUrls: ['./book-list.component.scss'],
})
export class BookListComponent {
  dataSource: MatTableDataSource<Book>;
  @ViewChild(MatPaginator) paginator: MatPaginator;
  BookData: any = [];
  displayedColumns: any[] = [
    '$key',
    'book_name',
    'author_name',
    'publication_date',
    'in_stock',
    'action',
  ];
  constructor(private bookApi: BookService) {
    this.bookApi
      .GetBookList()
      .snapshotChanges()
      .subscribe((books) => {
        books.forEach((item) => {
          let a = item.payload.toJSON();
          a['$key'] = item.key;
          this.BookData.push(a as Book);
        });
        /* Data table */
        this.dataSource = new MatTableDataSource(this.BookData);
        /* Pagination */
        setTimeout(() => {
          this.dataSource.paginator = this.paginator;
        }, 0);
      });
  }
  /* Delete */
  deleteBook(index: number, e) {
    if (window.confirm('Are you sure?')) {
      const data = this.dataSource.data;
      data.splice(
        this.paginator.pageIndex * this.paginator.pageSize + index,
        1
      );
      this.dataSource.data = data;
      this.bookApi.DeleteBook(e.$key);
    }
  }
}

Go to book-list.component.html file and import the following services.

<!-- Title group  -->
<div class="title-group">
  <h1 class="mat-h1">Book List</h1>
  <mat-divider fxFlex="1 0"></mat-divider>
</div>
<!-- No data message -->
<p *ngIf="BookData.length <= 0" class="no-data">There is no data added yet!</p>
<!-- Books list Angular material data table -->
<div class="container" *ngIf="BookData.length > 0">
  <div class="mat-elevation-z8">
    <table mat-table [dataSource]="dataSource">
      <ng-container matColumnDef="$key">
        <th mat-header-cell *matHeaderCellDef>Book ID</th>
        <td mat-cell *matCellDef="let element">{{ element.$key }}</td>
      </ng-container>
      <ng-container matColumnDef="book_name">
        <th mat-header-cell *matHeaderCellDef>Book Name</th>
        <td mat-cell *matCellDef="let element">{{ element.book_name }}</td>
      </ng-container>
      <ng-container matColumnDef="author_name">
        <th mat-header-cell *matHeaderCellDef>Author Name</th>
        <td mat-cell *matCellDef="let element">{{ element.author_name }}</td>
      </ng-container>
      <ng-container matColumnDef="publication_date">
        <th mat-header-cell *matHeaderCellDef>Publication Date</th>
        <td mat-cell *matCellDef="let element">
          {{ element.publication_date }}
        </td>
      </ng-container>
      <ng-container matColumnDef="in_stock">
        <th mat-header-cell *matHeaderCellDef>In Stock</th>
        <td mat-cell *matCellDef="let element">{{ element.in_stock }}</td>
      </ng-container>
      <ng-container matColumnDef="action">
        <th mat-header-cell *matHeaderCellDef>Action</th>
        <td mat-cell *matCellDef="let element; let i = index">
          <button
            mat-raised-button
            color="primary"
            class="push-right"
            [routerLink]="['/edit-book/', element.$key]"
          >
            Edit
          </button>
          <button
            mat-raised-button
            color="accent"
            (click)="deleteBook(i, element)"
          >
            Delete
          </button>
        </td>
      </ng-container>
      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
    </table>
    <mat-paginator
      [pageSizeOptions]="[5, 10, 20]"
      showFirstLastButtons
    ></mat-paginator>
  </div>
</div>

09. Edit a Book using Angular Material

To create edit functionality we have to create an edit button and bind it to click event in edit book component.

Also include the following code in the edit-book.component.html file.

<!-- Title group  -->
<div class="title-group">
  <h1 class="mat-h1">Edit Book</h1>
  <mat-divider fxFlex="1 0"></mat-divider>
</div>
<!-- Book form -->
<div class="inner-wrapper">
  <form [formGroup]="editBookForm" (ngSubmit)="updateBook()" novalidate>
    <mat-card>
      <div class="controlers-wrapper">
        <!-- Book name -->
        <mat-form-field class="example-full-width">
          <input matInput placeholder="Book name" formControlName="book_name" />
          <mat-error *ngIf="handleError('book_name', 'required')">
            You must provide a<strong>book name</strong>
          </mat-error>
        </mat-form-field>
        <!-- ISBN -->
        <mat-form-field class="example-full-width">
          <input
            matInput
            placeholder="ISBN-10"
            formControlName="isbn_10"
            pattern="[0-9]*"
            minlength="10"
            maxlength="10"
          />
          <mat-error *ngIf="handleError('isbn_10', 'required')">
            You must provide a <strong>10 digit ISBN</strong>
          </mat-error>
          <mat-error *ngIf="handleError('isbn_10', 'pattern')">
            Only numbers are allowed
          </mat-error>
          <mat-error *ngIf="handleError('isbn_10', 'minlength')">
            Your <strong>ISBN</strong> must be 10 digit
          </mat-error>
        </mat-form-field>
        <!-- Author name -->
        <mat-form-field class="example-full-width">
          <input
            matInput
            placeholder="Author name"
            formControlName="author_name"
          />
          <mat-error *ngIf="handleError('author_name', 'required')">
            You must provide an <strong>author name</strong>
          </mat-error>
        </mat-form-field>
        <!-- Publication date -->
        <mat-form-field>
          <input
            matInput
            readonly
            [matDatepicker]="picker"
            placeholder="Publication date"
            formControlName="publication_date"
            (dateChange)="formatDate($event)"
          />
          <mat-datepicker-toggle
            matSuffix
            [for]="picker"
          ></mat-datepicker-toggle>
          <mat-datepicker #picker></mat-datepicker>
          <mat-error *ngIf="handleError('publication_date', 'required')">
            Publication date is required
          </mat-error>
        </mat-form-field>
      </div>
    </mat-card>
    <mat-card>
      <div class="controlers-wrapper">
        <!-- Book binding -->
        <mat-form-field>
          <mat-label>Binding type</mat-label>
          <mat-select formControlName="binding_type">
            <mat-option
              [value]="bindingType"
              *ngFor="let bindingType of BindingType"
              >{{ bindingType }}</mat-option
            >
          </mat-select>
          <mat-error *ngIf="handleError('binding_type', 'required')">
            Binding type is required
          </mat-error>
        </mat-form-field>
        <!-- Book stock -->
        <div class="misc-bottom-padding">
          <mat-label>Available in stock: </mat-label>
          <mat-radio-group
            aria-label="Select an option"
            formControlName="in_stock"
          >
            <mat-radio-button value="Yes">Yes</mat-radio-button>
            <mat-radio-button value="No">No</mat-radio-button>
          </mat-radio-group>
        </div>
        <!-- Add languages -->
        <mat-form-field class="multiple-items">
          <mat-chip-grid #chipGrid>
            <mat-chip
              *ngFor="let lang of languageArray"
              [selectable]="selectable"
              [removable]="removable"
              (removed)="remove(lang)"
            >
              {{ lang.name }}
              <mat-icon matChipRemove *ngIf="removable">cancel</mat-icon>
            </mat-chip>
            <input
              placeholder="Add languages"
              [matChipInputFor]="chipGrid"
              [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
              [matChipInputAddOnBlur]="addOnBlur"
              (matChipInputTokenEnd)="add($event)"
            />
          </mat-chip-grid>
          <i
            class="material-icons tooltip-info"
            matTooltip="Enter item name and press enter to add multiple items"
          >
            info
          </i>
        </mat-form-field>
      </div>
    </mat-card>
    <!-- Submit & Reset -->
    <mat-card>
      <div class="full-wrapper button-wrapper">
        <div class="button-wrapper">
          <button mat-flat-button color="warn">Update</button>
          <button mat-flat-button color="war" type="button" (click)="goBack()">
            Go Back
          </button>
        </div>
      </div>
    </mat-card>
  </form>
</div>

Go to edit-book > edit-book.component.ts file and add the following imports and Language interface class.

import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Location } from '@angular/common';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
import { BookService } from './../../shared/book.service';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
export interface Language {
  name: string;
}
@Component({
  selector: 'app-edit-book',
  templateUrl: './edit-book.component.html',
  styleUrls: ['./edit-book.component.scss'],
})
export class EditBookComponent implements OnInit {
  visible = true;
  selectable = true;
  removable = true;
  addOnBlur = true;
  languageArray: Language[] = [];
  @ViewChild('chipList') chipList;
  readonly separatorKeysCodes: number[] = [ENTER, COMMA];
  selectedBindingType: string;
  editBookForm: FormGroup;
  BindingType: any = [
    'Paperback',
    'Case binding',
    'Perfect binding',
    'Saddle stitch binding',
    'Spiral binding',
  ];
  ngOnInit() {
    this.updateBookForm();
  }
  constructor(
    public fb: FormBuilder,
    private location: Location,
    private bookApi: BookService,
    private actRoute: ActivatedRoute,
    private router: Router
  ) {
    var id = this.actRoute.snapshot.paramMap.get('id');
    this.bookApi
      .GetBook(id)
      .valueChanges()
      .subscribe((data) => {
        this.languageArray = data.languages;
        this.editBookForm.setValue(data);
      });
  }
  /* Update form */
  updateBookForm() {
    this.editBookForm = this.fb.group({
      book_name: ['', [Validators.required]],
      isbn_10: ['', [Validators.required]],
      author_name: ['', [Validators.required]],
      publication_date: ['', [Validators.required]],
      binding_type: ['', [Validators.required]],
      in_stock: ['Yes'],
      languages: [''],
    });
  }
  /* Add language */
  add(event: MatChipInputEvent): void {
    let input = (event.value || '').trim();
    let value = event.value;
    // Add language
    if ((value || '').trim() && this.languageArray.length < 5) {
      this.languageArray.push({ name: value.trim() });
    }
    // Reset the input value
    event.chipInput!.clear();
  }
  /* Remove language */
  remove(language: any): void {
    const index = this.languageArray.indexOf(language);
    if (index >= 0) {
      this.languageArray.splice(index, 1);
    }
  }
  /* Get errors */
  public handleError = (controlName: string, errorName: string) => {
    return this.editBookForm.controls[controlName].hasError(errorName);
  };
  /* Date */
  formatDate(e) {
    var convertDate = new Date(e.target.value).toISOString().substring(0, 10);
    this.editBookForm.get('publication_date').setValue(convertDate, {
      onlyself: true,
    });
  }
  /* Go to previous page */
  goBack() {
    this.location.back();
  }
  /* Submit book */
  updateBook() {
    var id = this.actRoute.snapshot.paramMap.get('id');
    if (window.confirm('Are you sure you wanna update?')) {
      this.bookApi.UpdateBook(id, this.editBookForm.value);
      this.router.navigate(['books-list']);
    }
  }
}

10. Run Angular & Firebase CRUD Web Application

Open your terminal and enter the following command to start your project.

ng serve --open

Finally, we have created the basic Angular Firebase CRUD web app using Angular Material. For better project understanding, you can check out the GitHub repo of this project.

positronX.io - Tamso Ma Jyotirgamaya
Digamber

A Full-stack developer with a passion to solve real world problems through functional programming.