I have a field on a form with lots of validation.
At first, I had it structured into multiple directives, each with its own error message.
However, the validation uses a back-end asynchronous call, so suddenly for one field I was making 5 http calls for the same dataservice. I am trying to figure out how to write this more efficiently.
I was wondering if it is possible to have one $async
validator that calls the dataservice, and multiple regular $validators
inside of the first asynchronous function after .then
. I experimented with this but it doesn't seem to reach the nested $validators
at all.
I also tried to do the call once in a service, but I don't know how to get it to update when the modelValue on the field changes, and consequently pass the information to the respective validation directives. Could I do this as async validation in a service and attach the response to scope for the directives to look for?
TLDR;
How can I make ONE http call and based off of the returned data, perform multiple validation checks, each with its own error?
FOR EXAMPLE
I have about four directives that all look like this:
angular.module('validationForField').directive('po', ['$q', '$sce', '$timeout', 'myService', function ($q, $sce, $timeout, myService) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, ctrl, ngModel) {
ctrl.$asyncValidators.validateField = function (modelValue) {
var def = $q.defer();
myService.httpcall(modelValue)
.then(function (response, modelValue) {
if (response.data.status === "Error") {
return def.reject();
}
def.resolve();
}).catch(function(){
def.reject();
});
return def.promise;
}
}
}
}]);
Each one has different analysis of the data to return different error messages. Each one makes a call to myService.httpcall which ends up being redundant because they are all getting the same data.
I am trying to do
angular.module('validationForField').directive('po', ['$q', '$sce', '$timeout', 'myService', function ($q, $sce, $timeout, myService) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, ctrl, ngModel) {
ctrl.$asyncValidators.validateField = function (modelValue) {
var def = $q.defer();
myService.httpcall(modelValue)
.then(function (response, modelValue) {
if (response.data.status === "Error") {
return def.reject();
}
ctrl.$validators.checkStatus = function (response) {
if (response.data.data.status === "10"){
return false
}
ctrl.$validators.checkPermissions = function (response) {
return response.data.data.permission){
}
def.resolve();
}).catch(function(){
def.reject();
});
return def.promise;
}
}
}
}]);
This way there is the main async validator as to whether the http call is successful or not, and internal $validators that use that data when it returns
I assume the backend service accepts a value (the value of the field to be validated) and returns a single response for all validations, e.g.:
// true would mean valid, string would mean invalid with the given error:
{
businessRuleOne: true,
businessRuleTwo: "The format is incorrect",
...
}
I believe the solution is executing the HTTP call in a service that caches the promise; the async validators call the service and retrieve the same promise, which they return. Some sample code with inline explanation:
// the service:
app.service('myService', function($http, $q) {
// cache the requests, keyed by the model value
var requestMap = {};
this.httpcall = function(modelValue) {
// if cached, return that (and do not make extra call)
if( requestMap[modelValue] ) {
return requestMap[modelValue];
}
// if not cahced, make the call...
var promise = $http.get('....');
// ...cache it...
requestMap[modelValue] = promise;
// ...and remember to remove it from cache when done
promise.finally(function() {
delete requestMap[modelValue];
});
return promise;
};
});
Now the async validators can be implemented exactly as you post. Calling myService.httpcall(modelValue)
will invoke the remote service only for the first call, the rest will reuse the cached promise.
Two more points: (1) This technique is called memoization. It is implemented by many libraries, e.g. lodash, you may be able to use those to keep myservice.httpcall()
clean. (2) You do not need an extra promise from the async validators, e.g.:
angular.module('validationForField').directive('po', ['$q', '$sce', '$timeout', 'myService', function ($q, $sce, $timeout, myService) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, ctrl, ngModel) {
ctrl.$asyncValidators.validateField = function (modelValue) {
return myService.httpcall(modelValue)
.then(function (response) {
if (response.data.status === "Error") {
return $q.reject();
}
return response;
});
}
}
}
}]);