Angular generic directive/controller to disable button after detecting any ajax request - angularjs

i've seen many posts about this subject, but not specificly about this question.
I'm wondering if there could be a generic directive/controller in AngularJs to disable a button (that calls an ajax request) and re-enable it after the request ends.
I've used the word "generic" because i've seen some solutions using a callback after a specific ajax request, but it's not what i need.
I need that:
when clicking on a button, and the button calls an ajax request, it becomes disabled until the request ends.
Thank you for your help

Here is a possibility.
You can think of http service calls just as a promise. Once a http service call is fulfilled then it will call the next promise in the chain, if there is one.
You can receive a funcion in a directive, then do a wrapping call to it and chain another function to it. So this way you know when the promise is being executed and when it's fulfilled.
You need to be sure to retun the promise in the controller. Then pass that funcion to the directive.
Check the following example
https://codepen.io/bbologna/pen/zdpxoB
JS
var app = angular.module('testApp', []);
app.factory('fakeHttpService', ['$timeout', function($timeout){
var doSomething = function(value) {
return new Promise(function(resolve, reject) {
$timeout(() => { resolve(value + 1); $timeout() }, 1000);
})
}
return { doSomething: doSomething }
}])
app.controller('sampleController', ['$scope', 'fakeHttpService',
function($scope, fakeHttpService) {
$scope.doSomething = function(value){
return fakeHttpService.doSomething(value);
}
}
])
app.directive('buttonAsync', function(){
return {
restrict: 'E',
template: `<button ng-click="excecute()" ng-disabled="disabled">DO</button>`,
scope: {
on : '&'
},
controller : ['$scope', function($scope) {
$scope.disabled = false;
$scope.excecute = function() {
$scope.disabled = true;
$scope.on()
.then(function() {
$scope.disabled = false;
})
}
}]
}
})
Html
<div ng-app="testApp" ng-controller="sampleController">
<button-async on="doSomething(3)"></button-async>
</div>

Related

AngularJS - Multiple Directive Instances making XHR call multiple times

I have an Angularjs directive 'ExampleDirective' which has the controller 'ExampleController'. The controller defines two Promise objects where each Promise object makes an Http GET request and returns the response.
In the directive, we get the response data from the promise objects and process them to render the directive.
ExampleDirective gets instantiated twice within the same view and each instance makes it's own Http GET requests. This causes performance issues on the front end due to two requests sent at the same time to make expensive database calls and read from the same table as well.
Controller:
angular.module('exampleModule')
.constant("EXAMPLE_URL", "{% url 'example:get_example' %}")
.controller('exampleCtrl', ['$scope', '$http', 'EXAMPLE_URL', exampleCtrl]);
function exampleCtrl($scope, $http, EXAMPLE_URL) {
$scope.examplePromise = $http.get(EXAMPLE_URL).then(function(response) {
return response.data;
});
}
Directive:
angular.module('exampleModule')
.directive('exampleDirective', ['exampleFactory', 'STATIC_URL', '$http', '$window', exampleDirective]);
function exampleDirective(exampleFactory, STATIC_URL, $http, $window) {
return {
scope: {
title:'#?',
loadingImage:'#?',
},
restrict: 'AE',
templateUrl: STATIC_URL + 'example/example-template.html',
controller: "exampleCtrl",
link: function (scope, element, attrs) {
//add default options:
if (!scope.title) {
scope.title = 'Example Title';
}
if (!scope.loadingImage) {
scope.loadingImage = '';
}
scope.examplePromise.then(function(data) {
scope.exampleData = data;
// do something
});
}
};
}
Is there a way to instantiate a directive multiple times but not have to make the Http GET requests in the controller twice?
UPDATE
This is what I did, I added a service as suggested in the answer.
Service:
angular.module('chRatingsModule')
.factory('chExampleFactory', ['$http', 'EXAMPLE_URL', chExampleFactory]);
function chExampleFactory($http, EXAMPLE_URL) {
var api = {}
var promise = null;
api.examplePromise = examplePromise;
function examplePromise() {
if (promise == null) {
promise = $http.get(EXAMPLE_URL).then(function(response) {
return response.data;
});
}
return promise;
}
return api;
}
Updated Directive:
angular.module('exampleModule')
.directive('exampleDirective', ['exampleFactory', 'STATIC_URL', '$http', '$window', exampleDirective]);
function exampleDirective(exampleFactory, STATIC_URL, $http, $window) {
return {
scope: {
title:'#?',
loadingImage:'#?',
},
restrict: 'AE',
templateUrl: STATIC_URL + 'example/example-template.html',
link: function (scope, element, attrs) {
exampleFactory.examplePromise.then(function(data) {
scope.exampleData = data;
// do something
});
}
};
}
First solution, probably the best one: don't make the call from the directive, which should just be a graphical element. Do the call from the controller, and pass the data as argument to both directives.
Second solution, use a service in the directive, and always return the same promise:
myModule.factory('myService', function($http) {
var promise = null;
var getData = function() {
if (promise == null) {
promise = $http.get(...).then(...);
}
return promise;
};
return {
getData: getData
};
});
The controller defines two Promise objects where each Promise object
makes an Http GET request and returns the response.
Change to:
The SERVICE defines two Promise objects where each Promise object
makes an Http GET request and returns the response.
The service then can remember that it has already done the GET(s) and just return their result every subsequent time it is asked for them.

