Introduction
One of the most common features in any web application is providing a form to users to input some data. You use forms daily to log in, register, place orders, etc.
Processing user inputs before validating can have serious consequences. You may end up storing invalid data like an incorrect date, email, age, etc. It could also be a security issue due to attacks like Cross-Site Scripting (XSS).
The traditional way to validate HTML forms is by using JavaScript or JQuery. Unfortunately, this approach warrants a bunch of code.
Angular, being a full-fledged framework, has provided excellent support for validating user inputs and displaying validation messages. It has lots of commonly used built-in validators that you can take advantage of, or you can even write your custom validators.
Forms in Angular
An Angular form is a regular HTML form with few additional features. For each field (input, radio, select, etc.) in the form, we need an object of the FormControl
class. The FormControl
object gives information about that field. Its value
, if the value is valid
, and if it is not valid what are the validation errors
, etc.
It also provides the state of the field such as touched
, untouched
, dirty
, pristine
, etc.
Similarly, a FormGroup
is the collection of the FormControl
objects. Every Angular form has at least one FormGroup
. You may decide to have multiple FormGroup
s in use-cases like separating the handling of personal details and professional details sections of a user registration form.
All the properties of a FormGroup
(valid
, error
, etc.) are also available to the FormControl
. For instance, the valid
property of a FormControl
will return true
if all FormControl
instances are valid.
So to add validation to an Angular form we need two things:
- At least one
FormGroup
object for the form - A
FormControl
object for each field in the form
There are two different ways by which these control objects can be created. We can provide some directives in the template of the form and Angular can create such controls under the hood for us. Forms created by this way are called template-driven forms.
If we have some special use cases and we want more control over the form we can explicitly create such control objects. Forms created this way are called reactive forms.
Template-Driven Forms
In template-driven forms, we apply the ngModel
directive for every field in the template. Angular creates a FormControl
object under the hood for each such field and associate it with the respective field:
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name"
ngModel name="name">
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username"
ngModel name="username">
</div>
Note: With ngModel
, it is required to provide either the name
attribute or define the FormControl
as "standalone" in ngModelOptions
, otherwise Angular will throw an error.
Also, in app.module.ts
you would need to add FormsModule
to the array of imports:
import { FormsModule } from '@angular/forms';
// ...some other imports
imports: [
//...some other imports
FormsModule
]
Validation in Template-Driven Forms
Angular has provided some built-in validators to validate common use cases. In order to use built-in validators, you would need to apply validation attributes to each form field where you want some validation. These validation attributes are the same as the regular HTML5 validation attributes like required
, minlength
, maxlength
, etc. Under the hood, Angular has provided directives to match these attributes with the validator functions defined in the Angular framework.
Whenever a FormControl
's value changes, Angular generates a list of validation errors by running validation. If the list is empty it means it is a valid status, otherwise, it is an invalid status.
Let's say we want to put the following validations into it:
- As the fields Name and Username have the
required
attribute, we want to display a validation message if this field is left empty. - The Name field should have a value whose
minlegth
andmaxlength
should be 2 and 30 characters respectively. - If the username has spaces, display an invalid username message.
For every form-control in which we want to add validation, we need to add appropriate validation attributes and export ngModel
to a local template variable:
<input type="text" class="form-control" id="name"
required maxlength="30" minlength="2"
ngModel name="name" #name="ngModel">
In the above example, we have used the following built-in validators - required
, minlength
, and maxlength
.
We can use the template variable name
in the template to check for validation states of the used validators:
<div *ngIf="name.invalid && (name.dirty || name.touched)"
class="alert alert-danger">
<div *ngIf="name.errors.required">
Name is required.
</div>
<div *ngIf="name.errors.minlength">
Name cannot be more than 30 characters long.
</div>
<div *ngIf="name.errors.minlength">
Name must be at least 2 characters long.
</div>
</div>
As we've used a conditional statement to render the first div
, it'll only be displayed if the status of the built-in validator is invalid
. We've explained at the start of the section how the status is determined as valid
or invalid
.
Similarly, the inner div's
will be displayed only if the template variable name
has a property errors
and the errors
property has one of the following properties - required
, minlength
and maxlength
and the property value is true
. We've already discussed how the template variable binds to the ngModel
directive and it receives these properties every time there is any change in the form control and after Angular runs the validation for that field.
Note: It is important to check for dirty
and touched
states, otherwise the error message will be displayed the very first time the page is loaded, which is bad for user experience. We need the validation message to be displayed in one of the following conditions:
- The user changes some value, i.e the field is dirty (
formControlObject.dirty
) - The user uses tab or clicks to switch focus to some other element, i.e the field was touched (
formControlObject.touched
)
If you want to refer a full list of Angular's built-in validators, you may follow the Validators API.
Writing a Custom Validator
Sometimes the built-in validators may not cover your exact use-case. In this case, you may need to create your custom validator function.
A validator function implements the ValidatorFn
interface, which means it should have the signature:
interface ValidatorFn {
(control: AbstractControl): ValidationErrors | null
}
The ValidationErrors
should be an object that has one or more key-value pairs:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
type ValidationErrors = {
[key: string]: any;
};
The key should be a string and is used to denote the type of validation error like invalidEmail
, required
, etc. The value can be anything and is used to supply more information about the validation error.
For the above example, we want to write a custom validation function that validates if there are no spaces in the username.
While technically we can write this function anywhere in the application, it is always good practice to put all related validator functions inside a separate class:
import { ValidationErrors, AbstractControl } from '@angular/forms';
export class UserRegistrationFormValidators {
static usernameShouldBeValid(control: AbstractControl): ValidationErrors | null {
if ((control.value as string).indexOf(' ') >= 0) {
return { shouldNotHaveSpaces: true }
}
// If there is no validation failure, return null
return null;
}
}
Note: In this example, we have returned true
as the value of key shouldNotHaveSpaces
because we do not need to provide any details. In some cases you may need to provide details, for example:
return { maxlengthExceeded: {
maxLength: 20,
actual: control.value.length
}
}
Next, we can use this validator function UserRegistrationFormValidators.usernameShouldBeValid
for the username
form-control in our template-driven form:
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username"
required
UserRegistrationFormValidators.usernameShouldBeValid
[(ngModel)]="person.username" name="username">
</div>
Reactive Forms
In reactive forms, we create FormControl
objects explicitly in the component of that template. Here is the regular HTML form without any ngModel
directive or validations:
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name">
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username">
</div>
Let us assume we want to convert our template-driven form from the previous example into a reactive form.
For this, first, we need to explicitly create FormGroup
and FormControls
for each field in the component of the template:
form = new FormGroup({
'name': new FormControl(),
'username': new FormControl(),
})
Note: As discussed earlier, a form can have more than one FormGroup
. In this case, we can have a nested structure:
registrationForm = new FormGroup({
'personalDetailsForm': new FormGroup({
'name': new FormControl()
})
})
You can read more about FormGroup
in the Angular documentation.
Let me bring your attention back to our use-case.
Next, we need to associate these FormControl
objects to the fields in the HTML form.
<form [formGroup]="registrationForm">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name"
[formControlName]="name">
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username"
[formControlName]="username">
</div>
<form>
Here we applied the formGroup
directive and associated it with the FormGroup
object registrationForm
that we created in the component. We also associated the formControlName
directive with the respective FormControl
objects name
and username
.
Note: The directives to build reactive forms are defined in ReactiveFormsModule
. So if you get an error such as:
Can't bind to formGroup
...then you should check if you have imported that ReactiveFormsModule
in your main module app.module.ts
.
Validations in Reactive Forms
In reactive forms, we do not pass the ngModel
directive and we also do not use HTML5 validation attributes. We specify validators while creating the objects of the FormControl
in the component itself.
Here is the signature of the FormControl
class:
class FormControl extends AbstractControl {
constructor(formState: any = null, validatorOrOpts?: ValidatorFn | AbstractControlOptions | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[])
// ...
}
As we can see the first parameter is the initial state of the control which can be kept empty i.e ''
. The second parameter is ValidatorFn
.
To add the built-in validator functions for a FormControl
we can pass it the appropriate ValidatorFn
. For the following example we've used the following built-in validators required
, minLength
, and maxLength
- :
registrationForm = new FormGroup({
'name': new FormControl('Enter your name', [
Validators.required,
Validators.minLength(2),
Validators.maxLength(30)
]),
'username': new FormControl('', Validators.required),
})
Note: You would need to import Validators
in the component.
Please also notice, unlike Template-driven forms we do not use the validation attributes. We use the respective ValidatorFn
like Validators.required
, Validators.minLength(2) etc. Your code editor may provide auto-complete for all ValidatorFn
the moment you type Validators
followed by a dot .
.
We can go back to the template and write validation messages:
<form [formGroup]="registrationForm">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name"
[formControlName]="name">
<div *ngIf="registrationForm.get('name').invalid && (registrationForm.get('name').dirty || registrationForm.get('name').touched)"
class="alert alert-danger">
<div *ngIf="registrationForm.get('name').errors.required">
Name is required.
</div>
<div *ngIf="registrationForm.get('name').errors.minlength">
Name cannot be more than 30 characters long.
</div>
<div *ngIf="registrationForm.get('name').errors.minlength">
Name must be at least 2 characters long.
</div>
</div>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username"
[formControlName]="username">
</div>
<form>
Custom validators for Reactive forms
We need to write the custom validator function the same way as we did it for the Template-Driven form section. We can use the same custom validator function UserRegistrationFormValidators.usernameShouldBeValid
in the component for the reactive form:
registrationForm = new FormGroup({
'name': new FormControl('Enter your name', [
Validators.required,
Validators.minLength(2),
Validators.maxLength(30)
]),
'username': new FormControl('', [
Validators.required,
UserRegistrationFormValidators.usernameShouldBeValid
]),
})
Conclusion
In this tutorial, we explored the two different ways to handle user inputs - Template-Driven and Reactive forms. We learned how to put validation on both types of forms. And finally, we also wrote our custom validator function and included it with the built-in validators.
As we can see Angular has great support for forms and provides some under-the-hood useful features to validate forms. Providing every single feature with Angular forms is beyond the scope of this tutorial. You may read the Angular documentation for complete information.