How to take profit of Ionic and Angular features for App Development: The Service Layer
Introduction
The service layer serves as an intermediary between the back-end and the front-end. It communicates through HTTP requests to send and receive data.
Apart from that functionality, these services can also be used as a simple way to control the state of the application at feature level. This pattern is called Subject with a Service which we will see in the next article.
To demonstrate how the service layer is implemented, today we will create a service for our application that will be used to get the questions of a quiz and to answer them.
Previous steps
Before we start creating our services, we need to make a small modification to the auth.service.ts that we created in the Authentication article.
We will create a service that will be used as a helper to store headers and other usual bits and pieces that have to do with the HTTP protocol.
ionic g service utils/http-common --skipTests
import { HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class HttpCommonService {
private headers: HttpHeaders;
constructor() {}
setHeaders(headers: HttpHeaders) {
this.headers = headers;
}
getHeaders(): HttpHeaders {
return this.headers;
}
}
After that, we will proceed to modify the AuthService so that the headers are saved once the user is authenticated.
private setAuth(token: string) {
this.token$.next(token);
Storage.set({
key: AUTH_STORAGE_KEY,
value: token,
});
this.httpCommon.setHeaders(
new HttpHeaders({
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
})
);
this.isAuthenticated$.next(true);
}
private clearAuth() {
this.token$.next('');
Storage.remove({
key: AUTH_STORAGE_KEY,
});
this.httpCommon.setHeaders(undefined);
this.isAuthenticated$.next(false);
}
quiz.service.ts
First, we will create an Angular service dedicated to quizzes as a layer between the requests that will be made on the server, and our application.
ionic g service services/quiz/quiz
Before we start adding methods to the service, we will create the necessary interfaces to manage the quizzes in interfaces/quiz.ts
.
export interface Quiz {
id: number;
title: string;
mark?: number;
body?: string;
questions?: QuizQuestion[];
}
export interface QuizResults {
answers: QuizAnswer[];
}
interface QuizQuestion {
id: number;
label: string;
items: QuizQuestionItem[];
}
interface QuizQuestionItem {
id: number;
label: string;
value: number;
}
interface QuizAnswer {
id: number;
value: number | number[];
}
Once created, we will add the methods needed to cover the quiz needs.
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { catchError, mapTo } from 'rxjs/operators';
import { Quiz, QuizResults } from 'src/app/interfaces/quiz';
import { HttpCommonService } from 'src/app/utils/http-common.service';
import { environment } from 'src/environments/environment';
const GET_ALL_URL = '';
const GET_BY_ID_URL = '';
const SEND_RESULTS_URL = '';
@Injectable({
providedIn: 'root',
})
export class QuizService {
constructor(private http: HttpClient, private httpCommon: HttpCommonService) {}
getAll(): Observable<Quiz[]> {
return this.http.get<Quiz[]>(`${environment.apiUrl}/${GET_ALL_URL}`).pipe(
catchError((err) => {
console.error('quiz -> getAll', err);
return of([]);
})
);
}
get(id: string): Observable<Quiz> {
return this.http
.get<Quiz>(`${environment.apiUrl}/${GET_BY_ID_URL}`, {
headers: this.httpCommon.getHeaders(),
params: {
id,
},
})
.pipe(
catchError((err) => {
console.error('quiz -> get', err);
return of(undefined);
})
);
}
sendResults(id: number, results: QuizResults): Observable<boolean> {
return this.http
.post<boolean>(
`${environment.apiUrl}/${SEND_RESULTS_URL}`,
{
id,
results,
},
{
headers: this.httpCommon.getHeaders(),
}
)
.pipe(
mapTo(true),
catchError((err) => {
console.error('quiz -> sendResults', err);
return of(false);
})
);
}
}
Usage
The services will be used directly in the controller of the pages of our application. A good practice for using Observables is to create a variable ending with the character $
and assign it in the ngOnInit
method.
Let's test our service on the Home page.
To start with, we will use the getAll method to get a list of quizzes and display it on the screen.
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
import { Quiz } from 'src/app/interfaces/quiz';
import { QuizService } from 'src/app/services/quiz/quiz.service';
@Component({
selector: 'app-home',
templateUrl: './home.page.html',
styleUrls: ['./home.page.scss'],
})
export class HomePage implements OnInit {
quizzes$: Observable<Quiz[]>;
constructor(private quizService: QuizService) {}
ngOnInit() {
this.quizzes$ = this.quizService.getAll();
}
}
To subscribe to an Observable in the HTML we will use Angular's AsyncPipe, which unsubscribes automatically and we do not need to manage it.
<ion-header>
<ion-toolbar>
<ion-title>Quiz List</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list *ngIf="quizzes$ | async as quizzes">
<ion-item
*ngFor="let quiz of quizzes"
[routerLink]="['/quiz', quiz.id]"
>
<ion-label>{{ quiz.title }}</ion-label>
<ion-badge *ngIf="quiz.mark; else toDo" slot="end" color="primary">
{{ quiz.mark }}
</ion-badge>
<ng-template #toDo>
<ion-badge slot="end" color="warning"> TO DO </ion-badge>
</ng-template>
</ion-item>
</ion-list>
</ion-content>
To use the rest of the methods in our service, we have created a Quiz page to take the quizzes and send the results.
To pass a parameter as a variable path, you have to modify our path in
app/app-routing.module.ts
import { NgModule } from "@angular/core";
import { PreloadAllModules, RouterModule, Routes } from "@angular/router";
const routes: Routes = [
{
path: "",
pathMatch: "full",
redirectTo: "home",
},
{
path: "login",
loadChildren: () =>
import("./pages/login/login.module").then((m) => m.LoginPageModule),
},
{
path: "register",
loadChildren: () =>
import("./pages/register/register.module").then(
(m) => m.RegisterPageModule
),
},
{
path: "home",
loadChildren: () =>
import("./pages/home/home.module").then((m) => m.HomePageModule),
},
{
path: "quiz/:id",
loadChildren: () =>
import("./pages/quiz/quiz.module").then((m) => m.QuizPageModule),
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
In the Quiz page, we get the URL parameter using ActivatedRoute
.
import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { Quiz } from 'src/app/interfaces/quiz';
import { QuizService } from 'src/app/services/quiz/quiz.service';
@Component({
selector: 'app-quiz',
templateUrl: './quiz.page.html',
styleUrls: ['./quiz.page.scss'],
})
export class QuizPage implements OnInit {
id: string;
quiz$: Observable<Quiz>;
constructor(private quizService: QuizService, private activatedRoute: ActivatedRoute) {}
ngOnInit() {
this.id = this.activatedRoute.snapshot.params.id;
this.quiz$ = this.quizService.get(this.id);
}
submit() {
const results: any = {};
this.quizService.sendResults(+this.id, results).subscribe((success) => {
if (success) {
// success feedback
} else {
// error sending results
}
});
}
}
Finally, we render the HTML with the results obtained from the service.
<ng-container *ngIf="quiz$ | async as quiz">
<ion-header>
<ion-toolbar>
<ion-title>{{ quiz.title }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-text> {{ quiz.body }} </ion-text>
<ng-container *ngFor="let question of quiz.questions">
<ion-label>{{ question.label }}</ion-label>
<reorder-control [items]="question.items"></reorder-control>
</ng-container>
<ion-button (click)="submit()">Send</ion-button>
</ion-content>
</ng-container>
For the Promise-lovers
If you are a Promise-lover and you are not yet familiar with the use of Observables, you can always use this little trick :)
export class HomePage implements OnInit {
quizes: Quiz[];
constructor(private quizService: QuizService) {}
async ngOnInit() {
this.quizes = await this.quizService.getAll().toPromise();
}
}
Conclusion
This is as far as we have got in today's article. Services are an essential part of all applications that require a back-end. Therefore, it is very important to have them well-organised and with clean code.
In the next article we will deal with the Subject with a Service pattern to be able to control the state of our application without the need of extra libraries (Redux, NgRx, etc).