In this part of the tutorial you will fix some UI problems and you will learn some tricks to handle HTTP errors by using Promise
s and Observable
s
Promise
shareReplay
The saveHandler
method of UsersService
class now receives a NgForm
as argument.
However, it is preferable that the service has no references to the UI because if we update it in the future, we won't have to change the service as well.
So, open the service and update the current saveHandler
, from the following:
users.service.ts
saveHandler(f: NgForm) {
const user = f.value as User; // will be removed
this.http.post<User>(`${this.URL}/users/`, user)
.subscribe((dbUser) => {
this.users = [...this.users, dbUser];
f.reset({ gender: '' }); // will be removed
});
}
To the following one.
In fact the method now receives the form value instead of NgForm
and our service has no UI references anymore.
users.service.ts
saveHandler(user: User) {
this.http.post<User>(`${this.URL}/users/`, user)
.subscribe((dbUser) => {
this.users = [...this.users, dbUser];
});
}
The saveHandler
method won't really receive an object typed as User
but, in the next step, we'll pass the form.value
to it, that represents a "partial" version of the User
.
In fact it should be set to user: Partial<User>
instead but the type of form.value
is any
so it won't generate compiler errors.
Since I don't want to complicate the tutorial further we'll leave it at that.
However you'll get this error:
Why?
Your component still pass the NgForm
as paremeter to the service method so you need to update your component HTML template.
So, we have to update the current code in which we pass the NgForm instance
to the saveHandler
method:
(submit)="usersService.saveHandler(f)"
to the following one, in order to pass the form value
(the form content) instead of the form reference:
(submit)="usersService.saveHandler(f.value)"
app.component.ts
import { HttpClient } from '@angular/common/http';
import { NgForm } from '@angular/forms';
import { Component } from '@angular/core';
import { User } from './model/user';
import { UsersService } from './services/users.service';
@Component({
selector: 'app-root',
template: `
<div class="container">
<h1>Users</h1>
<form
class="card card-body mt-3"
#f="ngForm"
(submit)="usersService.saveHandler(f.value)"
[ngClass]="{
'male': f.value.gender === 'M',
'female': f.value.gender === 'F'
}"
>
<input
type="text"
[ngModel]
name="label"
placeholder="Add user name"
class="form-control"
required
#labelInput="ngModel"
[ngClass]="{'is-invalid': labelInput.invalid && f.dirty}"
>
<select
[ngModel]
name="gender"
class="form-control"
required
#genderInput="ngModel"
[ngClass]="{'is-invalid': genderInput.invalid && f.dirty}"
>
<option value="">Select option</option>
<option value="M">M</option>
<option value="F">F</option>
</select>
<button
class="btn"
[disabled]="f.invalid"
[ngClass]="{
'btn-success': f.valid,
'btn-danger': f.invalid
}"
>Save</button>
</form>
<hr>
<ul class="list-group">
<li
*ngFor="let u of usersService.users" class="list-group-item"
[ngClass]="{
'male': u.gender === 'M',
'female': u.gender === 'F'
}"
>
<i
class="fa fa-3x"
[ngClass]="{
'fa-mars': u.gender === 'M',
'fa-venus': u.gender === 'F'
}"
></i>
{{u.label}}
<i class="fa fa-trash fa-2x pull-right"
(click)="usersService.deleteHandler(u)"></i>
</li>
</ul>
</div>
`,
styles: [`
.male { background-color: #36caff; }
.female { background-color: pink; }
.card { transition: all 0.5s }
`]
})
export class AppComponent {
constructor(public usersService: UsersService) {
usersService.init();
}
}
The form is not cleaned anymore after adding a new user:
There are several solutions to solve this issue but since this is a beginner tutorial we'll find an easy way to fix it.
First we avoid to directly invoke the saveHandler
service method directly from the template:
❌ (submit)="usersService.saveHandler(f.value)"
and we invoke a method of AppComponent
class instead, passing again the whole NgForm
object:
✅ (submit)="saveHandler(f)"
Create a new saveHandler()
method in app.component.ts
and reset the form after invoking the service method:
app.component.ts
export class AppComponent {
constructor(public usersService: UsersService) {
usersService.init();
}
// NEW
saveHandler(form: NgForm) {
// invoke the saveHandler method of the service
this.usersService.saveHandler(form.value);
// NEW: Reset the form
form.reset({ gender: '' })
}
}
Be sure to import NgForm
and User
in AppComponent
:
import { NgForm } from '@angular/forms';
import { User } from './model/user';
// ...
Now your app should work again but it's not the great solution.
In fact the form is cleaned even when your REST API returns some errors when adding a new user, and it's not the right behavior.
The form should be cleaned only when server returns a valid response.
Promise
Our goal is cleaning the form after the HTTP requests are successfully done but only the service currently know when it happens.
So, how can we notify the component that we need to clean the form at a certain moment?
We can use a JavaScript concept knows as Promise
.
Open UsersService
, wrap all the saveHandler
content into a Promise
and return it:
services/users.service.ts
saveHandler(user: User): Promise<void> {
// return a new promise
return new Promise((resolve, reject) => {
this.http.post<User>(`${this.URL}/users/`, user)
.subscribe({
// success
next: (newUser) => {
this.users = [...this.users, newUser];
// nofity the promise has successfully resolved
resolve();
},
// fail
error: () => {
// notify the operation has failed and send an error message
reject('server side error!')
}
});
});
}
Since this time we also want to handle HTTP errors, we now pass an object (aka "Observer") to the subscribe
function of the HttpClient
request.
This object we'll invoke the function defined in the next
property if it is resolved successfully, otherwise it invoke the error
function.
Thanks to this approach we can now invoke the saveHandler
method in the class component and wait for the result (success or failed)
this.usersService.saveHandler(form.value)
.then( () => /* do something */ ) // success
.catch(() => /* do something */); // error
We can now open AppComponent
, handle the promise in the saveHandler()
method and finally reset the form when the HTTP request are successfully completed:
app.component.ts
export class AppComponent {
// NEW
error: string | null = null;
constructor(public usersService: UsersService) {
usersService.init();
}
saveHandler(form: NgForm) {
// NEW
this.usersService.saveHandler(form.value as User)
.then(() => {
// reset form
form.reset({ gender: '' });
// clean previous errors
this.error = null;
})
.catch((err) => {
// save error message to a new `error` class property
this.error = err;
})
}
}
Now we can use the error
property to display the error message in HTML template:
<div class="container">
<div class="alert alert-danger" *ngIf="error">Server side error</div>
<!-- missing part -->
We may also display the error message contained in the error
property instead
How to check if the "error" is shown?
Now add a new user: the operation fails and the error should be displayed just above the form:
No throttling
There are better solutions to solve this issue but, at least, now it works fine :)
app.component.ts
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';
import { User } from './model/user';
import { UsersService } from './services/users.service';
@Component({
selector: 'app-root',
template: `
<div class="container">
<h1>Users</h1>
<div class="alert alert-danger" *ngIf="error">Server side error</div>
<form
class="card card-body mt-3"
#f="ngForm"
(submit)="saveHandler(f)"
[ngClass]="{
'male': f.value.gender === 'M',
'female': f.value.gender === 'F'
}"
>
<input
type="text"
[ngModel]
name="label"
placeholder="Add user name"
class="form-control"
required
#labelInput="ngModel"
[ngClass]="{'is-invalid': labelInput.invalid && f.dirty}"
>
<select
[ngModel]
name="gender"
class="form-control"
required
#genderInput="ngModel"
[ngClass]="{'is-invalid': genderInput.invalid && f.dirty}"
>
<option value="">Select option</option>
<option value="M">M</option>
<option value="F">F</option>
</select>
<button
class="btn"
[disabled]="f.invalid"
[ngClass]="{
'btn-success': f.valid,
'btn-danger': f.invalid
}"
>Save</button>
</form>
<hr>
<ul class="list-group">
<li
*ngFor="let u of usersService.users" class="list-group-item"
[ngClass]="{
'male': u.gender === 'M',
'female': u.gender === 'F'
}"
>
<i
class="fa fa-3x"
[ngClass]="{
'fa-mars': u.gender === 'M',
'fa-venus': u.gender === 'F'
}"
></i>
{{u.label}}
<i class="fa fa-trash fa-2x pull-right"
(click)="usersService.deleteHandler(u)"></i>
</li>
</ul>
</div>
`,
styles: [`
.male { background-color: #36caff; }
.female { background-color: pink; }
.card { transition: all 0.5s }
`]
})
export class AppComponent {
error: string | null = null;
constructor(public usersService: UsersService) {
usersService.init();
}
saveHandler(form: NgForm) {
this.usersService.saveHandler(form.value as User)
.then(() => {
form.reset({ gender: '' });
this.error = null;
})
.catch((err) => this.error = err);
}
}
/services/users.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { User } from '../model/user';
@Injectable({ providedIn: 'root'})
export class UsersService {
users: User[] = [];
URL = 'http://localhost:3000';
constructor(private http: HttpClient) { }
init() {
this.http.get<User[]>(this.URL + '/users')
.subscribe(res => {
this.users = res;
});
}
deleteHandler(userToRemove: User) {
this.http.delete(`${this.URL}/users/${userToRemove.id}`)
.subscribe(() => {
this.users = this.users.filter(u => u.id !== userToRemove.id);
});
}
saveHandler(user: User): Promise<void> {
return new Promise((resolve, reject) => {
this.http.post<User>(`${this.URL}/users/`, user)
.subscribe({
next: (newUser) => {
this.users = [...this.users, newUser];
resolve();
},
error: () => reject('server side error!')
});
});
}
}
shareReplay
Another interesting alternative way to fix the same problem is by using RxJS only.
Don't worry if you don't completely understand the next example. It's tricky if you've never used reactive programming and RxJS.
We can update saveHandler
in users.service.ts
just as shown below:
/services/users.service.ts
saveHandler(user: User) {
// Save the HttpClient request
const usersReq$ = this.http.post<User>(`${this.URL}/users/`, user)
.pipe(shareReplay(1));
// subscribe the HttpClient request and add the new user to the array
// when the HTTP request is successfully completed
usersReq$.subscribe(newUser => {
this.users = [...this.users, newUser];
});
// return the Observable
return usersReq$;
}
Update app.component
:
app-component.ts
saveHandler(form: NgForm) {
this.usersService.saveHandler(form.value as User)
.subscribe({
next: () => {
form.reset({ gender: '' });
this.error = null;
},
error: () => {
this.error = 'Some errors here! 😅'
}
})
}
We have used the shareReplay
RxJS operator because we want to subscribe the users$
observable in both, the service and the component.
Since all the observable are COLD by default (it means that every subscribe generates a new execution of the observable),
the REST API will be invoked twice if we subcribe the HttpClient
in the service and in the component.
So we use the shareReplay
operator in order to share and replay the emission of the value generated by the HttpClient
avoiding the double request.
Don't worry if this process is not clear. RxJS is not so easy to understand
Resources:
The result is the same as before:
That's all. You have completed this tutorial but you can try to solve the next challenges to prove yourself or learn something new.