Full Angular 7 Reactive Forms Validation Tutorial

By Digamber Rawat Last updated on
In this tutorial, I am going to create an advance form using Angular 7 Reactive Form with ReactiveFormsModule API from scratch. I will go through the common concepts of Angular 7 Reactive Forms like FormGroup, FormControl, FormBuilder, and FormaArray to manage user’s input data.

There are 2 types of Forms in Angular, Reactive Forms & Template Driven Forms, I will talk about Reactive Forms in this tutorial.

Reactive forms In Angular follows model-driven approach to handle form data. Reactive forms offer more flexibility then template driven forms. Initially Reactive Form is complicated but once you are familiar with Reactive Forms, then you can easily manage the complex data.

By the end of this tutorial, you will be able to do:

  • Inject ReactiveFormsModule API in Angular 7
  • Set up the Reactive Forms in Angular 7
  • Bind user data to FormGroup API using Reactive Forms in Angular 7
  • Upload the image in Angular 7 using Reactive Forms & HTML5 FileReader API
  • Create dynamic form fields using Angular 7 FormArray service
  • Validate Form in Angular 7 using Reactive Form’s Validators service
  • Work with select dropdown in Angular 7
  • Work with Radio buttons in Angular
  • Create custom validator to confirm password validation

1. Importing ReactiveFormsModule API

In order to work with Reactive Forms in Angular 7 we must import ReactiveFormsModule API in app module file.

app.module.ts

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

@NgModule({
  imports: [
    ReactiveFormsModule
  ],
})

export class AppModule { }

2. Setting Up formGroup, ngSubmit & formControlName in Angular Form Template

Reactive forms communicate with component class in Angular to manage the form data. Let’s understand the Reactive Forms services & ngSubmit event:

  • AbstractControl: This is the main class for controlling the behavior and properties of, FormGroup, FormControl, and FormArray.
  • FormBuilder: It provides helpful methods to create control instances in Angular 7 Reactive Forms.
  • FormGroup: FormGroup is a top-level API which maintains the values, properties and validation state of a group of AbstractControl instance in Angular 7.
  • FormControl: It communicates with an HTML Form element like input or select tag, this api handles the individual form value and validation state.
  • FormArray: FormArray API maintains the values, properties and validation state of an array of the AbstractControl instances.
  • ngSubmit: This event is called when the form is submitted.
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()" novalidate>
  <input formControlName="firstName" placeholder="Your name">
  <input formControlName="email" placeholder="Your email">
  <input formControlName="phoneNumber" placeholder="Your message">

  <button type="submit">Register</button>
</form>

3. Using FormBuilder API

The FormBuilder service offers 3 useful methods: group(), control(), and array(). These methods generate instances in your component classes including form controls, form groups, and form arrays.

import { Component } from '@angular/core';
import { FormBuilder, FormArray } from "@angular/forms";

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

export class AppComponent {
  
  constructor(public fb: FormBuilder) {}

  registrationForm = this.fb.group({
    file: [null],
    fullName: this.fb.group({
      firstName: [''],
      lastName: ['']
    }),
    email: [''],
    phoneNumber: [''],
    address: this.fb.group({
      street: [''],
      city: [''],
      cityName: ['']
    }),
    gender: [''],
    PasswordValidation: this.fb.group({
      password: [''],
      confirmPassword: ['']
    }),
    addDynamicElement: this.fb.array([])
  })  

}

4. Upload Image Using Reactive Forms & HTML5 FileReader API in Angular

Let’s create a file upload functionality using Reactive Forms in Angular 7.

I am going to use HTML5 FileReader, changeDetectorRef, @ViewChild() API’s.

  • HTML5 FileReader API: This API is very useful to upload images and files from the client side in the web browser. Here is the detailed article about HTML5 FileReader API.
  • changeDetectorRef: If there is a change in the app, Angular will perform ChangeDetectorRef on all the components, whether it is a network request or user event. Understand Angular 7 change detection strategy
  • @ViewChild(): If you wish to gain access to a DOM element, directive or component from a parent component class then you rely on Angular 7 ViewChild. Read more about Angular 7 ViewChild.

