7

I have a form with some text input fields and a dynamic list of items stored in the $scope of the controller, with some functions to add/remove items in the list. I want to invalidate the form until the items list reaches a predefined length.

So I created a formRepeat directive which takes a ngModel attribute and then use the ngModelController to invalidate the form.

http://plnkr.co/edit/jSFvak?p=preview

This works but I think it's not the better way to do that as the directive is not very flexible.

The easiest would be to invalidate the form in the controller with something like :

$scope.myForm.$valid = false;

But this doesn't work.

Is there a better way ?

7 Answers 7

10

The easiest way for me was to target array's length as hidden number input and put min validation on it. Very clean solution, in fact.

<input style="display: none;" type="number" name="itemsCount" ng-model="items.length" min="{{min}}">

Check out the updated plunker

Sign up to request clarification or add additional context in comments.

1 Comment

This answer is easy, elegant and quick to implement - need to add 'required' attribute Brother.
3

Based upon your plunker. I'd use the following $watch function (similar to @NicolasMoise response)

$scope.$watch('items', function (items) {
    $scope.myForm.$setValidity('count', items.length >= 5);
}, true);

It's important to set the objectEquality flag to true so that the $watch will fire if any of the objects properties change

or, if only a shallow list (collection) comparison is made, use $watchCollection

$scope.$watchCollection('items', function (items) {
    $scope.myForm.$setValidity('count', items.length >= 5);
});

I've also never had any luck with $setValidity('$valid') or similar

2 Comments

Good idea to create a new 'error key', seems to be the cleanest way to handle this. Thanks !
how would you turn this into a custom validator?
3

The best way to do so (IMO) is to create a custom directive that uses the ngModelController Validators.

Validators get executed every time there is an update on the model and are used for form validity. Your directive may look something like this:

angular.module('directiveTest', []).directive('minLength', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ngModel) {
  scope.minlength = attrs.minLength || 1;
  ngModel.$validators.minLength  = function(modelValue){
    /* Assume TRUE when empty, as ngRequired should be used for mandatory values */
    return (ngModel.$isEmpty(modelValue)) ? true : (modelValue.length >= scope.minlength);
  };
}
};
});

And can call it from your HTML like this:

<input type="text" name="content" ng-list min-length="2" ng-model="content" />

You can find the working example on the following Plunker.

1 Comment

this has one issue: when array length is decreased to zero from controller then array instance is actually deleted
1

I don't think it's necessary to use a directive in this case. Just have an ng-repeat for items and inside your controller something like this

$scope.$watch('items', function(val){
    if(val.length<5){
        //$scope.myForm should be available here
        $scope.myForm.setValidity('$valid');
        //add additional form validation ($dirty, setting back to $invalid, etc...)
    }
})

1 Comment

Tried it here (see plunker). Like this it doesn't do anything, if a replace the '<' with a '>' it does invalidate the form after 5 items have been added... I didn't got any good result by replacing '$valid' by '$invalid'. After some thinking, I don't think it's a good idea to validate the form itself in the controller, as it would 'shadow' all the other input fields validation rules...
0

maybe it is what your looking for http://docs.angularjs.org/api/ng/type/form.FormController

3 Comments

Maybe I missed something but it seems we can only invalidate form controls with the form controller, not the form itself (setValidity only works on form controls). The problem is, I use ng-repeat to generate the DOM for the dynamic list stored as an array in the scope, so there isn't any form control I can use FormController on.
$scope.myForm should be available inside your controller. However, AFAIK you can't access inside directives (your own custom or inside ng-repeat)
You can also set validity for the entire form, not sure how it reacts when there are conflicts.
0

I think you can achieve this using ng-class.

Try this , In HTML,

<html data-ng-app="myApp">

  <head>
    <link data-require="bootstrap-css@*" data-semver="3.1.1" rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" />
    <script data-require="angular.js@*" data-semver="1.2.13" src="http://code.angularjs.org/1.2.13/angular.js"></script>
    <script data-require="angular-animate@*" data-semver="1.2.13" src="http://code.angularjs.org/1.2.13/angular-animate.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body data-ng-controller="MainCtrl">
    <div class="container">
      <div class="col-sm-12 col-md-6 col-md-offset-3">
        <form name="myForm" ng-class="formClass">
          <div class="form-group">
            <label>Name</label>
            <input type="text" name="name" data-ng-model="user.name" class="form-control" required="" />
          </div>
          <div class="form-group">
            <label>Country</label>
            <input type="text" name="country" data-ng-model="user.country" class="form-control" required="" />
          </div>
          <div class="form-group">
            <label>Items</label>
            <br />
            <button type="button" class="btn btn-primary" data-ng-click="addItem()">Add</button>
            <p data-ng-show="items.length < min">I need at least {{min}} items ! (so {{min - items.length}} more would be good)</p>
            <div data-ng-repeat="item in items">
              <button type="button" class="btn btn-danger" data-ng-click="removeItem($index)">Delete</button>
              <span>{{item}}</span>
            </div>
          </div>
        </form>
      </div>
    </div>
  </body>

</html>

In your script.js,

angular.module('myApp', ['ngAnimate']);

angular.module('myApp')
  .controller('MainCtrl', ['$scope', function ($scope) {
    $scope.items = [];
    $scope.min = 5;
    var _counter = 0;
     $scope.formClass="invalid";
    $scope.addItem = function () {
      $scope.items.push('item' + _counter);
      _counter++;
     $scope.isFormValid();
    };
    $scope.isFormValid=function(){
       if ($scope.items.length < 5) {
        $scope.formClass="invalid";
      } 
      else if ($scope.items.length >=5){
         $scope.formClass="valid";
      }
    }
    $scope.removeItem = function (index) {
      $scope.items.splice(index, 1);  
      $scope.isFormValid();
    };

  }]);

In css file,

body {
  padding: 16px;
  background: #555555;
}

/*.my-form {
  transition:0.5s linear all;
  padding: 16px;
  border-radius: 4px;
  background: #ffffea;
}*/

.invalid {
    transition:0.5s linear all;
  padding: 16px;
  border-radius: 4px;
  background: #ffffea;
  background: #ffeaea;
}

.valid {
    transition:0.5s linear all;
  padding: 16px;
  border-radius: 4px;
  background: #ffffea;
  background: #eaffea;
}

Do you want something like this?.

Please have a look at this plunker

1 Comment

Thanks. It's working but it's just a visual trick. What I try to do is to make the form itself aware that something is wrong, like any other form error.
0

a bit late, but I do that like this:

<button type="button" class="btn btn-success" 
        ng-disabled="form.$invalid || user.groups.length == 0>
        Submit
</button>

1 Comment

Thanks. It's working but it's just a visual trick like the other answer. What I try to do is to make the form itself aware that something is wrong, like any other form error (wrong input size, wrong email format, ...).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.