Custom validators in TypeScript for AngularJS

When working with AngularJS form validation I noticed that I write the same things over and over again. Further more, writing unit tests for these directives are always cumbersome. Because of this I created a small wrapper to ease the pain of writing validators and make tests as easy as possible.

This post will go through how I wrapped custom validators taking away the AngularJS details. The solution is available as an npm module named ng-custom-validator.

NOTE: This is for AngularJS, i.e. Angular 1. If you are using Angular, e.i. Angular 2+ use model driven (dynamic) forms instead.

Wrapping a custom validator

The AngularJS documentation have a small section of the documentation regarding custom validators. It might look easy enough, but write a handfull and you’ll get bored.

What we basically need to do is register a validator function on the $validators object of the ngModelController. If the function returns true the value is valid, if we return false the value is not valid and the field is marked appropriately.

In TypeScript directives are not a short thing to write and all the stuff with link functions and the value of this can be frustrating. Let’s hide that stuff and create an abstraction for the validation logic.

With the module, we register a custom validator with just two lines.

const minValueValidator = new CustomValidator(MinValueValidator, 'minValue');
app.directive('minValue', () => minValueValidator.factory());

The reference to MinValueValidator is the TypeScript class name and the second parameter is the directive name, used for setting proper error messages on the field. So what is going on behind the CustomValidator constructor?

Nothing fancy. In fact it is quite boring.

constructor(validator: { new (): IValidator; }, private directiveName: string) {
  if (!validator) {
    throw Error('CustomValidator: A validator must be supplied');
  }
  if (!directiveName) {
    throw Error('CustomValidator: A directive name must be supplied');
  }

  this.validator = new validator();
}

Some error handling and the instantiation of the validator. We set out to hide details like link functions, so here is what it does. We register the validator on the $validators object. Further more we observe the directive attribute. This is to pick up changes of values parsed in to the validator.

public link = (
  scope: ng.IScope,
  element: ng.IAugmentedJQuery,
  attrs: ng.IAttributes,
  ngModel: ng.INgModelController,
) => {
  ngModel.$validators[this.directiveName] = (modelValue, viewValue) => this.validateInput(modelValue, viewValue, ngModel, attrs);

  attrs.$observe(this.directiveName, () => ngModel.$validate());

You may notice that we did not just register the custom validator on the $validators object. A small wrapper function is used that handles empty model values. This ensures form fields with empty models do not show validation values before a user (or the application) have placed values in it. The wrapper is looking like this.

public validateInput = (modelValue: any, viewValue: any, ngModel: ng.INgModelController, attrs: ng.IAttributes): boolean => {
  if (ngModel.$isEmpty(modelValue)) {
    return true;
  }
 
  return this.validator.isValid(viewValue, attrs);
}

The custom validator is now in action and the value it returns tells AngularJS if the value is valid or not.

Handling dependencies

Say we have a custom validator that needs to access some service or constant to decide if the value is good or not. Here we need access to Angular’s dependency injection mechanism. To avoid to much hacking and to keep tests in mind, the module enables you to specify a setDependencies method used in the factory method.

To register a validator with injected dependencies it goes like this.

const dateValidValidator = new CustomValidator(DateFormatValidator, 'dateFormat');
app.directive('dateFormat', (dateFormat: string) => dateValidValidator.factory({ dateFormat }))

Just as one would expect. All most, that is. We map the dependencies into an object to keep type safety. Each validator in need of a dependencies specifies them with an interface. In the above example the interface is like this.

interface IDateFormatDependencies {
  dateFormat: string;
}

The magic happens in the factory method of the module.

public factory(dependencies?: TDependencies): ng.IDirective {
  if (dependencies && this.validator.setDependencies) {
    this.validator.setDependencies(dependencies);
  }
  return {
    link: this.link,
    require: 'ngModel',
    scope: false,
  };
}

Here the AngularJS specific stuff is handled. Creation of the directive object with the required directives etc. Furthermore if the validator have a setDependencies method, it gets calls with the supplied dependency object.

Writing a custom validator

So now we got all that is needed to make AngularJS happy. Now, what does the validators look like? Glad you asked. It is nothing but a class with an isValid method. Here is the DateFormat validator from above.

class DateFormatValidator implements IValidator {
  private dateFormat: string;
 
  public isValid(value: string): boolean {
    return moment(value, this.dateFormat, true).isValid();
  }
 
  public setDependencies(deps: IDateFormatDependencies): void {
    this.dateFormat = deps.dateFormat;
  }
}

It uses the global moment object from Moment.js to validate the input string and decide if it matches the format injected. In the HTML markup it is used like one would expect of any validation directive.

Another example, also used above, takes the directive value into account when validating if a value is larger than a minimum value.

interface IMinValueAttrs extends ng.IAttributes {
  minValue: string;
}
 
class MinValueValidator implements IValidator {
  public isValid(value: number, attrs?: IMinValueAttrs): boolean {
    if (!attrs.minValue) {
      return false;
    }
 
    return value >= parseInt(attrs.minValue, 10);
  }
}

This is used like so.

As these validators are nothing but plain TypeScript classes with no strings attached to AngularJS details they can be tested with no pains. Here it is done with Jasmine.

describe('minValue validator', () => {
  let customValidator: MinValueValidator;
 
  beforeEach(() => {
    customValidator = new MinValueValidator();
  });
 
  it('should return true if value is above minValue', () => {
    const value = 5;
    const attrs = { minValue: '4' } as IMinValueAttrs;
 
    const result = customValidator.isValid(value, attrs);
 
    expect(result).toBe(true);
  });
});

Complete example

Just to collect all the snippets into an overview this is what you need to enable a date format validator with the format injected by Angular’s dependency injection. First the IValidator and CustomValidator declarations.

interface IValidator {
  isValid(value: TValue, attrs?: ng.IAttributes): boolean;
  setDependencies?(dependencies: TDependencies): void;
}
 
class CustomValidator {
  public validator: IValidator;
  constructor(validator: { new (): IValidator; }, private directiveName: string) {
    if (!validator) {
      throw Error('CustomValidator: A validator must be supplied');
    }
    if (!directiveName) {
      throw Error('CustomValidator: A directive name must be supplied');
    }
 
    this.validator = new validator();
  }
 
  public link = (
    scope: ng.IScope,
    element: ng.IAugmentedJQuery,
    attrs: ng.IAttributes,
    ngModel: ng.INgModelController,
  ) => {
    ngModel.$validators[this.directiveName] = (modelValue, viewValue) => this.validateInput(modelValue, viewValue, ngModel, attrs);
 
    attrs.$observe(this.directiveName, () => ngModel.$validate());
  }
 
  public validateInput = (modelValue: any, viewValue: any, ngModel: ng.INgModelController, attrs: ng.IAttributes): boolean => {
    if (ngModel.$isEmpty(modelValue)) {
      return true;
    }
 
    return this.validator.isValid(viewValue, attrs);
  }
 
  public factory(dependencies?: TDependencies): ng.IDirective {
    if (dependencies && this.validator.setDependencies) {
      this.validator.setDependencies(dependencies);
    }
    return {
      link: this.link,
      require: 'ngModel',
      scope: false,
    };
  }
}

And the validator.

import * as angular from 'angular';
import * as moment from 'moment';
 
interface IDateFormatDependencies {
  dateFormat: string;
}
 
class DateFormatValidator implements IValidator {
  private dateFormat: string;
 
  public isValid(value: string): boolean {
    return moment(value, this.dateFormat, true).isValid();
  }
 
  public setDependencies(deps: IDateFormatDependencies): void {
    this.dateFormat = deps.dateFormat;
  }
}
 
const dateValidValidator = new CustomValidator(DateFormatValidator, 'dateFormat');
angular
  .module('app', [])
  .constant('dateFormat', 'MM/DD/YYYY')
  .directive('dateFormat', (dateFormat: string) => dateValidValidator.factory({ dateFormat }));

The end

That’s it. Grab the code from this post or depend on the npm module ng-custom-validator to get up to speed with custom validators. There is more details on the API and implementation on GitHub as well and if you got suggestions, issues or maybe a PR with some additional awesome functionality let me know

Posted in Angular
Write a comment