0

Form Validation in Angular 2

AngularJS is one of the greatest frameworks that provides us with a great deal of flexibility and power for building Single Page Applications. One of the magnificent features of AngularJS is the Form Validation. Through the act of decorating input fields with ng- attributes, angular validation is triggered automatically and let users know whether if an input field or a form is valid or not.

AngularJS 2 has been considered an ground-breaking change for developers in comparison with its v1 counterpart. In this article, let check out how the form validation mechanism functions.

1. Form Validation in Angular 1.x

In AngularJS 1.x, each time users change values of a form, AngularJS constantly updates the states of the form and the input fields. Forms and input fields have their respective state (such as $untouched, $pristine, $dirty, $valid, $invalid,..., which you can review in the reference link at the bottom of my article). All of these states are properties of AngularJS FormController.

<!-- form-demo.html -->
<div class="container">
  <div class="row">
    <div class="col-md-8">
      <form class="form-horizontal" name="userForm" ng-submit="onSubmit()" novalidate>
        <div class="form-group">
          <label class="col-md-3" for="name">Name</label>
          <div class="col-md-9">
            <input
              class="form-control"
              type="text"
              id="name"
              name="name"
              ng-min-length="8"
              ng-max-length="20"
              required
            >
            <div ng-if="userForm.name.$touched && userForm.name.$error" class="alert alert-danger">
                <span ng-if="userForm.name.$error.required">Username is required!</span>
                <span ng-if="userForm.name.$error.minlength">Username should be longer than 8 characters!</span>
                <span ng-if="userForm.name.$error.maxlength">Username should be no longer than 20 characters!</span>
            </div>
          </div>
        </div>
        <div class="form-group text-center">
          <button type="submit" name="button" class="btn btn-primary" ng-disabled="!userForm.$valid">Submit</button>
        </div>
      </form>
    </div>
  </div>
</div>

2. Form Validation in Angular 2

2.1. Template-driven Form

In AngularJS 2, we can achieve the same output. But with just "a little more" implementation effort and different syntax:

// form-demo.module.ts
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { FormDemoComponent } from './form-demo/form-demo.component';

@NgModule({
  imports: [ FormsModule ],
  declarations: [ FormDemoComponent ],
  exports: [ FormDemoComponent ]
})
export class FormDemoModule { }
<!-- ./form-demo/form-demo.component.html -->
<div class="container">
  <div class="row">
    <div class="col-md-8">
      <form class="form-horizontal" #userForm="ngForm" ngSubmit="onSubmit()" novalidate>
        <div class="form-group">
          <label class="col-md-3" for="name">Name</label>
          <div class="col-md-9">
             <input 
                 class="form-control"
                  type="text"
                  id="name"
                  name="name"
                  minlength="8"
                  maxlength="20"
                  [(ngModel)]="user.name"
                  #name="ngModel"
                  required
            >
            <div *ngIf="name.errors && (name.dirty || name.touched) class="alert alert-danger">
                <span *ngIf="name.errors.required">Username is required!</span>
                <span *ngIf="name.errors.minlength">Username should be longer than 8 characters!</span>
                <span *ngIf="name.errors.maxlength">Username should be no longer than 20 characters!</span>
            </div>
          </div>
        </div>
        <div class="form-group text-center">
          <button type="submit" name="button" class="btn btn-primary" [disabled]="!userForm.valid">Submit</button>
        </div>
      </form>
    </div>
  </div>
</div>
 // user/user.ts
export class User {
    name: string;

    constructor(name: string) {
        this.name = name;
    }
}
// ./form-demo/form-demo.component.ts
import { Component } from '@angular/core';
import { User } from '../user/user';

@Component({
  selector: 'app-form-demo',
  templateUrl: './form-demo.component.html',
  styleUrls: ['./form-demo.component.scss']
})
export class FormDemoComponent {

  user = new User('Barry');
  constructor() { }


  onSubmit() {
    this.user = new User('Barry');
    //do something
  }
}

Quite straight-forward, everything is shown on the HTML template and you can also understand the behaviors right from the moment you take a glance at the code. This approach is called Template-driven approach or Template-driven form, in which you arrange form elements in the templates and implicit control models (mostly ng...) to provide form functionalities.

There is, indeed, no denying that the code implemented is readable... at the beginning 😦 As the form grows larger, the number of input fields also increase and so do the number of the validation messages that you have to handle, depending on the business requirement of your project. While it seems no big deal as it does not affect the output, this might get out of hand when there are too many input fields and validation rules due to the fact that it takes a lot of HTML in order to represent all possible cases.

Luckily, Angular 2 was born with more unique features & packages which I am about to introduce in the next section. Of course, you can go on and implement like that in Angular 1.x versions but why do thing the old-fashioned way when we can make it easier?

2.2. Reactive Form

