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;
});
}
}
}
}]);
Related
In my Angular app, I have implemented this directive (code below) that basically allows me to show an element of my choosing whenever Angular detects an ajax request.
However, for slightly better usability, I would like to show the spinner only after some time has passed (say, 100 or 200 miliseconds) since the beginning of the request, to avoid those unnecessary split-second displays on every single request.
What would be the best way to implement such a thing? I'm having trouble getting setTimeout to play nicely within the if block because the element will never get hidden again, even if I no longer have a pending request.
.directive('loading', ['$http' ,function ($http)
{
return {
restrict: 'A',
link: function (scope, elm, attrs)
{
scope.isLoading = function () {
return $http.pendingRequests.length > 0;
};
scope.$watch(scope.isLoading, function (v)
{
if(v){
elm.show();
} else {
elm.hide();
}
});
}
};
}]);
Sounds like you can leverage interceptors and bind to a root variable instead of a directive to show your element for pending ajax requests (after the time threshold is met). Observe the following possibility...
app.factory('HttpInterceptor', ['$rootScope', '$q', '$timeout', function ($rootScope, $q, $timeout) {
return {
'request': function (config) {
$timeout(function() {
$rootScope.isLoading = true; // loading after 200ms
}, 200);
return config || $q.when(config);
},
'requestError': function (rejection) {
/*...*/
return $q.reject(rejection);
},
'response': function (response) {
$rootScope.isLoading = false; // done loading
return response || $q.when(response);
},
'responseError': function (rejection) {
/*...*/
return $q.reject(rejection);
}
};
}]);
// register interceptor
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push('HttpInterceptor');
/*...*/
}]);
<!-- plain element with binding -->
<div class="whatever" ng-show="isLoading"></div>
JSFiddle Link - working demo
For a single, globally-available loading indicator, an http interceptor is probably a better strategy. But assuming you want to attach this to individual elements separately, try something like this:
.directive('loading', ['$http', '$timeout', function($http, $timeout) {
return {
restrict: 'A',
link: function(scope, elm, attrs) {
scope.isLoading = function() {
return $http.pendingRequests.length > 0;
};
if (scope.isLoading) {
elm.hide(); // hide the loading indicator to begin with
// wait 300ms before setting the watcher:
$timeout(function() {
var watcher = scope.$watch(scope.isLoading, function(v) {
if (v) {
elm.show();
} else {
elm.hide();
watcher(); // don't forget to clear $watches when you don't need them anymore!
}
});
}, 300);
} else {
// No pending requests on link; hide the element and stop
elm.hide();
}
}
};
}]);
(You should probably also include a $destroy block on the directive to call watcher(), in case the directive goes out of scope while http requests are still pending.)
I've encounter a strange behavior happening in an Angular's directive I don't understand at all; let me just explain it using code, I think is the easiest way.
What does work:
.directive('myDirective', function ($http) {
return {
template: '...',
link: function (scope, elem, attrs) {
function getData() {
console.log('Starting...');
$http.get(...).success(/* update the template */);
}
getData();
}
};
});
What does not:
.directive('myDirective', function ($http) {
return {
template: '...',
link: function (scope, elem, attrs) {
function getData() {
console.log('Starting...');
$http.get(...).success(/* update the template */);
}
scope.$on('callMyDirective', getData);
}
};
});
The $http call is complete ignored in the second example, not even printing a thing in the finally() part of the promise (there is indeed no network activity), while the log is properly printed in both cases (in the second case, of course, after triggering the event, by clicking a button).
Any clue?
Thanks a lot.
Update:
I found the source of the error, but it is in turn another problem. The thing is that I have an Interceptor where I wrap all the requests but the one which is supposed to authenticate the user with a promise, which I resolve only once the authentication ends. So my interceptor looks like this:
request: function (config) {
if (userHasArrived || isAuthUrl) {
return config || $q.when(config);
}
var defer = $q.defer();
$rootScope.$on('user_logged_in', function (user) {
console.log('User logged in');
userHasArrived = true;
defer.resolve(config);
});
return defer.promise;
}
At the same time, I have a service that runs the auth request, and broadcast the event shown above on success. This is working for all the other requests but for the one I have the directive I mentioned above, which is in a different "page", and never receives the event in the Interceptor (the log never gets printed).
Only when I force a second request to my "auth" URL, the event is received, and the directive stuff works without problems. Could it be that the services is loaded before the interceptor somehow? And in that case, is there a way to control this load order?
Update:
Ok, sorry for bothering you, but if someone is still interested, I found the solution to the second problem. This was the order in which things were happening:
Authentication request starts
Authentication request ends, success is broadcasted
Directive request starts (on demand, clicking a button)
Authentication success starts to being listened
So the event was being fired before anyone listened to it. This would be the final code (an incomplete version of it):
$rootScope.$on('user_logged_in', function (user) {
userHasArrived = true;
});
return {
request: function (config) {
if (userHasArrived || isAuthUrl) {
return config || $q.when(config);
}
var defer = $q.defer();
var $off = $rootScope.$on('user_logged_in', function (user) {
userHasArrived = true;
$off(); // You better unsubscribe for the events too, if you do something along this lines
defer.resolve(config);
});
return defer.promise;
}
};
I hope this works
.directive('myDirective', function ($http) {
return {
template: '...',
link: function (scope, elem, attrs) {
scope.getData = function () {
console.log('Starting...');
$http.get(...).success(/* update the template */);
}
scope.$on('callMyDirective', function (event, args) {
scope.getData();
});
}
};
});
or other alternative
.directive('myDirective', function ($http) {
return {
template: '...',
link: function (scope, elem, attrs) {
var _this = this;
function getData() {
console.log('Starting...');
$http.get(...).success(/* update the template */);
}
scope.$on('callMyDirective', function (event, args){
$scope.$apply(function (){
_this.getData();
})
});
}
};
});
I am trying to create angular directive whom can run alone without a Controller, so I can set it anywhere I want, without adding the model to the controller.
The code is fairly simple:
App.directive('ngdPriceWithCurrencySelect',['CurrenciesService' ,function (CurrenciesService) {
return {
restrict: 'E',
replace: true,
scope: true,
link: function ($scope, $element, $attrs) {
$scope.currencies = CurrenciesService.getData();
$scope.$watch('currencies', function(newValue, oldValue){
console.log($scope.currencies);
});
},
template: '<select>\n\
<option ng-repeat="currency in currencies">{{currency.cur_name_he}}</option>\n\
</select>'
}
}]);
The Service actually return the data and the console.log($scope.currencies) shows object with the currencies.
but the repeater is not runing and I am not getting the result I want.
I thought this might be a scope problem, but I can't find a way to see the scope itsel. (angularjs batarang is not working in version 1.3+)
the problem can be in the Service as well so I am giving the service code:
App.service('CurrenciesService', ["$http", "$q", function ($http, $q) {
var service = {
returnedData: [],
dataLoaded: {},
getData: function (forceRefresh) {
var deferred = $q.defer();
if (!service.dataLoaded.genericData || forceRefresh){
$http.get("data/currencies").success(function (data) {
angular.copy(data, service.returnedData)
service.dataLoaded.genericData = true;
deferred.resolve(service.returnedData);
});
}
else{
deferred.resolve(service.returnedData);
}
return deferred.promise;
},
};
service.getData();
return service;
}]);
here is a JS fiddle for testing it: http://jsfiddle.net/60c0305v/2/
Your service returns a promise, not the data itself. You need to set a function for when the service resolves the promise:
link: function ($scope, $element, $attrs) {
// waiting for the service to resolve the promise by using the done method
CurrenciesService.getData().then(function(data) {
$scope.currencies = data;
});
$scope.$watch('currencies', function(newValue, oldValue){
console.log($scope.currencies);
});
}
Check this fiddle
I have two directives, each consuming the same factory wrapping a $q/$http call.
angular.module("demo").directive("itemA", ["restService", function(restService) {
return {
restrict: "A",
link: function(scope, element, attrs) {
restService.get().then(function(response) {
// whatever
}, function(response) {
// whatever
});
}
};
}]);
angular.module("demo").directive("itemB", ["restService", function(restService) {
return {
restrict: "A",
link: function(scope, element, attrs) {
restService.get().then(function(response) {
// whatever
}, function(response) {
// whatever
});
}
};
}]);
angular.module("demo").factory("restService", ["$http", "$q", function($http, $q) {
return {
get: function() {
var dfd = $q.defer();
$http.get("whatever.json", {
cache: true
}).success(function(response) {
// do some stuff here
dfd.resolve(response);
}).error(function(response) {
// do some stuff here
dfd.reject(response);
});
}
};
}]);
Problem: When I do this
<div item-a></div>
<div item-b></div>
I get the same web service fired off twice, because the GET from ItemA is still in progress when the GET for ItemB goes.
Is there a way for whichever fires second to know that there's already a request to this in progress, so that it can wait a minute and grab it for free?
I've thought about making an $http or $q wrapper which flags each URL as pending or not but I'm not sure that's the best way. What would I do if it was pending? Just return the existing promise and it'll resolve when the other resolves?
Yes, all you need to do is to cache the promise and clean it off after the request is done. Any subsequent request in between can just use the same promise.
angular.module("demo").factory("restService", ["$http", "$q", function($http, $q) {
var _cache;
return {
get: function() {
//If a call is already going on just return the same promise, else make the call and set the promise to _cache
return _cache || _cache = $http.get("whatever.json", {
cache: true
}).then(function(response) {
// do some stuff here
return response.data;
}).catch(function(response) {
return $q.reject(response.data);
}).finally(function(){
_cache = null; //Just remove it here
});
}
};
}]);
I've searched on Google but can't find information on how to do this properly. Seems like all the answers on Google are now outdated (using older versions of AngularJS).
I'm trying to setup two controllers on my AngularJS module. For example, the first controller is handling $http GET requests. And the second controller is displaying either a 'success' or 'error' message. I want to be able to call a method from the second controller with the success/error message that is to be displayed.
Or am I supposed to use a service/factory for this? I've read about services but can't figure out how to make something like this work.
var module = angular.module('app', []);
module.controller('ApiController', ['$scope', '$http', function ($scope, $http) {
$http.get('/api').
success(function(data){
// call AlertController('success')
}).
error(function(data){
// call AlertController('failed')
});
}]);
module.controller('AlertController', ['$scope', function ($scope) {
$scope.message = {
show_message: true,
type: 'info',
message: "Display message!"
};
}]);
Either doing it that way, or perhaps I would like to push the incoming alert onto a global object variable, and then remove it after it has been displayed.
Anyone know the proper way to set this up?
Ok let's try this - you should also check out Injecting $scope into an angular service function()
The Message service:
module.service('MessageService', function ($timeout) {
var messageQueue = [];
var DISPLAY_TIME = 5000; // each message will be displayed for 5 seconds
function startTimer() {
$timeout(function() {
// Remove the first message in the queue
messageQueue.shift();
// Start timer for next message (if there is one)
if (messageQueue.length > 0) startTimer();
}, DISPLAY_TIME);
}
function add(message) {
messageQueue.push(message);
// If this is the only message in the queue you need to start the timer
if (messageQueue.length==0) startTimer();
}
function get() {
if (messageQueue.length==0) return "";
else return messageQueue[0];
}
return { add: add, get: get };
});
You can still use this ApiService as well:
module.service('ApiService', ['$http', function ($http) {
return {
get: function(url) {
return $http.get(url);
}
};
}]);
Your Search controller:
module.controller('SearchController', ['$scope', 'ApiService', 'MessageService', function ($scope, api, messages) {
api.get('/yelp').
success(function(data){
messages.add('success');
}).
error(function(data){
messages.add('failed');
});
}]);
Your Alert controller:
module.controller('AlertController', ['$scope', 'MessageService', function ($scope, messages) {
$scope.getMessage = function() { messages.get(); }
}]);
So in your html you can have:
<div ng-controller="AlertController">
<div>{{ getMessage() }}</div>
</div>
here is how you make factory
module.factory('appService', ['$window', '$http', '$q', function(win, $http, $q) {
return{
backendcall: function(){
var deferred = $q.defer();
$http.get('/yelp').
success(function(data){
deferred.resolve(data);
}).
error(function(data){
deferred.resolve(status);
});
return deferred.promise;
}
}
}]);
and your controller will be like this
module.controller('AlertController', ['$scope', 'appService', function ($scope, appService) {
appService.backendcall().then(function(response){
$scope.message = {
show_message: true,
type: 'info',
message: "Display message!"
};
})
}]);