Keeping Angular form validation DRY

283 views Asked by At

I have a form that has maybe 15-20 fields. Each one of them contains an attribute that looks something like this:

ng-class='addressForm.city.$invalid && addressForm.city.$touched ? "error" : ""'

So I have that long string of code repeated 15-20 times. This sets my DRY alarm bells off bigtime.

I could devise my own way to make this more DRY but I don't want to re-invent the wheel. Is there a gererally-accepted way of keeping Angular form validation DRY?

4

There are 4 answers

0
Tanase Butcaru On

As @lain said, you don't have to add another class (like error) if the field is invalid, Angular adds that for you by default, it's just the name that differs (ng-invalid). You can see how is that used here (the official form example from Angular).

If you still want to do this in your way, this is the implementation of my latest comment, using ngChange directive.

The html:

<input type="text" ng-model="addressForm.city" required ng-change="fieldChanged(this, 'city')">

The change event:

$scope.fieldChanged = function(el, fieldName){
   if($scope.addressForm[fieldName].$invalid && $scope.addressForm[fieldName].$touched) angular.element(el).addClass('error');
   else angular.element(el).removeClass('error');
}

This is not good practice (to manipulate the DOM in the controller), you should implement that in a directive, but binding a directive to each field would add watchers and I, personally, try to avoid as much as possible using too many watchers.

A more elegant option would be to combine this ngChange with ngClass or simply go with that simple DOM manipulation within controller. It's your choise :)

1
Jason Swett On

I ended up creating my own directive for this. I believe the following directive, when applied, will behave equivalently to this:

form(name='addressForm')
  input(
    type='text'
    name='city'
    ng-class='addressForm.city.$invalid && (addressForm.city.$touched || addressForm.$submitted) ? "error" : ""'
  )

Instead of all that, I can do:

form(name='addressForm')
  input(
    type='text'
    name='city'
    validate-for='addressForm'
  )

The directive will check validity on:

  • Blur
  • Form submission
  • Value change

Here's the code (ES6):

'use strict';

class ValidateFor {
  constructor() {
    this.restrict = 'A';
    this.require = 'ngModel';

    this.link = ($scope, $element, $attrs, ngModel) => {
      var form = $scope[$attrs.validateFor];
      var field = form[$element.attr('name')];

      $scope.$on('form-submitted', () => {
        this.checkForErrors(field, $element);
      });

      $scope.$watch(() => ngModel.$modelValue, () => {
        if (field.$touched) {
          this.checkForErrors(field, $element);
        }
      });

      $element.bind('blur', () => {
        this.checkForErrors(field, $element);
      });
    };
  }

  checkForErrors(field, $element) {
    if (field.$invalid) {
      $element.addClass('error');
    } else {
      $element.removeClass('error');
    }
  }
}

ValidateFor.$inject = [];

You could probably even eliminate the necessity for supplying the form name in validate-for. I just did it that way because I have some nested form situations.

2
Iain On

If it's just for styling use CSS.

Angular will add .ng-invalid and .ng-touched to input elements that are invalid and touched.

Or you could wrap the whole thing in a directive something like

angular.module('module').directive('errorClass', function(){
    return{
        require: 'ngModel',
        link: function(scope, el, attr, model) {
             function setClass() {
                if(model.$touched && model.$invalid) {
                   if(!el.hasClass('error')) { 
                      el.addClass('error');
                   }
                } else {
                    el.removeClass('error');
                }
             }

             scope.$watch(function(){ return model.$touched; }, setClass);
             scope.$watch(function(){ return model.$invalid; }, setClass);
        }
    }
});

Also i havn't actually used this directive, so it may need some tweaking.

0
Jay Sullivan On

valdr looks great. I haven't used it yet, but I will try it, and will update this post later.