AngularJS calling service from custom directive

I have the following directive, service and controller inside my AngularJS app. The service is common between the directive and this controller (as well as other controllers using the same service in my app). As shown inside the controller I watch the service for changes, while in directive I communicate with the service to update it. For some reason which I don't know the directive is not updated my service, so can someone please tell me what I am doing wrong here? Thanks
Controller:
myapp.controller('ClientsCtrl', function ($scope, UserSvc) {
$scope.showForm = UserSvc.frmOpened;
$scope.$watch(function () {
return UserSvc.frmOpened;
}, function () {
$scope.showForm = UserSvc.frmOpened;
console.log('Changed... ' + $scope.showForm);
});
});
Service
myapp.factory('UserSvc', function ($log, $q, $http) {
return {
frmOpened: false
};
});
Directive:
myapp.directive('myDirective', ['UserSvc', function (UserSvc) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
angular.element(element).on("click", function () {
var parentElement = $(this).parent();
if (parentElement.hasClass('sample')) UserSvc.frmOpened = true; //This code never update the service
} else {
UserSvc.frmOpened = false; //This code never update the service
}
return false;
});
}
};
}]);
.on() is a jQuery method (also included in Angular's jqLite). The code inside the attached event handler lives outside of Angular, so you need to use $apply:
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life cycle of
exception handling, executing watches.
For example:
element.on("click", function() {
var parentElement = $(this).parent();
scope.$apply(function() {
if (parentElement.hasClass('sample')) {
UserSvc.frmOpened = true;
} else {
UserSvc.frmOpened = false;
}
});
return false;
});
Demo: http://plnkr.co/edit/mCl0jFwzdKW9UgwPYSQ9?p=preview
Also, the element in the link function is already a jqLite/jQuery-wrapped element, no need to perform angular.element() on it again.
It looks like you have some brackets where they shouldn't be in your directive. You don't have brackets surrounding the first part of your if statement, but you have a closing bracket before your else. I think this messes up things.
Try using this as your link-function and see if it changes anything:
link: function (scope, element, attrs) {
angular.element(element).on("click", function () {
var parentElement = $(this).parent();
if (parentElement.hasClass('sample')) {
UserSvc.frmOpened = true; //Surrounded this with brackets
} else {
UserSvc.frmOpened = false;
}
return false;
});
}

angularjs - Watching service properties in a controller scope... changing them from a directive... no update?

Let's say I have a very simple service with a few properties on it. If I use the service in a controller, put the service's properties on the scope so that they are bound to my view, and update them from the controller, they update in the view. This is the behavior I'd expect. However, if the same service's properties are modified from a directive outside of the controller's scope, the view is not updated (unless something triggers a watch to be updated in the controller's scope?). There is obviously something fundamental that I'm missing here, but search search searching has not led me to the answer.
Here is an example on JSFiddle.
app = angular.module('app', []);
// simple service to track application's logon status
app.factory('AuthService', function () {
var status = {
isLoggedIn: false
};
return {
status: status,
login: function () {
status.isLoggedIn = true;
console.log('user logged in');
},
loggedIn: function () {
return status.isLoggedIn;
},
logout: function () {
status.isLoggedIn = false;
console.log('user logged out');
}
}
});
app.controller('AuthViewCtrl', function ($scope, AuthService) {
// bind some service attributes, functions to the scope so that we can use them in our view
$scope.loggedIn = AuthService.loggedIn;
$scope.login = AuthService.login;
$scope.logout = AuthService.logout;
$scope.stat = AuthService.status;
});
// a simple directive to allow elements to log out of the app on click
app.directive('appLogout', function (AuthService) {
return function (scope, element) {
element.bind('click', function () {
AuthService.logout();
});
}
});
// a simple directive to allow elements to log into the app on click
app.directive('appLogin', function (AuthService) {
return function (scope, element) {
element.bind('click', function () {
AuthService.login();
});
}
});
And the accompanying html:
<div ng-app="app">
<div ng-controller="AuthViewCtrl">
<strong>Are we logged in?</strong>
<ul>
<li>service func on scope: <strong>{{ loggedIn() }}</strong></li>
<li>service prop on scope: <strong>{{ stat.isLoggedIn }}</strong></li>
</ul>
<button ng-click="login()">log in from controller scope</button>
<button ng-click="logout()">log out from controller scope</button>
<button ng-click="loggedIn()">call AuthService.loggedIn()</button>
</div>
<button app-login>log in from directive</button>
<button app-logout>log out from directive</button>
</div>
The app is logged out when you start. If you "log in[/out] from controller," which calls a service function published to the scope, the watched service values are updated immediately in the view. However, if you hit "log in[/out] from directive," the watched service values are not updated (they will update if you simply call AuthService.loggedIn() from within the scope).
So, I guess my question is, what's the best way to handle this? Where have I gone astray with watching service values?
Thanks,
Adam
The problem is that you're calling your service "outside" of angular:
element.bind('click', function () {
AuthService.login();
});
So, you need to wrap the call in an $apply:
element.bind('click', function () {
scope.$apply(function() {
AuthService.login();
});
});
Updated Fiddle: http://jsfiddle.net/SAsBa/46/

