Angular Form Validation

Introduction

One of the most common features in any web application is providing a form to users to input some data. You use forms daily to log in, register, place orders, etc.

Processing user inputs before validating can have serious consequences. You may end up storing invalid data like an incorrect date, email, age, etc. It could also be a security issue due to attacks like Cross-Site Scripting (XSS).

The traditional way to validate HTML forms is by using JavaScript or JQuery. Unfortunately, this approach warrants a bunch of code.

Angular, being a full-fledged framework, has provided excellent support for validating user inputs and displaying validation messages. It has lots of commonly used built-in validators that you can take advantage of, or you can even write your custom validators.

Forms in Angular

An Angular form is a regular HTML form with few additional features. For each field (input, radio, select, etc.) in the form, we need an object of the FormControl class. The FormControl object gives information about that field. Its value, if the value is valid, and if it is not valid what are the validation errors, etc.

It also provides the state of the field such as touched, untouched, dirty, pristine, etc.

Similarly, a FormGroup is the collection of the FormControl objects. Every Angular form has at least one FormGroup. You may decide to have multiple FormGroups in use-cases like separating the handling of personal details and professional details sections of a user registration form.

All the properties of a FormGroup (valid, error, etc.) are also available to the FormControl. For instance, the valid property of a FormControl will return true if all FormControl instances are valid.

So to add validation to an Angular form we need two things:

  • At least one FormGroup object for the form
  • A FormControl object for each field in the form

There are two different ways by which these control objects can be created. We can provide some directives in the template of the form and Angular can create such controls under the hood for us. Forms created by this way are called template-driven forms.

If we have some special use cases and we want more control over the form we can explicitly create such control objects. Forms created this way are called reactive forms.

Template-Driven Forms

In template-driven forms, we apply the ngModel directive for every field in the template. Angular creates a FormControl object under the hood for each such field and associate it with the respective field:

<div class="form-group">
  <label for="name">Name</label>
  <input type="text" class="form-control" id="name"
         ngModel name="name">
</div>

<div class="form-group">
  <label for="username">Username</label>
  <input type="text" class="form-control" id="username"
         ngModel name="username">
</div>

Note: With ngModel, it is required to provide either the name attribute or define the FormControl as "standalone" in ngModelOptions, otherwise Angular will throw an error.

Also, in app.module.ts you would need to add FormsModule to the array of imports:

import { FormsModule } from '@angular/forms';
// ...some other imports

imports: [
    //...some other imports
    FormsModule
]

Validation in Template-Driven Forms

Angular has provided some built-in validators to validate common use cases. In order to use built-in validators, you would need to apply validation attributes to each form field where you want some validation. These validation attributes are the same as the regular HTML5 validation attributes like required, minlength, maxlength, etc. Under the hood, Angular has provided directives to match these attributes with the validator functions defined in the Angular framework.

Whenever a FormControl's value changes, Angular generates a list of validation errors by running validation. If the list is empty it means it is a valid status, otherwise, it is an invalid status.

Let's say we want to put the following validations into it:

  • As the fields Name and Username have the required attribute, we want to display a validation message if this field is left empty.
  • The Name field should have a value whose minlegth and maxlength should be 2 and 30 characters respectively.
  • If the username has spaces, display an invalid username message.

For every form-control in which we want to add validation, we need to add appropriate validation attributes and export ngModel to a local template variable:

<input type="text" class="form-control" id="name"
    required maxlength="30" minlength="2"
    ngModel name="name" #name="ngModel">

In the above example, we have used the following built-in validators - required, minlength, and maxlength.

We can use the template variable name in the template to check for validation states of the used validators:

<div *ngIf="name.invalid && (name.dirty || name.touched)"
    class="alert alert-danger">
  <div *ngIf="name.errors.required">
    Name is required.
  </div>
  <div *ngIf="name.errors.minlength">
    Name cannot be more than 30 characters long.
  </div>
  <div *ngIf="name.errors.minlength">
    Name must be at least 2 characters long.
  </div>
</div>

As we've used a conditional statement to render the first div, it'll only be displayed if the status of the built-in validator is invalid. We've explained at the start of the section how the status is determined as valid or invalid.

Similarly, the inner div's will be displayed only if the template variable name has a property errors and the errors property has one of the following properties - required, minlength and maxlength and the property value is true. We've already discussed how the template variable binds to the ngModel directive and it receives these properties every time there is any change in the form control and after Angular runs the validation for that field.

Note: It is important to check for dirty and touched states, otherwise the error message will be displayed the very first time the page is loaded, which is bad for user experience. We need the validation message to be displayed in one of the following conditions:

  • The user changes some value, i.e the field is dirty (formControlObject.dirty)
  • The user uses tab or clicks to switch focus to some other element, i.e the field was touched (formControlObject.touched)

If you want to refer a full list of Angular's built-in validators, you may follow the Validators API.

Writing a Custom Validator

Sometimes the built-in validators may not cover your exact use-case. In this case, you may need to create your custom validator function.

A validator function implements the ValidatorFn interface, which means it should have the signature:

interface ValidatorFn {
    (control: AbstractControl): ValidationErrors | null
}

The ValidationErrors should be an object that has one or more key-value pairs:

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

type ValidationErrors = {
    [key: string]: any;
};

The key should be a string and is used to denote the type of validation error like invalidEmail, required, etc. The value can be anything and is used to supply more information about the validation error.

For the above example, we want to write a custom validation function that validates if there are no spaces in the username.

While technically we can write this function anywhere in the application, it is always good practice to put all related validator functions inside a separate class:

import { ValidationErrors, AbstractControl } from '@angular/forms';

