Published: May 03 2023
Last updated: May 11 2023

Angular 15/16 Free Course #4 - Registration Form & Service Methods

Built and tested with Angular 15 and Angular 16

In this free step by step Angular course we'll be covering how to implement routing, authentication, registration and CRUD functionality in Angular.

Other parts available in this Angular course:


Angular Tutorial Part 4

In part 4 of this Angular tutorial series we're going to add a registration form and method to the account service for registering new users.

Code on GitHub

The complete source code for this part of the tutorial is available on GitHub at https://github.com/cornflourblue/angular-16-tutorial in the part-4 folder. If you haven't completed Part 3 (Login Form, Authentication & Route Guard) but want to follow the steps in this part of the course you can start with the code in the part-3 folder of the GitHub repo.


Tutorial Steps

  1. Add Register Route to Fake Backend
  2. Add Register Method to Account 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 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 initialized from local storage when the app starts, so registered users will persist when the browser is refreshed or closed. The default hardcoded user (test) has been removed because you 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-8 have been updated and the new lines are 21-22 and 41-52.

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, materialize, dematerialize } from 'rxjs/operators';

// array in local storage for registered users
const usersKey = 'angular-tutorial-users';
let users: any[] = JSON.parse(localStorage.getItem(usersKey)!) || [];

@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const { url, method, headers, body } = request;

        return handleRoute();

        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({
                ...basicDetails(user),
                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(usersKey, JSON.stringify(users));
            return ok();
        }

        // helper functions

        function ok(body?: any) {
            return of(new HttpResponse({ status: 200, body }))
                .pipe(delay(500)); // delay observable to simulate server api call
        }

        function error(message: string) {
            return throwError(() => ({ error: { message } }))
                .pipe(materialize(), delay(500), dematerialize()); // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648);
        }

        function basicDetails(user: any) {
            const { id, username, firstName, lastName } = user;
            return { id, username, firstName, lastName };
        }
    }
}

export const fakeBackendProvider = {
    // use fake backend in place of Http service for backend-less development
    provide: HTTP_INTERCEPTORS,
    useClass: FakeBackendInterceptor,
    multi: true
};


Add Register Method to Account Service

The account service currently handles all HTTP communication with the backend for authentication (login() & logout()) and provides access to the currently logged in user.

We're going to extend this functionality with to support user registration. 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 (`${environment.apiUrl}/users/register`), passing the user object in the request body.

Update Account Service

This is how the account service (account.service.ts) should look after the changes, the new register() method is on lines 44-46.

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { environment } from '@environments/environment';
import { User } from '@app/_models';

@Injectable({ providedIn: 'root' })
export class AccountService {
    private userSubject: BehaviorSubject<User | null>;
    public user: Observable<User | null>;

    constructor(
        private router: Router,
        private http: HttpClient
    ) {
        this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!));
        this.user = this.userSubject.asObservable();
    }

    public get userValue() {
        return this.userSubject.value;
    }

    login(username: string, password: string) {
        return this.http.post<User>(`${environment.apiUrl}/users/authenticate`, { username, password })
            .pipe(map(user => {
                // store user details and jwt token in local storage to keep user logged in between page refreshes
                localStorage.setItem('user', JSON.stringify(user));
                this.userSubject.next(user);
                return user;
            }));
    }

    logout() {
        // remove user from local storage and set current user to null
        localStorage.removeItem('user');
        this.userSubject.next(null);
        this.router.navigate(['/account/login']);
    }

    register(user: User) {
        return this.http.post(`${environment.apiUrl}/users/register`, user);
    }
}


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 accountService.userValue 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.form property. The parameters passed to the FormBuilder tell it to create four form controls - firstName, lastName, username and password, the form controls are all initialized with empty strings ('') 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 register component and f in the register 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.
  • resets the error message to an empty string to remove any alert.
  • checks if the form is valid by checking this.form.invalid and prevents submission if it is invalid.
  • sets the this.loading property to true before submitting the registration details via the account 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.accountService.register() method and passing it the form data (this.form.value). The account service returns an Observable that we .subscribe() to for the results of the registration. On success the user is redirected to the /account/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 update it with the following TypeScript:

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 { AccountService } from '@app/_services';

@Component({ templateUrl: 'register.component.html' })
export class RegisterComponent implements OnInit {
    form!: FormGroup;
    loading = false;
    submitted = false;
    error?: string;

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private accountService: AccountService
    ) {
        // redirect to home if already logged in
        if (this.accountService.userValue) {
            this.router.navigate(['/']);
        }
    }

    ngOnInit() {
        this.form = 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.form.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alert on submit
        this.error = '';

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.loading = true;
        this.accountService.register(this.form.value)
            .pipe(first())
            .subscribe({
                next: () => {
                    this.router.navigate(['/account/login'], { queryParams: { registered: true }});
                },
                error: 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]="form" attribute directive binds the form to the form 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 form.

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 5, 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 5. 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 5.

Lastly the cancel link uses the routerLink="../login" directive to link back to the /account/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 update it with the following HTML:

<div class="container col-md-6 offset-md-3 mt-5">
    <div *ngIf="error" class="alert alert-danger">{{error}}</div>
    <div class="card">
        <h4 class="card-header">Register</h4>
        <div class="card-body">
            <form [formGroup]="form" (ngSubmit)="onSubmit()">
                <div class="mb-3">
                    <label class="form-label">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="mb-3">
                    <label class="form-label">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="mb-3">
                    <label class="form-label">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="mb-3">
                    <label class="form-label">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>
                    <button [disabled]="loading" class="btn btn-primary">
                        <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
                        Register
                    </button>
                    <a routerLink="../login" class="btn btn-link">Cancel</a>
                </div>
            </form>
        </div>
    </div>
</div>


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 resets both this.success and this.error properties 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 14, 34-37 and 48.

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 { AccountService } from '@app/_services'

@Component({ templateUrl: 'login.component.html' })
export class LoginComponent implements OnInit {
    form!: FormGroup;
    loading = false;
    submitted = false;
    error?: string;
    success?: string;

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private accountService: AccountService
    ) {
        // redirect to home if already logged in
        if (this.accountService.userValue) {
            this.router.navigate(['/']);
        }
    }

    ngOnInit() {
        this.form = this.formBuilder.group({
            username: ['', Validators.required],
            password: ['', Validators.required]
        });

        // show success message after registration
        if (this.route.snapshot.queryParams.registered) {
            this.success = 'Registration successful';
        }
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alerts on submit
        this.error = '';
        this.success = '';

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.loading = true;
        this.accountService.login(this.f.username.value, this.f.password.value)
            .pipe(first())
            .subscribe({
                next: () => {
                    // get return url from query parameters or default to home page
                    const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
                    this.router.navigateByUrl(returnUrl);
                },
                error: 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 3.

<div class="container col-md-6 offset-md-3 mt-5">
    <div *ngIf="error" class="alert alert-danger">{{error}}</div>
    <div *ngIf="success" class="alert alert-success">{{success}}</div>
    <div class="card">
        <h4 class="card-header">Login</h4>
        <div class="card-body">
            <form [formGroup]="form" (ngSubmit)="onSubmit()">
                <div class="mb-3">
                    <label class="form-label">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="mb-3">
                    <label class="form-label">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>
                    <button [disabled]="loading" class="btn btn-primary">
                        <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
                        Login
                    </button>
                    <a routerLink="../register" class="btn btn-link">Register</a>
                </div>
            </form>
        </div>
    </div>
</div>


Start Angular Application!

Run the command npm start from the project root folder (where the package.json is located) to start the Angular application, then open a browser tab to the URL http://localhost:4200.

Register and login

You can now register a new user and login with the registered username and password.

The command output should look something like this:

PS C:\Projects\angular-tutorial> npm start

> [email protected] start
> ng serve

✔ Browser application bundle generation complete.

Initial Chunk Files   | Names         |  Raw Size
vendor.js             | vendor        |   2.44 MB |
polyfills.js          | polyfills     | 314.29 kB |
styles.css, styles.js | styles        | 209.42 kB |
main.js               | main          |  55.83 kB |
runtime.js            | runtime       |   6.54 kB |

                      | Initial Total |   3.01 MB

Build at: 2023-05-03T00:20:07.050Z - Hash: b2b0f65c2ffb4024 - Time: 8187ms

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **


√ Compiled successfully.

 


Need Some Angular 16 Help?

Search fiverr for freelance Angular 16 developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by