Web Page Form Validations with Angular

Introduction

Greetings, my fellow codeproject readers. Happy holidays! This is my last tutorial for the year 2022. It has been a great year. I greatly appreciate that you are reading my tutorials. For this one, I want it to be simple and useful. One of the most useful skills I have learned in the past few years is how to do form validation with AngularJS. Angular 2 and above are a different kind of beast in terms of syntax and usage. But my research shows that in terms of form validation, whatever is applicable to AngularJS can also be used in Angular based application as well. In this tutorial, I will discuss several techniques of form fields validation with Angular framework. As you can see, not only these can be easily done, but also they are extremely powerful with web application design.

What is form validation? To me, it is the task of validating the fields on the form before the form data are submitted to back end server. For Angular and AngularJS applications, we have went beyond the traditional form submission (from the days of Web 1.0 and 1.5). Most of the client side validation can be done on the web page directly, and no need of round trip to the back end server. The client side validation is done via JavaScript or any JavaScript based framework. For AngularJS, validation and display of error on the page are build-in. It is not the easiest to work with. Angular framework made this even more complicated. The reason for this is to provide every developer the means to design the mechanism for their application. But not every functionality is essential. I believe what can be done with AngularJS can also be used with Angular application. This tutorial will discuss such approach. What I have done before is what I want to do now. I believe this is the best way to approach solution unless it is so awkward that I have to learn a new one.

Overall Architecture

The sample application for this tutorial is a pure node.js application. There is no Java or .NET back end. The application is a single page application. Within the application, there is just one index page -- a form with a lot of fields, and two buttons, one is Save and one is Clear. The fields validations are all happening on the page itself. This is probably the only tutorial that I didn't include a java or .NET back end application. They (Java or .NET application code) are not needed. All we need for this application is just page side validations. And the application can run in a node.js based web server.

The application development uses Angular framework (version 14.1.0). I also included Bootstrap (version 5.2.0) and JQuery (3.6.0). The other frameworks are not needed. There are two ways to add BootStrap and JQuery. The easy way is to use the Angular CLI command "ng add". I don't know how this works because I didn't do it using this command. The second way is the hard way, find the config file for the application project and add the additional framework by modifying these configuration files. The two files I had to modify are the files package.json and angular.json. If you are interested in how this is done, check these two files. The modification of package.json is to include the dependencies of Bootstrap and JQuery. The modification of angular.json is a bit hard to understand. It will actually include these dependencies into the application so that when the browser loads the application, it also loads these dependencies. Without this, these framework will not be load and application will not work as expected.

There are three different kinds of validations I will demonstrate in this sample application:

  • Check required field and display associated error message.
  • Check field value's validity and display associated error message.
  • Dynamically transform the field value. And preventing invalid characters to be entered.

These can be done by the build-in form validation mechanism and with field event handling. Like I said, I want to use the most familiar and most simple approach and perform the most powerful handling of input validation. Without further ado, let's begin with the actual tutorial.

Angular Form Validation How-To

The approach I have adopted for this tutorial is I believed to be the simplest approach for form validation. It is similar to the way AngularJS used for form validation. The way AngularJS does it is that the form name, and name of the fields can be used as objects in the AngularJS code. The way I have learned is that I can set all the fields on a form to dirty, this will immediately expose all the fields that are required but do not have a value entered. Every field will have a child tag that is initially invisible, but when there is an error with the field, the tag will display. For real time value validation, I can use the blur (field out of focus) event handling to validate the field, if it is not valid, the associated error tag can immediately display. The same event can also be used for field display value transformation. Finally, for real time field value validation. I use the value changed event handling to do this. The same principles can be used in Angular application. In the next few sub sections, I will describe how each mechanism works.

Programmatically Force Form Validation at Form Submission

For Angular, to make a form an object or fields of a form as objects that are accessible to the JavaScript, there are several way to do it. The most familiar approach (compare to AngularJS) is to declare the form object and all the fields as objects right on the form itself. This is how I have done it with the form:

<form novalidate #formSignUp="ngForm">
...
...
...
</form>

The attribute novalidate is to set the form not to explicitly validate the form fields via HTML5 build-in functionality. This is necessary because without it, the browser will validate and display build-in error prompts. If I want to customize the error display, the browser default behavior must be removed. Next, I declare a variable object called #formSignUp and assign the form object to this variable.

The question is, why is this necessary? Form fields only display error message and the red rectangle wrap around the input field when the fields are dirty and the fields is invalid/has errors. When the form has no data entered, and the user clicks on the button "Sign up", the form is still in the pristine state, so nothing of errors will be displayed. This is a bug and the way to fix is to have code in the button click event handler to force form to be dirty, and also make all the fields dirty. This will trigger form's build-in validation against the fields, such as required field validation, maximum length, and email address value validation, etc. These validation will also set error/invalid on the individual fields and form itself. I put such mechanism in a common service class:

import { Injectable } from '@angular/core';

@Injectable({
   providedIn: 'root',
})
export class FormsService {
   public makeFormFieldsClean(formToCheck: any):void {
      if (formToCheck != null) {
         Object.keys(formToCheck.controls).forEach(key =< {
            formToCheck.controls[key].markAsUntouched({});
            formToCheck.controls[key].markAsPristine();
         });
      }
   }
   
   public makeFormFieldsDirty(formToCheck: any):void {
      if (formToCheck != null) {
         Object.keys(formToCheck.controls).forEach(key =< {
            formToCheck.controls[key].markAsDirty();
         });
      }
   }
}

In this service class, I have defined two methods, one is called makeFormFieldsClean(). I will get to this method a bit later. The one I want to point out is the method makeFormFieldsDirty(). This is the method will force all the fields in the form object to be dirty. It is just a for loop through all the sub controls of the form, and invoke the control's markAsDirty(). Now, get back to the first method in this service class. Basically, this method revert the state of fields to untouched/pristine. This will hide all the errors. This method will be called when the users click the button "Clear". After all the values are cleared, this method will force every field in the form to be pristine/untouched, and all the errors will disappear.

Let's take a simple example. In the sample application, we have a field called "Name". It is used for holding user's real name for user registration. And it is a required field. The way I set this field up is like this:

<div class="row mt-2">
   <div class="col-xs-12 col-sm-6">
      <label for="signup_UserRealName">Name</label>
      <input class="form-control"
             type="text"
             id="signup_UserRealName"
             name="userRealName"
             [(ngModel)]="userRealName"
             [ngClass]="{'invalid-input': (usrRealName.dirty || usrRealName.touched) && usrRealName.invalid}"
             required
             maxlength="80"
             #usrRealName="ngModel"/>
      <span class="badge bg-danger"
            *ngIf="(usrRealName.dirty || usrRealName.touched) && usrRealName.errors?.['required']">Required</span>
   </div>
</div>

I use the bold font to highlight the parts in this field that are important. The keyword required sets this field as a required field. If no value is entered in this field, when the field is set to dirty, the browser will automatically conduct validation on this field. This part #usrRealName="ngModel" is declaring a local variable contains the object of this field. There are two reasons this is necessary. One, look at the last part of the declaration, where ngIf is used, it needs the field object name to check the error state in order to hide/show the error. Two, this variable can be passed into Angular methods and be used for programmatically manipulating the input field. The last part of the above HTML mark up is the span element that has "Required" error. This element is only displayed when the input field is touched or dirty and has the error state of required. You might ask, why can I just display it when it has the error of required field but value has not entered? The reason, we don't do that is this error will show as soon as the form is displayed, and it will not be removed when user click the "Clear" button. It is best to include the check of dirty or touched states so that the error does not show up when the field has not been modified or even touched.

As I mentioned before, if a user does not touch the field at all, the error will never display. The error has to be forced to show. This is the code I have for the "Sign up" button event handling:

import { FormsService } from '../common/forms.service';
...
...
public onClickSaveChange(formToValidate: any):void {
   this._formsService.makeFormFieldsDirty(formToValidate);

   if (formToValidate.valid === true) {
      alert("User sign up is successfully.");
   }
}

This method is very simple. It forces all fields of the form to be dirty, which in turn conduct HTMl5 validations of the fields automatically. With this, if the above field has no value entered, the required red label will show. On top of the code snippet, I imported the form service class type, which I have shown early, it is the makeFormFieldsDirty() method that will force all field to be validated. Please review if you have forgotten what this piece of code works.

To clear the errors so that the form does not have any error displays, the functionality is done in the event handling method of the click button "Clear". This is the method:

public onClickClear(formToValidate:any):void {
   this.clearAllViewData();
   this._formsService.makeFormFieldsClean(formToValidate);
}

After I cleared all the values of the form fields, I used the form service object and invoked the method makeFormFieldsClean() to set all the fields as clean and pristine. This should remove all the error display. The code for the method makeFormFieldsClean() is listed earlier, please review if you forgot how it works.

This is the most essential part of the whole setup for the validations to work. It is also the most difficult part. Now we are over it, it will be easier to add more functionalities to this sample application. Next, I will show how to add dynamically validations for user interaction.

Dynamic Field Validation for User Interactions

In this sub section, I will kick the validation mechanism up one notch. This form is intended for user registration. There is a User Name field. I want to simulate the situation where user enters a user name, the user name value has to check with back end data repository and see if the user name has already been taken. If the user name is already taken, then an error should be displayed.

The other validation I want to show is the value validation. Basically, user name can only contain specific characters. If the user name contains characters that are not in the set of allowed characters, then invalid user name error will be displayed. In addition to these two, the field is also a required field.

Here is definition of the user name input field:

<div class="row">
   <div class="col-xs-12 col-sm-8 col-md-8">
      <label for="signup_UserName" class="form-label">User Name</label>
      <input class="form-control"
               type="text"
               id="signup_UserName"
               name="userName"
               [(ngModel)]="userName"
               [ngClass]="{'invalid-input': (usrName.dirty || usrName.touched) && usrName.invalid}"
               (blur)="checkUserName(usrName)"
               required
               maxlength="60"
               #usrName="ngModel"/>
      <span class="badge bg-danger"
            *ngIf="(usrName.dirty || usrName.touched) && usrName.errors?.['required']">Required</span>
      <span class="badge bg-danger"
            *ngIf="(usrName.dirty || usrName.touched) && usrName.errors?.['invalid_username']">Invalid user name</span>
      <span class="badge bg-danger"
            *ngIf="(usrName.dirty || usrName.touched) && usrName.errors?.['duplicated_name']">Try a different user name</span>
   </div>
</div>

I have highlighted the important part of this code snippet. The most important part is the handling for the blur event. This blur event happens when the field loses focus, which happens when another field gains focus. This is a powerful mechanism for a lot of mechanisms, such as performing validation, value transformation, or many other different actions. In this case, the event handling will perform two actions:

  • Check if the user name has been taken. If it is taken already, set the error of choosing a different user name.
  • Check the user name contains any characters that are not allowed. And if some characters are not allowed, set error of invalid user name.

This is the line I set the event handling for blur event:

<input 
...
...
(blur)="checkUserName(usrName)"
...
...
/>

This method invocation takes in a parameter, and the value is userName. This is the local variable that contains the user name field. You will see how this value is being used next. The following code snippet shows the two methods for user name validations:

public checkUserNameFieldValid(userNameField:any):void {
   if (this._userName != null && this._userName.trim() !== "" && !this.checkUserNameValidity(this._userName)) {
      userNameField?.control?.setErrors({"invalid_username": true});
   }
}

public checkUserName(userNameField: any):void {
   this.checkUserNameFieldValid(userNameField);

   if (this._userName && this._userName.trim() !== "") {
      if (this._existingUserNames && this._existingUserNames.length > 0) {
         let i = 0;
         for (; i < this._existingUserNames.length; i++) {
            if (this._existingUserNames[i] &&
               this._existingUserNames[i].trim() !== "" &&
               this._existingUserNames[i].toLowerCase() === this._userName.toLowerCase()) {
               userNameField?.control?.setErrors({"duplicated_name": true});
               break;
            }
         }
      }
   }
}

The first method (checkUserNameFieldValid()) checks the user name contain any un-allowed characters. If the check did find characters not allowed, the high lighted part is how I set the field with specific error:

...
userNameField?.control?.setErrors({"invalid_username": true});
...
}

The reason I used the question mark "?" is that it can first make sure the object variable is not null, then it can reference the subsequent property or invoke the object's method. This is a great way of avoiding the null reference exception.

To check whether the user name has been take or not, I created an array, and put in three user names: "testuser1", "testuser2", and "testuser3". The input user name will compare against this list and if any matches (regardless case differences), then it sets the error on the field:

...
   if (this._userName && this._userName.trim() !== "") {
      if (this._existingUserNames && this._existingUserNames.length > 0) {
         let i = 0;
         for (; i < this._existingUserNames.length; i++) {
            if (this._existingUserNames[i] &&
               this._existingUserNames[i].trim() !== "" &&
               this._existingUserNames[i].toLowerCase() === this._userName.toLowerCase()) {
               userNameField?.control?.setErrors({"duplicated_name": true});
               break;
            }
         }
      }
   }
...

When you test this input field, all you need to do is enter some characters, then use mouse cursor or key "Tab" to switch from this field to the next, the validation will execute. When you enter "testuser1", you will see the error display: Try a different user name.

If you type in value "test##$user!1", the validation will display: Invalid user name.

I used the same approach for two more fields. The passwords field will have a password values equality validation. The blur event handling is attached to the password field #2, and if the two passwords mismatch, an error would display. Also for the email address field, the value is validated against an Regex. If the pattern matching fails, an error would also display. Note that the Regex pattern is a simple one. It does not cover all types of valid email addresses, hence the validation mechanism cannot be for production use.

If you type in a valid value like "shebanger_69", no error will display.

This is not bad, probably not as hard as the initial work to set up the validation. Let me kick the mechanism up one last notch - using automatic value transformation to remove invalid characters and transform the value for display.

Input Field Value Transformation and Clean-up

Here is a new scenario, assuming you have a input field, you want to restrict the input to numeric characters only. One approach would be set the input type as number. This approach is limited in terms of functionality. If the field is intended to display a phone number in U.S. region, then the value can be formatted to "(XXX) XXX-XXXX". Clearly, such scenario using number type input is not going to work. So I have to rely on two different event handling mechanisms to 1) prevent invalid characters to be entered; 2) transform the value for display and for data storage.

One of the mechanisms is the blur event handling. The other mechanism is the value change event handling. The value change event handler is used to do value validation. For phone number, all I am interested in is numeric characters. All other characters will be removed. The value change event is called every time the value in the input field changed. This made the handling an expensive operation to perform. The reason I choose to use this event handling as the place to strip the invalid characters is that it will create the effect that the character is entered and immediately removed so that user know it was not allowed, you can try with the phone number input field and see the effect.

The blur event handling is used for the transformation, where the phone number is transformed with the U.S. phone number format. If I do this in the value change event handling, it will be a fairly expensive operation. So I only have it perform when the focus leaves the input field. In addition to the transformation, I also strip all the non-numerical characters out of the phone number, then assign the value to a different property. This value would be saved to the database (if the saving to back end database is implemented). This is my implementation of the two methods for event handling:

...

// for value changed event
public userPhoneNumFieldValueChanged():void {
   this._userPhoneNum = this._userPhoneNum.replace(/\D/g, '');
}

// for blur evnent
public userPhoneNumFieldBlurred():void {
   if (this._userPhoneNum != null && this._userPhoneNum.trim() !== "") {
      let temp_phNum:string = this._userPhoneNum.replace(/\D/g, '');
      temp_phNum = temp_phNum.substring(0, 10);
      if (temp_phNum.length >= 7) {
         this._userPhoneNum = "(" + temp_phNum.substring(0, 3) + ") " + temp_phNum.substring(3, 6) + "-" + temp_phNum.substring(6);
      } else if (temp_phNum.length > 3 && temp_phNum.length < 7) {
         this._userPhoneNum = "(" + temp_phNum.substring(0, 3) + ") " + temp_phNum.substring(3);
      } else if (temp_phNum.length >= 1 && temp_phNum.length <= 3) {
         if (temp_phNum.length < 3) {
            this._userPhoneNum = "(" + temp_phNum.substring(0);
         } else {
            this._userPhoneNum = "(" + temp_phNum.substring(0) + ")";
         }
      }
   }
   this._userPhoneNumVal = this._userPhoneNum.replace(/\D/g, '');
}
...

You can see the difference between the two methods, the top one uses Regex to strip out any unwanted characters. The bottom one is a complicated method that slice the entire string into three pieces and re-arrange the value into a new one like "(XXX) XXX-XXXX". After the value transformed, I would strip again and assign the all numerical value to other variable.

The phone number field is defined as the following:

<div class="row mt-2">
   <div class="col-xs-12 col-sm-6 col-md-5">
      <label for="signup_UserPhoneNum">Phone #</label>
      <input class="form-control"
               type="text"
               id="signup_UserPhoneNum"
               name="userPhoneNum"
               [(ngModel)]="userPhoneNum"
               [ngClass]="{'invalid-input': (usrPhoneNum.dirty || usrPhoneNum.touched) && usrPhoneNum.invalid}"
               (change)="userPhoneNumFieldValueChanged()"
               (blur)="userPhoneNumFieldBlurred()"
               required
               maxlength="15"
               #usrPhoneNum="ngModel"/>
      <span class="badge bg-danger"
            *ngIf="(usrPhoneNum.dirty || usrPhoneNum.touched) && usrPhoneNum.errors?.['required']">Required</span>
   </div>
</div>

As you can see, it is a required field and has just one error tag to it. The reason no need for another because the blur and value change event handling should take care of any invalid input characters. The only error that will happen is that user doesn't enter any value at all.

Anyways, the same mechanism is applied to the field zip code. The value for the field is all numerical. When user enters the value it will be formatted to U.S. format "XXXXX-XXXX".

This is it. With the tweak and twist of event handling of the form fields, it is possible to build any kinds of validation for the form inputs. The ones I described in this section should give you a head start. In the next section, I will discuss how to build and run the sample application.

How to Run and Test This Sample Application

The fun time is here. You know all the tricks used in this sample application. It is time to run it and see all the tricks in action. After you download the source code in a zip file, unzip it in a folder. Before you run any thing, there is just one file "karma.conf.sj", renaming it to "karma.conf.js".

Next, check your system, make sure you have the latest node.js installed. I have v18.2.1 installed. The sample application probably won't run on v10. So please install the latest if you can. Then npm, and Angular, and Angular-CLI.

You must install all the required packages. This is the command you run:

npm install

The install of packages will take a few moment to download. Afterwards, you might need to audit and fix any packages that have high severity issues. Run this command:

npm audit fix

With the versions I used for the packages, there are two high severity vulnerabilities. The setup I used is for integrating the ui-bootstrap components for a different project, so I am OK with this problem for now. I am sure it can be fixed later.

Next, we can just serve the application with Angular-CLI. This is the command:

ng serve

When it is started up successfully, you will see the console displaying the output looks like this:

To run the application, use the browser and navigate to the end point:

http://localhost:4200/

You will see the following page displayed:

Run the Sample Application

There are several different scenarios you can try it out. The first one is that you can scroll down to the bottom and click the button "Sign up", all the input fields will lit up with error. The input field will have a red bound and a red tag underneath it showing error "Required". This is a screenshot:

When you click on the button "Clear", all the errors will disappear. Next, try the user name input, if you enter value "testuser2" and go to the next input field, you will see the error "Try a different user name".

And if you change the value to "testuser$#%^&1", the error will change to "Invalid user name". Once you change to a value like "shebanger_69", the user name will be correct.

For user email input field, this is a valid value "shebanger69@testmail.com". And an invalid value would be "shebanger_69@testmail.com". Again, you can test the input by switch to a different input field and you will see error display. Even though both emails are valid, because the rule I have used here will show one is valid and one is not. Like I said, the email validation is not correct and cannot be for production use.

For user passwords, try two different password for the inputs, and you will see the error: "Password mismatch"

The last scenarios you want to try are the zip code and phone number. Using phone number as an example, there are several values you can try:

  • If you enter 1 or 2 numeric characters, the format of the value will be: "(X" or "(XX".
  • If you enter 3 numeric characters, the format of the value will be: "(XXX)".
  • If you enter 4 to 6 numeric characters, the format of the value will be: "(XXX) X", "(XXX) XX", or "(XXX) XXX".
  • If you enter 7 to 10 numeric characters, the format of the value will be: "(XXX) XXX-X", "(XXX) XXX-XX", "(XXX) XXX-XXX", or "(XXX) XXX-XXXX",.

Same with the zip code. the format of zip code would be either 5 digits or 5 digits then a dash, and follow by up to 4 digits of extension.

Summary

This is the end of the tutorial. It is not much comparing with my other work for this year. yet I had a lot of fun writing this tutorial and designing the sample application. As I have suspected, the basic validation mechanism is almost the same as AngularJS, simple and reusable.

In my sample application, I have discussed programmatically triggering form validation, and resetting the form. It also has the example of more advanced validation such as validating value of the input and set error when it is invalid. Finally, I throw in two example of value transformation for validation and display. These are some really easy mechanism you can apply and expand with your own invention of complex input validations. As long as you know the basics, the more complex ones can be implemented with a bit more efforts. Although the front end validation is powerful, it is inadequate. Imagine that a malicious user who uses postman or other tools to post HTTP request containing invalid input data instead of going though the front end web pages, your client end validation is useless against such abuse. So it is vitally important that you must copy the same validations to the back end server side implementation. You will be amazed that there are so many applications that does only front side validation, and no validation at all at the back end, leaving a huge security hole for attackers.

Thank you for reading my tutorial article. This is the last one for 2022. For 2023, I will be putting out more tutorials on Angular, such as integration with ui-bootstrap, other 3rd party components. My goal is to make myself an expert of Angular application developer, and share more intermediary and advanced design approaches with this community. Merry Christmas and happy new year!

FYI

Han is currently looking for exciting and challenging jobs. Han has 19 years of combined software engineering experience. Han has worked on the successful release of numerous software products as a developer, a QA, and developer lead. If you would like to add a CodeProject MVA (2022) to your project team for the next great product, please contact Han at "sunhanbo (at) duck.com". Thank you for being interested.

History

  • 12/25/2022 - Initial draft.

Your Comment


Required
Required
Required

All Related Comments

Loading, please wait...
{{cmntForm.errorMsg}}
{{cmnt.guestName}} commented on {{cmnt.createDate}}.

There is no comments to this post/article.