27
All About Validators in Angular + Creating Custom Sync & Async Validators
Validators in angular are just simple functions that check our form values and return an error if some things are not the way its meant to be. Angular ships with a bunch of Validators out of the box. These can be used directly without the need for any configuration.
Validators can be set up in different ways to validate user inputs in form elements. Angular provides a lot of validators that are commonly needed for any form.
If we special validation requirements or if we are dealing with custom components, the default validators might not just cut it. We can always create custom validators for these cases.
Angular ships with different validators out of the box, both for use with Template forms as well as Reactive forms.
- In-built Validator Directives.
- A set of validator functions exported via the
Validators
class.
Both the directives and Validators
class use the same function under the hood. We can provide multiple validators for an element. What Angular does is stack up the validators in an array and call them one by one.
Native HTML form has inbuilt validation attributes like required
, min
, max
, etc. Angular has created directives to match each of these native validation attributes. So when we place these attributes on an input
, Angular can get access to the element and call a validation function whenever the value changes.
Here's how you would use a validator directive:
<input type="email" required minlength [(ngModel)]="email"/>
The attributes required
and minlength
are selectors for the RequiredValidator
( ref ) and MinLengthValidator
( ref ) directives respectively. These can be used with both Template drive forms and Reactive Forms.
Here's how the required
directive looks like:
@Directive({
...
providers: [{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => RequiredValidator),
multi: true
}],
...
})
export class RequiredValidator implements Validator {
// .....
validate(control: AbstractControl): ValidationErrors|null {
return this.required ? requiredValidator(control) : null;
}
// .....
}
Let's break down the code:
- The class
RequiredValidator
implements an interface calledValidator
. - The
Validator
interface forces the class to implement avalidate()
method. - The method will be called for validation of the value.
- The validation logic can be performed in the method and just have to return an object if there is an error or
null
if there is no error. - Now, we need to let Angular know about this custom validation that we've set up.
- We use the
NG_VALIDATORS
Injection token for this. - We ask Angular to use our same
RequiredValidator
class by using theuseExisitng
property.
So when the user places required
on a form control, the RequiredValidator
directive gets instantiated and the validator also gets attached to the element.
Take a look into the source code for Required Validator
( ref ).
Validators
class exposes a set of static methods that can be used when dealing with Reactive Forms like so:
import { FormControl, Validators } from '@angular/forms';
export class AppComponent{
email = new FormControl('', [Validators.required, Validators.minLength(5)]);
}
Here's the list of all the function inside the class:
class Validators {
static min(min: number): ValidatorFn
static max(max: number): ValidatorFn
static required(control: AbstractControl): ValidationErrors | null
static requiredTrue(control: AbstractControl): ValidationErrors | null
static email(control: AbstractControl): ValidationErrors | null
static minLength(minLength: number): ValidatorFn
static maxLength(maxLength: number): ValidatorFn
static pattern(pattern: string | RegExp): ValidatorFn
static nullValidator(control: AbstractControl): ValidationErrors | null
static compose(validators: ValidatorFn[]): ValidatorFn | null
static composeAsync(validators: AsyncValidatorFn[]): AsyncValidatorFn | null
}
We can also create custom validators in Angular which are tailored to our particular use case. You can't just always rely on the built-in capabilities of Angular.
Validators are just functions of the below type:
export interface ValidatorFn {
(control: AbstractControl): ValidationErrors|null;
}
Let's create a custom validator function that checks if a domain is secure (https
) or not.
export const ProtocolValidator: ValidatorFn = (control) => {
const { value } = control;
const isSecure = (value as string).startsWith("https://");
return isSecure ? null : { protocol: `Should be https URI` };
};
If we want our custom validator to be more configurable and re-use in multiple places, we can pass parameters to our validator function to create a validator based on the provided parameters.
For example, if the Secure validator needs to validate Websocket URIs also, we can modify the ProtocolValidator
to accommodate this change:
export const ProtocolValidator = (protocol: string): ValidatorFn => (control) => {
const { value } = control;
const isSecure = (value as string).startsWith(protocol);
return isSecure ? null : { protocol: `Should be ${protocol} URI` };
};
We can directly use the function in the reactive forms like so:
const urlControl = new FormControl('', [Validators.required, ProtocolValidator('https://')]);
and the template will be something like this:
<input type="text" [formControl]="urlControl" />
If we want to use these validators with Template-drive forms, we need to create a directive.
@Directive({
selector: "[protocol]",
providers: [{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ProtocolValidatorDirective),
multi: true
}]
})
export class ProtocolValidatorDirective implements Validator {
@Input() protocol!: string;
validate(control: AbstractControl): ValidationErrors|null {
return ProtocolValidator(this.protocol)(control);
}
}
and we use it like this:
<input type="text" protocol="wss://" [(ngModel)]="url" />
Note: For the directive selector, it's always a good idea to also look for whether there is a valid form connector added to the element:
@Directive({
selector: '[protocol][formControlName],[protocol][formControl],[protocol][ngModel]'
})
What this translates to is that the element where our protocol
directive is placed should also have either of these attributes:
- formControlName
- formControl
- ngModel
This makes sure that the directive is not activated on non-form elements and you won't get any unwanted errors.
The process of creating async validators in angular is exactly the same, except this time we are doing our validation in an async way (by calling an API for example).
Here is the type of async validator function:
interface AsyncValidatorFn {
(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>
}
The only thing that is different here is that the method now returns either an Observable or a Promise.
Let's create an async validator by modifying the above validator that we wrote. Ideally, we will be using async validation for meaningful validations like:
- Check username availability
- Whether a user is blocked
- If the user's phone number is part of a blocklist.
Let's create an async validator to check if a username is available.
We are gonna be creating 3 things:
- Username Service - which makes the API call to see if the username is available
- Validator Service - which contains the validation logic
- Validator Directive - for using template-driven forms
We'll mock the logic for this:
@Injectable({
providedIn: "root"
})
export class UsernameService {
constructor(private http: HttpClient) {}
isUsernameAvailable(username: string) {
return this.http.get("https://jsonplaceholder.typicode.com/users").pipe(
map((users: any[]) => users.map((user) => user?.username?.toLowerCase())),
map(
(existingUsernames: string[]) => !existingUsernames.includes(username)
),
startWith(true),
delay(1000)
);
}
}
This is the main part of our validation process.
@Injectable({
providedIn: "root"
})
export class UsernameValidatorService implements Validator {
constructor(private usernameService: UsernameService) {}
validatorFunction: AsyncValidatorFn = (control) =>
control?.value !== ""
? this.usernameService
.isUsernameAvailable(control.value)
.pipe(
map((isUsernameAvailable) =>
isUsernameAvailable
? null
: { username: "Username not available" }
)
)
: of(null);
validate(control: AbstractControl) {
return this.validatorFunction(control);
}
}
Why did we create a separate validatorFunction()
? Why can't the logic be placed inside the validate()
method itself?
This is done so that, we can use the validatorFunction()
when we are using Reactive Forms:
export class AppComponent {
constructor(private usernameValidator: UsernameValidatorService) {}
username = new FormControl("", {
asyncValidators: this.usernameValidator.validatorFunction
});
}
Now to use the validator with Template-driven forms, we need to create a Directive to bind the validator to the element.
@Directive({
selector: "[username][ngModel]",
providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: UsernameValidatorService,
multi: true
}
]
})
export class UsernameValidatorDirective {}
We provide NG_ASYNC_VALIDATORS
instead of NG_VALIDATORS
in this case. And since we already have the UsernameValidatorService
(which implements the Validator
interface).
Note: UsernameValidatorService
is providedIn: 'root'
, which means the Injector has the service instance with it. So we just say use that same instance of UsernameValidatorService
by using the useExisitng
property.
Angular takes care of subscriptions of these validators so we don't have to worry about cleaning the subscriptions later.
Note: Open in a new window to see the demo properly.
Make sure to not just use the code as-is. For the scope of this post, things are kept simple and straightforward. Take some time to see if you can improve something in the code before you use it.
Do add your thoughts in the comments section.
Stay Safe ❤️
27