Angular is one of the most popular open-source JavaScript frameworks available in the market and the main reason behind its demand is that it makes it easier for developers to create dynamic and robust web apps. While working with Angular, developers can also use HTML as a template language to avoid much coding with the dependency injection and data binding features. Additionally, Angular offers seamless features while working with any server technology. This is why Angular development services companies are high in demand.
Developers face one of the biggest challenges in handling dynamic data for users when it comes to dynamic apps, typically achieved through the use of dynamic forms. With the use of Angular, the developers can easily create forms and work with them to check if the user has provided the right set of information or not. For all these things, the developers use form validations.
To know more about form validation, let’s go through this blog and understand how Angular offers form-validating functions to make users input details and show validation messages on the screen for the users. Here, we will explore everything about form validation, including its creation and how to validate forms using the Angular form validation approach.
1. What is a Form Field Validator?
If any developer has ever used Reactive Forms modules or Angular Forms, they will know that form comes with some important properties, such as –
- Form Value – The form consists of values for every individual field it has.
- Form Validity State – This is the state where the value is true, it means that all the forms in the application are valid, and if the value is false, it means that it means that at least one of the forms in the application has its fields invalid.
Here each field of the form comes with its own set of rules for business validation and all the fields in the form are mandatory for the users to fill, they must have a minimum length of 10 characters, and more.
To understand simple reactive form with few standard validators, let’s go through the code below –
User Login Form:
<form [formGroup]="form"> <input matInput type="email" name="userEmail" placeholder="User Email" formControlName="userEmail"> <input matInput type="password" placeholder="User Password" formControlName="userPassword"> <button [disabled]="!form.valid"> Login </button> </form> |
The above code is for a simple login form with fields for email and password. And as it is a reactive form, there is no business validation rule as the validation is defined on the component and not on the template.
To understand this clearly, let’s go through the corresponding component class code here –
@Component({ selector: 'login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent { form = this.fb.group({ userEmail: ['', { validators: [ Validators.required, Validators.email ], updateOn: 'blur' }], userPassword: [ '', [Validators.required, Validators.minLength(8), createPasswordStrengthValidator() ] ] }); constructor(private fb: FormBuilder) {} get userEmail() { return this.form.controls['userEmail']; } get userPassword() { return this.form.controls['userPassword']; } } |
In the above code, we have used various form validation rules and to understand all those for future use, let’s go through all of them. In this reactive form, we use the FormBuilder API to define the validation rules and form fields in a brief way. For instance, in this code, the email field is mandatory for the users to fill by using the Validators.required validator. Besides this, the format of valid email is also followed as the Validators.email validator is used.
2. Custom Validators for Reactive Forms
In the below code, we will use customer validators that ensure that the password of the form has enough strength. This strength requirement for the password necessitates the inclusion of not only uppercase and lowercase alphabets but also numeric characters in the password. Let’s go through the below code to understand the custom validator function for reactive forms –
import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms'; export function createPasswordStrengthValidator(): ValidatorFn { return (control:AbstractControl) : ValidationErrors | null => { const value = control.value; if (!value) { return null; } const hasLowerCase = /[a-z]+/.test(value); const hasUpperCase = /[A-Z]+/.test(value); const hasNumeric = /[0-9]+/.test(value); const userPasswordValid = hasUpperCase && hasLowerCase && hasNumeric; return !userPasswordValid ? {passwordStrength:true}: null; } } |
To understand the above code properly, we will break it down for you. Here,
createPasswordStrengthValidator() is a function that is not itself a validator function. It is actually a function that helps in creating a validator and then helps in returning its output to the validator. This means that it is a function that returns the type ValidatorFn.
Here, we use the validator function, which can hold any arguments required to set up the validator and then returns itself as the output of the validator function. The form calls this function to check whether the field value in the form is valid or not.
2.1 How to Write a Validator Function?
When creating a validator function, follow specific rules.
- When developers write a validator function, it only allows one input argument which is of type AbstractControl. The validator function can contain the value that needs validation with the help of the control.value property; this is the reason behind it.
- When the validation function is executed and any error is found, the function has to return an object of the ValidationErrors type.
- If the function finds no errors, it needs to return null in the field value, indicating that the value added by the users in the form is valid.
- Here, the ValidationErrors object can have multiple errors found as property and details of the error as value.
- The ValidationErrors function’s value can be an object with any properties which enable the developers to offer information about the error if needed
If we see this in our case, the above code has a password strength validator function and there we are checking whether all the required characters are present or not. And this function comes with rules like –
- When the function returns the output error object as {passwordStrength:true}, this means that the password entered by the user is not strong enough.
- When the function returns the output as null, it means that the password is valid and there is no error.
And here we can see in our code that in the error object, we are only returning a boolean flag which means that an error was found. It doesn’t give any more details about the error, just indicates that there is an error. But instead of this, we can also return an error object showed in the below code –
{ passwordStrength: { hasLowerCase: true, hasUpperCase: true, hasNumeric: false } } |
Here, in the above code, the ValidationErrors object can hold any form as required. So we can return nested objects if we want to convey all the information about the error and what caused it.
And after the creation of a new custom validator function, we have to plug it into the reactive form that we have created before. You can do this using the code shown below.
form = this.fb.group({ userEmail: ... userPassword: [ '', [ Validators.required, Validators.minLength(8), createPasswordStrengthValidator() ] ] }); |
Here, the createPasswordStrengthValidator() function did not require any input arguments, but it is possible to define arguments as per the requirements for performing validation.
2.2 Displaying Error Messages for Custom Validators
When the developer has written a function in the code that makes the users fill passwords up to a limit to fulfill its strength limit, and if it is not met, to inform the users the following message is added to the template-
<input matInput type="password" placeholder="User Password" formControlName="userPassword"> <div class="field-message" *ngIf="userPassword.errors?.passwordStrength"> Your password must have lowercase, uppercase, and numeric characters. </div> |
In this code, the custom validator added to the password field errors object returns an output in the form of the ValidationErrors object when the password doesn’t match the strengthening limit specified by the developer. This means that if the password entered by the user isn’t strong enough, the ngIf condition will be true and it will show an error message to the user.
3. Custom Validators for Template-Driven Forms
Angular websites or applications require an important custom form field validator for all types of forms. However, when it comes to template-driven forms, defining custom form field validators is a bit of a difficult task for the developers. The business uses this validator to define all validation rules in a specific manner by matching the level of the template, rather than that of the component class, using directives.
To understand this concept clearly in a practical environment, we will go through an example, where we will see how the same login form looks like in its template-driven version –
<form #loginForm="ngForm" (ngSubmit)="login(loginForm, $event)"> <input matInput type="email" name="userEmail" ngModel #userEmail="ngModel" required email minlength="3" maxlength="20" placeholder="User Email"> <input matInput type="password" name="userPassword" required passwordStrength minlength="8" ngModel #userPassword="ngModel" placeholder="User Password"> </mat-form-field> <button type="submit" [disabled]="!loginForm.valid"> Login </button> </form> |
As seen in the above code, there is a lot going on in the template code when it comes to template-driven forms. For instance, here there are various rules like email directives, the standard required, maxlength, and minlength applied to the fields email and password.
Here in this code, the directives use corresponding validator functions such as Validators.required, Validators,maxLength, and Validators.minlength internally. In addition, developers need to create a non-standard passwordStrength directive for the password field. This means that in template-driven forms, the developers need to create custom directives alongside the validator function, otherwise using custom validators won’t be possible.
3.1 How to Write a Custom Validator Directive
Here in the below-given code, we will see what the custom form field passwordStrength directive actually looks like in an Angular application –
import {Directive} from '@angular/core'; import {AbstractControl, ValidationErrors, NG_VALIDATORS, Validator} from '@angular/forms'; import {createPasswordStrengthValidator} from '../validators/password-strength.validator'; @Directive({ selector: "[passwordStrength]", providers: [{ useExisting:PasswordStrengthDirective, provide: NG_VALIDATORS, multi: true }] }) export class PasswordStrengthDirective implements Validator { validate(control: AbstractControl): ValidationErrors | null { return createPasswordStrengthValidator()(control); } } |
Now to understand the above code clearly, we will break it down and will go through the entire process step-by-step starting with the custom validation directive functionality’s implementation –
- The very first thing to do is implement custom validation directive functionality and to do so, the developer needs to implement the Validator interface that comes with the validation method.
- This validation method that comes with the Validator interface calls the validation creation function and then it passes the control reference to the validator of the form.
- And the validator returns a null value if the password is false on a ValidationErrors or otherwise.
But here, if you want to validate a custom validation directive, you need more than just the implementation of the Validator interface. We will learn about the required configuration for the Angular dependency injection system in the next step.
3.2 Understanding the DI Configuration of a Custom Validator Directive
When it comes to the Angular forms module, it requires knowing that there are various custom validation directives available. And this can be done by using the dependency injection system for the token named NG_VALIDATORS injection. This is the type multi:true dependency which means that it comes with various values. And this is expected as there are various custom validator directives available.
Here, when creating a new provider, the system processes the directive declaration, setting a new value in the dependency injection system and making PasswordStrengthDirective available. Additionally, due to the NG_VALIDATORS provider having multi:true, PasswordStrengthDirective is added to the current list of custom validation directives instead of replacing the entire list with a single value.
This shows that when proper dependency injection configuration is added to the custom validation directive, everything works perfectly, and the validate() method is also triggered very easily.
4. Difference in Custom Validation in Template-driven vs Reactive Forms
Here we will go through the major point of differences between custom validation in reactive forms and template-driven forms.
4.1 Form-level (Multi-field) Validators
You can also define a custom validator at the level of each input field and form. And this is beneficial for multi-field validation when the validation can be either true or false as per the values entered in the form fields. The below-given code illustrates this. Here the reactive form will take two different input dates, a start, and an end date.
<form [formGroup]="form"> <input placeholder="Start date" formControlName="startDate"> <input placeholder="End date" formControlName="endDate"> </form> |
In the above code, the developer has mandated users to input two dates for the form to be considered valid. Additionally, a validation rule requires that the start date precedes the end date. Achieving this necessitates the implementation of a custom validation at the form field level. Defining a custom validator function at the form level accomplishes this. The following practical example utilizes the form-level validator to ensure that the start date comes before the end date.
import {FormGroup, ValidatorFn, Validators} from '@angular/forms'; export function creatDateRangeValidator(): ValidatorFn { return (form: FormGroup): ValidationErrors | null => { const startDate:Date = form.get("startDate").value; const endDate:Date = form.get("endDate").value; if (startDate && endDate) { const isRangeValid = (endDate.getTime() - startDate.getTime() > 0); return isRangeValid ? null : {dateRange:true}; } return null; } } |
The above code defines a form-level validator which is similar to the field-level validator but the only difference is that the function’s input is a FormGroup object and not a form control. Here by using FormGroup, developers have made it easy to access any value of the form and compare various values together.
After the developers write the form-level validator function, they can apply it in the form configuration like this:
form = this.fb.group({ startDate: [null, Validators.required], endDate: [null, Validators.required] }, { validators: [createDateRangeValidator()] }); |
The code shows that you need to configure the validator at the form group level as a separate configuration object. And this form-level configuration object can take updated and asyncValidators property.
4.2 Asynchronous Form Field Validators
In this blog, all the custom validators we have looked at are synchronous. This means that the validator immediately calculates the new validity state as soon as the validator function is called. And in this case, simple validators like password strength, minimum length, and more are mandatory. When calculating a new validity state in an asynchronous way, such as calling a backend or using a promise-based library, this makes sense. In such cases, synchronous validators won’t work. The form button must only be enabled if the backend system has not yet used the user’s email, exemplifying this email field.
And if the system checks if the user already exists, the backend must be called and the query must be fired on the database, which requires an asynchronous operation. Here’s a practical example of what we are discussing –
Create User Form:
<form [formGroup]="form"> <input matInput type="email" name="userEmail" placeholder="Enter the new user email" formControlName="userEmail" #userEmail> <div class='field-message' *ngIf="userEmail?.errors.userExists"> An user with email {{userEmail.value}} already exists. </div> <input matInput type="password" placeholder="User Password" formControlName="userPassword"> <input matInput type="password" placeholder="Confirm User Password" formControlName="confirmUserPassword"> <button [disabled]="!form.valid"> Create User </button> </form> |
In the above code, the system will display an error message to inform users that the entered email address already exists. And this is possible because of the property userExists that is present at the field level errors object.
Now, let’s have a look at what the component class looks like and here we will only focus on the email address field.
@Component({ selector: 'login', templateUrl: './create-user.component.html', styleUrls: ['./create-user.component.css'] }) export class CreateUserComponent { form = this.fb.group({ userEmail: ['', { validators: [ Validators.required, Validators.email ], asyncValidators: [userExistsValidator(this.user)] updateOn: 'blur' }], .... }); constructor(private fb: FormBuilder, private user: UserService) {} get userEmail() { return this.form.controls['userEmail']; } } |
In the above code, we utilized powerful configuration object syntax since using both synchronous and asynchronous validators together is not possible. And here the syntax binds both the email field and the asynchronous validator which means that the email field will send the error message to the users very easily if something goes wrong.
4.2.1 How to Write an Asynchronous Validator?
Here, we will use the userExistsValidator for asynchronous validation and see what it does –
import {AbstractControl, AsyncValidatorFn} from '@angular/forms'; export function userExistsValidator(user: UserService):AsyncValidatorFn { return (control: AbstractControl) => { return user.findUserByEmail(control.value) .pipe( map(user => user ? {userExists:true} : null) ); } } |
As seen in the above code, though it is quite similar to a normal validator, the main difference is the type that this validator returns when using userExistsValidator(). The validator creation function is of type AsyncValidatorFn. This implies that the function must return an observable or promise value of type ValidationErrors.
5. Conclusion
As seen in this blog, Angular is a highly-used front-end framework for creating dynamic and robust web applications. And when it comes to creating forms for the application or web app, Angular is the perfect choice as it enables the creation of forms with proper validation to it. This framework ensures security and enables the users to fill out forms smoothly. Additionally, it allows developers to add validations using built-in validators, making it easier to check whether app users have filled all the fields of the form or not. And by looking at the Angular form validation details, it is sure that Angular is the perfect choice for companies that have applications that will require the users to fill different forms.
Comments
Leave a message...