DEV Community

Cover image for Custom error handling in Angular reactive forms
Vishesh
Vishesh

Posted on

Custom error handling in Angular reactive forms

If you don't know what are reactive forms check this first. https://dev.to/vishesh1king/angular-reactive-forms-formsmodule-is-it-necessary-2aca

In this article i will be explaining the below two things

  1. Handling reactive form errors
  2. Adding custom validation functions to form controls

Current problem

Let's assume we have an application with large list of forms in many pages. Thus you have decided to use reactive form controls. Great ! I assume we will come up with something like below.

Component TS

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-profile-editor',
  templateUrl: './profile-editor.component.html',
  styleUrls: ['./profile-editor.component.css']
})
export class ProfileEditorComponent {
  loginForm = new FormGroup({
    email: new FormControl('', [Validators.required, Validators.minLength(8)]),
    password: new FormControl('', [Validators.required, Validators.minLength(8)]),
  });
}
Enter fullscreen mode Exit fullscreen mode

Component HTML

<input id="name" type="email" class="form-control"
      required [(ngModel)]="loginForm.email" #name="ngModel" >

<span *ngIf="name.invalid && (loginForm.email.dirty || loginForm.email.touched)"
    class="alert alert-danger">
    Email must is required and must be greater than 8 charecters
</span>
Enter fullscreen mode Exit fullscreen mode

But as you can see in the HTML. We have used a span tag to display the error message. Here, we are displaying same error message for all errors. This is not right. Because we always have to show the user correct error message rather than showing all/one common message and confusing user.

The direct solution could be is to write new spans for each type of error. Yes, this can work. Here is a simple calculation

No of forms = 5
No of fields in 1 form = 5
No of error messages for a field = 4 (required, min, max, pattern)

∴ 5*5 = 25 fields in the application
∴ 25*4 = 100 error conditions hard coded in the application.

This is lot of work. Even in case we did it. What if you want to change something ? Or change the basic styling of error message. You will have to modify all places and retest all items. This is huge change.

Thus now as u understood the problem. Lets take a look at the solution.

Solution

Simple ! Create form fields as separate components and use them as child components in the forms.

This promotes code reuse, have single source for validation error messages, etc... Below i have demonstrated an example UI components. Full code can be found here

Solution Pseudocode

  • Create a reusable input component. It must accept input attributes including form control from parent component.
  • Handle form control errors in this input reusable component.
  • Use this component instead of direct html input tags. As the error handing is already done in the reusable input component.
  • Add custom error functions to the form control that takes the field name and validates and returns the exact error message. Lets store in common file called > app.utility.ts

Thus the folder structure will be like below,
Alt Text

Let's start

Step 1: Create a new folder UI components. This is where we will have to store all our form fields(email, password, text, etc...) as separate components.

Input component (input.component.ts)

import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-input',
  templateUrl: './input.component.html',
  styleUrls: ['./input.component.css']
})
export class InputComponent implements OnInit {

  constructor(
    private ref: ChangeDetectorRef
  ) {
    this.onBlur = new EventEmitter();
  }

  @Input() appAutoFocus = false;
  @Input() formGroup: FormGroup;
  @Input() control: FormControl;
  @Input() type: 'text' | 'password' | 'email' = 'text';
  @Input() id = '';
  @Input() name: string = this.id || '';
  @Input() placeholder = '';
  @Input() label = '';
  @Input() errorMessage: string | boolean = null;
  @Input() extraClass: string | string[] = '';

  @Input() maxLength = 15;
  @Input() minLength = 0;

  // tslint:disable-next-line: no-output-on-prefix
  @Output() onBlur: EventEmitter<boolean>;

  // HTML helpers
  objectFn = Object;

  ngOnInit() { }

  blur() {
    this.onBlur.emit(true);
  }

}
Enter fullscreen mode Exit fullscreen mode

In the component we get the input elements basic attributes like name, id, label, placeholder, control (Form control), group (Form group), etc. Also we can emit the input elements events like blur, enter, click, etc. This events can be used in parent component and perform any activity based on it.

Input component (input.component.html)

<div class="form-control" [formGroup]="formGroup"
  [ngClass]="extraClass" [class.invalid]="control.touched && control.invalid">
  <label *ngIf="label">{{label}}</label>
  <input
    [type]="type"
    [placeholder]="placeholder"
    [attr.name]="name"
    [attr.id]="id"
    [formControl]="control"
    [maxlength]="maxLength"
    [minLength]="minLength"
    autocomplete="off"
    (blur)="blur()" />
    <span class="error-msg" *ngIf="control.errors && control.touched">
      {{ control.errors[objectFn.keys(control.errors)[0]] }}
  </span>
</div>
Enter fullscreen mode Exit fullscreen mode

In the HTML we have just applied the input attributes. Also displayed the first error message if its present. Since we going to use custom error messages this will work perfect. [check app.utility.ts app.component.ts].

Step 2: Create the form control in the form component. This is where we will add the custom error function.

Form component (app.component.ts)

