Angular 10 Material Datatable Inline HttpClient CRUD Operations using RestFull APIs
In this Angular tutorial, we’ll learn how to use HttpClient module in Angular application to make RESTFull API Ajax calls.
We’ll set up a new Angular 10 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
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
For this tutorial, we have installed v10.1.6
#Create a new Angular 10 project
Next, create a new Angular project by running following ng command in the terminal
$ ng new ang-datatable-app
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 ang-datatable-app
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 ~ang-datatable-app/API/data.json
The data.json file will work as RESTfull server. We will add some dummy employeess 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/employees
Following are the API URLs available on our server:
- GET /employees - fetch all employees
- GET /employees/: id - fetch a single employee detail by id
- POST /employees - create a new employee
- PUT /employees/: id - update a employee by id
- PATCH /employees/: id - partially update a employee by id
- DELETE /employees/:id - delete a employee 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 10 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 Employees 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 employees records and perform the various inline operation on employees 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 employees rows we will add Material Form as well, for that we will also import **FormsModule**
,**ReactiveFormsModule**
, **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';
@NgModule({
declarations: [
AppComponent
],
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 Employees data by running following command defining the type of values for employee item.
$ ng generate class models/Employee
then replace the following content in the newly created file “~/models/employee.ts”
export class Employee {
id: number;
userId: string;
jobTitleName: string;
firstName: string;
lastName: string;
preferredFullName: string;
employeeCode: string;
region: string
phoneNumber: string;
emailAddress: 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/employees';
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</span>
</h4>
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 emloyees data in our mock server which we created using **json-server**
.
Create an Employee
The new employee will be created using the **post()**
method
// Create a new item
createItem(item): Observable<Employee> {
return this.http
.post<Employee>(this.base_path, JSON.stringify(item), this.httpOptions)
.pipe(
retry(2),
catchError(this.handleError)
)
}
The **createItem()**
method is accepting **item**
attribute with employee details to add.
Retrieve Employee Details
To fetch single employee details we use **get()**
method with employee **id**
whose detail needs to be checked.
// Get single employee data by ID
getItem(id): Observable<Employee> {
return this.http
.get<Employee>(this.base_path + '/' + id)
.pipe(
retry(2),
catchError(this.handleError)
)
}
Retrieve All Employees
Similarly, we will make **get**
call to fetch all employees list
// Get employees data
getList(): Observable<Employee> {
return this.http
.get<Employee>(this.base_path)
.pipe(
retry(2),
catchError(this.handleError)
)
}
Update single employee
The **put()**
method will update single employee with id passed
// Update item by id
updateItem(id, item): Observable<Employee> {
return this.http
.put<Employee>(this.base_path + '/' + id, JSON.stringify(item), this.httpOptions)
.pipe(
retry(2),
catchError(this.handleError)
)
}
Delete a single employee
The **delete()**
HTTP method will delete a single record whose id is passed
// Delete item by id
deleteItem(id) {
return this.http
.delete<Employee>(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 { Employee } from '../models/employee';
import { Observable, throwError } from 'rxjs';
import { retry, catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class HttpDataService {
// API path
base_path = 'http://localhost:3000/employees';
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<Employee> {
return this.http
.post<Employee>(this.base_path, JSON.stringify(item), this.httpOptions)
.pipe(
retry(2),
catchError(this.handleError)
)
}
// Get single employee data by ID
getItem(id): Observable<Employee> {
return this.http
.get<Employee>(this.base_path + '/' + id)
.pipe(
retry(2),
catchError(this.handleError)
)
}
// Get employees data
getList(): Observable<Employee> {
return this.http
.get<Employee>(this.base_path)
.pipe(
retry(2),
catchError(this.handleError)
)
}
// Update item by id
updateItem(id, item): Observable<Employee> {
return this.http
.put<Employee>(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<Employee>(this.base_path + '/' + id, this.httpOptions)
.pipe(
retry(2),
catchError(this.handleError)
)
}
}
#Create new pages
To show employees data in a table, we will create a new employees component and update the app-routing.module.ts file to open /employees
page on application load.
Create the **EmployeesComponent**
by running below generate command:
$ ng generate component pages/employees
Setup the App Routing Module Now update the app-routing.module.tsfile with below code.
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { EmployeesComponent } from './pages/employees/employees.component';
const routes: Routes = [
{
path: '',
redirectTo: '/employees',
pathMatch: 'full'
},
{
path: 'employees',
component: EmployeesComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
#Using HttpDataService in Employees 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 employee’s page at ~src/app/pages/employees/employees.component.ts, we need to **impor**t
it and then add in component the contractor() method as shown below:
...
import { HttpDataService } from 'src/app/services/http-data.service';
@Component({
selector: 'app-employees',
templateUrl: './employees.component.html',
styleUrls: ['./employees.component.css']
})
export class EmployeesComponent implements OnInit {
constructor(private httpDataService: HttpDataService) { }
...
}
#Adding Angular Material Datatable
Next, we will add a Datatable with Employees 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 employees.component.html file with below code to build a Material datatable:
<!-- 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"> </td>
</ng-container>
<ng-container matColumnDef="userId">
<th mat-header-cell *matHeaderCellDef> User Id </th>
<td mat-cell *matCellDef="let element"> </td>
</ng-container>
<ng-container matColumnDef="jobTitleName">
<th mat-header-cell *matHeaderCellDef> jobTitle Name </th>
<td mat-cell *matCellDef="let element"> </td>
</ng-container>
<ng-container matColumnDef="firstName">
<th mat-header-cell *matHeaderCellDef> First Name </th>
<td mat-cell *matCellDef="let element"> </td>
</ng-container>
<ng-container matColumnDef="lastName">
<th mat-header-cell *matHeaderCellDef> Last Name </th>
<td mat-cell *matCellDef="let element"> </td>
</ng-container>
<ng-container matColumnDef="preferredFullName">
<th mat-header-cell *matHeaderCellDef> Preferred FullName </th>
<td mat-cell *matCellDef="let element"> </td>
</ng-container>
<ng-container matColumnDef="employeeCode">
<th mat-header-cell *matHeaderCellDef> Employee Code </th>
<td mat-cell *matCellDef="let element"> </td>
</ng-container>
<ng-container matColumnDef="region">
<th mat-header-cell *matHeaderCellDef> Region </th>
<td mat-cell *matCellDef="let element"> </td>
</ng-container>
<ng-container matColumnDef="phoneNumber">
<th mat-header-cell *matHeaderCellDef> Phone Number </th>
<td mat-cell *matCellDef="let element"> </td>
</ng-container>
<ng-container matColumnDef="emailAddress">
<th mat-header-cell *matHeaderCellDef> Email Address </th>
<td mat-cell *matCellDef="let element"> </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)="editItem(element)">Edit</a> |
<a href="javascript:void(0)" (click)="deleteItem(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': employeeData.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 **editEmployee()**
method and Delete with **deleteEmployee()**
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()" #employeeForm="ngForm">
<mat-form-field>
<input matInput placeholder="User Id" name="userId" required [(ngModel)]="employeeData.userId">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Job TitleName" name="jobTitleName" required [(ngModel)]="employeeData.jobTitleName">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="first Name" name="firstName" required [(ngModel)]="employeeData.firstName">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Last Name" name="lastName" required [(ngModel)]="employeeData.lastName">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Preferred FullName" name="preferredFullName" required [(ngModel)]="employeeData.preferredFullName">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Employee Code" name="employeeCode" required [(ngModel)]="employeeData.employeeCode">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Region" name="region" required [(ngModel)]="employeeData.region">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Phone Number" name="phoneNumber" required [(ngModel)]="employeeData.phoneNumber">
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Email Address" name="emailAddress" required [(ngModel)]="employeeData.emailAddress">
</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 employees.component.ts file with required methods.
First, initialize the Template driven form with the **NgForm**
@ViewChild('employeeForm', { static: false })
employeeForm: NgForm;
Import the Employee
class which we created and define a new variable employeeData
of type Employee
employeeData: Employee;
Then define the **dataSource**
and **displayedColumns**
with **MatPaginator**
class to build our Datatable
dataSource = new MatTableDataSource();
displayedColumns: string[] = ['id', 'userId', 'jobTitleName', 'firstName','lastName','preferredFullName','employeeCode','region', 'phoneNumber','emailAddress','actions'];
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
Then we will add methods to Add, Delete, Edit, Update and Get Employees list in the class file as shown below:
...
getAllEmployees() {
this.httpDataService.getList().subscribe((response: any) => {
this.dataSource.data = response;
});
}
editItem(element) {
this.employeeData = _.cloneDeep(element);
this.isEditMode = true;
}
cancelEdit() {
this.isEditMode = false;
this.employeeForm.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: Employee) => {
return o.id !== id ? o : false;
})
console.log(this.dataSource.data);
// Approach #2 to re-call getAllEmployees() to fetch updated data
// this.getAllEmployees()
});
}
addEmployee() {
this.httpDataService.createItem(this.employeeData).subscribe((response: any) => {
this.dataSource.data.push({ ...response })
this.dataSource.data = this.dataSource.data.map(o => {
return o;
})
});
}
updateEmployee() {
this.httpDataService.updateItem(this.employeeData.id, this.employeeData).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: Employee) => {
if (o.id === response.id) {
o = response;
}
return o;
})
// Approach #2 to re-call getAllEmployees() to fetch updated data
// this.getAllEmployees()
this.cancelEdit()
});
}
...
We are calling HttpDataService
methods to communicate with our json-server
After adding the above method the final employees.component.ts file will look like this:
// employees.component.ts
import { Component, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { HttpDataService } from 'src/app/services/http-data.service';
import * as _ from 'lodash';
import { Employee } from 'src/app/models/employee';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
@Component({
selector: 'app-employees',
templateUrl: './employees.component.html',
styleUrls: ['./employees.component.css']
})
export class EmployeesComponent implements OnInit {
@ViewChild('employeeForm', { static: false })
employeeForm: NgForm;
employeeData: Employee;
dataSource = new MatTableDataSource();
displayedColumns: string[] = ['id', 'userId', 'jobTitleName', 'firstName','lastName','preferredFullName','employeeCode','region', 'phoneNumber','emailAddress','actions'];
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
isEditMode = false;
constructor(private httpDataService: HttpDataService) {
this.employeeData = {} as Employee;
}
ngOnInit(): void {
// Initializing Datatable pagination
this.dataSource.paginator = this.paginator;
// Fetch All Employees on Page load
this.getAllEmployees();
}
getAllEmployees(){
this.httpDataService.getList().subscribe((response: any) => {
this.dataSource.data = response;
});
}
editItem(element) {
this.employeeData = _.cloneDeep(element);
this.isEditMode = true;
}
cancelEdit() {
this.isEditMode = false;
this.employeeForm.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: Employee) => {
return o.id !== id ? o : false;
})
console.log(this.dataSource.data);
// Approach #2 to re-call getAllEmployees() to fetch updated data
// this.getAllEmployees()
});
}
addEmployee() {
this.httpDataService.createItem(this.employeeData).subscribe((response: any) => {
this.dataSource.data.push({ ...response })
this.dataSource.data = this.dataSource.data.map(o => {
return o;
})
});
}
updateEmployee() {
this.httpDataService.updateItem(this.employeeData.id, this.employeeData).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: Employee) => {
if (o.id === response.id) {
o = response;
}
return o;
})
// Approach #2 to re-call getAllEmployees() to fetch updated data
// this.getAllEmployees()
this.cancelEdit()
});
}
onSubmit() {
if (this.employeeForm.form.valid) {
if (this.isEditMode)
this.updateEmployee()
else
this.addEmployee();
} 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