grivescorbett
grivescorbett

Reputation: 1625

AngularJS validate form array length

Say I have a model object with the attribute favoriteColors

{
    ...
    favoriteColors: ['red', 'green', 'blue']
    ....
}

I expose them to the user with an ng-repeat

<form name="userForm">
    ...
    <ul>
        <li ng-repeat="color in user.favoriteColors">
            <input type="text" ng-model="color" />
            <a href="" ng-click="delete(color)">remove</a>
        </li>
    </ul>
    <a href="" ng-click="add()">Add a new favorite color</a>
    ...
</form>

I would like to be able to check the validity of the favoriteColors field doing something like this

<div ng-show="userForm.favoriteColors.$error">
    You must have at least one favorite color
</div>

It doesn't seem possible to do this using a built in validator, and I'm not sure on which element I would put a custom directive in order to get the ngModelController for favoriteColors.

Upvotes: 3

Views: 8970

Answers (4)

Betty St
Betty St

Reputation: 2860

There is another way by adding a directive:

/**
 * Validator for checking an array length.
 * @example
 *   <input ng-model="user.favoriteColors" validate-length />
 */
app.directive('validateLength', function() {
  return {
    require: 'ngModel',
    link: function(scope, element, attrs, ngModel) {

      // do not set invalid model to undefined, should stay []
      ngModel.$options = {
        allowInvalid: true
      };

      scope.$watch(function () { return ngModel.$modelValue && ngModel.$modelValue.length; }, function() {
        ngModel.$validate(); // validate again when array changes
      });

      ngModel.$validators.length = function() {
        var arr = ngModel.$modelValue;
        if(!arr) { return false; }

        return arr.length > 0;
      };

    }
  };
});

Upvotes: 4

firebean
firebean

Reputation: 221

@Loi Pham, unfortunately I am not allowed to comment, so I have to add a post. I like your approach. However I had to add ng-model to the input to make the validation work:

<input style="display: none" type="number" name="length" readonly ng-model="colors.length" min="1">

Upvotes: 11

Loi Pham
Loi Pham

Reputation: 137

My solution is add one hidden input tag and binding with length of array

    <form name="userForm">
      ...
      <ul>
        <li ng-repeat="color in user.favoriteColors">
          <input type="text" ng-model="color" />
          <a href="" ng-click="delete(color)">remove</a>
        </li>
      </ul>
      <a href="" ng-click="add()">Add a new favorite color</a>
      ...

      <!-- new line -->
      <input style="display:none" name="colorsLength" type="number" min=1 value="{{user.favoriteColors.length}}"/>
    </form>

So, you can use userForm.colorsLength.$error for your validation. Good luck!

Upvotes: 13

Craig Squire
Craig Squire

Reputation: 2141

To have validation the way you are requesting, you have to use an ng-model to put your array of colors into the form so that the array can be validated.

Here's a quick example in a plunker where I push a validator on the $parsers pipeline of the ngModelController which will check the colors array length. Keeping the colorRequired directive separate will allow you to have situations where a color is not required. You could also add to that directive so it will take a boolean argument on the attribute so you can decide at run-time if a color should be required.

http://plnkr.co/edit/yFuSXxacSW811WfZqaPC

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.colors = ['red', 'blue', 'green'];
});

app.directive("colorGroup", [function() {
    "use strict";
    return {
        restrict: 'E',
        require: 'ngModel',
        template: '<ng-form name="userForm">\
               <ul>\
               <li ng-repeat="color in model">\
              <input type="text" ng-model="color" />\
              <a href="" ng-click="delete($index)">remove</a>\
              </li>\
              </ul>\
              </ng-form>',
        link: function($scope, element, attrs, ngModelCtrl)
        {
            $scope.$watch(function(){
                return ngModelCtrl.$modelValue;
            }, function(){
                $scope.model = ngModelCtrl.$viewValue;
            });

            $scope.delete = function(idx)
            {
                        ngModelCtrl.$viewValue.splice(idx, 1);
                        ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue);
                        $scope.model = ngModelCtrl.$viewValue;
            }
        }
    }
}]);

app.directive("colorRequired", function() {
    "use strict";
    return {
        restrict: 'A',
        require: 'ngModel',
        link: function($scope, element, attrs, ngModelCtrl)
        {
                ngModelCtrl.$setValidity('colorrequired', (ngModelCtrl.$viewValue.length > 0));

                ngModelCtrl.$parsers.push(function(viewValue) {
                    var valid = viewValue.length > 0;
                    ngModelCtrl.$setValidity('colorrequired', valid);
                    return valid ? viewValue : undefined;
                });
            }
        }
    });

Upvotes: -1

Related Questions