May 22 2019

Angular 7 Tutorial Part 5 - Registration Form & User Service

Other parts available in Angular 7 tutorial series:


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:

  1. Add Register Route to Fake Backend
  2. Create User Service
  3. Add Form Logic to Register Component
  4. Add Form HTML to Register Component Template
  5. Add Success Alert To Login Component
  6. 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 calling this.formBuilder.group() and assigns it to the this.registerForm property. The parameters passed to the FormBuilder tell it to create four form controls - firstName, lastName, username and password, the form controls are all initialised with empty strings ('') as values and set to required with the Validators.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 an Observable that we .subscribe() to for the results of the registration. On success the user is redirected to the /login route by calling this.router.navigate(), passing registered=true as a query parameter so the login page can display a success message. On fail the error message is assigned to the this.error property to be displayed by the template and the this.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.

 

Subscribe or Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter or GitHub to be notified when I post new content.

 


Sponsored by