How to display loading image, then make it disappear after 4 ajax requests in angularjs?

Assuming I have a div that I want to show when the page first loads showing the "number of ajax requests not complete" / "number of ajax requests completed", then make it disappear after X number of ajax requests are completed, (X can be a number set in javascript). Would like to see examples on how this would work. So far I only know that you can "emit" a "LOADING" and "DONELOADING" event for the show/hide of the one div, though that only works with a single http request.
Can use a promise array and $q.all() to determine when all requests are done.
Simple example since no code was provided
var promises=[];
for(i=0; i<5; i++){
var request=$http.get('someFile');
promises.push(request};
}
/* inject $q as dependency wherever you use this*/
$q.all(promises).then(function(){
/* remove loader*/
});
Don't necessarily have to use events to change loader visibility, could use ng-show and change the model property you assign to ng-show within $q.all(). For more detailed approach would need to see some code examples from your app
angular $q docs
To count the request you should use the requestInterceptor api of angulars $http service.
var module = angular.module("app", []);
module.factory("RequestStatistic", function () {
var requests = 0,
responses = 0,
incrementRequest = function () {
requests++;
},
incrementResponse = function () {
responses++;
},
getTotalRequests = function () {
return requests;
},
getTotalResponses = function () {
return responses;
},
getPendingRequests = function () {
return requests - responses;
};
return {
incrementRequest: incrementRequest,
incrementResponse: incrementResponse,
getTotalRequests: getTotalRequests,
getTotalResponses: getTotalResponses,
getPendingRequests: getPendingRequests
};
});
module.factory("RequestStatisticInterceptor", function ($q, RequestStatistic) {
return {
request: function (config) {
RequestStatistic.incrementRequest();
return config || $q.when(config);
},
response: function (response) {
RequestStatistic.incrementResponse();
return response || $q.when(response);
},
responseError: function (rejection) {
RequestStatistic.incrementResponse();
return $q.reject(rejection);
}
};
});
module.config(function($httpProvider){
$httpProvider.interceptors.push('RequestStatisticInterceptor');
});
Now you could make a directive that wathces the RequestStatistic Service and reacts appropriattly
Not exactly sure of what you mean with "ajax request", but i'm assuming you're calling some sort of asynchronous service for example:
angular.module('myApp').factory('myService', [
'$q',
'$timeout',
function($q, $timeout){
return {
load: function(millisecs) {
var deferred = $q.defer();
$timeout(function(){
deferred.resolve({foo: 'bar'});
}, (Math.floor(Math.random() * 9) + 1) * 1000);
return deferred.promise;
}
}
}
]);
You could keep a counter in your controller and increment it when each request completes:
angular.module('myApp').controller('myController', [
'$scope',
'$timeout',
'myService',
function($scope, myService){
$scope.requests = {
count: 10,
started: 0,
completed: 0
}
while($scope.requests.started < $scope.requests.count){
$scope.requests.started++
myService.load().then(function(){
$scope.requests.completed++
});
}
}
]);
Example # JSFiddle: http://jsfiddle.net/iH473/3rGjm/

