10101010101001

0101010101001

AngularJS custom validation with business rules

with 11 comments

Long time, no posts :)
I’ve recently switched from silverlight to html5/javascript and we’re using angular – which is great! (and not so great sometimes).
My thoughts about angular validation is that it has been designed with very simple scenarios in mind.
If you just have a login form or one registration form, I would say that it provides enough.
If, however, you are building an app with dozens of screens and dozens of fields then you’ve come to the right place.

Let’s define some requirements first of what I want from my validation mechanism:
– When I first enter a screen and don’t do anything I want the fields to be pristine – not marked with any kind of error information
– If I modify a field and the value is invalid, I want the field marked with an error
– If I enter a screen, don’t modify any field and press submit, I want all my validation rules to be checked and mark the fields that have errors with error.
– I want localized and parameterized error messages (e.g. for a field with a minimum length rule I want to be able to show an error message like ‘value must have at least %s characters’)
– Some validation rules cannot be checked client side and the service I call will return the validation errors on a response. So I want to have the possibility to somehow mark the field with an error and set a custom error message.
– Some validation errors might not be related to an input field. E.g. you’re doing a student/courses app and you want to be able to select the courses for a student. There are some rules in what he can select (he cannot have certain courses as they are for year 2 and he is in year 1 for example). You’re also not using an input field to select the courses.

– Some validation errors are related to multiple input fields at the same time. E.g. you have a start date/end date/resolution date combo that have a rule that start date < end date < resolution date.

– I want to be able to validate at will all the rules of a domain model object (e.g student object) and see which fail. I want to do this in a service/controller. (e.g. validator.validate(student))

Let’s see what we can do with angular out of the box:
Suppose we have a simple form for entering data for a person name. Name is required and can have maximum length 512 characters.

We quickly write some code:

<form name=”personForm” novalidate=”" ng-app=”PersonApp” ng-controller=”PersonController” >
	<input name=”personName” type=”text” placeholder=”Person Name” ng-model=”viewModel.person.name” required ng-Maxlength=”3″/>
	<span ng-show=”personForm.personName.$invalid”>Person name is required and must be smaller than 3 characters</span>
</form>

What I don’t like about this code is that the length constant  “3” is defined in the UI. Then it is duplicated in two places and the error message is not localizable in its current form.

An easy fix would be to  define the constant ‘3’ in an angular module.constant, then refer to the constant in the controller and bind the UI to it instead of defining it in the UI. Also the error message should be defined in the controller using a localization mechanism.

Then we get to this requirement: “If I enter a screen, don’t modify any field and press submit, I want all my validation rules to be checked and mark the fields that have errors with error”

There’s no easy fix for this but Andy Norborg has written an excellent article on writing a directive that helps you accomplish that.

Initially I though about using that, but I would still have to figure a way on how to do these requirements:

– Some validation errors might not be related to an input field. E.g. you’re doing a student/courses app and you want to be able to select the courses for a student. There are some rules in what he can select (he cannot have certain courses as they are for year 2 and he is in year 1 for example). You’re also not using an input field to select the courses.

– Some validation errors are related to multiple input fields at the same time. E.g. you have a start date/end date/resolution date combo that have a rule that start date < end date < resolution date.

– I want to be able to validate at will all the rules of a domain model object (e.g student object) and see which fail. I want to do this in a service/controller. (e.g. validator.validate(student))”

Stop and think about it. How would you do that? I can see you struggling to break outside the form and out of ng-model. Ask yourself why should validation be strongly coupled to an ng-model and to a form? In the real world I don’t see any coupling between the two concepts. All I see are a set of validation rules which in many cases are used to to help validate form. That doesn’t mean it’s the only place you will ever need to use them. Well, sadly, the angular out of the box validation solution is saying to us that a form is the only place you will ever need to do any validation. Why it says that? I think because it fits >90% of validation usage patterns which are for fairly simple forms. Simplicity wins over flexibility which is not bad as long as you have an alternative for the more complex cases.

So back on how to solve this requirement. It can be done by writing a directive which would bind to a collection of validation rules defined in the controller and would execute those rules on input changes/submit. That’s what I did and by doing this, I bypassed the angular way of doing validation. (built in + custom validators). You know how it felt? Liberating! :) It feels good to be free! I want to give this freedom to you so here goes my suggested way of doing validation.

Validation should be based around business rules (validation rules). Those rules should be defined in a way they can be used/reused in any context (services/controller) (not just UI context).

Let’s write a validation rule that tells me if a a string has a length smaller then a maximum length.