import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  loginForm = new FormGroup({
    name: new FormControl('', [
          this.util.requiredValidator('Name'),
          this.util.minlengthValidator('Name', 3),
          this.util.maxlengthValidator('Name', 25),
        ]),
    email: new FormControl('', [
          this.util.requiredValidator('Email ID'),
          this.util.emailValidator,
          this.util.minlengthValidator('Email ID', 8),
          this.util.maxlengthValidator('Email ID', 45),
        ]),
    password: new FormControl('', [
          this.util.requiredValidator('Password'),
          this.util.minlengthValidator('Password', 8),
          this.util.maxlengthValidator('Password', 16),
        ]),
  });

  login() {
    this.loginForm.markAllAsTouched();
  }

}
Enter fullscreen mode Exit fullscreen mode

You might be wondering why i have used a custom function while form module provides default validators. But if we use it, its difficult to edit the error message or its styling (Camel casing, label addition, etc...). Thus i have written own validator functions in utility component.

Utility component (app.utility.ts)

import { Injectable } from "@angular/core";
import { FormControl } from "@angular/forms";

@Injectable({
  providedIn: "root"
})
export class UtilityFunctions {
  constructor() {}

  /** Validate the text passed */
  validateText(str: string, length?, maxLength?): boolean {
    str = str ? str.toString() : "";
    if (str) {
      if (
        !str.trim() ||
        str.trim() === "" ||
        (length && str.length < length) ||
        (maxLength && str.length > maxLength)
      ) {
        return false;
      }
      return true;
    }
    return false;
  }

  // Required validator function
  public requiredValidator(
    fieldName: string = ""
  ) {
    return (control: FormControl) => {
      const name = control.value;
      if (!name || !this.validateText(name)) {
        return {
          required: "Please enter your " + fieldName
        };
      }
      return null;
    };
  }

  // Required validator function
  public maxlengthValidator(fieldName: string = "", length: number) {
    return (control: FormControl) => {
      const name = control.value;
      if (name && !this.validateText(name, null, length)) {
        return {
          maxlength: `${fieldName} can't be greater than ${length} characters`
        };
      }
      return null;
    };
  }

  // Required validator function
  public minlengthValidator(fieldName: string = "", length: number) {
    return (control: FormControl) => {
      const name = control.value;
      if (name && !this.validateText(name, length)) {
        return {
          minlength: `${fieldName} can't be lesser than ${length} characters`
        };
      }
      return null;
    };
  }

  // Email form control validator function
  public emailValidator = function(control: FormControl) {
    const email = control.value;
    const reg = /^([a-z0-9_\-\.]+)@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$/;
    if (email && !reg.test(email)) {
      return {
        email: "Please enter a valid email address"
      };
    }
    return null;
  };

  // Only alpha numeric hyphen validator
  public password(fieldName: string = "") {
    return (control: FormControl) => {
      const name = control.value;
      if (
        name &&
        !/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&-_])[A-Za-z\d@$!%*?&-_]{8,50}$/.test(
          name
        )
      ) {
        return {
          password:
            fieldName +
            " must contain minimum 8 and maximum 50 characters, at least one uppercase letter, one lowercase letter, one number and one special character"
        };
      }
      return null;
    };
  }
}

Enter fullscreen mode Exit fullscreen mode

You can notice that i have taken label names fieldName from the form component itself so that the error messages have label in them too. You can do much more...

Form component (app.component.html)

<form class="main-form" (submit)="login()">
  <h3>Form</h3>
  <div class="form-control">
    <app-input [type]="'text'" [placeholder]="'Name'" [control]="loginForm.get('name')"
              [minLength]="3" [maxLength]="25"
              [id]="'name'" [formGroup]="loginForm" [label]="'Name'"></app-input>
  </div>
  <div class="form-control">
    <app-input [type]="'email'" [placeholder]="'Email ID'" [control]="loginForm.get('email')"
              [minLength]="6" [maxLength]="55"
              [id]="'email'" [formGroup]="loginForm" [label]="'Email ID'"></app-input>
  </div>
  <div class="form-control">
    <app-password [placeholder]="'Password'" [control]="loginForm.get('password')"
              [minLength]="8" [maxLength]="15"
              [id]="'password'" [formGroup]="loginForm" [label]="'Password'"></app-password>
  </div>
  <button type="submit">
    Save
  </button>
</form>
Enter fullscreen mode Exit fullscreen mode

Finally in the form component html call our newly created input components instead of default input tags. Pass the label, id, name, placeholder and importantly the group and control values. That's it...

Improvements

Based on the need we can keep getting more input data from parent component and add conditions. But its not recommended to do so because, it brings too much complications in html code. Thus its better to split as two different components. Ex: One for basic input text or email elements. One for password element where we need to display a hint to know password format.

These can further be used through-out the application. Thus tomorrow when u need to add new kind of error. Just mention in the form control validations and add a new span condition in input component if necessary. Since we are reusing this input component. It will be available in all the forms, we just need to mention the error function in form control validations.

Conclusion

This might sound like a lot of work. But once you do it. You will never have most of your validation and error message display issues. This will also force you to write all validations like min, max, name and id which we forget in many times and run into lot of small issues.

Happy Coding !!

Top comments (0)