Mastering Strictly Typed Reactive Forms in Angular: A Step-by-Step Guide

Jollen Moyani - Jan 18 '23 - - Dev Community

In this article, we will learn how to create strictly typed reactive forms in Angular. We will create a student registration form as an example. The form will have support for built-in form validations. We will also create asynchronous custom validators for the form.

The following custom validations will be implemented on the reactive form:

  • Password pattern validation.
  • Match passwords in two different fields.
  • Check for username availability.

Prerequisites

We will set up the Angular development environment on our machine:

  1. Install Node.js.
  2. Install the latest version of Angular CLI.

In this tutorial, we will be using Visual Studio Code as our preferred IDE. Please install the latest version from the Visual Studio Code download page.

Creating the Angular application

Use the Angular CLI to create a new Angular application.

Open a command window inside the folder where you want to create the application. Run the following command.

ng new typed-reactive-forms --routing=false --style=scss
Enter fullscreen mode Exit fullscreen mode

The routing option allows us to create a routing module for the application. However, this is a small demo app. So, we do not need to add a separate module for routing. The style option helps us to specify the file extension or preprocessor that we use for style files.

Run the following command to change the directory to the new project.

cd typed-reactive-forms
Enter fullscreen mode Exit fullscreen mode

Open the project in VS Code using the following command.

code .
Enter fullscreen mode Exit fullscreen mode

Install the Syncfusion Angular UI Components

We will use the Syncfusion Angular components for styling the form. Run the following command to install the necessary NuGet packages to create a form.

npm i @syncfusion/ej2-angular-buttons @syncfusion/ej2-angular-inputs
Enter fullscreen mode Exit fullscreen mode

Configure the App module

Now, import the modules for reactive forms and the Syncfusion Angular components in the app module file.

Add the following code in the app.module.ts file.

import { ReactiveFormsModule } from '@angular/forms';
import { ButtonModule } from '@syncfusion/ej2-angular-buttons';
import { TextBoxModule } from '@syncfusion/ej2-angular-inputs';
...
@NgModule({
  ...
  imports: [
    ...
    ReactiveFormsModule,
    ButtonModule,
    TextBoxModule,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Create the custom validation service

We will use a service to create custom validators for our reactive form.

Run the following command to create a new service.

ng g s services/custom-form-validator
Enter fullscreen mode Exit fullscreen mode

Executing this command will create a folder named services and create the service file custom-form-validator.service.ts inside it.

Update the CustomFormValidatorService class by adding the following code.

export class CustomFormValidatorService {
  passwordPatternValidator(): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      if (!control.value) {
        return null;
      }
      const regex = new RegExp('^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$');
      const validPattern = regex.test(control.value);
      return validPattern ? null : { invalidPassword: true };
    };
  }

  matchPasswordValidator(password: string, confirmPassword: string) {
    return (formGroup: AbstractControl): ValidationErrors | null => {
      const passwordControl = formGroup.get(password);
      const confirmPasswordControl = formGroup.get(confirmPassword);

      if (!passwordControl || !confirmPasswordControl) {
        return null;
      }

      if (
        confirmPasswordControl.errors &&
        !confirmPasswordControl.errors['passwordMismatch']
      ) 
      {
        return null;
      }

      if (passwordControl.value !== confirmPasswordControl.value) {
        confirmPasswordControl.setErrors({ passwordMismatch: true });
        return { passwordMismatch: true };
      } 
      else {
        confirmPasswordControl.setErrors(null);
        return null;
      }
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, we use the passwordPatternValidator function to validate the strength of the password entered by the user. This function will accept a parameter of type AbstractControl. In this function, we will use regex to check for the strength of the password based on the following criteria:

  • Must have a minimum of eight characters.
  • It should have at least one lowercase letter.
  • It should have at least one uppercase letter.
  • It should contain at least one number.

In case the password entered by the user fails the regex check, we will set the invalidPassword property to true.

The function matchPasswordValidator helps us to check if the password entered in two fields of the form is an exact match.

Create asynchronous validator

An asynchronous validator helps us to validate the values entered in the form field against an asynchronous source of data, such as a value coming via an HTTP call.

Run the following command to create a new service.

ng g s services\user-name-validation
Enter fullscreen mode Exit fullscreen mode

Update the UserNameValidationService class by adding the following code.

export class UserNameValidationService implements AsyncValidator {
  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    return this.validateUserName(control.value).pipe(
      map((duplicateUserName) => {
        if (duplicateUserName) {
          return { userNameNotAvailable: true };
        } 
        else {
          return null;
        }
      }),
      catchError(() => of(null))
    );
  }

  private validateUserName(username: string): Observable<boolean> {
    const UserList = ['admin', 'user', 'superuser'];
    return of(UserList.includes(username.toLocaleLowerCase()));
  }
}
Enter fullscreen mode Exit fullscreen mode

A custom async validator must implement the AsyncValidator interface and should override the validate() function provided by the interface.

The UserNameValidationService helps us to validate if the username entered in the form is available or not. The validate function will accept a parameter of type AbstractControl.

The validateUserName function is used to match the value entered in the form against a static array. If the value entered by the user is already present in the array, we will set the userNameNotAvailable property to true.

Note: For the simplicity of this blog, we are verifying the availability of the username against a list of static values. However, in an ideal scenario, we should check this against a database via an API call.

Create the interface

Add a new folder named model inside the src folder. Create a file named studentRegistration.ts and add the following code inside it.

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

export interface StudentRegistration {
  firstName: FormControl<string>;
  lastName: FormControl<string>;
  email: FormControl<string>;
  username: FormControl<string>;
  password: FormControl<string>;
  confirmPassword: FormControl<string>;
  age: FormControl<number>;
}
Enter fullscreen mode Exit fullscreen mode

So, we have created an interface to define the type of our form. This interface will contain all the fields of the form.

Creating the registration component

Run the following command to create the registration component. This component will allow us to register a new student’s record.

ng g c registration
Enter fullscreen mode Exit fullscreen mode

Add the following code inside the RegistrationComponent class.

protected studentRegistrationForm!: FormGroup<StudentRegistration>;
protected submitted = false;

constructor(
    private readonly formBuilder: NonNullableFormBuilder,
    private readonly customFormValidator: CustomFormValidatorService,
    private readonly userNameValidationService: UserNameValidationService
) 
{
   this.initializeForm();
}

private initializeForm(): void {
  this.studentRegistrationForm = this.formBuilder.group(
  {
     firstName: this.formBuilder.control('', Validators.required),
     lastName: this.formBuilder.control('', Validators.required),
     email: this.formBuilder.control('', [
         Validators.required,
         Validators.email,
     ]),
     username: this.formBuilder.control('', {
       asyncValidators: [
         this.userNameValidationService.validate.bind(
           this.userNameValidationService
         ),
       ],
       validators: [Validators.required],
       updateOn: 'blur',
     }),
     password: this.formBuilder.control('', [
       Validators.required,        
       this.customFormValidator.passwordPatternValidator(),
     ]),
     confirmPassword: this.formBuilder.control('', Validators.required),
     age: this.formBuilder.control(14, [
        Validators.required,
        Validators.min(14),
        Validators.max(25),
     ]),
  },
  {
    validators: [
      this.customFormValidator.matchPasswordValidator(
        'password',
        'confirmPassword'
      ),
    ],
  }
 );
}
Enter fullscreen mode Exit fullscreen mode

Now, we have created a strongly typed FormGroup of type StudentRegistration.

The NonNullableFormBuilder class helps us to construct a nonnullable form control. If we invoke the reset function on a nonnullable form control, then the value will be set to the initial value.

Here, all the form controls are marked as required. The email form control has a validation to match the valid email address.

The username control has both synchronous and asynchronous validations attached to it. An asynchronous validation always happens after the synchronous validation, and it is performed only if the synchronous validation is successful.

We have invoked the custom passwordPatternValidator on the password control.

The age form control has the min and max values defined, and this field will only accept a value within the given range.

A cross-field validator is a custom validator that can compare the values of multiple form controls. We have added the cross-field validator to the password and confirmPassword fields to match their values.

Using the NonNullableFormBuilder class to construct the form is optional. If you want to allow nullable values for your form control, then you can use the FormBuilder class. However, to allow the nullable values, you should update the interface as shown in the following. Everything else should remain the same.

export interface StudentRegistration {
  firstName: FormControl<string | null>;
  lastName: FormControl<string | null>;
  email: FormControl<string | null>;
  username: FormControl<string | null>;
  password: FormControl<string | null>;
  confirmPassword: FormControl<string | null>;
  age: FormControl<number | null>;
}
Enter fullscreen mode Exit fullscreen mode

Add the onSubmit function, as shown.

protected onSubmit(): void {
    this.submitted = true;
    if (this.studentRegistrationForm.valid) {
      alert('Form submitted successfully!!!');
      console.table(this.studentRegistrationForm.value);
    }
  }
Enter fullscreen mode Exit fullscreen mode

This function will check whether the form is valid and then print the form values on the browser console. In a real-world application, we can invoke an HTTP call and send the form data to an API.

Finally, add the following two functions.

protected resetForm(): void {
    this.studentRegistrationForm.reset();
}
protected get registrationFormControl() {
    return this.studentRegistrationForm.controls;
}
Enter fullscreen mode Exit fullscreen mode

The resetForm function is used to reset the values of the form control. The registrationFormControl is a getter function used to get the values of the form control.

Open registration.component.html and add the following code to it.

<div class="form-container">
  <div class="title-container">
    <h1>Student Registration</h1>
  </div>

  <div class="card-layout">
    <div class="e-card">
      <div class="e-card-content">
        <form [formGroup]="studentRegistrationForm" (ngSubmit)="onSubmit()">
          <div class="e-input-section">
            <ejs-textbox
              placeholder="First name"
              cssClass="e-outline"
              floatLabelType="Auto"
              formControlName="firstName"
            ></ejs-textbox>
          </div>
          <div
            *ngIf="(registrationFormControl.firstName.touched || submitted) 
            && registrationFormControl.firstName.errors?.['required']"
            class="e-error"
          >
            First name is required.
          </div>

          <div class="e-input-section">
            <ejs-textbox
              placeholder="Last name"
              cssClass="e-outline"
              floatLabelType="Auto"
              formControlName="lastName"
            ></ejs-textbox>
          </div>
          <div
            *ngIf="(registrationFormControl.lastName.touched || submitted) 
            && registrationFormControl.lastName.errors?.['required']"
            class="e-error"
          >
            Last name is required.
          </div>

          <div class="e-input-section">
            <ejs-textbox
              placeholder="Email"
              cssClass="e-outline"
              floatLabelType="Auto"
              formControlName="email"
            ></ejs-textbox>
          </div>
          <div
            *ngIf="(registrationFormControl.email.touched || submitted) 
            && registrationFormControl.email.errors?.['required']"
            class="e-error"
          >
            Email is required.
          </div>
          <div
            *ngIf="registrationFormControl.email.touched &&
            registrationFormControl.email.errors?.['email']"
            class="e-error"
          >
            Enter a valid email address.
          </div>

          <div class="e-input-section">
            <ejs-textbox
              placeholder="User Name"
              cssClass="e-outline"
              floatLabelType="Auto"
              formControlName="username"
            ></ejs-textbox>
          </div>
          <div
            *ngIf="(registrationFormControl.username.touched || submitted) 
            && registrationFormControl.username.errors?.['required']"
            class="e-error"
          >
            User Name is required.
          </div>
          <div
            *ngIf="registrationFormControl.username.touched &&
            registrationFormControl.username.errors?.['userNameNotAvailable']"
            class="e-error"
          >
            User Name is not available.
          </div>

          <div class="e-input-section">
            <ejs-textbox
              type="password"
              placeholder="Password"
              cssClass="e-outline"
              floatLabelType="Auto"
              formControlName="password"
            ></ejs-textbox>
          </div>
          <div
            *ngIf="(registrationFormControl.password.touched || submitted) 
            && registrationFormControl.password.errors?.['required']"
            class="e-error"
          >
            Password is required.
          </div>
          <div
            *ngIf="registrationFormControl.password &&
            registrationFormControl.password.errors?.['invalidPassword']"
            class="e-error"
          >
            Password should have minimum 8 characters, at least 1 uppercase
            letter, 1 lowercase letter and 1 number.
          </div>

          <div class="e-input-section">
            <ejs-textbox
              type="password"
              placeholder="Confirm password"
              cssClass="e-outline"
              floatLabelType="Auto"
              formControlName="confirmPassword"
            ></ejs-textbox>
          </div>
          <div
            *ngIf="(registrationFormControl.confirmPassword.touched || submitted) 
            && registrationFormControl.confirmPassword.errors?.['required']"
            class="e-error"
          >
            Confirm password is required.
          </div>
          <div
            *ngIf="registrationFormControl.confirmPassword.touched &&
            registrationFormControl.confirmPassword.errors?.['passwordMismatch']"
            class="e-error"
          >
            Passwords does not match.
          </div>

          <div class="e-input-section">
            <ejs-textbox
              type="number"
              placeholder="Age"
              cssClass="e-outline"
              floatLabelType="Auto"
              formControlName="age"
            ></ejs-textbox>
          </div>
          <div
            *ngIf="(registrationFormControl.age.touched || submitted) &&
            registrationFormControl.age.errors?.['required']"
            class="e-error"
          >
            Age is required.
          </div>
          <div
            *ngIf="(registrationFormControl.age.touched || submitted) && 
            (registrationFormControl.age.errors?.['min'] || registrationFormControl.age.errors?.['max'])"
            class="e-error"
          >
            Age should be greater than 14 years and less than 25 years.
          </div>

          <div class="e-card-actions">
            <button type="submit" ejs-button cssClass="e-success">
              Register
            </button>
            <button
              type="reset"
              ejs-button
              cssClass="e-warning reset-button"
              (click)="resetForm()"
            >
              Reset
            </button>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

We have created a form and added the error message to be displayed when there are errors in the form control. We have used the formControlName property to bind the form fields in the template to the form controls of our FormGroup studentRegistrationForm.

The onSubmit function will be invoked when we submit the form. Clicking the Reset button will invoke the resetForm function.

Update the app component

Open the app.component.html file and replace the existing code with the selector for the registration component, as shown.

<app-registration></app-registration>
Enter fullscreen mode Exit fullscreen mode

Execution demo

Run the following command to start the application.

ng serve
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:4200/ in the browser. You can see the output displayed in the following.

Creating Student Registration Form

Resource

The complete source code of this application is available on GitHub.

The application is hosted at https://aquamarine-cassata-be3458.netlify.app/. Navigate to the website and play around for a better understanding.

Summary

Thanks for reading! In this blog, we learned how to create strictly typed reactive forms, with an example of a student registration form. We implemented built-in as well as custom validations on the form.

Syncfusion’s Angular UI component library is the only suite you will ever need to build an app. It contains over 75 high-performance, lightweight, modular, and responsive UI components in a single package.

For existing customers, the newest Essential Studio version is available for download from the License and Downloads page. If you are not yet a Syncfusion customer, you can try our 30-day free trial to check out the available features. Also, check out our demos on GitHub.

If you have questions, you can contact us through our support forums, support portal, or feedback portal. We are always happy to assist you!

Related blogs

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .