17 Feb 2018
cat: Angular 2+
0 Comments

Angular 4 – Validation Errors

Introduction

Whenever we show a form to the user we also give users some feedback based on their inputs in a form of validation errors. In Angular this is very easy to implement especially when we use reactive forms. We use a bunch of HTML and CSS to show users a nice looking validation errors like bootstrap. Over time you realize you repeat the same chunk of codes over and over and the codes become bloated.

In this tutorial we will create a basic form using reactive forms. Next we will create a better way to show validation error messages. This will be helpful in applications where the use of form fields are used everywhere.

Setup

First, we need import bootstrap to our project since we will be using some CSS classes for the validation errors. For now we let’s just use a CDN and link it to src/index.html.

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <link rel="stylesheet"
        href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
  <title>ErrorUi</title>
  <base href="/">

  <meta name="viewport"
        content="width=device-width, initial-scale=1">
  <link rel="icon"
        type="image/x-icon"
        href="favicon.ico">
</head>

<body>
  <app-root></app-root>
</body>

</html>

Second we need to import the ReactiveFormsModule to our AppModule. Open src/app/app.module.ts, then add these codes.

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

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

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

Reactive Forms

Now we can create a sample registration form. In src/app/app.component.ts we create and instance of a FormGroup.

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {

  registrationForm: FormGroup;

  // Inject FormBuilder and use it to create FormGroup, FormControl, and FormArray.
  constructor(
    private formBuilder: FormBuilder
  ) {
  }

  ngOnInit() {
    this.createForm();
  }

  createForm() {
    // Create the password field separately so we can pass it to our custom validator.
    let passwordControl = this.formBuilder.control('', [Validators.required, Validators.minLength(6)]);

    this.registrationForm = this.formBuilder.group({
      name: ['', [Validators.required]],
      email: ['', [Validators.required, Validators.email]],
      password: passwordControl,
      // We pass the password field to compare with password confirm field.
      passwordConfirm: ['', [this.validatePasswordConfirm(passwordControl)]]
    });
  }

  // Accept an instance of FormControl to match against the FormControl it is validating.
  validatePasswordConfirm(passwordControl: FormControl) {
    return (passwordConfirmControl: FormControl) => {

      // Whenever the passwordControl changes, we update the value and validity or passwordConfirmControl.
      passwordControl.valueChanges.subscribe(() => {
        passwordConfirmControl.updateValueAndValidity();
      });

      // If the 2 fields' values are not equal then we return an error.
      if (passwordConfirmControl.value != passwordControl.value) {
        return {
          validatePasswordConfirm: {
            valid: false
          }
        }
      }
    };
  }

}

src/app/app.component.html

<div class="container mt-3">
  <div class="col-md-6 offset-md-3">

    <form [formGroup]="registrationForm">
      <div class="form-group">
        <label for="name">Name</label>
        <input type="text"
               class="form-control"
               id="name"
               placeholder="Full Name"
               formControlName="name"
               [ngClass]="{'is-invalid': registrationForm.get('name').invalid && registrationForm.get('name').dirty}" />
        <div class="invalid-feedback"
             *ngIf="registrationForm.get('name').hasError('required') && registrationForm.get('name').dirty">
          Name is required.
        </div>
      </div>
      <div class="form-group">
        <label for="email">Email address</label>
        <input type="email"
               class="form-control"
               id="email"
               placeholder="Enter email"
               formControlName="email"
               [ngClass]="{'is-invalid': registrationForm.get('email').invalid && registrationForm.get('email').dirty}" />
        <div class="invalid-feedback"
             *ngIf="registrationForm.get('email').hasError('required') && registrationForm.get('email').dirty">
          Email address is required.
        </div>
        <div class="invalid-feedback"
             *ngIf="registrationForm.get('email').hasError('email') && registrationForm.get('email').dirty">
          Please enter a valid email.
        </div>
      </div>
      <div class="form-group">
        <label for="password">Password</label>
        <input type="password"
               class="form-control"
               id="password"
               placeholder="Password"
               formControlName="password"
               [ngClass]="{'is-invalid': registrationForm.get('password').invalid && registrationForm.get('password').dirty}" />
        <div class="invalid-feedback"
             *ngIf="registrationForm.get('password').hasError('required') && registrationForm.get('password').dirty">
          Password is required.
        </div>
        <div class="invalid-feedback"
             *ngIf="registrationForm.get('password').hasError('minlength') && registrationForm.get('password').dirty">
          Password must be at least 6 characters.
        </div>
      </div>
      <div class="form-group">
        <label for="passwordConfirm">Confirm Password</label>
        <input type="password"
               class="form-control"
               id="passwordConfirm"
               placeholder="Password"
               formControlName="passwordConfirm"
               [ngClass]="{'is-invalid': registrationForm.get('passwordConfirm').invalid && registrationForm.get('passwordConfirm').dirty}"
        />
        <div class="invalid-feedback"
             *ngIf="registrationForm.get('passwordConfirm').hasError('validatePasswordConfirm') && registrationForm.get('passwordConfirm').dirty">
          Must be equal to password.
        </div>
      </div>
      <button type="submit"
              class="btn btn-primary">Submit</button>
    </form>

  </div>
</div>

In the previous code, we gave the form tag the registrationForm. This will allow us to specify what FormControl an input field will bind to using the formControlName directive. We also give each input fields an is-invalid CSS class from bootstrap when a certain condition is met. In this case if the FormControl is invalid and dirty.

We also show a block of HTML to show what kind of error a FormControl has. As an example, if the “email” field is not a valid email, we show a “Please enter a valid email.” text to the user.

The codes is repetitive, hard to maintain, and adds up to the size of the application when building.

Directive

To clean up the input tags we will create a directive that will give it the is-invalid CSS class automatically. I created the directive using Angular CLI.

ng generate directive my-input --spec=false

This is the code for the new directive.

import { Directive, Self, SkipSelf, Host } from '@angular/core';
import { NgControl, FormGroupDirective } from '@angular/forms';

@Directive({
  selector: '[MyInput]',
  host: {
    '[class.is-invalid]': 'isInvalid'
  }
})
export class MyInputDirective {

  constructor(
    @Host() @SkipSelf() private form: FormGroupDirective,
    @Self() private control: NgControl
  ) {
  }

  get isInvalid() {
    return this.control.invalid && (this.control.dirty || this.form.submitted);
  }

}

First we inject instances of FormGroupDirective and NgControl. FormGroupDirective is the class for [formGroup], so it will give us data about the registrationForm. We decorate it with @Host() and @SkipSelf() so it will only find providers in our directive’s parent.

We get an instance of NgControl only from within the directive itself and where it is attached using @Self. NgControl is the base class for the directives [formControlName], [formGroupName], and [formArrayName]. So control property will give us data about the form control it is attached to.

Next we use these injected properties in the isInvalid getter to determine if the input field is invalid or not.

To use the directive we just created, just add a MyInput attribute to the input tags and remove the [ngClass] attribute.

<!-- ommited for brevity -->
<input MyInput
       type="text"
       class="form-control"
       id="name"
       placeholder="Full Name"
       formControlName="name" />
<!-- ommited for brevity -->

Validation Errors

Finally we will create a better way to show the validation errors. To do that we will create a new component.

ng generate component my-error --flat --spec=false -is

Update the src/app/my-error.component.ts

import { Component, Input, Host, SkipSelf } from '@angular/core';
import { FormGroupDirective } from '@angular/forms';

@Component({
  selector: 'my-error',
  templateUrl: './my-error.component.html'
})
export class MyErrorComponent {

  @Input() controlName: string;
  @Input() errorKey: string;

  constructor(
    @Host() @SkipSelf() private form: FormGroupDirective
  ) {
  }

  get isInvalid() {
    let control = this.form.form.get(this.controlName);
    return control.hasError(this.errorKey) && (control.dirty || this.form.submitted);
  }

}

First we declare some properties with @Input() decorator. This will allow our component to have attributes named “controlName” and “errorKey”. We use the controlName to get any instance of FormControl from the FormGroup. FormGroupDirective has a public form property that holds the FormGroup instance and we can use that to resolve the FormControl.

For the template src/app/my-error.component.html

<div class="invalid-feedback"
     *ngIf="isInvalid"
     style="display: block;">
  <ng-content></ng-content>
</div>

I added a style because the div tag will now be inside a my-error tag and bootstrap will not work properly. Then to show the error message we just render it using ng-content.

We can now use our new component.

<!-- omitted for brevity -->
<div class="form-group">
    <label for="email">Email address</label>
    <input MyInput
           type="email"
           class="form-control"
           id="email"
           placeholder="Enter email"
           formControlName="email" />
    <my-error controlName="email"
              errorKey="required">Email Address is required.</my-error>
    <my-error controlName="email"
              errorKey="email">Please enter a valid email.</my-error>
</div>
<!-- omitted for brevity -->

That is it for this tutorial. Thanks for reading.

Resources and links

Be the first to write a comment.

Post A Comment