I am trying to add a custom validation function to Angular's ngMessages.
Specifically, I want the value of a few inputs (the number of inputs will be dynamic, but for now stick with 2) to total 100.
I have created a new directive called totalOneHundred
which is triggering on a form change, but I cannot figure out how to access other form values from the link:
call back.
I have posted my code below. Is there something I am missing? Also, if there is a better way to accomplish this (a sum()
function in the controller and an ng-show
, for example) please call me out.
Thanks for your help.
The form:
<!-- input box to be validated -->
<input type="number" class="form-control" name="lowBound" ng-model="ctrl.lowBound" total-one-hundred required>
<!-- validation messages -->
<div ng-messages="form['lowBound'].$error" role="alert">
<div ng-message="required">Cannot be empty</div>
<div ng-message="totalOneHundred">Sum of tasks must = 100</div>
</div>
<!-- input box to be validated -->
<input type="number" class="form-control" name="highBound" ng-model="ctrl.highBound" total-one-hundred required>
<!-- validation messages -->
<div ng-messages="form['highBound'].$error" role="alert">
<div ng-message="required">Cannot be empty</div>
<div ng-message="totalOneHundred">Sum of tasks must = 100</div>
</div>
The directive:
return {
restrict: "A",
require: ["^^form", "ngModel"],
link: function(scope, element, attributes, controllers) {
// At first, form is assigned the actual form controller...
const form = controllers[0];
const model = controllers[1];
model.$validators.totalOneHundred = function (modelValue, form, element, scope) {
// however, the value of form here is "1".
// modelValue is the value of the triggering input,
// but how can I access the other form inputs?
return true;
};
}
};
Initially I took your code and implemented this fiddle. A sum()
method of the parent controller calculates the total (simple in the fiddle, but since the parent controller knows the entire, dynamic model, it is doable in the real case too). The total-one-hundred
takes the sum as argument, i.e.:
<input type="number" class="form-control" name="lowBound" ng-model="ctrl.lowBound"
total-one-hundred="ctrl.sum()" required />
Alas, it doesn't work correctly! Problem: each input displays the "Sum of tasks must = 100" error. If you change a field and the total becomes correct, that field becomes valid and stops displaying the message. But the other fields do not!
EDIT: Well, it can work even this way. The secret is to add a watch on the sum for each validation directive and re-apply validation on that field; the new link function:
link: function(scope, element, attributes, controllers) {
const model = controllers[0];
var totalEvaluator = $parse(attributes['totalOneHundred']);
scope.$watch(totalEvaluator, function(newval, oldval) {
if( newval !== oldval ) {
model.$validate();
}
})
model.$validators.totalOneHundred = function (modelValue) {
return totalEvaluator(scope) === 100;
};
}
(NOTE that this costs an extra watch per field!)
Now however, the sum()
function (which may potentially be expensive) is called many times. Watching the inputs of this function and calling it only when they change, may improve the situation.
An updated fiddle: https://jsfiddle.net/m8ae0jea/1/ (I still prefer model validation -see last paragraph- but it is good to be aware of all alternatives and their side-effects.)
This is a conceptual problem with cross-field validations. Where does the validation belong? If you can refactor your model so what gets validated is an entire object, then you can use Angular's custom controls as in this fiddle.
Now the model looks like:
this.model = {
lowBound: <a number>,
highBound: <a number>
};
And there is an editor for the entire model, complete with its own messages:
<model-editor name="entireModel" ng-model="ctrl.model" form="form"
total-one-hundred="ctrl.sum()"></model-editor>
<div ng-messages="form['entireModel'].$error" role="alert">
<div ng-message="totalOneHundred">Sum of tasks must = 100</div>
</div>
As you can see the total validation applies to the entire model.
The second example works correctly, if you can live with just a single message for the entire "total" validation. But I do not like it...
Angular's validation is (IMHO) a quick and dirty solution suited for simple things. Say a field must not be empty, another field must comply with a regular expression and so on. For complex things (like this case) I find it inappropriate to define business logic in the view. I prefer doing model validation and binding the validation results with Angular. To that extent, I created egkyron which is well suited for such things.