Reactive Forms are one of the best features of the Angular framework. But despite their built-in validators that handle a lot of use-cases, it’s likely you’ll want to create your own custom validator for reactive forms at some point - and here’s how!
We’re going to look at two different approaches in this article, first using Angular’s Validators.pattern method, and secondly abstracting patterns away into a custom validator to help us share the code across the application.
For this example, we’ll focus on a simple password validator.
Here’s our App
component with some simple reactive form config:
export class App {
form: FormGroup;
constructor(private formBuilder: FormBuilder) {
this.form = this.formBuilder.group({
password: ['', [Validators.required]],
});
}
}
Note that we’re using Validators.required
already, which is pretty common for everyone to use.
But let’s say for security reasons we want to ensure users are providing a strong and safe password, we might want to compose a custom validator to enforce it upon signing up to our app.
Using Validators.pattern
is one approach, and if you’re just writing a single component is perfectly acceptable.
Here’s a Regular Expression that will match at least one uppercase character, a number, and a minimum of 8 characters:
/^(?=.*[A-Z])(?=.*\d).{8,}$/
Here’s how the Regular Expression works:
^
matches the beginning of the string(?=.*[A-Z])
is a positive lookahead assertion that matches any character, followed by an uppercase letter(?=.*\d)
is a positive lookahead assertion that matches any character, followed by a digit.{8,}
matches any character (except newline characters) at least 8 times$
matches the end of the string
Let’s add it to Validators.pattern
:
export class App {
form: FormGroup;
constructor(private formBuilder: FormBuilder) {
this.form = this.formBuilder.group({
password: [
'',
[Validators.required, Validators.pattern(/^(?=.*[A-Z])(?=.*\d).{8,}$/)],
],
});
}
}
Then, in the template
we can write the ngIf
logic to show the errors when appropriate:
<form [formGroup]="form">
<input
formControlName="password"
type="password">
<div
*ngIf="form.get('password').invalid && form.get('password').touched">
<div
*ngIf="form.get('password').errors.required">
Password is required.
</div>
<div
*ngIf="form.get('password').errors.pattern">
Password must be at least 8 characters long and contain at least one uppercase letter and digit.
</div>
</div>
</form>
🧠 Please note that by simply enforcing specific characters doesn’t instantly make a password “safe”, your backend security is important on this one.
Notice here how we reference errors.pattern
inside the template? For me, this limits the particular patterns we can use as if we want to use multiple patterns we’ll see the same error - but also it’s not very descriptive. What pattern didn’t match?
So, while this works - I believe using a custom validator in Angular could be a better option for us. Not to mention, we can reuse the validator across multiple forms without repeating our Regular Expression over and over.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
First, create a new file called password.validator.ts
(this is just a convention I like to use), and drop in a function:
export const passwordValidator = () => {}
A custom validator returns a function, which gets called by Angular internally. This is a critical step as we get access to any FormControl
objects, via the AbstractControl
class, and from there we would test the value:
export const passwordValidator = () => {
return (control: AbstractControl) => {
// test RegEx against `control.value`
};
}
All we need to do is either return null
or an object (which must contain a property with a Boolean
value, used in our template as a reference later).
Sprinkling on the correct types and our end result would look like this:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export const passwordValidator = (): ValidatorFn => {
return (control: AbstractControl): ValidationErrors | null => {
const passwordRegex = /^(?=.*[A-Z])(?=.*\d).{8,}$/;
if (passwordRegex.test(control.value)) {
return null; // passes the test, return null to clear any errors
}
// fails the test, therefore the password is of invalid structure
return { invalidPassword: true };
};
};
The passwordRegex.test()
returns true
or false
based on whether the control.value
matches the requirement. Our control.value
here is the FormControl
value.
We use AbstractControl
here as the parameter type as FormControl
inherits from it - they have the same properties and methods to a degree.
We could compress things down using a ternary operator as well given that it’s a simple validator:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export const passwordValidator = (): ValidatorFn => {
return (control: AbstractControl): ValidationErrors | null => {
const passwordRegex = /^(?=.*[A-Z])(?=.*\d).{8,}$/;
return passwordRegex.test(control.value) ? null : { invalidPassword: true };
};
};
So now instead of using Validators.pattern
inside our FormGroup
, we can import our function and call it:
export class App {
form: FormGroup;
constructor(private formBuilder: FormBuilder) {
this.form = this.formBuilder.group({
password: ['', [Validators.required, passwordValidator()]],
});
}
}
Followed by a simple template adjustment to show our new error message when the custom validator triggers:
<form [formGroup]="form">
<input
formControlName="password"
type="password">
<div
*ngIf="form.get('password').invalid && form.get('password').touched">
<div
*ngIf="form.get('password').errors.required">
Password is required.
</div>
<div
*ngIf="form.get('password').errors.invalidPassword">
Password must be at least 8 characters long and contain at least one uppercase letter and digit.
</div>
</div>
</form>
Note how the change was from errors.pattern
to errors.invalidPassword
, and that’s it. Much more readable and we get the added benefit of being able to use our validator elsewhere in our application without repeating ourselves.
Give it a try:
It’s also fully testable and encapsulated, so any changes can be made in a single place.
Here’s an example unit test for our custom validator too:
describe('passwordValidator', () => {
let validatorFn;
beforeEach(() => {
validatorFn = passwordValidator();
});
it('should return null when the input value is a valid password', () => {
const validPassword = 'Abcdefg1';
const control = new FormControl(validPassword);
const result = validatorFn(control);
expect(result).toBeNull();
});
it('should return an error object when the input value is an invalid password', () => {
const invalidPassword = 'invalidpassword';
const control = new FormControl(invalidPassword);
const result = validatorFn(control);
expect(result).toEqual({ invalidPassword: true });
});
});
🚀 Learn more techniques, best practices and real-world expert knowledge, I’d highly recommend checking out my Angular courses - they will guide you through your journey to mastering Angular to the fullest!
Happy validating!