app.component.html

<!-- Upload image -->
<div class="avatar-upload">
   <div class="avatar-edit">
      <input type='file' id="imageUpload" accept=".png, .jpg, .jpeg" #fileInput (change)="uploadFile($event)" />
      <label for="imageUpload" *ngIf="editFile" [ngClass]="['custom-label', 'upload-image']"></label>
      <label *ngIf="removeUpload" [ngClass]="['custom-label', 'remove-image']" (click)="removeUploadedFile()"></label>
   </div>
   <div class="avatar-preview">
      <div id="imagePreview" [style.backgroundImage]="'url('+ imageUrl +')'">
      </div>
   </div>
</div>

app.component.ts

import { Component, ChangeDetectorRef, ElementRef, ViewChild } from '@angular/core';
import { FormBuilder, FormArray } from "@angular/forms";

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

export class AppComponent {
  
  constructor(
    public fb: FormBuilder,
    private cd: ChangeDetectorRef
  ) {}

  /*##################### Registration Form #####################*/
  registrationForm = this.fb.group({
    file: [null]
  })  

  /*########################## File Upload ########################*/
  @ViewChild('fileInput') el: ElementRef;
  imageUrl: any = '/assets/dummy-user.jpg';
  editFile: boolean = true;
  removeUpload: boolean = false;

  uploadFile(event) {
    let reader = new FileReader(); // HTML5 FileReader API
    let file = event.target.files[0];
    if (event.target.files && event.target.files[0]) {
      reader.readAsDataURL(file);

      // When file uploads set it to file formcontrol
      reader.onload = () => {
        this.imageUrl = reader.result;
        this.registrationForm.patchValue({
          file: reader.result
        });
        this.editFile = false;
        this.removeUpload = true;
      }
      // ChangeDetectorRef since file is loading outside the zone
      this.cd.markForCheck();        
    }
  }

  // Function to remove uploaded file
  removeUploadedFile() {
    let newFileList = Array.from(this.el.nativeElement.files);
    this.imageUrl = '/assets/dummy-user.jpg';
    this.editFile = true;
    this.removeUpload = false;
    this.registrationForm.patchValue({
      file: [null]
    });
  }
}

Note: I am saving base64 url in file’s form control array just for demo purpose. You should not save base64 file url in real world app, it will consume more memory and its also not good from SEO point of view. Rather you should save your file in the database storage and include the file location URL in the file field.

5. Working with Select DropDown in Reactive Forms with Angular 7

When we work on select dropdown in Angular application, we must set dynamic values in the form control array. Since the user can select any values from the dropdown list we should use setValue() method to assign the dynamic values to form control array.

app.component.html

<div class="mb-3">
   <label>State</label>
   <select class="custom-select d-block w-100" (change)="changeCity($event)" formControlName="cityName">
      <option value="">Choose...</option>
      <option *ngFor="let city of City" [ngValue]="city">{{city}}</option>
   </select>
</div>

app.component.ts

import { Component } from '@angular/core';
import { FormBuilder } from "@angular/forms";

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

export class AppComponent {

   // City names
   City: any = ['Florida', 'South Dakota', 'Tennessee', 'Michigan']

   registrationForm = this.fb.group({
    address: this.fb.group({
      //...
      city: ['']
      //...
    })
   })  

  // Choose city using select dropdown
  changeCity(e) {
   this.registrationForm.get('address.cityName').setValue(e.target.value, {
    onlySelf: true
   })
  }

}

6. Working with Radio Buttons using Angular 7 Reactive Forms

I am going to share with you how you can work with radio buttons in Angular. Let’s understand how you can set dynamic values of a radio button using setValue() method.

app.component.html

<!-- Radio Buttons -->
<div class="group-gap">
   <h5 class="mb-3">Gender</h5>
   <div class="d-block my-3">
      
      <div class="custom-control custom-radio">
         <input id="male" type="radio" class="custom-control-input" name="gender" formControlName="gender" value="male"
            checked>
         <label class="custom-control-label" for="male">Male</label>
      </div>

      <div class="custom-control custom-radio">
         <input id="female" type="radio" class="custom-control-input" name="gender" formControlName="gender" value="female">
         <label class="custom-control-label" for="female">Female</label>
      </div>

   </div>
</div>

app.component.ts

import { Component } from '@angular/core';
import { FormBuilder } from "@angular/forms";

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

export class AppComponent {
  
  registrationForm = this.fb.group({
    gender: ['male']
  })

  // Choose city using select dropdown
  changeCity(e) {
    this.registrationForm.get('address.cityName').setValue(e.target.value, {
     onlySelf: true
    })
  }

}

How to Set Selected Value of Radio Buttons using Angular Reactive Forms?

Pass the radio button value name within the form control array, to set the selected value in radio button using Angular 7.

import { Component } from '@angular/core';
import { FormBuilder } from "@angular/forms";

@Component({
   //...
})

export class AppComponent {
  
  registrationForm = this.fb.group({
    gender: ['male'] // Assign the value name in the form control array
  })

}

7. Create Nested Form using Angular’s FormArray API with Reactive Forms

Creating dynamic form fields in Angular is very easy we just have to use formArrayName & FormArray service it will allow us to create dynamic form fields.

app.module.html

<!-- Add Super Powers Dynamically-->
<div class="group-gap" formArrayName="addDynamicElement">
   <h5 class="mb-3">Add Super Powers</h5>
   <div class="mb-3">
      <button type="button" class="btn btn-sm btn-success mb-3 btn-block" (click)="addSuperPowers()">Add Powers</button>
      <ul class="subjectList">
         <li *ngFor="let item of addDynamicElement.controls; let i = index">
            <input type="text" class="form-control" [formControlName]="i">
         </li>
      </ul>
   </div>

   <!-- Submit Button -->
   <button type="submit" class="btn btn-danger btn-lg btn-block">Create Superhero</button>
</div>

app.component.ts

import { Component } from '@angular/core';
import { FormBuilder, FormArray } from "@angular/forms";

@Component({
  //...
})

export class AppComponent {
  
  constructor(public fb: FormBuilder) {}

  /*##################### Registration Form #####################*/
  registrationForm = this.fb.group({
    addDynamicElement: this.fb.array([])
  })  


  /*############### Add Dynamic Elements ###############*/
  get addDynamicElement() {
    return this.registrationForm.get('addDynamicElement') as FormArray
  }

  addSuperPowers() {
    this.addDynamicElement.push(this.fb.control(''))
  }

}

8. Create Confirm Password Validation using Custom Validator with Angular 7

Angular’s Validators class offers some useful validators like pattern required, minLength and maxLength. However, sometimes we require to use validate some complex values. In this type of condition, custom validators are very useful.

Reactive Forms in Angular lets us define custom validators very easily. In this tutorial, I am going to create confirm password validation. To do that I will be creating a separate folder by the name of must-match and keep my custom validator file there. I will name it validate-password.ts so it will sound generic.

must-match > validate-password.ts
import { AbstractControl } from '@angular/forms';

export class ValidatePassword {

  static MatchPassword(abstractControl: AbstractControl) {
    let password = abstractControl.get('password').value;
    let confirmPassword = abstractControl.get('confirmPassword').value;
     if (password != confirmPassword) {
         abstractControl.get('confirmPassword').setErrors({
           MatchPassword: true
         })
    } else {
      return null
    }
  }
  
}

Using the Custom Validator in Reactive Form

import { FormBuilder Validators } from "@angular/forms";
import { ValidatePassword } from "./must-match/validate-password";

@Component({
  //...
})

export class AppComponent {

   registrationForm = this.fb.group({
     PasswordValidation: this.fb.group({
        password: ['', Validators.required],
        confirmPassword: ['', Validators.required]
     },{
        validator: ValidatePassword.MatchPassword // custom validation
     })
   })

}

9. Full Angular 7 Reactive Forms and Form Validation