(function (angular) {

    angular.module('servicesModule')
        .factory('MaxLengthRule', function () {
            return function (maxLength, errorMessage) {
                this.errorCode = 'maxLength';
                this.validate = function (value) {
                    return {
                        result: (value !== undefined && value !== null) ? value.length <= maxLength : true,
                        errorMessage: _.str.sprintf(errorMessage, maxLength),
                    };
                };
            };
        });

})(angular);

A validation rule is identified uniquely by an error code and it has a validate method that tells us if the rule is valid or not.
As you can see, we’re not returning a true/false value when validating the rule. The reason for this is that for a more complex validation rule the error message is not always the same. Imagine writing a validation rule to check if an arithmetical expression entered by the user is correct. So the user can enter: 1 + 1 * 2 (correct), 1 – – 1 (error: cannot have two minuses in a row), 1 * (2 – 1 * (3 + 5) (error: round bracket doesn’t close), etc.
If you just return a false message how will you know what error message to display? You could always display a generic: “calculation expression invalid” but it wouldn’t be too user friendly. That’s why I prefer to have a validation rule return a ValidationResult composed of a true/false result of validation and the error message of why the rule is invalid.

Next, one can group various validation rules in sets of validation rules. This proves useful when validating all the rules for a specific domain object field. For example we can define the set of rules that apply to the student name field. Let’s say they are [MaxLength(5), Required]. This represents a set of rules or RuleSet.

Now we just need a way to use those validation rules in the UI. What I mean by that is to mark fields with validation errors and show error messages. We need to do this whenever the value of a field changes or the user tries to submit (without changing any field) or some other custom scenarios.

To do this I wrote two directives. One is called validator.
It looks like this:

<input id="personName" ng-model="viewModel.person.name" validator="viewModel.validator" ruleSetName="personName" />

It can be placed on any field that supports ng-model and it will observe value changes and validation error changes. Whenever the value changes or a validation changed event is raised it will validate a RuleSet and mark the field with a has-error class if validation rules are failing. (basically makes the field red/borders red).
I didn’t want to couple the validation mechanism to the concept of “submitting” a form. That’s why there’s this concept of “validation changed” event which can be triggered manually by anything (e.g. a form submit) and it will cause the validator directive to re-validate the field on which it is placed.

Can you use this directive only on a field that has ng-model? If provided, ng-model controller will be used for detecting dirtiness, but it can easily be used without “ng-model” optional and then you just look at the validation changed event for more “exotic” types of input that don’t use ng-model. You can trigger the “validation changed” event manually yourself.

To show the validation error message I use another directive called validationMessageFor.
It looks like this:

<input id="personName" ng-model="viewModel.person.name" validator="viewModel.validator" ruleSetName="personName" />
<span validationMessageFor="personName" />

This directive can be placed on any element – a span for a example.
It will watch whenever the input it refers to changes or when a validation changed event is raised.
While I could’ve done some trick with the validator directive – make it inject a span next to the input field, I preferred to wrote another directive. This is because I wanted flexibility in showing an error messages in whatever way I want and anywhere on the page. E.g. I could do an errors area, if needed at the bottom of the page, some baloons with errors, etc.
Plus I can easily extend this directive to show errors for validation rules that are not specific to a single field or to any field if needed.

Now getting back to the validation solution you probably noticed that the validator directive is bound to a validator object.
This object keeps a collection of RuleSet. For example it can keep all the rule sets for a student object. As discussed, each RuleSet will contains the set of validation rules for a specific field (e.g. minlength, required).
In addition to this it has a few methods to validate all the rulesets and store the errors that resulted in an errors collection.
It also exposes a validation changed event that can be raised to notify observers (e.g. UI) that they need to re-validate the rules.

The code for the validator looks like this:

(function (angular) {

    angular.module('servicesModule')
           .factory('Validator', function () {
               return function (validationRules) {

                   // this.validationRules example:
                   // {
                   //    contactTitleRules: [rules.isRequired(), rules.minLength(3)],
                   //    contactFirstNameRules: [rules.isRequired(), rules.minLength(3), rules.maxLength(7)],
                   // }
                   this.validationRules = validationRules;

                   // this.errors example:
                   //  {
                   //     ruleSetName1 : { errorCode1: true, errorCode2: false }
                   //     ruleSetName2 : { errorCode1: true, errorCode2: false }
                   //  }
                   this.errors = {};

                   this.validationChangedObservers = [];

                   this.triggerValidationChanged = function () {
                       for (var i = 0; i < this.validationChangedObservers.length; i++) {
                           this.validationChangedObservers[i]();
                       }
                   };

                   this.watchValidationChanged = function(onValidationChanged) {
                       this.validationChangedObservers.unshift(onValidationChanged);
                   };

                   this.setHasError = function (ruleSetName, errorCode, isValid) {
                       if (!this.errors[ruleSetName]) {
                           this.errors[ruleSetName] = {};
                       }

                       this.errors[ruleSetName][errorCode] = !isValid;
                   };

                   this.validateRuleSet = function(ruleSetName, value) {
                       var rules = this.validationRules[ruleSetName];
                       for (var i = 0; i < rules.length; i++) {
                           var ruleIsValid = rules[i].validate(value).result;
                           this.setHasError(ruleSetName, rules[i].errorCode, ruleIsValid);
                       }
                   };

                   this.validateAllRules = function () {
                       for (var ruleSetName in this.validationRules) {
                           this.validateRuleSet(ruleSetName);
                       }
                   };

                   this.hasRuleSet = function(ruleSetName) {
                       return this.validationRules != undefined && this.validationRules[ruleSetName] != undefined;
                   };

                   this.ruleSetHasErrors = function(ruleSetName) {
                       var ruleSetErrors = this.errors[ruleSetName];
                       if (ruleSetErrors != undefined) {
                           for (var errorCode in ruleSetErrors) {
                               if (ruleSetErrors[errorCode] === true) {
                                   return true;
                               }
                           }
                       }

                       return false;
                   };

                   this.ruleSetHasError = function(ruleSetName, errorCode) {
                       var ruleSetErrors = this.errors[ruleSetName];
                       return ruleSetErrors != undefined ? this.errors[ruleSetName][errorCode] : false;
                   };

                   this.hasErrors = function () {
                       for (var ruleSetName in this.errors) {
                           if (this.ruleSetHasErrors(ruleSetName) === true) {
                               return true;
                           }
                       }

                       return false;
                   };
               };
           });
})(angular);

The only thing left is to construct the validator object in the controller.
For this I wrote a validation factory to help build the rule sets easier. (e.g. you don’t have to provide an error message every time you construct a MaxLength rule for example, as it can use a generic error message).

It looks like this:

    var rules = new ValidationRuleFactory(genericErrorMessages);
    $scope.viewModel.validationRules = {
      personFirstNameRules: [rules.isRequired(), rules.minLength(3)],
      personEmailRules: [rules.isRequired(), rules.minLength(3), rules.maxLength(7)],
    };

You can see the entire solution in action in this Plnkr
While I really like this solution I must warn you that the code is probably not perfect. It’s probably not using the best practices of angular / javascript in some places as I’m fairly new to javascript/angular with just a few weeks of experience with them.

Feel free to comment with your thoughts or things that you see that need improvement.

Advertisements

Written by Liviu Trifoi

October 19, 2013 at 8:30 pm

11 Responses

Subscribe to comments with RSS.

  1. Impressive workaround! A pity AngularJS doesn’t support this sort of thing natively. I’m currently working on a project with very similar needs, and was wondering whether you managed to solve all of your requirements, specifically:

    – Some validation errors are related to multiple input fields at the
    same time. E.g. you have a start date/end date/resolution date combo
    that have a rule that start date < end date < resolution date.

    – Some validation rules cannot be checked client side and the service
    I call will return the validation errors on a response. So I want to
    have the possibility to somehow mark the field with an error and set a
    custom error message.

    I'd be interested to see how you approached solving these requirements.

    Ashley

    January 20, 2014 at 4:52 am

    • For dependent validation fields so far I’ve managed to get away with using the same business rule for each of the dependent field in the set.
      E.g If you have a “start date < end date < resolution date" rule then I would reuse this rule on all 3 fields and mark all 3 with error whenever one of them changes.
      To accomodate this you canchange the validation code such that the validationRulesFactory takes a value provider on the constructor like this: new ValidationRuleFactory(genericErrorMessages, dataProvider);
      The dataProvider is a map of functions that return the field values: e.g. var dataProvider = { startDate: function () { return self.startDate; } }
      Then you can create a dateValidRule that takes as argument all the needed dates from dataProvider (startDate, endDate, resolutionDate) and verifies that they satisfy the condition: "start date < end date < resolution date".
      You can reuse this rule on all 3 fields.

      “Some validation rules cannot be checked client side and the service
      I call will return the validation errors on a response. So I want to
      have the possibility to somehow mark the field with an error and set a
      custom error message.”
      I didn’t have to implement this yet, but I guess you could validate on the server, then take the server validation result as a JSON. { “startDate” : { “isValid” : false, “message” : “startDate is invalid ….” } }
      Then you could extend validationMessageFor directive to watch for some event you define and trigger that also contains the server side errorMessage and just shows it.

      Liviu Trifoi

      January 21, 2014 at 9:44 am

  2. Nice work.

    I was looking to add the form in a dynamic way using ng-repeat. However it seems it his having issues with the id being set dynamically.

    TypeError: Cannot read property ‘validationInfoIsDefined’ of undefined

    Any suggestions.

    {{field.label}}

    Submit

    Tom Kennedy

    June 14, 2014 at 7:56 am

    • form class=”form-horizontal”
      div class=”control-group” ng-repeat=”field in viewModel.fields”
      label class=”control-label”{{field.label}}/label
      div class=”controls” ng-switch=”field.type”
      input ng-switch-when=”text” type=”text” id=”field.label” ng-model=”viewModel.field” validator=”viewModel.validator” ruleSetName=”personFirstNameRules”/
      span ng-switch-when=”text” validation-Message-For=”{{field.label}}”/span
      /div
      /div

      buttonSubmit/button
      /form

      Tom Kennedy

      June 14, 2014 at 8:04 am

    • Hi Tom. The id of the input you are showing the error message for is the string “field.label”. But in the validation-message-for directive you are using {{field.label}} instead of “field.label”.
      I don’t know the exact way you’re generating unique id’s for elements but I’m guessing you should make both as “{{field.label}”.

      The validation-message-for directive searches for the element with the provided id. Then, it looks at it’s validator/rule-set properties and shows an error message when that rule-set becomes invalid.
      Another solution is to modify the validation message directive to bind directly to the validator and rule-set as parameters and not use id lookups at all to search for elements.
      If solution number 1 doesn’t work, use solution number 2. That’s what we eventually ended up using as the code-base evolved. (But for entire different reasons than yours).

      Liviu Trifoi

      June 16, 2014 at 7:22 am

  3. Excellent article

    Narendran

    June 16, 2014 at 6:26 am

    • Hi Liviu, I am seeking a way how to implement complex validation and I have the same conceptual issues with validations in Angularjs. Especially that the data integrity is defined in the View. Same as you describe … ng-model=”viewModel.person.name” validator=”viewModel.validator” … The person name validity depends on the View definition. Personally I am missing Metadata definition for the Model defined outside the View.

      I have found one way how to define Model is use the angular factory (e.g. https://medium.com/opinionated-angularjs/angular-model-objects-with-javascript-classes-2e6a067c73bc). Implement validate/save function there and call it from the controller. The validation result and the metadata can be store in the Model and used in the View. (Way how to use factory in controller(s) is describe https://egghead.io/lessons/angularjs-sharing-data-between-controllers)

      However that means re-implement basic validations like required, max, min lenght, etc.

      Any way I would welcome your comment and potential draw backs.

      Jarda K

      December 5, 2014 at 8:42 am

  4. Hi Liviu, I am seeking a way how to implement complex validation and I have the same conceptual issues with validations in Angularjs. Especially that the data integrity is defined in the View. Same as you describe … ng-model=”viewModel.person.name” validator=”viewModel.validator” … The person name validity depends on the View definition. Personally I am missing Metadata definition for the Model defined outside the View.

    I have found one way how to define Model is use the angular factory (e.g. https://medium.com/opinionated-angularjs/angular-model-objects-with-javascript-classes-2e6a067c73bc). Implement validate/save function there and call it from the controller. The validation result and the metadata can be store in the Model and used in the View. (Way how to use factory in controller(s) is describe https://egghead.io/lessons/angularjs-sharing-data-between-controllers)

    However that means re-implement basic validations like required, max, min lenght, etc.

    Any way I would welcome your comment and potential draw backs.

    Jarda K

    December 5, 2014 at 8:56 am

  5. One of the best generalized client-side validations that I’ve seen. Wrote one earlier with jQuery and have been searching one for AngularJS and finally found one. Awesome Liviu.

    Gopi Sundharam (@libragopi)

    February 14, 2015 at 3:15 am

  6. What is _.str ?

    rijuvashisht

    April 21, 2015 at 2:23 pm

    • An underscore extension that does string placeholder replacements in a manner similar to the sprintf from C language.
      Anyway, this post is quite old and you should not use that anymore. You should use ES6 template literals

      Liviu Trifoi

      September 25, 2016 at 10:33 am


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: