Angular Material Form Validation For Beginners

Daniel Kreider
Daniel Kreider - 04. Oct. 2022
  • Angular Form
  • Angular Material
  • Angular Form Validation
  • Material Form Validation
  • Angular Material Form Validation

Table of Contents

Garbage in.

And...

Garbage out.

Have you ever heard someone say that? ☝️

thinking

If you build forms with Angular and the popular Angular Material library then validating user input is one of the best ways to avoid hazards.

If your Angular form accepts any kind of input under the sun... with no or even minimal input validation... expect it to blow up in your face. 💥💥💥

So how do we validate user input when working with Angular forms?

Angular%20register%20form

How do you validate an Angular form?

There are 2 different ways to specify the way an Angular form is validated.

And although the Angular docs specifically say that you should avoid HTML validation attributes when using a reactive Angular form, you still can do some validation with the HTML attributes.

So which one should you use?

My experience has been that it really doesn't matter. It boils down to personal choice or your teams decision.

How to use HTML validation attributes with an Angular form

An Angular form that uses HTML validators would look something like this.

<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
    <div fxLayout="column" fxLayoutAlign="center center">
      <div fxFlex="100%">
        <mat-form-field appearance="outline" class="form-field">
          <mat-label>Full Name</mat-label>
          <input matInput formControlName="name" name="name" required pattern="/\s/">
        </mat-form-field>
      </div>
      <div fxFlex="100%">
        <mat-form-field appearance="outline" class="form-field">
          <mat-label>Email</mat-label>
          <input matInput formControlName="email" name="email" required type="email">
        </mat-form-field>
      </div>
      <div fxFlex="100%">
        <mat-form-field appearance="outline" class="form-field">
          <mat-label>Password</mat-label>
          <input matInput formControlName="password" name="password" type="password" required pattern="^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{8,}$">
          <mat-error *ngIf="registerForm.value.password">Password must be a combination of lower-case, upper-case, numbers and at least 9 characters long</mat-error>
        </mat-form-field>
      </div>
      <div fxFlex="100%">
        <mat-form-field appearance="outline" class="form-field">
          <mat-label>Confirm Password</mat-label>
          <input matInput formControlName="confirmPassword" name="confirmPassword" type="password" required pattern="{{registerForm.value.password}}">
          <mat-error *ngIf="registerForm.value.verifyPassword">Passwords do not match.</mat-error>
        </mat-form-field>
      </div>
      <div fxFlex="100%">
          <button mat-stroked-button color="primary" type="submit" [disabled]="!registerForm.valid">Register<mat-icon>chevron_right</mat-icon></button>
      </div>
    </div>
</form>

And our Typescript file.

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

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

  registerForm = new FormGroup({
    name: new FormControl(''),
    email: new FormControl(''),
    password: new FormControl(''),
    confirmPassword: new FormControl('')
  });

}

Notice that in this example we've used HTML validators such as...

One of the advantages of this approach is that we're not so dependent on the validation tools that the Angular framework gives us - allowing us to decouple from the framework.

If you're interested in learning more about all the available HTML validators then you'll find this article about Constraint validation helpful.

How to specify form validation inside the FormGroup?

But what if we want to avoid HTML validators? And use the Angular form validators instead?

Grabbing the same example above, here's what it would look like beginning with our Typescript file.

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

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

  registerForm = new FormGroup({
    name: new FormControl('', [Validators.pattern(/\s/), Validators.required]),
    email: new FormControl('', [Validators.required, Validators.email]),
    password: new FormControl('', [Validators.required, Validators.pattern('^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{8,}$')]),
    confirmPassword: new FormControl('', Validators.required)
  });

  onSubmit(): void {
    // display some fireworks
  }
}

And our HTML file.

<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
  <div fxLayout="column" fxLayoutAlign="center center">
    <div fxFlex="100%">
      <mat-form-field appearance="outline" class="form-field">
        <mat-label>Full Name</mat-label>
        <input matInput formControlName="name" name="name">
      </mat-form-field>
    </div>
    <div fxFlex="100%">
      <mat-form-field appearance="outline" class="form-field">
        <mat-label>Email</mat-label>
        <input matInput formControlName="email" name="email">
      </mat-form-field>
    </div>
    <div fxFlex="100%">
      <mat-form-field appearance="outline" class="form-field">
        <mat-label>Password</mat-label>
        <input matInput formControlName="password" name="password" type="password">
        <mat-error *ngIf="registerForm.value.password">Password must be a combination of lower-case, upper-case, numbers and at least 9 characters long</mat-error>
      </mat-form-field>
    </div>
    <div fxFlex="100%">
      <mat-form-field appearance="outline" class="form-field">
        <mat-label>Confirm Password</mat-label>
        <input matInput formControlName="confirmPassword" name="confirmPassword" type="password">
        <mat-error *ngIf="registerForm.value.confirmPassword">Passwords do not match.</mat-error>
      </mat-form-field>
    </div>
    <div fxFlex="100%">
        <button mat-stroked-button color="primary" type="submit" [disabled]="!registerForm.valid">Register<mat-icon>chevron_right</mat-icon></button>
    </div>
  </div>
