Published:

Angular 10 - Reactive Forms Validation Example

Example built with Angular 10.0.2

Other versions available:

This is a quick example of how to setup form validation in Angular 10 using Reactive Forms. The example is a simple registration form with pretty standard fields for title, first name, last name, date of birth, email, password, confirm password and an accept terms and conditions checkbox. All fields are required including the checkbox, the dob must be a valid date, the password field must have a min length of 6 and the email address must be in a valid format. There's also a custom MustMatch validator which is used to validate that the confirm password and password fields match.

I've setup the form to validate on submit rather than as soon as each field is changed, this is implemented with a submitted property in the app component that is set to true when the form is submitted for the first time, and reset to false if the cancel button is clicked.

Styling of the example is all done with Bootstrap 4.5 CSS, for more info about Bootstrap see https://getbootstrap.com/docs/4.5/getting-started/introduction/.

Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-10-reactive-form-validation-example)


App Component

The app component contains our example sign up form which creates the form fields and validators using an Angular FormBuilder to create an instance of a FormGroup that is stored in the registerForm property. The registerForm is then bound to the <form> element in the app template below using the [formGroup] directive.

The component contains a convenience getter property f to make it easier to access form controls from the template. So for example you can access the confirmPassword field in the template using f.confirmPassword instead of registerForm.controls.confirmPassword.

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

// import custom validator to validate that password and confirm password fields match
import { MustMatch } from './_helpers/must-match.validator';

@Component({ selector: 'app', templateUrl: 'app.component.html' })
export class AppComponent implements OnInit {
    registerForm: FormGroup;
    submitted = false;

    constructor(private formBuilder: FormBuilder) { }

    ngOnInit() {
        this.registerForm = this.formBuilder.group({
            title: ['', Validators.required],
            firstName: ['', Validators.required],
            lastName: ['', Validators.required],
            // validates date format yyyy-mm-dd
            dob: ['', [Validators.required, Validators.pattern(/^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$/)]],
            email: ['', [Validators.required, Validators.email]],
            password: ['', [Validators.required, Validators.minLength(6)]],
            confirmPassword: ['', Validators.required],
            acceptTerms: [false, Validators.requiredTrue]
        }, {
            validator: MustMatch('password', 'confirmPassword')
        });
    }

    // 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;
        }

        // display form values on success
        alert('SUCCESS!! :-)\n\n' + JSON.stringify(this.registerForm.value, null, 4));
    }

    onReset() {
        this.submitted = false;
        this.registerForm.reset();
    }
}


App Component Template

The app component template contains all the html markup for displaying the example registration form in your browser. The form element uses the [formGroup] directive to bind to the registerForm FormGroup in the app component above.

The form binds the form submit event to the onSubmit() handler in the app component using the Angular event binding (ngSubmit)="onSubmit()". Validation messages are displayed only after the user attempts to submit the form for the first time, this is controlled with the submitted property of the app component.

The cancel button click event is bound to the onReset() handler in the app component using the Angular event binding (click)="onReset()".

<!-- main app container -->
<div class="card m-3">
    <h5 class="card-header">Angular 10 Reactive Form Validation</h5>
    <div class="card-body">
        <form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
            <div class="form-row">
                <div class="form-group col">
                    <label>Title</label>
                    <select formControlName="title" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.title.errors }">
                        <option value=""></option>
                        <option value="Mr">Mr</option>
                        <option value="Mrs">Mrs</option>
                        <option value="Miss">Miss</option>
                        <option value="Ms">Ms</option>
                    </select>
                    <div *ngIf="submitted && f.title.errors" class="invalid-feedback">
                        <div *ngIf="f.title.errors.required">Title is required</div>
                    </div>
                </div>
                <div class="form-group col-5">
                    <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="form-group col-5">
                    <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>
            <div class="form-row">
                <div class="form-group col">
                    <label>Date of Birth</label>
                    <input type="date" formControlName="dob" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.dob.errors }" />
                    <div *ngIf="submitted && f.dob.errors" class="invalid-feedback">
                        <div *ngIf="f.dob.errors.required">Date of Birth is required</div>
                        <div *ngIf="f.dob.errors.pattern">Date of Birth must be a valid date in the format YYYY-MM-DD</div>
                    </div>
                </div>
                <div class="form-group col">
                    <label>Email</label>
                    <input type="text" formControlName="email" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.email.errors }" />
                    <div *ngIf="submitted && f.email.errors" class="invalid-feedback">
                        <div *ngIf="f.email.errors.required">Email is required</div>
                        <div *ngIf="f.email.errors.email">Email must be a valid email address</div>
                    </div>
                </div>
            </div>
            <div class="form-row">
                <div class="form-group col">
                    <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 class="form-group col">
                    <label>Confirm Password</label>
                    <input type="password" formControlName="confirmPassword" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" />
                    <div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback">
                        <div *ngIf="f.confirmPassword.errors.required">Confirm Password is required</div>
                        <div *ngIf="f.confirmPassword.errors.mustMatch">Passwords must match</div>
                    </div>
                </div>
            </div>
            <div class="form-group form-check">
                <input type="checkbox" formControlName="acceptTerms" id="acceptTerms" class="form-check-input" [ngClass]="{ 'is-invalid': submitted && f.acceptTerms.errors }" />
                <label for="acceptTerms" class="form-check-label">Accept Terms & Conditions</label>
                <div *ngIf="submitted && f.acceptTerms.errors" class="invalid-feedback">Accept Ts & Cs is required</div>
            </div>
            <div class="text-center">
                <button class="btn btn-primary mr-1">Register</button>
                <button class="btn btn-secondary" type="reset" (click)="onReset()">Cancel</button>
            </div>
        </form>
    </div>
</div>


Custom "Must Match" Validator

The reactive forms custom MustMatch validator is used in this example to validate that both of the password fields - password and confirmPassword - are matching. However it can be used to validate that any pair of fields is matching (e.g. email and confirm email fields).

It works slightly differently than a typical custom validator because I'm setting the error on the second field instead of returning it to be set on the formGroup. I did it this way because I think it makes the template a bit cleaner and more intuitive, the mustMatch validation error is displayed below the confirmPassword field so I think it makes sense that the error is attached the the confirmPassword form control.

import { FormGroup } from '@angular/forms';

// custom validator to check that two fields match
export function MustMatch(controlName: string, matchingControlName: string) {
    return (formGroup: FormGroup) => {
        const control = formGroup.controls[controlName];
        const matchingControl = formGroup.controls[matchingControlName];

        if (matchingControl.errors && !matchingControl.errors.mustMatch) {
            // return if another validator has already found an error on the matchingControl
            return;
        }

        // set error on matchingControl if validation fails
        if (control.value !== matchingControl.value) {
            matchingControl.setErrors({ mustMatch: true });
        } else {
            matchingControl.setErrors(null);
        }
    }
}


App Module

There isn't much going on in the app module other than the standard stuff, the main thing you need to remember for using reactive forms in Angular is to import the ReactiveFormsModule from '@angular/forms' and include it in the imports array of the @NgModule decorator.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule
    ],
    declarations: [
        AppComponent
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }

 

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.

 


Supported by