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.