Reactive Form takes a different approach in which, the form control model is created inside the component class; the form elements in the HTML template are decorated with directive attribute from the Angular ReactiveFormsModule. At runtime, Angular binds the elements to the control model based on instructions implemented.

// form-demo.module.ts
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';

import { FormDemoComponent } from './form-demo/form-demo.component';

@NgModule({
  imports: [ ReactiveFormsModule ],
  declarations: [ FormDemoComponent ],
  exports: [ FormDemoComponent ]
})
export class FormDemoModule { }
<!-- ./form-demo/form-demo.component.html -->
<div class="container">
  <div class="row">
    <div class="col-md-8 col-md-offset-2 col-sm-8 col-sm-offset-2 col-xs-12">
      <form class="form-horizontal" [formGroup]="userForm" (ngSubmit)="onSubmit()">
        <div class="form-group">
          <label class="col-md-3 col-sm-3" for="name">Name</label>
          <div class="col-md-9 col-sm-9">
            <input
              class="form-control"
              type="text"
              id="name"
              formControlName="name"
              required
            >
            <div *ngIf="formErrors.name" class="alert alert-danger">
              {{ formErrors.name }}
            </div>
          </div>
        </div>
        <div class="form-group text-center">
          <button type="submit" name="button" class="btn btn-primary" [disabled]="!userForm.valid">Submit</button>
        </div>
      </form>
    </div>
  </div>
</div>

Key changes are:

  • No more validation attributes on the form elements (except required for the purpose of css styling and accessibility) as validating now takes place in code.
  • The formControlName replaces the name attribute, which serves the same purpose of correlating the input with the Angular form control.
  • The two-way [(ngModel)] binding is gone. The reactive approach does not involve data binding to manipulate data into and out of the form controls. That's all in code.
// ./form-demo/form-demo.component.ts
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { User } from '../user/user';

@Component({
  selector: 'app-form-demo',
  templateUrl: './form-demo.component.html',
  styleUrls: ['./form-demo.component.scss']
})
export class FormDemoComponent implements OnInit {

  user = new User('Barry');
  userForm: FormGroup;
  formErrors = { name: ''};
  validationMessages = {
    name: {
      required: 'Name is required.',
      minlength: 'Name must be at least 4 characters long.',
      maxlength: 'Name cannot be more than 24 characters long.'
    }
  };

  constructor(private formBuilder: FormBuilder) { }

  ngOnInit() {
    this.buildForm();
  }

  buildForm(): void {
    this.userForm = this.formBuilder.group({
      name: [
        this.user.name, [
          Validators.required,
          Validators.minLength(4),
          Validators.maxLength(24)
        ]
      ]
    });

    this.userForm.valueChanges
      .subscribe(data => this.onValueChanged(data));
  }

  //fired after a field of current form is changed
  onValueChanged(data?: any): void {
    if (!this.userForm) {
      return;
    }
    const form = this.userForm;
    for (const field in this.formErrors) {
      //clear previous error messages (if any)
      this.formErrors[field] = '';
      const control = form.get(field);
      if (control && control.dirty && !control.valid) {
        const messages = this.validationMessages[field];
        for (const key in control.errors) {
          this.formErrors[field] = `${messages[key]} `;
        }
      }
    }
  }

  onSubmit() {
    this.user = new User('Barry');
    //do something
  }
}

The component class is now in charge of defining and managing the form control model. Angular no longer derives the control model from the template (as there is no more [(ngModel)] binding). Hence, you can no longer query for it. Nevertheless, Angular allows creating form control model explicitly with the intervention of the FormBuilder class. Things to make change:

  • Inject FormBuilder into the constructor of the component class.
  • Call the buildForm method in the ngOnInit lifecycle hook method (it is when the user data is retrieved) and call it again in the onSubmit method.
  • The buildForm method uses the FormBuilder formbuilder to declare the form control model, then attaches the onValueChanged handler & calls it immediately to set error messages for the new control model.

The FormBuilder declaration object specifies the three controls of the sample's user form.

Each control spec is a control name with an array value. The first array element is the current value of the corresponding user field. The optional second value is a validator function or an array of validator functions.

3. Benefit of Reactive Form

Clearly, the template got substantially smaller while the component code got substantially larger. Still, it is not the problem as it is not easy to see the benefit when there are only a small number of input fields with validation rules. But consider what happens if the number of input fields and validation rules increases? In general, HTML is harder to read and maintain than code (this is so true!!!).The initial template was already large and threatening to get rapidly worse with the addition of more validation message

elements.

After moving the validation messaging to the component, the template grows more slowly and proportionally. Each field has approximately the same number of lines no matter its number of validation rules. The component also grows proportionally, at the rate of one line per validated field and one line per validation message.

4. References

Angular 2 - Form Control Angular 2 - Form Validation


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí