Angular Form Builder and Validation Management
Angular 1 has the handy ngMessages modules to help manage error messages and validation in forms. This post I’ll show how to build a custom messages component in Angular to easily manage validation similar to ng1’s ngMessages.
Angular has a new helper Class called FormBuilder
. FormBuilder
allows us to explicitly declare forms in our components. This allows us to also explicitly list each form control’s validators.
In our example we are going to build a small form with three inputs, user name, email and profile description.
We will start with looking at our app.module.ts
file.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { ControlMessagesComponent } from './control-messages.component';
import { ValidationService } from './validation.service';
@NgModule({
imports: [BrowserModule, ReactiveFormsModule],
declarations: [ControlMessagesComponent, AppComponent],
providers: [ValidationService],
bootstrap: [AppComponent]
})
export class AppModule {}
In our AppModule
we are registering our components and services for our application. Once registered we can bootstrap our AppModule
for our application. For us to use the form features in this post we will use the ReactiveFormsModule
. To read more about @NgModule
check out the documentation. Now lets take a look at our AppComponent
.
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { ValidationService } from 'app/validation.service';
@Component({
selector: 'demo-app',
templateUrl: 'app/app.component.html'
})
export class AppComponent {
userForm: any;
constructor(private formBuilder: FormBuilder) {
this.userForm = this.formBuilder.group({
name: ['', Validators.required],
email: ['', [Validators.required, ValidationService.emailValidator]],
profile: ['', [Validators.required, Validators.minLength(10)]]
});
}
saveUser() {
if (this.userForm.dirty && this.userForm.valid) {
alert(
`Name: ${this.userForm.value.name} Email: ${this.userForm.value.email}`
);
}
}
}
First we import the FormBuilder
class. We inject it through our App component constructor. In our constructor is the following:
this.userForm = this.formBuilder.group({
name: ['', Validators.required],
email: ['', [Validators.required, ValidationService.emailValidator]],
profile: ['', Validators.required]
});
This creates a new form with our desired controls. The first parameter in the control we leave empty as this lets you initialize your form control with a value. The second parameter can be a list of Validators. Angular has some built in validators such as required
and minLength
. In this example we have our own Validation Service with a few more custom Validators as well. Now that we have our userForm
created lets take a look at our form.
<form [formGroup]="userForm" (submit)="saveUser()">
<label for="name">Name</label>
<input formControlName="name" id="name" #name="ngControl" />
<div *ngIf="name.touched && name.hasError('required')">Required</div>
<label for="email">Email</label>
<input formControlName="email" id="email" #email="ngControl" />
<div *ngIf="email.touched && email.hasError('email')">Invalid</div>
<label for="profile">Profile Description</label>
<input formControlName="email" id="profile" #profile="ngControl" />
<div *ngIf="profile.touched && profile.hasError('required')">Invalid</div>
<button type="submit" [disabled]="!userForm.valid">Submit</button>
</form>
We could do something like this example where we show and hide based on input properties. We can create template variables with the #
syntax. This would work fine but what if our form grows with more controls? Or what if we have multiple validators on our form controls like our email example? Our template will continue to grow and become more and more complex.
So lets look at building a custom component that helps abstract our validation
logic out of our forms. Here is the same form but with our new component.
<form [formGroup]="userForm" (submit)="saveUser()">
<label for="name">Name</label>
<input formControlName="name" id="name" />
<control-messages [control]="userForm.get('name')"></control-messages>
<label for="email">Email</label>
<input formControlName="email" id="email" />
<control-messages [control]="userForm.get('email')"></control-messages>
<label for="profile">Profile Description</label>
<textarea formControlName="profile" id="profile"></textarea>
<control-messages [control]="userForm.get('profile')"></control-messages>
<button type="submit" [disabled]="!userForm.valid">Submit</button>
</form>
Here our control-messages component takes in a reference of the control input to check its validation. This is what the rendered form looks like with our validation.
Here is the example code for our control-messages component.
import { Component, Input } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { ValidationService } from './validation.service';
@Component({
selector: 'control-messages',
template: `
<div *ngIf="errorMessage !== null"></div>
`
})
export class ControlMessages {
errorMessage: string;
@Input() control: FormControl;
constructor() {}
get errorMessage() {
for (let propertyName in this.control.errors) {
if (
this.control.errors.hasOwnProperty(propertyName) &&
this.control.touched
) {
return ValidationService.getValidatorErrorMessage(
propertyName,
this.control.errors[propertyName]
);
}
}
return null;
}
}
Our control-messages component takes in an input property named control which passes us a reference to a formControl. If an error does exist on the form control it looks for that error in our validation service. We store what messages we would like to show in a central location in the validation service so all validation messages are consistent application wide. Our validation service takes in a name of the error and an optional value parameter for more complex error messages.
Here is an example of our validation service:
export class ValidationService {
static getValidatorErrorMessage(validatorName: string, validatorValue?: any) {
let config = {
required: 'Required',
invalidCreditCard: 'Is invalid credit card number',
invalidEmailAddress: 'Invalid email address',
invalidPassword:
'Invalid password. Password must be at least 6 characters long, and contain a number.',
minlength: `Minimum length ${validatorValue.requiredLength}`
};
return config[validatorName];
}
static creditCardValidator(control) {
// Visa, MasterCard, American Express, Diners Club, Discover, JCB
if (
control.value.match(
/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/
)
) {
return null;
} else {
return { invalidCreditCard: true };
}
}
static emailValidator(control) {
// RFC 2822 compliant regex
if (
control.value.match(
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
)
) {
return null;
} else {
return { invalidEmailAddress: true };
}
}
static passwordValidator(control) {
// {6,100} - Assert password is between 6 and 100 characters
// (?=.*[0-9]) - Assert a string has at least one number
if (control.value.match(/^(?=.*[0-9])[a-zA-Z0-9!@#$%^&*]{6,100}$/)) {
return null;
} else {
return { invalidPassword: true };
}
}
}
In our service we have our custom validators and a list of error messages with
corresponding text that should be shown in given use case.
Our control-messages component can now be used across our application and help
prevent us from writing extra markup and template logic for validation messages.
Read more about Angular forms in the documentation.