« back

Ng-model and custom form validation

Last week, I was playing a bit with the popular angularjs framework and discover how to handle with custom form validation.

I had a form with multiple selects to select countries but each country selected has to be unique.

screen1

Here the simply html of the current form (without the Add/Remove all buttons) :

   
    <form name="myForm" ng-submit="doSomething()">
        <div ng-repeat="price in prices">
            <select name="country" ng-model="price.country" required="required" ng-options="key as value for (key, value) in countries"></select>
        </div>
        <button type="submit" ng-disabled="myForm.$invalid">Save</button>
    </form>

Let's now deal with the custom form validator [1] and create a angular directive called unique. This validator will check if an existing country already exists in scope.prices.

   
    angular.directive('unique', function() {
        return {
            require: 'ngModel',
            link: function(scope, elem, attr, ngModel) {

                function isUnique(value, minOccur) {
                    var isValid = true,
                        count = 0;

                    if ("undefined" == typeof(minOccur)) {
                        minOccur = 0;
                    }

                    $.each(scope.prices, function(index, el) {
                        if (el.country == value) {
                            if (++count > minOccur) {
                                isValid = false;
                                return;
                            }
                        }
                    });

                    return isValid;
                }

                function validate(value) {
                    var valid = isUnique(value);
                    ngModel.$setValidity('unique', valid);

                    return valid ? value : undefined;
                }

                ngModel.$parsers.unshift(function(value) {
                    ngModel.$setValidity('unique', isUnique(value));

                    return value;
                });

                ngModel.$formatters.unshift(function(value) {
                    ngModel.$setValidity('unique', isUnique(value, 1));

                    return value;
                });
            }
        }
    });

Note: ngModel.$parsers.unshift is called on value changed and BEFORE the value is selected (so the number of same element must be 0 to be invalid).
While ngModel.$formatters.unshift is called during page load (number of found elements must be > 1 to be invalid).

This directive is using "ngModel.$setValidity" to change the validity of the model.

To use it, just add "unique" in the html element

   
    <select ng-model="price.country" required="required" ng-options="key as value for (key, value) in countries" unique></select>

Playing with ng-show, we can easily display an error message

   
 <span ng-show="myForm.element.$error.unique">
      "\{\{ data.country \}\}" is not unique.
</span>


However, with the current implementation, our unique directive is not reusable : Scope need to have a 'prices' element (which is an object).
Let's change that by setting the unique element directly in the attribute and use the 'name' attribute if the unique element is an object.

   
function isUnique(value, minOccur) {
    if ("undefined" == typeof(minOccur)) {
        minOccur = 0;
    }

    var isValid = true,
        count = 0;

    jQuery.each(scope[attr.unique], function(index, el) {
        if (el == value || el[attr.name] == value) {
            if (++count > minOccur) {
                isValid = false;
                return;
            }
        }
    });
    return isValid;
}
   
    <select ng-model="price.country" required="required" ng-options="key as value for (key, value) in countries" unique="prices"></select>

Now our directive is completely reusable, let style it a bit.
Angular automatically add classes to (in)valid form element (ng-valid/ng-invalid).

   
    <style>
    .ng-invalid {
      border: 1px solid red;
    }
    </style>

screen2

To finish, we can play with ng-disabled [3] on the submit button to prevent the user to send the form if it is invalid.

   
    <button type="submit" ng-disabled="myForm.$invalid">Update</button>

screen3

Note: myForm is invalid because we changed the element validity in the directive using ngModel.$setValidity.


Links
1 2 3