angular: Validate multiple dependent fields

Problem

Let's say I have the following (very simple) data structure:

$scope.accounts = [{
   percent: 30,
   name: "Checking"},
 { percent: 70,
   name: "Savings"}];

Then I have the following structure as part of a form:

<div ng-repeat="account in accounts">
    <input type="number" max="100" min="0" ng-model="account.percent" />
    <input type="text" ng-model="account.name" />
</div>

Now, I want to validate that the percents sum to 100 for each set of accounts, but most of the examples I have seen of custom directives only deal with validating an individual value. What is an idiomatic way to create a directive that would validate multiple dependent fields at once? There are a fair amount of solutions for this in jquery, but I haven't been able to find a good source for Angular.

EDIT: I came up with the following custom directive ("share" is a synonym for the original code's "percent"). The share-validate directive takes a map of the form "{group: accounts, id: $index}" as its value.

app.directive('shareValidate', function() {
return {
    restrict: 'A',
    require: 'ngModel',
    link: function(scope, elem, attr, ctrl) {
        ctrl.$parsers.unshift(function(viewValue) {
            params = angular.copy(scope.$eval(attr.shareValidate));
            params.group.splice(params.id, 1);
            var sum = +viewValue;
            angular.forEach(params.group, function(entity, index) {
                sum += +(entity.share);
            });
            ctrl.$setValidity('share', sum === 100);
            return viewValue;
        });
    }
};
});

This ALMOST works, but can't handle the case in which a field is invalidated, but a subsequent change in another field makes it valid again. For example:

Field 1: 61
Field 2: 52

If I take Field 2 down to 39, Field 2 will now be valid, but Field 1 is still invalid. Ideas?

Problem courtesy of: HaskellMan

Solution

Ok, the following works (again, "share" is "percent"):

app.directive('shareValidate', function () {
return {
    restrict: 'A',
    require: 'ngModel',
    link: function(scope, elem, attr, ctrl) {
        scope.$watch(attr.shareValidate, function(newArr, oldArr) {
            var sum = 0;
            angular.forEach(newArr, function(entity, i) {
                sum += entity.share;
            });
            if (sum === 100) {
                ctrl.$setValidity('share', true);
                scope.path.offers.invalidShares = false;
            }
            else {
                ctrl.$setValidity('share', false);
                scope.path.offers.invalidShares = true;
            }
        }, true); //enable deep dirty checking
    }
};
});

In the HTML, set the attribute as "share-validate", and the value to the set of objects you want to watch.

Solution courtesy of: HaskellMan

Discussion

I have a case where I have a dynamic form where I can have a variable number of input fields on my form and I needed to limit the number of input controls that are being added.

I couldn't easily restrict the adding of these input fields since they were generated by a combination of other factors, so I needed to invalidate the form if the number of input fields exceeded the limit. I did this by creating a reference to the form in my controller ctrl.myForm, and then each time the input controls are dynamically generated (in my controller code), I would do the limit check and then set the validity on the form like this: ctrl.myForm.$setValidity("maxCount", false);

This worked well since the validation wasn't determined by a specific input field, but the overall count of my inputs. This same approach could work if you have validation that needs to be done that is determined by the combination of multiple fields.

Discussion courtesy of: Jason White

You can check angularui library (ui-utility part). It has ui-validate directive.

One way you can implement it then is

<input type="number" name="accountNo" ng-model="account.percent"
ui-validate="{overflow : 'checkOverflow($value,account)' }">

On the controller create the method checkOverflow that return true of false based on account calculation.

I have not tried this myself but want to share the idea. Read the samples present on the site too.

Discussion courtesy of: Chandermani

For my sanity

HTML

<form ng-submit="applyDefaultDays()" name="daysForm" ng-controller="DaysCtrl">
<div class="form-group">
    <label for="startDate">Start Date</label>
    <div class="input-group">
        <input id="startDate"
               ng-change="runAllValidators()"
               ng-model="startDate"
               type="text"
               class="form-control"
               name="startDate"
               placeholder="mm/dd/yyyy"
               ng-required
        />
    </div>
</div>
<div class="form-group">
    <label for="eEndDate">End Date</label>
    <div class="input-group">
        <input id="endDate"
               ng-change="runAllValidators()"
               ng-model="endDate"
               type="text"
               class="form-control"
               name="endDate"
               placeholder="mm/dd/yyyy"
               ng-required
        />
    </div>
</div>
<div class="text-right">
    <button ng-disabled="daysForm.$invalid" type="submit" class="btn btn-default">Apply Default Dates</button>
</div>

JS

'use strict';

angular.module('myModule')
  .controller('DaysCtrl', function($scope, $timeout) {
    $scope.initDate = new Date();
    $scope.startDate = angular.copy($scope.initDate);
    $scope.endDate = angular.copy($scope.startDate);
    $scope.endDate.setTime($scope.endDate.getTime() + 6*24*60*60*1000);

    $scope.$watch("daysForm", function(){
      //fields are only populated after controller is initialized
      $timeout(function(){
        //not all viewalues are set yet for somereason, timeout needed
        $scope.daysForm.startDate.$validators.checkAgainst = function(){
          $scope.daysForm.startDate.$setDirty();
          return (new Date($scope.daysForm.startDate.$viewValue)).getTime() <=
            (new Date($scope.daysForm.endDate.$viewValue)).getTime();
        };

        $scope.daysForm.endDate.$validators.checkAgainst = function(){
          $scope.daysForm.endDate.$setDirty();
          return (new Date($scope.daysForm.startDate.$viewValue)).getTime() <=
            (new Date($scope.daysForm.endDate.$viewValue)).getTime();
        };
      });
    });

    $scope.runAllValidators = function(){
      //need to run all validators on change
      $scope.daysForm.startDate.$validate();
      $scope.daysForm.endDate.$validate();
    };

    $scope.applyDefaultDays = function(){
        //do stuff
    }
  });
Discussion courtesy of: googamanga

This recipe can be found in it's original form on Stack Over Flow.