Implementing form validation using Reactive Forms in Angular is pretty easy. In order to add form validation in Angular we must import Validators class in Angular app. Validators class communicates directly with form control instance, It wraps single or multiple validation into an array.

Form validation to be covered:

  • Required field validation
  • First name must be 2 characters
  • Email validation
  • Phone number validation
  • Password must match validation

app.component.html

<div class="container">
  <div class="row custom-wrapper">
    <div class="col-md-12">

      <!-- Form starts -->
      <form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">

        <div class="group-gap">

          <!-- Upload image -->
          <div class="avatar-upload">
            <div class="avatar-edit">
              <input type='file' id="imageUpload" accept=".png, .jpg, .jpeg" #fileInput (change)="uploadFile($event)" />
              <label for="imageUpload" *ngIf="editFile" [ngClass]="['custom-label', 'upload-image']"></label>
              <label *ngIf="removeUpload" [ngClass]="['custom-label', 'remove-image']" (click)="removeUploadedFile()"></label>
            </div>
            <div class="avatar-preview">
              <div id="imagePreview" [style.backgroundImage]="'url('+ imageUrl +')'">
              </div>
            </div>
          </div>

          <!-- Full name -->
          <div formGroupName="fullName">
            <div class="mb-3">
              <label [ngClass]="{'error': submitted && myForm.fullName.controls.firstName.errors}">
                First name</label>
              <input type="text" class="form-control" formControlName="firstName" [ngClass]="{'error': submitted && myForm.fullName.controls.firstName.errors}">
              <!-- error block -->
              <div class="invalid-feedback" *ngIf="submitted && myForm.fullName.controls.firstName.errors?.required">
                <sup>*</sup>Enter your name
              </div>
              <div class="invalid-feedback" *ngIf="submitted && myForm.fullName.controls.firstName.errors?.minlength">
                <sup>*</sup>Name must be 2 characters long
              </div>
              <div class="invalid-feedback" *ngIf="submitted && myForm.fullName.controls.firstName.errors?.pattern">
                <sup>*</sup>No special charcter allowed
              </div>
            </div>

            <div class="mb-3">
              <label [ngClass]="{'error': submitted && myForm.fullName.controls.lastName.errors}">
                Last name</label>
              <input type="text" class="form-control" formControlName="lastName" [ngClass]="{'error': submitted && myForm.fullName.controls.lastName.errors}">
              <!-- error block -->
              <div class="invalid-feedback" *ngIf="submitted && myForm.fullName.controls.lastName.errors?.required">
                <sup>*</sup>Please enter your surname
              </div>
            </div>
          </div>

          <!-- Email -->
          <div class="mb-3">
            <label [ngClass]="{'error': submitted && myForm.email.errors}">Email</label>
            <input type="email" class="form-control" formControlName="email" [ngClass]="{'error': submitted && myForm.email.errors}">

            <!-- error block -->
            <div class="invalid-feedback" *ngIf="submitted && myForm.email.errors?.required">
              <sup>*</sup>Please enter your email
            </div>
            <div class="invalid-feedback" *ngIf="submitted && myForm.email.errors?.pattern">
              <sup>*</sup>Please enter valid email
            </div>
          </div>

          <!-- Phone number -->
          <div class="mb-3">
            <label [ngClass]="{'error': submitted && myForm.phoneNumber.errors}">Phone Number</label>
            <input type="text" class="form-control" formControlName="phoneNumber" [ngClass]="{'error': submitted && myForm.phoneNumber.errors}">

            <!-- error block -->
            <div class="invalid-feedback" *ngIf="submitted && myForm.phoneNumber.errors?.maxLength">
              <sup>*</sup>Phone number must be 10 digit long
            </div>
            <div class="invalid-feedback" *ngIf="submitted && myForm.phoneNumber.errors?.required">
              <sup>*</sup>Please enter your phone number
            </div>
            <div class="invalid-feedback" *ngIf="submitted && myForm.phoneNumber.errors?.pattern">
              <sup>*</sup>Please enter valid phone number
            </div>
          </div>
        </div>

        <!-- Address -->
        <div class="group-gap" formGroupName="address">
          <h5 class="mb-3">Address</h5>
          <div class="mb-3">
            <label [ngClass]="{'error': submitted && myForm.address.controls.street.errors}">Street</label>
            <input type="text" class="form-control" formControlName="street" [ngClass]="{'error': submitted && myForm.address.controls.street.errors}">

            <!-- error block -->
            <div class="invalid-feedback" *ngIf="submitted && myForm.address.controls.street.errors?.required">
              <sup>*</sup>Please enter your street
            </div>
          </div>

          <div class="mb-3">
            <label [ngClass]="{'error': submitted && myForm.address.controls.city.errors}">City</label>
            <input type="text" class="form-control" formControlName="city" [ngClass]="{'error': submitted && myForm.address.controls.city.errors}">

            <!-- error block -->
            <div class="invalid-feedback" *ngIf="submitted && myForm.address.controls.city.errors?.required">
              <sup>*</sup>Please enter your street
            </div>
          </div>

          <div class="mb-3">
            <label [ngClass]="{'error': submitted && myForm.address.controls.cityName.errors}">State</label>
            <select class="custom-select d-block w-100" (change)="changeCity($event)" formControlName="cityName"
              [ngClass]="{'error': submitted && myForm.address.controls.cityName.errors}">
              <option value="">Choose...</option>
              <option *ngFor="let city of City" [ngValue]="city">{{city}}</option>
            </select>

            <!-- error block -->
            <div class="invalid-feedback" *ngIf="submitted && myForm.address.controls.cityName.errors?.required">
              <sup>*</sup>Please enter your city name
            </div>
          </div>
        </div>

        <!-- Gender -->
        <div class="group-gap">
          <h5 class="mb-3">Gender</h5>
          <div class="d-block my-3">
            <div class="custom-control custom-radio">
              <input id="male" type="radio" class="custom-control-input" name="gender" formControlName="gender" value="male"
                checked>
              <label class="custom-control-label" for="male">Male</label>
            </div>

            <div class="custom-control custom-radio">
              <input id="female" type="radio" class="custom-control-input" name="gender" formControlName="gender" value="female">
              <label class="custom-control-label" for="female">Female</label>
            </div>
          </div>
        </div>

        <!-- Password -->
        <div formGroupName="PasswordValidation">
          <div class="group-gap">
            <div class="mb-3">
              <label [ngClass]="{'error': submitted && myForm.PasswordValidation.controls.password.errors}">Password</label>
              <input type="password" class="form-control" formControlName="password" [ngClass]="{'error': submitted && myForm.PasswordValidation.controls.password.errors}">

              <!-- error block -->
              <div class="invalid-feedback" *ngIf="submitted && myForm.PasswordValidation.controls.password.errors">
                <sup>*</sup>Please enter password
              </div>
            </div>

            <div class="mb-3">
              <label [ngClass]="{'error': submitted && myForm.PasswordValidation.controls.confirmPassword.errors}">Confirm
                Password</label>
              <input type="password" class="form-control" formControlName="confirmPassword" [ngClass]="{'error': submitted && myForm.PasswordValidation.controls.confirmPassword.errors}">
            </div>

            <!-- error block -->
            <div class="invalid-feedback" *ngIf="submitted && myForm.PasswordValidation.controls.confirmPassword.errors">
              <sup>*</sup>Password mismatch
            </div>
          </div>
        </div>

        <!-- Add Super Powers Dynamically-->
        <div class="group-gap" formArrayName="addDynamicElement">
          <h5 class="mb-3">Add Super Powers</h5>
          <div class="mb-3">
            <button type="button" class="btn btn-sm btn-success mb-3 btn-block" (click)="addSuperPowers()">Add Powers</button>
            <ul class="subjectList">
              <li *ngFor="let item of addDynamicElement.controls; let i = index">
                <input type="text" class="form-control" [formControlName]="i">
              </li>
            </ul>
          </div>

          <!-- Submit Button -->
          <button type="submit" class="btn btn-danger btn-lg btn-block">Create Superhero</button>
        </div>

      </form><!-- Form ends -->

    </div>
  </div>
</div>

Below file contains the entire logic of our Angular Reactive Forms.

app.module.ts

import { Component, ChangeDetectorRef, ElementRef, ViewChild } from '@angular/core';
import { FormBuilder, FormArray, Validators } from "@angular/forms";
import { ValidatePassword } from "./must-match/validate-password";

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

export class AppComponent {
  submitted = false;

  // City names
  City: any = ['Florida', 'South Dakota', 'Tennessee', 'Michigan']
  
  constructor(
    public fb: FormBuilder,
    private cd: ChangeDetectorRef
  ) {}

  /*##################### Registration Form #####################*/
  registrationForm = this.fb.group({
    file: [null],
    fullName: this.fb.group({
      firstName: ['', [Validators.required, Validators.minLength(2), Validators.pattern('^[_A-z0-9]*((-|\s)*[_A-z0-9])*$')]],
      lastName: ['', [Validators.required]]
    }),
    email: ['', [Validators.required, Validators.pattern('[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$')]],
    phoneNumber: ['', [Validators.required, Validators.maxLength(10), Validators.pattern('^[0-9]+$')]],
    address: this.fb.group({
      street: ['', [Validators.required]],
      city: ['', [Validators.required]],
      cityName: ['', [Validators.required]]
    }),
    gender: ['male'],
    PasswordValidation: this.fb.group({
      password: ['', Validators.required],
      confirmPassword: ['', Validators.required]
    },{
      validator: ValidatePassword.MatchPassword // your validation method
    }
    ),
    addDynamicElement: this.fb.array([])
  })  

