Angular 7 Tutorial Part 5 - Registration Form & User Service
Other parts available in Angular 7 tutorial series:
- Part 1 (Windows) - Setup Development Machine on Windows
- Part 1 (Mac) - Setup Development Machine on Mac
- Part 2 - Create Base Project Structure & Webpack Config
- Part 3 - Add Routing & Multiple Pages
- Part 4 - Login Form, Authentication Service & Route Guard
- Part 6 - Home Page & Alert Component
- Part 7 (Optional) - Migrating to an Angular CLI Project
Angular 7 Tutorial Part 5
In part 5 of this Angular 7 tutorial series we're going to add a registration form and user service for registering new users.
The complete source code for this part of the tutorial is available on github at https://github.com/cornflourblue/angular-7-tutorial in the part-5
folder. If you haven't completed Part 4 (Login Form, Authentication Service & Route Guard) but want to follow the steps in this part of the tutorial series you can start with the code in the part-4
folder of the github repo.
Steps:
- Add Register Route to Fake Backend
- Create User Service
- Add Form Logic to Register Component
- Add Form HTML to Register Component Template
- Add Success Alert To Login Component
- Start Angular 7 Application!
Add Register Route to Fake Backend
To simulate the behaviour of a real api with a database, the users
array will be saved in browser local storage and initialised from local storage when the app starts, so registered users will persist when the browser is refreshed or closed. The default hardcoded user has been removed because the user will now register before logging in to the application.
The register route (/users/register
) has been added to the handleRoute()
function, requests to the register route are handled by the new register()
function.
The register()
function:
- checks if the username is already taken and returns an error if it is.
- calculates the next
user.id
by adding 1 to the current highest user id (Math.max(...users.map(x => x.id)) + 1
) or defaults to 1 for the first user. - pushes the new user to the
users
array and saves the updated array in local storage. - returns an empty
ok
response to indicate that registration was successful.
Update Fake Backend
This is how the fake-backend.ts
file should look after the changes, lines 6-7
have been updated and the new lines are 25-26
and 48-60
.
import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';
// array in local storage for registered users
let users = JSON.parse(localStorage.getItem('users')) || [];
@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const { url, method, headers, body } = request;
// wrap in delayed observable to simulate server api call
return of(null)
.pipe(mergeMap(handleRoute))
.pipe(materialize()) // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648)
.pipe(delay(500))
.pipe(dematerialize());
function handleRoute() {
switch (true) {
case url.endsWith('/users/authenticate') && method === 'POST':
return authenticate();
case url.endsWith('/users/register') && method === 'POST':
return register();
default:
// pass through any requests not handled above
return next.handle(request);
}
}
// route functions
function authenticate() {
const { username, password } = body;
const user = users.find(x => x.username === username && x.password === password);
if (!user) return error('Username or password is incorrect');
return ok({
id: user.id,
username: user.username,
firstName: user.firstName,
lastName: user.lastName,
token: 'fake-jwt-token'
})
}
function register() {
const user = body
if (users.find(x => x.username === user.username)) {
return error('Username "' + user.username + '" is already taken')
}
user.id = users.length ? Math.max(...users.map(x => x.id)) + 1 : 1;
users.push(user);
localStorage.setItem('users', JSON.stringify(users));
return ok();
}
// helper functions
function ok(body?) {
return of(new HttpResponse({ status: 200, body }))
}
function error(message) {
return throwError({ error: { message } });
}
}
}
export const fakeBackendProvider = {
// use fake backend in place of Http service for backend-less development
provide: HTTP_INTERCEPTORS,
useClass: FakeBackendInterceptor,
multi: true
};
Create User Service
The user service handles all HTTP communication with the backend for CRUD (create, read, update, delete) operations on user data.
Each of the methods sends an HTTP request to the backend api and returns the response, initially we'll only be using the register()
method, the getAll()
and delete()
methods will be used in the next part of the tutorial series.
The register()
method accepts a user
object parameter containing the user details from the registration form, it sends a POST
request to the register route on the backend (`${config.apiUrl}/users/register`
), passing the user
object in the request body.
Create User Service
Create a file named user.service.ts
in the _services
folder and add the following TypeScript code to it:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) { }
getAll() {
return this.http.get<any[]>(`${config.apiUrl}/users`);
}
register(user) {
return this.http.post(`${config.apiUrl}/users/register`, user);
}
delete(id) {
return this.http.delete(`${config.apiUrl}/users/${id}`);
}
}
Add User Service to Services Barrel File
Open the services barrel file (/src/app/_services/index.ts
) and add the line export * from './user.service';
, this enables the user service to be imported using only the folder path (e.g. import { UserService } from './_services'
).
This is how the services barrel file should look after the update:
export * from './authentication.service';
export * from './user.service';
Add Form Logic to Register Component
The register component contains all of the logic for validating the register form and handling form submission.
The constructor()
method:
- specifies the dependencies that are required by the component as parameters, these are automatically injected by the Angular Dependency Injection (DI) system when the component is created.
- checks if the user is already logged in by checking
authenticationService.currentUserValue
and redirects to the home page if they are.
The ngOnInit()
method:
- is an Angular lifecycle hook that runs once after the component is created. For more info on Angular lifecycle hooks see https://angular.io/guide/lifecycle-hooks.
- creates a new
FormGroup
by callingthis.formBuilder.group()
and assigns it to thethis.registerForm
property. The parameters passed to theFormBuilder
tell it to create four form controls -firstName
,lastName
,username
andpassword
, the form controls are all initialised with empty strings (''
) as values and set to required with theValidators.required
Angular validator. The password field is also required to have at least 6 characters with the use of the minLength validator (Validators.minLength(6)
).
The f()
getter is a convenience property to enable shorthand access to the register form controls via this.f
in the login component and f
in the login component template that we'll create in the next step.
The onSubmit()
method:
- sets the
this.submitted
property to true to indicate that an attempt has been made to submit the form, this property is used in the register component template to display validation errors only after the first submission has been attempted. - checks if the form is valid by checking
this.registerForm.invalid
and prevents submission if it is invalid. - sets the
this.loading
property to true before submitting the registration details via the user service, this property is used in the register component template to display a loading spinner and disable the register button. - registers the user by calling the
this.userService.register()
method and passing it the form data (this.registerForm.value
). The user service returns anObservable
that we.subscribe()
to for the results of the registration. On success the user is redirected to the/login
route by callingthis.router.navigate()
, passingregistered=true
as a query parameter so the login page can display a success message. On fail the error message is assigned to thethis.error
property to be displayed by the template and thethis.loading
property is reset back to false.
The call to.pipe(first())
unsubscribes from the observable immediately after the first value is emitted.
Update Register Component
Open the register.component.ts
file and add the following TypeScript code to it:
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';
import { UserService, AuthenticationService } from '../_services';
@Component({ templateUrl: 'register.component.html' })
export class RegisterComponent implements OnInit {
registerForm: FormGroup;
loading = false;
submitted = false;
error: string;
constructor(
private formBuilder: FormBuilder,
private router: Router,
private authenticationService: AuthenticationService,
private userService: UserService
) {
// redirect to home if already logged in
if (this.authenticationService.currentUserValue) {
this.router.navigate(['/']);
}
}
ngOnInit() {
this.registerForm = this.formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
username: ['', Validators.required],
password: ['', [Validators.required, Validators.minLength(6)]]
});
}
// convenience getter for easy access to form fields
get f() { return this.registerForm.controls; }
onSubmit() {
this.submitted = true;
// stop here if form is invalid
if (this.registerForm.invalid) {
return;
}
this.loading = true;
this.userService.register(this.registerForm.value)
.pipe(first())
.subscribe(
data => {
this.router.navigate(['/login'], { queryParams: { registered: true }});
},
error => {
this.error = error;
this.loading = false;
});
}
}
Add Form HTML to Register Component Template
The register component template contains the HTML and Angular template syntax for displaying the form in the browser and binding the form to the properties and methods in the register component above.
The [formGroup]="registerForm"
attribute directive binds the form to the registerForm
property of the register component. When the form is submitted in the browser the [formGroup]
directive emits an ngSubmit
event that triggers the onSubmit()
method of the register component. The (ngSubmit)="onSubmit()"
event binding attribute binds the ngSubmit
event to the onSubmit()
method.
Each form input has a formControlName
attribute which syncs the form input with the FormControl
of the same name in the registerForm
.
The [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }"
attribute directive adds the is-invalid
CSS class to firstName input if the form has been submitted and the firstName is invalid (contains errors) based on the validation rules defined in the register component. The same [ngClass]
directive is used on all of the inputs. The is-invalid
class is part of Bootstrap 4, it makes the border of the input red to indicate it is invalid.
The div with CSS class invalid-feedback
following each input is for displaying validation error messages, the messages appear as red text which are styled by Bootstrap 4. The *ngIf="submitted && f.firstName.errors"
directive renders the firstName validation messages if the form has been submitted and the firstName is invalid, the same *ngIf
directive is used for all input validation errors. Within the invalid-feedback
div is a child div for each specific error message, the *ngIf="f.firstName.errors.required"
renders the message First Name is required
if the firstName input is empty. The Validators.required
validator is attached to the firstName control in the register component above and has a value of true
if the input is empty.
The register button is used to submit the form, the [disabled]="loading"
attribute directive disables the button when the loading
property of the register component is true
, a spinner is also displayed in the button while loading with the use of the *ngIf="loading"
directive. The spinner styling and animation is part of Bootstrap 4.
Lastly the cancel link uses the routerLink="/login"
directive to link back to the /login
route. The routerLink
directive uses Angular to navigate between routes using partial page updates. Don't use the standard HTML href
attribute because it results in a full application reload each time you click a link.
Update Register Component Template
Open the register.component.html
file and add the following HTML code to it:
<div *ngIf="error" class="alert alert-danger">{{error}}</div>
<h2>Register</h2>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="firstName">First Name</label>
<input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }" />
<div *ngIf="submitted && f.firstName.errors" class="invalid-feedback">
<div *ngIf="f.firstName.errors.required">First Name is required</div>
</div>
</div>
<div class="form-group">
<label for="lastName">Last Name</label>
<input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.lastName.errors }" />
<div *ngIf="submitted && f.lastName.errors" class="invalid-feedback">
<div *ngIf="f.lastName.errors.required">Last Name is required</div>
</div>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
<div *ngIf="submitted && f.username.errors" class="invalid-feedback">
<div *ngIf="f.username.errors.required">Username is required</div>
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
<div *ngIf="submitted && f.password.errors" class="invalid-feedback">
<div *ngIf="f.password.errors.required">Password is required</div>
<div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
</div>
</div>
<div class="form-group">
<button [disabled]="loading" class="btn btn-primary">
<span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
Register
</button>
<a routerLink="/login" class="btn btn-link">Cancel</a>
</div>
</form>
Add Success Alert To Login Component
On successful registration the user is redirected back to the login route and the alert message 'Registration successful' is displayed.
The success alert is implemented in the login component by checking if the query parameter registered=true
exists in the url, the query parameter is added by the register component when it redirects to the login route after successful registration.
The new code in the ngOnInit()
method checks if the registered
property exists on the this.route.snapshot.queryParams
object, if it does the success message 'Registration successful'
is assigned to the this.success
property to be displayed in the register component template.
The new code in the onSubmit()
method sets the this.success
and this.error
properties both to null
to reset the UI when the login form is submitted and prevent both messages from being displayed at the same time.
Update Login Component
This is how the login component should look after adding the success alert logic, the new lines are 15
, 38-41
and 50-52
.
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';
import { AuthenticationService } from '../_services'
@Component({ templateUrl: 'login.component.html' })
export class LoginComponent implements OnInit {
loginForm: FormGroup;
loading = false;
submitted = false;
returnUrl: string;
error: string;
success: string
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private authenticationService: AuthenticationService
) {
// redirect to home if already logged in
if (this.authenticationService.currentUserValue) {
this.router.navigate(['/']);
}
}
ngOnInit() {
this.loginForm = this.formBuilder.group({
username: ['', Validators.required],
password: ['', Validators.required]
});
// get return url from route parameters or default to '/'
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
// show success message on registration
if (this.route.snapshot.queryParams['registered']) {
this.success = 'Registration successful';
}
}
// convenience getter for easy access to form fields
get f() { return this.loginForm.controls; }
onSubmit() {
this.submitted = true;
// reset alerts on submit
this.error = null;
this.success = null;
// stop here if form is invalid
if (this.loginForm.invalid) {
return;
}
this.loading = true;
this.authenticationService.login(this.f.username.value, this.f.password.value)
.pipe(first())
.subscribe(
data => {
this.router.navigate([this.returnUrl]);
},
error => {
this.error = error;
this.loading = false;
});
}
}
Update Login Component Template
This is how the login component template should look after adding the success alert message, the new code is on line 2
.
<div *ngIf="error" class="alert alert-danger">{{error}}</div>
<div *ngIf="success" class="alert alert-success">{{success}}</div>
<h2>Login</h2>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="username">Username</label>
<input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
<div *ngIf="submitted && f.username.errors" class="invalid-feedback">
<div *ngIf="f.username.errors.required">Username is required</div>
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
<div *ngIf="submitted && f.password.errors" class="invalid-feedback">
<div *ngIf="f.password.errors.required">Password is required</div>
</div>
</div>
<div class="form-group">
<button [disabled]="loading" class="btn btn-primary">
<span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
Login
</button>
<a routerLink="/register" class="btn btn-link">Register</a>
</div>
</form>
Start Angular 7 Application!
Run the command npm start
from the project root folder (where the package.json is located) to launch the Angular 7 application.
Need Some Angular 7 Help?
Search fiverr for freelance Angular 7 developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!