Note: You can read the same post published on Codebrag blog.
You know the case when you hit submit button quickly enough that it sends the form twice? Or when you hit submit button and it seems that nothing happens, so you click it once again and another request is being fired? Duplicated requests issue - you can experience it in both traditional web application as well as in modern so-called Single Page Applications. I’d like to show you how it can be handled in AngularJS and how we did it in Codebrag.
AngularJS itself has no built-in stuff for that. You can fire as many HTTP requests as you want and it is up to you and your application’s code to handle it the way that would prevent double form submission. So let’s see what weapons we are armed with to win this battle.
Disable button after submit
Suppose you want to prevent user to double-submit the same form. You can write pretty simple AngularJS directive to disable button when clicked. It will effectively block this button from being pressed again until e.g. response comes back. I personally don’t really like this one, because you need to remember to put this directive on every form in you application. It looks like violating DRY a bit. So let’s look for something better.
Asynchronous UI approach
This is not a solution to this particular problem per se but more an approach to building web applications. Alex McCaw has great post about it which I highly recommend reading. Basically the trick is not to wait for server’s response if it is not absolutely required. Simply act as if action was performed succesfully and immediately do what should be done on success. So let’s take our form submit example. When user clicks submit button you fire HTTP request as usual and close form (possibly doing something more too, like adding comment to list) without waiting for response from server. It effectively prevents user from resubmitting form as it disappears immediately after button click. Obviously there are some drawbacks too and there are also use-cases (like payments etc) where this approach isn’t recommended, but in most cases it will work fine. For more details I really recommend reading Alex’s post.
Ok but this approach above requires rethinking and possibly changing significant stuff in you application. What if you don’t really want to do that? It turns out that solution for our initial problem can be quite easily implemented with tooling already available in AngularJS.
What we need to do is to “intercept”
$http service calls and decide if given request should be sent or not. Angular has concept of http interceptors but only for responses, not for requests (yet). But fear not, we’ll use decorators. As official doc says
Decoration of service, allows the decorator to intercept the service instance creation. The returned instance may be the original instance, or a new instance which delegates to the original instance.
and that’s exactly what we need. So this is how decorator can be applied:
1 2 3 4 5
I’ll show you the details of implementation soon. Next issue to solve is how to determine when requests are identical and if one is already in progress? In the simplest form HTTP requests in AngularJS are done as below:
All other methods like shortcut
post as well as
$resource use this form internally.
It turns out we can freely add our own properties to this
config object. They will help us identify requests. So we can for example modify the config above as follows:
1 2 3 4 5 6 7
This new config contains two new properties
requestId. The first one determines if for given types of requests we should check for duplicates or let all of them be sent (we probably can let all GET requests to go through without this check, and have e.g. POSTs checked). The second one,
requestId is a property we’ll match on when looking for duplicates. In this case it has constant value (create-comment) which means that only one POST request to
http://api.myapp.com/comment should be pending at any time. This value can be dynamically calculated (e.g. using
data for more fine-grained control).
Ok, but how can we find out which requests are currently in progress?
$http service has one neat property called
pendingRequests which is array of
config objects for request that were sent. So matching duplicated requests is just a matter of searching through
pendingRequests for request with identical
requestId as one in our request we are about to send. So here is first part of implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
It works fine if you try it, but has one huge drawback.
$http service calls return promises so you can attach to them using
then function and wait for them to be either resolved or rejected. Our current implementation is not consistent in return types. In fact it returns nothing when duplicate is detected, but returns regular promise when request is passed to original
$http. To fix it we need to construct deferred using
$q service as below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
Done, we have fully working implementation. Just one note, it works only for direct
$http calls, if you try to fire
$http.post or even
$resource it won’t work, because those use shortcut calls defined directly on
$http. Fix for that would be to define such functions on our modified version of
$http service, but I’ll leave it to you.
And that’s all. This is how we solved this in Codebrag without violating DRY (at least I think so). We have fully working decorator implementation that can prevent duplicated requests from sending. It can be configure separately for every
$http request group you define in your application. Just add
requestId parameters to request config.
I’m sure there are other methods for doing this kind of stuff. If you know one, let me know about it in comments below.