  /*########################## File Upload ########################*/
  @ViewChild('fileInput') el: ElementRef;
  imageUrl: any = 'https://i.pinimg.com/236x/d6/27/d9/d627d9cda385317de4812a4f7bd922e9--man--iron-man.jpg';
  editFile: boolean = true;
  removeUpload: boolean = false;

  uploadFile(event) {
    let reader = new FileReader(); // HTML5 FileReader API
    let file = event.target.files[0];
    if (event.target.files && event.target.files[0]) {
      reader.readAsDataURL(file);

      // When file uploads set it to file formcontrol
      reader.onload = () => {
        this.imageUrl = reader.result;
        this.registrationForm.patchValue({
          file: reader.result
        });
        this.editFile = false;
        this.removeUpload = true;
      }
      // ChangeDetectorRef since file is loading outside the zone
      this.cd.markForCheck();        
    }
  }

  // Function to remove uploaded file
  removeUploadedFile() {
    let newFileList = Array.from(this.el.nativeElement.files);
    this.imageUrl = 'https://i.pinimg.com/236x/d6/27/d9/d627d9cda385317de4812a4f7bd922e9--man--iron-man.jpg';
    this.editFile = true;
    this.removeUpload = false;
    this.registrationForm.patchValue({
      file: [null]
    });
  }  

  // Getter method to access formcontrols
  get myForm() {
    return this.registrationForm.controls;
  }
  
  // Choose city using select dropdown
  changeCity(e) {
    this.registrationForm.get('address.cityName').setValue(e.target.value, {
      onlySelf: true
    })
  }

  /*############### Add Dynamic Elements ###############*/
  get addDynamicElement() {
    return this.registrationForm.get('addDynamicElement') as FormArray
  }

  addSuperPowers() {
    this.addDynamicElement.push(this.fb.control(''))
  }

  // Submit Registration Form
  onSubmit() {
    this.submitted = true;
    if(!this.registrationForm.valid) {
      alert('Please fill all the required fields to create a super hero!')
      return false;
    } else {
      console.log(this.registrationForm.value)
    }
  }

}

10. Angular Reactive Forms & Form Validation Demo

Open the console to check the submitted form data

Following will be the final output:

Angular Reactive Form Validation Demo

11. Angular Reactive Forms GitHub Project Files

GitHub Resources

Digamber Rawat
Digamber Rawat

Full stack developer with a passion for UI/UX design. I create beautiful and useful digital products to solve people’s problem and make their life easy.

Hire Me