One does not simply develop an app without hitting the server. In this article we will explore how Angular handles the HTTP communication.
Angular has its own way of handling HTTP communication. As a framework itself Angular has a HttpClient that takes care of the HTTP needs. Again, we don't have to hunt down any NPM package to perform the API calls. We will resume our journey of Angular learning with the same Notesy App; we will continue from where we left in the previous article Angular Journey: Part 3 - Angular Routes.
We will head in this direction:
Steps to setup JSON server are easy-peasy. First install the package globally:
npm install -g json-server
Add this command to the package.json file:
"scripts": {
"server": "json-server data.json --delay 2000"
}
As you can see in the command, we are referring to a data.json file. This file will hold the data for us, to which the json-sever will do the transaction (CRUD). The data.json file will be acting as a backend database in this case.
Let us create the data.json file for the same:
{
"notes": [
{
"id": "44b0102d-bd91-4d12-b218-141ec217a6ac",
"content": "Note 1"
},
{
"id": "7a7d50cc-fe43-47df-931c-40e197a38251",
"content": "Note 2"
}
]
}
Here, we have defined an array of notes. Each note object has id and name. For starter we are going with some initial data. Also, notice we are using a GUID for id.
We need to run the below command in the terminal to run our server, make sure you run this command before our "npm start" otherwise you will get API failure errors:
npm run server
Once this executed your API is up and running on the default port 3000 and localhost. You will get default path to your endpoint as notes and get bunch of HTTP method like GET, POST, PUT, DELETE to perform a CRUD:
GET http://localhost:3000/notes/
GET http://localhost:3000/notes/{id}
POST http://localhost:3000/notes/
PUT http://localhost:3000/notes/{id}
DELETE http://localhost:3000/notes/{id}
For, GET, PUT and DELETE we must need to pass the id. For POST and PUT we need to pass the note object as part of the request body.
Just a refresher, we already have a Note Service which holds the static notes data while the app runs on the local host. We will use the same Note Service to have the HTTP methods for the Notesy App. Currently, the Note Service looks like this:
import { Injectable } from '@angular/core';
import { notes as notesData } from '../data';
@Injectable({
providedIn: 'root'
})
export class NoteService {
notes = notesData;
constructor() { }
}
Before modifying the Note Service lets add the API URL which points to the JSON Server API path in the environment file. Add this API URL to both environment files:
export const environment = {
apiUrl: 'http://localhost:3000/',
};
Add the necessary imports to the Note Service:
import { Note } from '../models/note.model';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
@Injectable({...})
export class NoteService {...}
Here, we have added the HttpClient along with the model and environment references.
Add the HttpClient dependecy and properties to the class:
@Injectable({...})
export class NoteService {
notes: Note[] = [];
apiUrl = environment.apiUrl + 'notes';
constructor(private http: HttpClient) {}
}
Add the GET, POST, and DELETE method for the Notes API:
@Injectable({...})
export class NoteService {
getNotes() {
return this.http.get<Note[]>(this.apiUrl);
}
addNote(note: Note) {
return this.http.post(this.apiUrl, note);
}
deleteNoteById(id: string) {
return this.http.delete(`${this.apiUrl}/${id}`);
}
}
Here, we can see that the HttpClient offers bunch of methods for HTTP communication.
The entire Note Service class will look like this:
import { Injectable } from '@angular/core';
import { Note } from '../models/note.model';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class NoteService {
notes: Note[] = [];
apiUrl = environment.apiUrl + 'notes';
constructor(private http: HttpClient) {}
getNotes() {
return this.http.get<Note[]>(this.apiUrl);
}
addNote(note: Note) {
return this.http.post(this.apiUrl, note);
}
deleteNoteById(id: string) {
return this.http.delete(`${this.apiUrl}/${id}`);
}
}
Let us now consume the getNotes metod in the list component where we are loading the initial notes data. Previously, since the data was static it directly obtained when the app runs on localhost. But now we have to manually trigger a GET call when the list component is initialized. For this we will use the ngOnInit life cycle method of list component:
@Component({...})
export class ListComponent implements OnInit {
constructor(private noteService: NoteService) {}
get notes() {
return this.noteService.notes;
}
set notes(value: Note[]) {
this.noteService.notes = value;
}
ngOnInit() {
this.noteService.getNotes().subscribe({
next: (notes) => (this.notes = notes),
error: (error) => alert(`Error Occured: ${error.message}`),
complete: () => this.loaderService.hide(),
});
}
}
Here, we are calling the getNotes method of Note Service. Notice that the getNotes method is returning the Observable as a response which further we need to subscribe to know then the method call provided the next updated value. Once we obtained the data, we are using the next property method to assign the received notes to the note's property of the list component. We also have error property method defined to handle the error scenario. Just for ease of understanding for beginners, the subscribe method is very much similar to the Promise method.
NOTE: There is a RxJS way of handling the subscription in a declarative way. This will be seen in upcoming article as the currently shown code is a subscription based which has it own challenges when we have to manage multiple calls etc.
Focusing on the form component now where we are doing the add operation. Will simply make the API call to the addNote method once we are done creating the new note object:
@Component({...})
export class FormComponent {
onAdd(event: Event) {
...
const newNote: Note = {
id: uuid(),
content: this.content,
};
this.noteService.addNote(newNote).subscribe({
next: () => this.noteService.notes.push(newNote),
error: (error) => alert(`Error Occured: ${error.message}`),
});
}
}
Here, we are calling the addNote method and once the method provides the next value from the API call response we will push the received value to the notes array of Note Service. Notice that we are using the same notes array of Note Service evert where in the entire app to maintain the same copy of the data across app.
Bringing back our attention to the remove operation which is present in the list component. Let's invoke the deleteNotesById method to perform actual deletion of the note on remove:
@Component({...})
export class ListComponent implements OnInit {
onRemove(note: Note) {
this.noteService.deleteNoteById(note.id).subscribe({
next: () => (this.notes = this.notes.filter((n) => n.id !== note.id)),
error: (error) => alert(`Error Occured: ${error.message}`),
});
}
}
Here, we are performing the removal by note id and once the operation completed will then update the original notes array of Note Service with updated value.
Currently, we have already taken care of error handling using the error property method of the subscription. For now, we are using the alert method of JavaScript to show some alert to the user. We can improvise to use the Bootstrap based modal popups or Popovers to show the detailed errors.
Most important aspect while dealing any async operation is to show proper indication to the end user whenever current operation is carrying out and when it's get completed. These indications are crucial, otherwise user won't come to know when the operation completed or begin. To handle this will first add one loader component along with its styling and then will create a loader service which will be used across components wherever we are making any API calls.
Create a loader service:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class LoaderService {
private _loader = false;
get loader() {
return this._loader;
}
show() {
this._loader = true;
}
hide() {
this._loader = false;
}
}
Here, we have created a bare minimum loader service having a private boolean property called loader. This will indicate if the loader is shown or hidden.
Create a loader component:
import { Component } from '@angular/core';
import { LoaderService } from '../../services/loader.service';
@Component({
selector: 'app-loader',
templateUrl: './loader.component.html',
styleUrl: './loader.component.scss',
})
export class LoaderComponent {
constructor(private loaderService: LoaderService) {}
get loader() {
return this.loaderService.loader;
}
}
Add the loader component template logic:
<h1 class="loader" *ngIf="loader">
<i class="bi bi-snow2 text-success"></i>
</h1>
Add the loader component style:
.loader {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.5);
z-index: 5;
}
.loader i {
animation: spin infinite 1.5s linear;
font-size: 4rem;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
All set, now let's start consuming the Loader Service show and hide method in the list and form component's getNotes, addNote, and deleteNoteById methods.
Add the show-hide loader methods to the list component for getNotes and deleteNoteById methods:
@Component({...})
export class ListComponent implements OnInit {
...
ngOnInit() {
this.loaderService.show();
this.noteService.getNotes().subscribe({
next: (notes) => (this.notes = notes),
error: (error) => alert(`Error Occured: ${error.message}`),
complete: () => this.loaderService.hide(),
});
}
onRemove(note: Note) {
this.loaderService.show();
this.noteService.deleteNoteById(note.id).subscribe({
next: () => (this.notes = this.notes.filter((n) => n.id !== note.id)),
error: (error) => alert(`Error Occured: ${error.message}`),
complete: () => this.loaderService.hide(),
});
}
}
Here, we are using the complete property method of the subscription for hiding the loader and we are calling the show method before making the API calls. The complete will run in both success and failure cases.
Add the show-hide loader methods to form component for addNote method:
@Component({...})
export class FormComponent {
onAdd(event: Event) {
...
this.loaderService.show();
this.noteService.addNote(newNote).subscribe({
next: () => this.noteService.notes.push(newNote),
error: (error) => alert(`Error Occured: ${error.message}`),
complete: () => {
this.loaderService.hide();
this.content = '';
},
});
}
}
Here, we are also setting the content to empty for better user experience.
Feel free to comment down if you are following along and finding errors and issues will be happy to help.
December 31, 2020
October 19, 2020
March 02, 2022