following the DRY principal, i want to write a button directive which keeps button disabled for the duration of $http class.
I want to do this so as to forbid user from clicking the buttons multiple times, but i am not able to think on how to get function promise status inside a directive, given that the function resides on $scope
the scenario is very generic, buttons ng-click does call a function which in turn makes $http calls. on user click : button should get disabled and should be enabled only after the $http call is resolved, either success or failure.
This directive will disable the button until the save/promises is not fulfilled. single-click must return promises otherwise it will not disable the button.
In case there is no promises but still want to disable the button, then it is recommended to write own logic of disabling the button
app.directive('singleClick', function ($parse) {
return {
compile: function ($element, attr) {
var handler = $parse(attr.singleClick);
return function (scope, element, attr) {
element.on('click', function (event) {
scope.$apply(function () {
var promise = handler(scope, { $event: event }); /// calls and execute the function specified in attrs.
if (promise && angular.isFunction(promise.finally)) { /// will execute only if it returns any kind of promises.
element.attr('disabled', true);
promise.finally(function () {
element.attr('disabled', false);
});
}
});
});
};
}
};
});
Why not just make it easier.
<button ng-click="save()" ng-disabled="isProcessing">Save</button>
$scope.save = function(){
$scope.isProcessing = true;
$http.post('Api/Controller/Save', data).success(
$scope.isProcessing = false;
);
}
Sure it's the case if you need this logic in very few places across your app.
If you have such logic repeating many times (and if you are not lazy :) ), so in order to follow SOLID principles it definetely better to wrap this functionality into directive (check out other answers for this question to see examples of such directive).
Although I would be careful of over-engineering, a way to do this would be by using a custom directive. This directive
finally
of the promise, it re-enables the button-
app.directive('clickAndDisable', function() {
return {
scope: {
clickAndDisable: '&'
},
link: function(scope, iElement, iAttrs) {
iElement.bind('click', function() {
iElement.prop('disabled',true);
scope.clickAndDisable().finally(function() {
iElement.prop('disabled',false);
})
});
}
};
});
This can be used on a button as follows:
<button click-and-disable="functionThatReturnsPromise()">Click me</button>
You can see this in action at http://plnkr.co/edit/YsDVTlQ8TUxbKAEYNw7L?p=preview , where the function that returns the promise is:
$scope.functionThatReturnsPromise = function() {
return $timeout(angular.noop, 1000);
}
But you could replace $timeout
with a call to $http
, or a function from any service that returns a promise.
I like @codef0rmer 's solution because it is centralized--that is there's no additional code needed for each HTTP request, and you just need to check the global progress
flag in your HTML. However, using transformResponse
to determine when the request has completed is unreliable because the server may not return anything; in that case the handler isn't called and progress
is never set back to false
. Also, as written that answer doesn't account for multiple simultaneous requests (progress
may return false
before all requests have completed).
I've come up a similar solution which uses interceptors to address these issues. You can put it in your Angular app's config function:
.config(function ($httpProvider) {
$httpProvider.interceptors.push(function($q, $rootScope) {
var numberOfHttpRequests = 0;
return {
request: function (config) {
numberOfHttpRequests += 1;
$rootScope.waitingForHttp = true;
return config;
},
requestError: function (error) {
numberOfHttpRequests -= 1;
$rootScope.waitingForHttp = (numberOfHttpRequests !== 0);
return $q.reject(error);
},
response: function (response) {
numberOfHttpRequests -= 1;
$rootScope.waitingForHttp = (numberOfHttpRequests !== 0);
return response;
},
responseError: function (error) {
numberOfHttpRequests -= 1;
$rootScope.waitingForHttp = (numberOfHttpRequests !== 0);
return $q.reject(error);
}
};
});
})
Now you can just use the waitingForHttp
to disable buttons (or show a "busy" page). Using interceptors gives an added bonus that now you can use the error functions to log all HTTP errors in one place if you want.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With