</form>

But how do you use an Angular form validation pattern?

Angular has a lot of validators to choose from like...

It also has the pattern validator that allows us to require that a certain input match a specified regex expression.

A great scenario for this one is validating the strength of a password.

In the example below, we've declared a FormGroup with a password field. The validators pattern specifies that the password must have upper-case and lower-case letters as well as numbers. And it also needs to be at least 9 characters long.

registerForm = new FormGroup({
    name: new FormControl('', Validators.pattern(/\s/)),
    email: new FormControl('', Validators.email),
    password: new FormControl('', Validators.pattern('^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{8,}$')),
    confirmPassword: new FormControl('')
});

Any input in your Angular form that can be validated with a regex express is a great place to use the pattern validator.

How do you create a custom validator?

Now, what if the built-in Angular validators are not enough?

What if you want to do some custom validation? And create a custom validator?

One example scenario would be to compare the password fields in a register form to make sure the user entered the same password twice.

Here's how it's done.

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

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

  registerForm = new FormGroup({
    name: new FormControl('', [Validators.pattern(/\s/), Validators.required]),
    email: new FormControl('', [Validators.required, Validators.email]),
    password: new FormControl('', [Validators.required, Validators.pattern('^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{8,}$')]),
    confirmPassword: new FormControl('', Validators.required)
  }, { validators: confirmPasswordValidator});

  onSubmit(): void {
    // crack an egg or split a nut
  }

}

export const confirmPasswordValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
  const password = control.get('password');
  const confirmPassword = control.get('confirmPassword');

  return password && confirmPassword && password.value === confirmPassword.value ? { confirmPassword: true } : null;
};

And how do you display an error message?

So how do you display any validation errors to the user?

This is where Angular Material really shines because of it's mat-error directive.

Here's how to display a custom error message if validation fails.

<mat-form-field>
   <mat-label>Password</mat-label>
    <input matInput formControlName="password" name="password" type="password" required>
    <mat-error *ngIf="registerForm.value.password">Password must be a combination of lower-case, upper-case, numbers and at least 9 characters long</mat-error>
 </mat-form-field>

But what if you have multiple validators?

And you want to display different error messages depending on which validation fails?

 <mat-form-field appearance="outline" class="form-field">
        <mat-label>Full Name</mat-label>
        <input matInput formControlName="name" name="name">
        <mat-error *ngIf="registerForm.get('name').hasError('required')">This field is required</mat-error>
        <mat-error *ngIf="registerForm.get('name').hasError('pattern')">Must be your full name</mat-error>
</mat-form-field>

Here's a screenshot of the error messages our form will display, when invalid.

invalid-angular-material-form-validation

What about displaying an error message for a custom validator?

But what if we have a custom validator and want to specify an error message when our validator fails?

The first step is to modify our custom validator to look like this.

export const confirmPasswordValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
  const password = control.get('password');
  const confirmPassword = control.get('confirmPassword');

  return password && confirmPassword && password.value === confirmPassword.value ? { confirmPassword: true } : { confirmPassword: false };
};

All we've changed is that instead of returning null we now return confirmPassword with a false value.

And finally, our Angular Material form field.

<mat-form-field appearance="outline" class="form-field">
    <mat-label>Confirm Password</mat-label>
    <input matInput formControlName="confirmPassword" name="confirmPassword" type="password" required pattern="{{registerForm.value.password}}">
    <mat-error *ngIf="registerForm.value.confirmPassword">Passwords do not match.</mat-error>
</mat-form-field>

This will produce an error like the one below.

passwords-dont-match

How do you validate an Angular form on submit?

By now we've got the validators set up and rolling...

...but...

How do we make sure that an invalid form is never submitted? Or ensure that invalid data never gets posted to an API?

One way to do it is to check the state of our form in the submit function.

We could declare an onSubmit function like this.

<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
    ...
</form>

And then inside of our Typescript file we can declare the onSubmit function and check if our Angular form is valid.

onSubmit(): void {
    if (this.registerForm.invalid) {
      return;
    }
    // continue work here
  }

Another way to do it is by disabling the submit button if the form is not valid. Like this.

<button 
        mat-stroked-button 
        color="primary" 
        type="submit" 
        [disabled]="!registerForm.valid">
    Submit
</button>

What's your evaluation?

Validating user input is important and also helpful in that we can show our users helpful error messages. That said you should never trust your users or your forms. Instead, you'll earn a heap of security benefits by always verifying user input in the back-end as well.

What do you think could happen if input was only validated in our Angular form?

Let me know in the comments below.

signature

Angular Consultant

P.S. If you're a skimmer like me then here's what this article is all about.

  • How to validate your Angular Material form - even if you're just getting started with Angular.
  • The different ways you can use to validate an Angular Material form.
  • How to use Angular validation patterns and create your own.
  • How to validate user input and verify that no "junk" has been entered.
  • The easiest way to display an error field if form validation fails.
  • Making sure that your form is never submitted with invalid data.