export class UserRegistrationFormValidators {
    static usernameShouldBeValid(control: AbstractControl): ValidationErrors | null {
        if ((control.value as string).indexOf(' ') >= 0) {
            return { shouldNotHaveSpaces: true }
        }

        // If there is no validation failure, return null
        return null;
    }
}

Note: In this example, we have returned true as the value of key shouldNotHaveSpaces because we do not need to provide any details. In some cases you may need to provide details, for example:

return { maxlengthExceeded: {
        maxLength: 20,
        actual: control.value.length
    }
}

Next, we can use this validator function UserRegistrationFormValidators.usernameShouldBeValid for the username form-control in our template-driven form:

<div class="form-group">
  <label for="username">Username</label>
  <input type="text" class="form-control" id="username"
         required
         UserRegistrationFormValidators.usernameShouldBeValid
         [(ngModel)]="person.username" name="username">
</div>

Reactive Forms

In reactive forms, we create FormControl objects explicitly in the component of that template. Here is the regular HTML form without any ngModel directive or validations:

<div class="form-group">
  <label for="name">Name</label>
  <input type="text" class="form-control" id="name">
</div>

<div class="form-group">
  <label for="username">Username</label>
  <input type="text" class="form-control" id="username">
</div>

Let us assume we want to convert our template-driven form from the previous example into a reactive form.

For this, first, we need to explicitly create FormGroup and FormControls for each field in the component of the template:

form = new FormGroup({
    'name': new FormControl(),
    'username': new FormControl(),
})

Note: As discussed earlier, a form can have more than one FormGroup. In this case, we can have a nested structure:

registrationForm = new FormGroup({
    'personalDetailsForm': new FormGroup({
        'name': new FormControl()
    })
})

You can read more about FormGroup in the Angular documentation.

Let me bring your attention back to our use-case.

Next, we need to associate these FormControl objects to the fields in the HTML form.

<form [formGroup]="registrationForm">
<div class="form-group">
  <label for="name">Name</label>
  <input type="text" class="form-control" id="name"
         [formControlName]="name">
</div>

<div class="form-group">
  <label for="username">Username</label>
  <input type="text" class="form-control" id="username"
         [formControlName]="username">
</div>
<form>

Here we applied the formGroup directive and associated it with the FormGroup object registrationForm that we created in the component. We also associated the formControlName directive with the respective FormControl objects name and username.

Note: The directives to build reactive forms are defined in ReactiveFormsModule. So if you get an error such as:

Can't bind to formGroup

...then you should check if you have imported that ReactiveFormsModule in your main module app.module.ts.

Validations in Reactive Forms

In reactive forms, we do not pass the ngModel directive and we also do not use HTML5 validation attributes. We specify validators while creating the objects of the FormControl in the component itself.

Here is the signature of the FormControl class:

class FormControl extends AbstractControl {
    constructor(formState: any = null, validatorOrOpts?: ValidatorFn | AbstractControlOptions | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[])

    // ...
}

As we can see the first parameter is the initial state of the control which can be kept empty i.e ''. The second parameter is ValidatorFn.

To add the built-in validator functions for a FormControl we can pass it the appropriate ValidatorFn. For the following example we've used the following built-in validators required, minLength, and maxLength - :

registrationForm = new FormGroup({
    'name': new FormControl('Enter your name', [
        Validators.required,
        Validators.minLength(2),
        Validators.maxLength(30)
    ]),
    'username': new FormControl('', Validators.required),
})

Note: You would need to import Validators in the component.

Please also notice, unlike Template-driven forms we do not use the validation attributes. We use the respective ValidatorFn like Validators.required, Validators.minLength(2) etc. Your code editor may provide auto-complete for all ValidatorFn the moment you type Validators followed by a dot ..

We can go back to the template and write validation messages:

<form [formGroup]="registrationForm">
<div class="form-group">
  <label for="name">Name</label>
  <input type="text" class="form-control" id="name"
         [formControlName]="name">
  <div *ngIf="registrationForm.get('name').invalid && (registrationForm.get('name').dirty || registrationForm.get('name').touched)"
    class="alert alert-danger">
    <div *ngIf="registrationForm.get('name').errors.required">
       Name is required.
    </div>
    <div *ngIf="registrationForm.get('name').errors.minlength">
       Name cannot be more than 30 characters long.
    </div>
    <div *ngIf="registrationForm.get('name').errors.minlength">
       Name must be at least 2 characters long.
    </div>
  </div>
</div>

<div class="form-group">
  <label for="username">Username</label>
  <input type="text" class="form-control" id="username"
         [formControlName]="username">
</div>
<form>

Custom validators for Reactive forms

We need to write the custom validator function the same way as we did it for the Template-Driven form section. We can use the same custom validator function UserRegistrationFormValidators.usernameShouldBeValid in the component for the reactive form:

registrationForm = new FormGroup({
    'name': new FormControl('Enter your name', [
        Validators.required,
        Validators.minLength(2),
        Validators.maxLength(30)
    ]),
    'username': new FormControl('', [
        Validators.required,
        UserRegistrationFormValidators.usernameShouldBeValid
    ]),
})

Conclusion

In this tutorial, we explored the two different ways to handle user inputs - Template-Driven and Reactive forms. We learned how to put validation on both types of forms. And finally, we also wrote our custom validator function and included it with the built-in validators.

As we can see Angular has great support for forms and provides some under-the-hood useful features to validate forms. Providing every single feature with Angular forms is beyond the scope of this tutorial. You may read the Angular documentation for complete information.

Last Updated: August 21st, 2023
Was this article helpful?

Improve your dev skills!

Get tutorials, guides, and dev jobs in your inbox.

No spam ever. Unsubscribe at any time. Read our Privacy Policy.

Shadab AnsariAuthor

A Software Engineer who is passionate about writing programming articles.

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms