Published May 05, 2016Last updated Jan 18, 2017

Angular Form Validation - ngModelController

Angular Form Validation - ngModelController

Form validation the "Angular way" is an extremely powerful tool. We have the ability to pass a simple regular expression to the ng-pattern attribute of the input field for simple input validation, or we can make our own custom validation functions utilizing the NgModelController.

So you might be thinking to yourself, yeah, but I can already use a pattern for validation in HTML5. True, indeed you can, but when you couple all the bits and pieces together and let Angular do its thing, you have a complete tool set at your disposal.

In this post, I wanna show how we can use the NgModelController in a custom validation directive to validate any piece of logic.

If you take a look at the Angular docs on the ngModel directive, you'll see this:

ngModel is responsible for:

  • Binding the view into the model, which other directives such as input, textarea or select require.
  • Providing validation behavior (i.e. required, number, email, url).
  • Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors).
  • Setting related CSS classes on the element (ng-valid, ng-invalid, ng-dirty, ng-pristine, ng-touched, ng-untouched), including animations.
  • Registering the control with its parent form.

You are no doubt already using this little piece of logic in your code, and ng-model is one of the first pieces we learn when starting out with Angular - enter the ubiquitous hello world example.

<input type="text" ng-model="sometext" />
<h1>Hello {{ sometext }}</h1>   

That's right. That's all there is to the ng-model directive, it's easy to forget that every piece of code you use in the DOM from Angular is, yes, a directive.

However, we aren't interested in the ng-model directive. We wanna get down and dirty with the NgModelController, which provides the API for the ngModel directive.

Again, from the documentation:

The NgModelController contains services for data-binding, validation, CSS updates, and value formatting and parsing. It purposefully does not contain any logic which deals with DOM rendering or listening to DOM events. Such DOM related logic should be provided by other directives which make use of NgModelController for data-binding.

So, in order to use it, we must do as the docs say. Let's create our own directive which makes use of the NgModelController.

angular.module('PTTP.Directives.Validation', [])
  .directive('pttpValidator', function() {  
    return {        
      restrict: 'A',
      require: '?ngModel',
      link: linker,
      scope: {
        validateFunction: '&'
      }
    }
  });
    

That's the basis of the directive; an attribute that has an isolate scope passing in a validateFunction - simple enough.

What about the linker? Well, that's where the real goodies live.

var linker = function(scope, element, attr, ngModelCtrl) {
        /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    ngModel Controller Utilization
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
  ngModelCtrl.$parsers.push(function (value) {
    //Fire our validator function returning a boolean
    var result = scope.validatorFunction({ 'value': value });
    //Check the result to make sure it returned boolean and not undefined or null
    if (result || result === false) {
      ngModelCtrl.$setValidity(attrs.validatorName, result);
      return result ? value : undefined;  //Pass the value down the pipeline - here*
    }
    //*Or here
    return value;
  });
};

What the heck is going on here? So again, let's decompose the logic line by line.

To begin with, we are using the $parsers property to push a function with a value parameter. The $parsers pipeline simply is an array of functions that subsequently get called, passing their return value to the next function. You can think of it like this:

ngModelCtrl.$parsers = [func1, func2, func3]; //Initial $parsers setup
ngModelCtrl.$parsers.push(func4); //Push a new function
console.log(ngModelCtrl.$parsers); //[func1, func2, func3, func4];

So every time the ngModel value is changed this collection of functions is called each passing their return value to the next. Pretty straight forward.

From the docs...

For validation, the parsers should update the validity state using $setValidity(), and return undefined for invalid values.

If we examine deeper into the passed in function, we can see first off we are assigning a boolean value to the result variable. So, simple put, our validatorFunction that we pass in returns either true or false. Next, we check if the result is defined - either true or false. Then, based on the value of the result, we call $setValidity passing in the validatorName and the boolean result. Finally, we return a value, passing it along to the next function in the pipeline.

Here is how we consume the directive.

HTML

<form novalidate ng-controller="FormController"> 
    <!--Email-->
    <label for="email">
      Email
    </label>
    <input
      id="email"
      name="email"
      type="email"
      required
    />
            
    <!--Email Confirmation -->
    <label for="email-confirmation">
      Email Confirmation
    </label>
    
    <input
      pttp-validator
      id="email-confirmation"
      type="email"
      name="email-confirmation"
      validator-name="emailMatch"
      validator-function="checkEmailMatch(value)"
      required
    />
</form>
    

Controller

angular.module('PTTP.Controllers.Form',[])
  .controller('FormController', function($scope) { 
    $scope.formElements = {
      email: '',
      emailConfirmation: ''
    };    
    $scope.checkEmailMatch = function(value) {
      return value === $scope.formElements.email;  
    };
  };
    

In the HTML, we have created a simple form with a novalidate attribute, so as to stop the browser from validating the form itself, we want Angular to do this for us. Next, we have set up two input fields that are both expecting emails types, the second of which has our pttp-validator directive. This element has three attributes worth mentioning: name, validator-name, validator-function.

First - name - this is an important attribute, as Angular uses it to reference the input name. I will explain the use of it later, but for now, just make sure it exists.

<input name="email" />

Second - validator name - this is the name of the class Angular will append to the input when the input is either valid or invalid

<input class="ng-invalid-email-match" />
<!-- vs -->
<input class="ng-valid-email-match" />

Third - validator function - this lives outside the scope of this directive, which is important so the directive remains generic and reusable. We have a FormController wrapping the form, and through it, we have access to its scoped methods and variables. In this case, the checkEmailMatch method.

$scope.checkEmailMatch = function(value) {
  return value === $scope.formElements.email;  
};
    

That's pretty much all there is to it! This is the basis of the use of the NgModelController for form validation. Now go and validate the shit outta yer forms!

Compiling SASS and PostCSS with Angular CLI
Beginner's Guide to Setting Up an Ionic project the Right Way
Angular/Node - Building a Command Line Tool to Generate Projects Pt.2 - Angular and your FileSystem
Report