This is the third part of the series on creating forms in Angular. In the first two tutorials, we used Angular’s template-driven and model-driven approach to create forms. However, while detailing both the approaches, there was something that we didn’t cover—custom validator functions. This tutorial will cover everything you need to know about writing custom validators that meet your requirements.
Prerequisites
You don’t need to have followed part one or two of this series for part three to make sense. However, if you are entirely new to forms in Angular, you should head over to the first tutorial of this series and start from there.
Otherwise, grab a copy of this code from our GitHub repo and use that as a starting point.
Built-in Validators
Angular doesn’t boast a huge built-in validator library. As of Angular 4, we have the following popular validators in Angular:
- required
- minlength
- maxlength
- pattern
There are actually a few more, and you can see the full list in the Angular docs.
We can use the above built-in validators in two ways:
1. As directives in template-driven forms.
2. As validators inside the FormControl
constructor in model-driven forms.
name = new FormControl('', Validators.required)
If the above syntax doesn’t make sense, follow my previous tutorials on building a signup form using a template-driven approach or a model-driven approach and then drop back!
The built-in form validators hardly cover all the validation use cases that might be required in a real-world application. For instance, a signup form might need to check whether the values of the password and confirm password control fields are equal and display an error message if they don’t match. A validator that blacklists emails from a particular domain is another common example.
Here is a fact: Template-driven forms are just model-driven forms underneath. In a template-driven form, we let the template take care of the model creation for us. The obvious question now is, how do you attach a validator to a form?
Validators are just functions. In a model-driven form, attaching validators to FormControl is straightforward. In a template-driven form, however, there is a bit more work to be done. In addition to the validator function, you will need to write a directive for the validator and create instances of the directive in the template.
Diving Into the Details
Although this has been already covered, we will go through a quick recap of the code for the signup form. First, here’s the reactive approach.
app/signup-form/signup-form.component.ts
// Use the formbuilder to build the Form model this.signupForm = this.fb.group({ email: ['',[Validators.required, Validators.pattern('[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,3}$')]], password: this.fb.group({ pwd: ['', [Validators.required, Validators.minLength(8)]], confirmPwd: ['', [Validators.required, Validators.minLength(8) ]] }, { validator: PasswordMatch }), gender: ['', Validators.required], })
FormBuilder
is a syntax sugar that creates the FormGroup
and FormControl
instances. A FormControl
tracks the value and the validation status of an individual form element. A FormGroup
, on the other hand, comprises a group of FormControl
instances, and it tracks the value and validity of the whole group.
Here’s the structure that we have been following:
FormGroup -> 'signupForm' FormControl -> 'email' FormGroup -> 'password' FormControl -> 'pwd' FormControl -> 'confirmPwd' FormControl -> 'gender'
Depending on the requirements, we can attach a validator to a FormControl
or a FormGroup
. An email blacklisting validator would require it to be attached to the FormControl
instance of the email.
However, for more complex validations where multiple control fields have to be compared and validated, it’s a better idea to add the validation logic to the parent FormGroup
. As you can see, password
has a FormGroup
of its own, and this makes it easy for us to write validators that check the equality of pwd
and confirmPwd
.
For the template-driven form, all that logic goes into the HTML template, and here is an example:
app/signup-form/signup-form.component.html
ngModel
creates an instance of FormControl
and binds it to a form control element. Similarly, ngModelGroup
creates and binds a FormGroup
instance to a DOM element. They share the same model domain structure discussed above.
It’s also interesting to note that FormControl
, FormGroup
, and FormArray
extend the AbstractControl
class. What this means is that the AbstractControl
class is responsible for tracking the values of form objects, validating them, and powering other things such as pristine, dirty, and touched methods.
Now that we are acquainted with both the form techniques, let’s write our first custom validator.
Custom Validator Function for Model-Driven Forms
Validators are functions that take a FormControl
/FormGroup
instance as input and return either null
or an error object. null
is returned when the validation is successful, and if not, the error object is thrown. Here’s a very basic version of a validation function.
app/password-match.ts
import { FormGroup } from '@angular/forms'; export function passwordMatch( control: FormGroup):{[key: string]: boolean} { }
I’ve declared a function that accepts an instance of FormGroup
as an input. It returns an object with a key of type string and a true/false value. This is so that we can return an error object of the form below:
{ mismatch: true }
Next, we need to get the value of the pwd
and confirmPwd
FormControl instances. I am going to use control.get()
to fetch their values.
export function passwordMatch (control: FormGroup):{[key: string]: boolean} { //Grab pwd and confirmPwd using control.get const pwd = control.get('pwd'); const confirmPwd = control.get('confirmPwd'); }
Now we need to make the comparison and then return either null or an error object.
app/password-match.ts
import { AbstractControl } from '@angular/forms'; export function passwordMatch (control: AbstractControl):{[key: string]: boolean} { //Grab pwd and confirmPwd using control.get const pwd = control.get('pwd'); const confirmPwd = control.get('confirmPwd'); // If FormControl objects don't exist, return null if (!pwd || !confirmPwd) return null; //If they are indeed equal, return null if (pwd.value === confirmPwd.value) { return null; } //Else return false return { mismatch: true }; }
Why did I replace FormGroup
with AbstractControl
? As you know, AbstractControl
is the mother of all Form* classes, and it gives you more control over the form control objects. It has the added benefit that it makes our validation code more consistent.
Import the passwordMatch
function in the SignupForm
component and declare it as a validator for the password FormGroup
instance.
app/password-match.ts
import { passwordMatch } from './../password-match'; . . . export class SignupFormComponent implements OnInit { ngOnInit() { // Use the formbuilder to build the Form model this.signupForm = this.fb.group({ ... password: this.fb.group({ pwd: ['', [Validators.required, Validators.minLength(8)]], confirmPwd: ['', [Validators.required, Validators.minLength(8) ]] }, { validator: passwordMatch }), ... }) } }
Displaying the Errors
If you did everything right, password.errors?.mismatch
will be true whenever the values of both the fields don’t match.
{{ password.errors?.mismatch } json }}
Although there are alternative ways to display errors, I am going to use the ngIf
directive to determine whether an error message should be displayed or not.
First, I am going to use ngIf
to see if the password is invalid.
We use password.touched
to ensure that the user is not greeted with errors even before a key has been pressed.
Next, I am going to use the ngIf =”expression; then a else b” syntax to display the right error.
app/signup-form/signup-form.component.html
Password do not match Password needs to be more than 8 characters
There you have it, a working model of the validator that checks for password equality.
Demo for Custom Validators in Model-Driven Forms
Custom Validator Directive for Template-Driven Forms
We will be using the same validator function that we created for the model-driven form earlier. However, we don’t have direct access to instances of FormControl
/FormGroup
in a template-driven form. Here are the things that you will need to do to make the validator work:
- Create a
PasswordMatchDirective
that serves as a wrapper around thepasswordMatch
validator function. We will be registering the directive as a validator using theNG_VALIDATORS
provider. More on this later. - Attach the directive to the template form control.
Let’s write the directive first. Here’s what a directive looks like in Angular:
app/password-match.ts
import { AbstractControl } from '@angular/forms'; export function passwordMatch (control: AbstractControl):{[key: string]: boolean} { //Grab pwd and confirmPwd using control.get const pwd = control.get('pwd'); const confirmPwd = control.get('confirmPwd'); // If FormControl objects don't exist, return null if (!pwd || !confirmPwd) return null; //If they are indeed equal, return null if (pwd.value === confirmPwd.value) { return null; } //Else return false return { mismatch: true }; } //PasswordMatchDirective @Directive({ selector: '', providers: [ ] }) export class PasswordMatchDirective { }
The @Directive
decorator is used to mark the class as an Angular directive. It accepts an object as an argument that specifies the directive configuration meta-data such as selectors for which the directive should be attached, and the list of Providers to be injected, etc. Let’s fill in the directive meta-data:
app/password-match.ts
@Directive({ selector: '[passwordMatch][ngModelGroup]', //1 providers: [ //2 { provide: NG_VALIDATORS, useValue: passwordMatch, multi: true } ] }) export class PasswordMatchDirective { }
- The directive is now attached to all input controls that have the attributes
ngModelGroup
andpasswordMatch
. - We extend the built-in validators using the
NG_VALIDATORS
provider. As previously mentioned,NG_VALIDATORS
is a provider that has an extensible collection of validators. ThepasswordMatch
function that we created earlier is declared as a dependency. Themulti: true
sets this provider to be a multi-provider. What this means is that we will be adding to the existing collection of validators provided byNG_VALIDATORS
.
Now, add the directive to the declarations array in ngModule
.
app/app.module.ts
... import {PasswordMatchDirective} from './password-match'; @NgModule({ declarations: [ AppComponent, SignupFormComponent, PasswordMatchDirective ], imports: [ BrowserModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
Displaying Error Messages
To display the validation error messages, I am going to use the same template that we created for the model-driven forms.
Password do not match Password needs to be more than 8 characters
Demo for Custom Validators in Template-Driven Forms
Conclusion
In this tutorial, we learned about creating custom Angular validators for forms in Angular.
Validators are functions that return null or an error object. In model-driven forms, we have to attach the validator to a FormControl/FormGroup instance, and that’s it. The procedure was a bit more complex in a template-driven form because we needed to create a directive on top of the validator function.
If you’re interested in continuing to learn more about JavaScript, remember to check out what we have in Envato Market.
I hope that you’ve enjoyed this series on Forms in Angular. I would love to hear your thoughts. Share them through the comments.
Powered by WPeMatico