Angular 12 Material Datatable Inline HttpClient CRUD Operations using RestFull APIs

In this Angular tutorial, we’ll learn how to use the HttpClient module in Angular application to make RESTFull API Ajax calls.

We’ll set up a new Angular 12 project and create API endpoints to perform Create, Read, Update and Delete operations using HTTP methods using a mock server using the json-server package. Using json-server we can create a local JSON file which can act as a database for our application. There is no difference between a local mock server and a real database.

We’ll perform HTTP operation using API endpoints to add, edit, delete and get list of items listed in an Angular Material Datatable. This Material Datatable will have action column using which user an Edit or Delete a row. There will be a form over the table to Add or Update existing rows in the table.

After implementation, our application will look like this

 

First, let’s have a look at Angular HttpClient and its features.

What is Angular HttpClient?

A reactive application like Angular, communicate with server databases to fetch data in form of JSON object using Ajax API calls.

These API Ajex calls use XMLHttpRequest service for old browsers or fetch() methods under the hood.

The HttpClient service provided by Angular’s @angular/common/http package provides a simple interface to make HTTP calls with many optimized and efficient browser support features.

Moreover, we can also use RxJS based Observables and operators to handle client-side or server-side errors.

Using Interceptors service we can modify requests and responses of API calls and even cancels them.

Let’s get started!

 

# Setup Angular CLI

Angular CLI is the most prefered and official way for creating a new Angular project.

Make sure you have installed the latest version on Angular CLI tool on your system.

Run following npm command to install

$ npm install -g @angular/cli

 

# Create a new Angular  project

Next, create a new Angular project by running following ng command in the terminal

$ ng new angular-httpclient-tutorial

On hitting above command, ng CLI will ask few configurational questions

? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? CSS

Now move to the project folder

$ cd angular-httpclient-tutorial

Run the project by executing below command

$ ng serve --open

As we have created the Angular project successfully, lets mover further to create a dummy mock JSON server.

# Setup a Mock JSON Server

For testing and learning HttpClient module, we need to test Http methods, which will communicate to the server by calling Rest APIs.

These RESTFull APIs return JSON data which is then consumed by Angular application. This API JSON data may come from any third-party API, Server Database or any local server database.

Here we will create a dummy mock JSON API server using the json-server package. using this package we can create a mock server using which we can perform all HTTP methods like GET, POST, PUT, PATCH and DELETE.

First, install the json-server package by running bellow npm command in the terminal window:

$ npm install -g json-server

 

After that create a new folder API in the project root and place JSON file data.json at ~angular-httpclient-tutorial/API/data.json

The data.json file will work as RESTfull server. We will add some dummy students data. So that will act like a database on which we will perform CRUD operations.

# Start JSON Server

To start the JSON server using json-server, run following command in a new terminal:

$ json-server --watch ./API/data.json

 

Now you can access our mock server at http://localhost:3000/students

Following are the API URLs available on our server:

GET /posts – fetch all students
GET /posts/: id – fetch a single student detail by id
POST /posts – create a new student
PUT /posts/: id – update a student by id
PATCH /posts/: id – partially update a student by id
DELETE /posts/:id – delete a student by id

As we are ready with our server, next we will import HttpClientModule in Angular project to use HTTP services.

# Configure HttpClient in Angular 9 project

Before using HTTP services, we need to import HttpClientModule from @angular/common/http class.

Now open the app.module.ts file, then make the following changes:

// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

 

We will add Angular Material Datatable to perform CRUD operations on Students data. So let us install and configure Material UI library in our project.

# Install and Setup Angular Material

Angular Material is a UI library which provides several easy to use feature components. In this tutorial, we will use Material Datatables to show students records and perform the various inline operation on students records.

Run following npm command in terminal to install Material library and answer some configuration answers.

$ ng add @angular/material

? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink        [ Preview: https://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? Yes 
? Set up browser animations for Angular Material? Yes

 

To use Material UI components, we need to import modules of components which we are going to use in the application’s module so that these will be available in our class components to use.

As we will be using Material Datatables with pagination, so we need to import MatTableModule and MatPaginatorModule.

To update and add students rows we will add Material Form as well, for that we will also import FormsModuleReactiveFormsModule, MatInputModule, and MatButtonModule as well

in the app.module.ts file as shown below:

// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';

import { StudentsComponent } from './pages/students/students.component';

@NgModule({
  declarations: [
    AppComponent,
    StudentsComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    BrowserAnimationsModule,
    FormsModule,
    ReactiveFormsModule,

    // Material Modules 
    MatTableModule,
    MatPaginatorModule,
    MatInputModule,
    MatButtonModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

 

# Creating a Service to Communicate Server through HTTP methods

Now we’ll create a new service to separate all HTTP methods which will communicate to server and do CRUD operations.

Let’s create a new serve HttpDataService under services folder by running following ng command in the terminal window:

$ ng generate service services/http-data

Above generate command will create the HttpDataService for us at this location ~src/app/services/http-data.service.ts

Also, create an Interface class for Students data by running following command defining the type of values for student item.

$ ng generate class models/Student

then replace the following content in the newly created file “~/models/student.ts

export class Student {
   id: number;
   name: string;
   age: string;
   address: string;
}

 

Our service will be going to play an important role in maintaining a connection with the server. Let us dive deep into this file and check what it will have?

Add the server API URL for end-points and define in the base_path variable. This is the path which opens up on running our json-server

  // API path
  base_path = 'http://localhost:3000/students';

Note: Make sure your server is still running.

We’ll import these three classes

HttpClient : This class provides HTTP methods like get(), post(), put() and delete().

HttpHeaders: For setting request headers in HTTP calls we use this class.

  // Http Options
  httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json'
    })
  }

 

HttpErrorResponse: Used to efficiently handle errors from client-side or server-side.

  // Handle API errors
  handleError(error: HttpErrorResponse) {
    if (error.error instanceof ErrorEvent) {
      // A client-side or network error occurred. Handle it accordingly.
      console.error('An error occurred:', error.error.message);
    } else {
      // The backend returned an unsuccessful response code.
      // The response body may contain clues as to what went wrong,
      console.error(
        `Backend returned code ${error.status}, ` +
        `body was: ${error.error}`);
    }
    // return an observable with a user-facing error message
    return throwError(
      'Something bad happened; please try again later.');
  };

 

# RxJs functions and operators to the rescue

The RxJs library provides many useful function and operator which we will use in our service:

Observables: Observables are used to perform asynchronous tasks like HTTP calls. We can subscribe them to get success or error response. They provide several other features file cancellations and continuous event retrieval, unlike promises.

throwError: This method is used to intentionally throw an error with a message when an HTTP request fails.

retry(): The retry operator is used to make HTTP call again for the number of times specified when a call fails due to network server issues.

catchError(): This method catches the error and throws to errorHandler

# Defining CRUD Methods

Now we will add the methods to do CRUD operation on students data in our mock server which we created using json-server.

Create a Student

The new student will be created using the post() method

  // Create a new item
  createItem(item): Observable<Student> {
    return this.http
      .post<Student>(this.base_path, JSON.stringify(item), this.httpOptions)
      .pipe(
        retry(2),
        catchError(this.handleError)
      )
  }

The createItem() method is accepting item attribute with student details to add.

Retrieve Student Details

To fetch single student details we use get() method with student id whose detail needs to be checked.

  // Get single student data by ID
  getItem(id): Observable<Student> {
    return this.http
      .get<Student>(this.base_path + '/' + id)
      .pipe(
        retry(2),
        catchError(this.handleError)
      )
  }

Retrieve All Students

Similarly, we will make get call to fetch all students list

  // Get students data
  getList(): Observable<Student> {
    return this.http
      .get<Student>(this.base_path)
      .pipe(
        retry(2),
        catchError(this.handleError)
      )
  }

Update single student

The put() method will update single student with id passed

  // Update item by id
  updateItem(id, item): Observable<Student> {
    return this.http
      .put<Student>(this.base_path + '/' + id, JSON.stringify(item), this.httpOptions)
      .pipe(
        retry(2),
        catchError(this.handleError)
      )
  }

Delete a single student

The delete() HTTP method will delete a single record whose id is passed

  // Delete item by id
  deleteItem(id) {
    return this.http
      .delete<Student>(this.base_path + '/' + id, this.httpOptions)
      .pipe(
        retry(2),
        catchError(this.handleError)
      )
  }

After combining all explained code the final http-data.service.ts file will look like this:

// http-data.servie.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Student } from '../models/student';
import { Observable, throwError } from 'rxjs';
import { retry, catchError } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class HttpDataService {


  // API path
  base_path = 'http://localhost:3000/students';

  constructor(private http: HttpClient) { }

  // Http Options
  httpOptions = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json'
    })
  }

  // Handle API errors
  handleError(error: HttpErrorResponse) {
    if (error.error instanceof ErrorEvent) {
      // A client-side or network error occurred. Handle it accordingly.
      console.error('An error occurred:', error.error.message);
    } else {
      // The backend returned an unsuccessful response code.
      // The response body may contain clues as to what went wrong,
      console.error(
        `Backend returned code ${error.status}, ` +
        `body was: ${error.error}`);
    }
    // return an observable with a user-facing error message
    return throwError(
      'Something bad happened; please try again later.');
  };


  // Create a new item
  createItem(item): Observable<Student> {
    return this.http
      .post<Student>(this.base_path, JSON.stringify(item), this.httpOptions)
      .pipe(
        retry(2),
        catchError(this.handleError)
      )
  }

  // Get single student data by ID
  getItem(id): Observable<Student> {
    return this.http
      .get<Student>(this.base_path + '/' + id)
      .pipe(
        retry(2),
        catchError(this.handleError)
      )
  }

  // Get students data
  getList(): Observable<Student> {
    return this.http
      .get<Student>(this.base_path)
      .pipe(
        retry(2),
        catchError(this.handleError)
      )
  }

  // Update item by id
  updateItem(id, item): Observable<Student> {
    return this.http
      .put<Student>(this.base_path + '/' + id, JSON.stringify(item), this.httpOptions)
      .pipe(
        retry(2),
        catchError(this.handleError)
      )
  }

  // Delete item by id
  deleteItem(id) {
    return this.http
      .delete<Student>(this.base_path + '/' + id, this.httpOptions)
      .pipe(
        retry(2),
        catchError(this.handleError)
      )
  }
}

# Create new pages

To show students data in a table,  we will create a new students component and update the app-routing.module.ts file to open /students page on application load.

Create the StudentsComponent by running below generate command:

$ ng generate component pages/students

Setup the App Routing Module

Now update the app-routing.module.ts file with below code.

// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { StudentsComponent } from './pages/students/students.component';


const routes: Routes = [
  {
    path: '',
    redirectTo: '/students',
    pathMatch: 'full'
  },
  {
    path: 'students',
    component: StudentsComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

# Using HttpDataService in Students Page

As we defined our HttpDataService as providedIn: 'root'so we can directly use it in our Angular application. This will share a single instance across the application.

To use service methods in our student’s page at ~src/app/pages/students/students.component.ts, we need to import it and then add in component the contractor() method as shown below:

...
import { HttpDataService } from 'src/app/services/http-data.service';

@Component({
  selector: 'app-students',
  templateUrl: './students.component.html',
  styleUrls: ['./students.component.css']
})
export class StudentsComponent implements OnInit {

  constructor(private httpDataService: HttpDataService) { }

  ...
}

# Adding Angular Material Datatable

Next, we will add a Datatable with Students columns and an extra Actions column where we will do inline Edit, Delete and Update operations.

For creating the Material datatable, the mat-table directive is used. We are also adding pagination by appending the mat-paginator directive just after ending </table> tag.

Update the students.component.html file with below code to build a Material datatable:

<div class="table-wrapper">

        <!-- Form to edit/add row -->

        <table mat-table [dataSource]="dataSource" class="mat-elevation-z8">

        <ng-container matColumnDef="id">
            <th mat-header-cell *matHeaderCellDef> #Id </th>
            <td mat-cell *matCellDef="let element"> {{element.id}} </td>
        </ng-container>

        <ng-container matColumnDef="name">
            <th mat-header-cell *matHeaderCellDef> Name </th>
            <td mat-cell *matCellDef="let element"> {{element.name}} </td>
        </ng-container>

        <ng-container matColumnDef="age">
            <th mat-header-cell *matHeaderCellDef> Age </th>
            <td mat-cell *matCellDef="let element"> {{element.age}} </td>
        </ng-container>

        <ng-container matColumnDef="address">
            <th mat-header-cell *matHeaderCellDef> Address </th>
            <td mat-cell *matCellDef="let element"> {{element.address}} </td>
        </ng-container>

        <ng-container matColumnDef="actions">
            <th mat-header-cell *matHeaderCellDef> Actions </th>
            <td mat-cell *matCellDef="let element">
                <a href="javascript:void(0)" (click)="editStudent(element)">Edit</a> |
                <a href="javascript:void(0)" (click)="deleteStudent(element.id)">Delete</a>
            </td>
        </ng-container>

        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
        <tr mat-row *matRowDef="let row; columns: displayedColumns;"
            [ngClass]="{'editable-row': studentData.id === row.id}"></tr>
    </table>
    <mat-paginator [pageSize]="5" [pageSizeOptions]="[5, 10, 15]" showFirstLastButtons></mat-paginator>
</div>

In the last actions column there are two actions to Edit with editStudent() method and Delete with deleteStudent() method for the row.

To add a new row or update the data in the existing row we will add a form above table.

    <form (submit)="onSubmit()" #studentForm="ngForm">

        <mat-form-field>
            <input matInput placeholder="Name" name="name" required [(ngModel)]="studentData.name">
        </mat-form-field>
        <mat-form-field>
            <input matInput placeholder="Age" name="age" required [(ngModel)]="studentData.age">
        </mat-form-field>
        <mat-form-field>
            <input matInput placeholder="Address" name="address" required [(ngModel)]="studentData.address">
        </mat-form-field>

        <ng-container *ngIf="isEditMode; else elseTemplate">
            <button mat-button color="primary">Update</button>
            <a mat-button color="warn" (click)="cancelEdit()">Cancel</a>
        </ng-container>
        <ng-template #elseTemplate>
            <button mat-button color="primary">Add</button>
        </ng-template>

    </form>

The text in form submit button will change based on the boolean value in the isEditMode variable.

# Update Component Class

After adding Material datatable and Form, let us update students.component.ts file with required methods.

First, initialize the Template driven form with the NgForm 

  @ViewChild('studentForm', { static: false })
  studentForm: NgForm;

Import the Students class which we created and define a new variable studentData of type Students

studentData: Student;

Then define the dataSource and displayedColumns with MatPaginator class to build our Datatable

  dataSource = new MatTableDataSource();
  displayedColumns: string[] = ['id', 'name', 'age', 'address', 'actions'];
  @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;

Then we will add methods to Add, Delete, Edit, Update and Get Students list in the class file as shown below:

...
  getAllStudents() {
    this.httpDataService.getList().subscribe((response: any) => {
      this.dataSource.data = response;
    });
  }

  editItem(element) {
    this.studentData = _.cloneDeep(element);
    this.isEditMode = true;
  }

  cancelEdit() {
    this.isEditMode = false;
    this.studentForm.resetForm();
  }

  deleteItem(id) {
    this.httpDataService.deleteItem(id).subscribe((response: any) => {

      // Approach #1 to update datatable data on local itself without fetching new data from server
      this.dataSource.data = this.dataSource.data.filter((o: Student) => {
        return o.id !== id ? o : false;
      })

      console.log(this.dataSource.data);

      // Approach #2 to re-call getAllStudents() to fetch updated data
      // this.getAllStudents()
    });
  }

  addStudent() {
    this.httpDataService.createItem(this.studentData).subscribe((response: any) => {
      this.dataSource.data.push({ ...response })
      this.dataSource.data = this.dataSource.data.map(o => {
        return o;
      })
    });
  }

  updateStudent() {
    this.httpDataService.updateItem(this.studentData.id, this.studentData).subscribe((response: any) => {

      // Approach #1 to update datatable data on local itself without fetching new data from server
      this.dataSource.data = this.dataSource.data.map((o: Student) => {
        if (o.id === response.id) {
          o = response;
        }
        return o;
      })

      // Approach #2 to re-call getAllStudents() to fetch updated data
      // this.getAllStudents()

      this.cancelEdit()

    });
  }
...

We are calling HttpDataService methods to communicate with our json-server

After adding the above method the final students.component.ts file will look like this:

// students.component.ts
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { HttpDataService } from 'src/app/services/http-data.service';
import * as _ from 'lodash';
import { NgForm } from '@angular/forms';
import { Student } from 'src/app/models/student';

@Component({
  selector: 'app-students',
  templateUrl: './students.component.html',
  styleUrls: ['./students.component.css']
})
export class StudentsComponent implements OnInit {


  @ViewChild('studentForm', { static: false })
  studentForm: NgForm;

  studentData: Student;

  dataSource = new MatTableDataSource();
  displayedColumns: string[] = ['id', 'name', 'age', 'address', 'actions'];
  @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;

  isEditMode = false;

  constructor(private httpDataService: HttpDataService) {
    this.studentData = {} as Student;
  }

  ngOnInit(): void {

    // Initializing Datatable pagination
    this.dataSource.paginator = this.paginator;

    // Fetch All Students on Page load
    this.getAllStudents()
  }

  getAllStudents() {
    this.httpDataService.getList().subscribe((response: any) => {
      this.dataSource.data = response;
    });
  }

  editItem(element) {
    this.studentData = _.cloneDeep(element);
    this.isEditMode = true;
  }

  cancelEdit() {
    this.isEditMode = false;
    this.studentForm.resetForm();
  }

  deleteItem(id) {
    this.httpDataService.deleteItem(id).subscribe((response: any) => {

      // Approach #1 to update datatable data on local itself without fetching new data from server
      this.dataSource.data = this.dataSource.data.filter((o: Student) => {
        return o.id !== id ? o : false;
      })

      console.log(this.dataSource.data);

      // Approach #2 to re-call getAllStudents() to fetch updated data
      // this.getAllStudents()
    });
  }

  addStudent() {
    this.httpDataService.createItem(this.studentData).subscribe((response: any) => {
      this.dataSource.data.push({ ...response })
      this.dataSource.data = this.dataSource.data.map(o => {
        return o;
      })
    });
  }

  updateStudent() {
    this.httpDataService.updateItem(this.studentData.id, this.studentData).subscribe((response: any) => {

      // Approach #1 to update datatable data on local itself without fetching new data from server
      this.dataSource.data = this.dataSource.data.map((o: Student) => {
        if (o.id === response.id) {
          o = response;
        }
        return o;
      })

      // Approach #2 to re-call getAllStudents() to fetch updated data
      // this.getAllStudents()

      this.cancelEdit()

    });
  }

  onSubmit() {
    if (this.studentForm.form.valid) {
      if (this.isEditMode)
        this.updateStudent()
      else
        this.addStudent();
    } else {
      console.log('Enter valid data!');
    }
  }
}

That’s it now run you server by executing $ json-server --watch ./API/data.json then run Angular application in another terminal by executing $ ng serve --open

You can get source code of this tutorial in my GitHub repo here.

 

Conclusion: In this tutorial, we get to know how to use Http services to make server communication, use get, post, put and delete methods on data server. We use RxJs methods and operators to handle errors and network issues using retry() . Added Angular Material UI library to show data rows in a Datatable. In the Material Datatable, we performed CRUD operation using Inline approach

Leave a Comment

Your email address will not be published. Required fields are marked *