Reputation: 2174
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?
Upvotes: 28
Views: 31315
Reputation: 42669
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 or 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.
Upvotes: 8
Reputation: 391
You can define a single directive that is only responsible for this check.
<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>
<!-- HERE IT IS -->
<sum-up-to-hundred accounts="accounts"></sum-up-to-hundred>
</form>
And here's the simple directive's code.
app.directive('sumUpToHundred', function() {
return {
scope: {
accounts: '<'
},
require: {
formCtrl: '^form'
},
bindToController: true,
controllerAs: '$ctrl',
controller: function() {
var vm = this;
vm.$doCheck = function(changes) {
var sum = vm.accounts.map((a)=> a.percent).reduce((total, n)=> total + n);
if (sum !== 100) {
vm.formCtrl.$setValidity('sumuptohundred', false);
} else {
vm.formCtrl.$setValidity('sumuptohundred', true);
}
};
}
};
});
Here's a plunker.
Upvotes: 0
Reputation: 142
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
}
});
Upvotes: 0
Reputation: 4792
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.
Upvotes: 5
Reputation: 2174
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.
Upvotes: 15