Asynchronous request and loading button with AngularJS

DOM manipulation within an Angular controller seems to be wrong. But this is not coming without a few headaches :)
I have a button, and on ng-click, it will perform an asynchronous request in the background. During the time of that asynchronous request I would like all the buttons (and maybe a few more elements on the page) to be disabled and the clicked button to have a loading icons playing.
What is the cleanest way of doing this?
I usually do this with a variable on the $scope called loading. Whenever an asynch operation is happening, just set it to true. Then anything that's need to be disabled or otherwise affected can base it's state off of that.
Here's a dummy control:
function TestCtrl($scope, $http) {
$scope.loading = false;
$scope.doASynch = function () {
$scope.loading = true;
$http.get("/url").success(function () {
$scope.loading = false;
});
}
}
And here's a sample template.
<div ng-controller="TestCtrl">
<a class="button" ng-disabled="loading" ng-click="doASynch()">
<span ng-hide="loading">Click me!</span>
<span ng-show="loading">Loading....</span>
</a>
</div>
Here is exactly what you are looking for
Loading animations with Asynchronous HTTP Requests in Angular JS
var app = angular.module('myapp', ["ui.utils", "ui.router"]);
app.factory('iTunesData', function($http) {
return {
doSearch: function(sQuery) {
//return the promise directly.
return $http.jsonp('http://itunes.apple.com/search', {
params: {
"callback": "JSON_CALLBACK",
"term": sQuery
}
});
}
}
});
app.controller('iTunesSearch', function($scope, $location, $routeParams, iTunesData) {
$scope.search = function() {
iTunesData2.doSearch($scope.searchTerm)
.then(function(result) {
$scope.data = result.data;
$location.path($scope.searchTerm);
});
}
$scope.searchTerm = $location.$$path.split("/")[1];
if($scope.searchTerm!="") {
$scope.search();
}
});

Resources