There are many things to consider when you have a webapp that dynamically generates reports and that generation takes a while. This article is about giving the user some feedback that their report is being processed and hiding that message once the report has completely downloaded.
This is a followup to the article Gracefully handling inherently slow webapp actions
The intent is to avoid the user swamping the server with repeated requests (if they are the button-mashing type), inform them that we're on the case, and finally let them know when we're done.
Here is an approach I've found works quite well, along with some code snippets for the important bits. In my case, the webapps are usually Java/Spring/Tomcat based, with basic HTML/JS/jQuery front-ends, but the overall idea should work with pretty much any webapp stack.
The main trick is hiding the "please wait" message once the report has finished downloading. There is no direct Javascript/DOM event we can hang a callback on when an HTTP response results in the browser handling it as a downloaded file.
Instead, the key is to set a cookie on the response header and have a JS poller looking for that cookie. When the poller sees the cookie, it knows the download has happened, so removes the wait message.
Summary
- When the user hits the "download" button, hide it to avoid the button being hit again
- validate the form (we'll assume it's ok)
- display a "please wait" message plus an animated gif
- do the form submit to the server.
- Here's the clever bit Start a JS thread polling for a page cookie with a name unique to this report from this webapp (e.g. mySpecialCookie)
- when the webapp starts doing its server-side stuff, it adds a Cookie named mySpecialCookie to the HTTP response object
- the report generation completes and the download gets back to the browser
- the browser sets the cookie and asks the user what to do with the PDF
- the poller thread picks up the new cookie, stops any more polling, clears the cookie and finally closes the "wait" message
The HTML
This is fairly simple, the main parts are:
- Include
jquery.cookie.js
afterjquery.js
. This lets us easily set and view cookies (string fiddling ondocument.cookie
should work if you prefer). - put your form contents in an element which can be hidden (
<form>
should be just fine) - add a hidden
<div>
element containing your wait message and animated image
<html>
<head>
<script type="text/javascript" src="blah/jquery/jquery.js"></script>
<script type="text/javascript" src="blah/jquery/jquery-cookie/jquery.cookie.js"></script>
...
</head>
<body>
<form id="myForm" action="/myApp/makeReport" method="post">
...
<button id="mySubmit" onclick="requestReport();" class="submit">
download
</button>
</form>
<div id="progressMsg" class="hidden">
<img style="display:block;margin:auto" src="/images/swirly.gif"/>
<p>Generating your report.</p>
<p>Please be patient, this may take a minute or two.</p>
</div>
</body>
</html>
The JavaScript
This uses the jquery.cookie.js
plugin, but you can use document.cookie.indexOf("mySpecialCookie")
, and some string replacement to remove it if you prefer.
// hide the form, show the wait message
function disableForm() {
$("form#myForm").hide();
$("div#progress").show();
}
// the cookie poller
var downloadChecker;
function requestReport() {
if (validateTheForm()) {
// disable as well as hiding - belt and braces, shouldn't be needed
$("button#mySubmit").attr("disabled", "disabled");
$('#myForm').submit();
// IE8 workaround to ensure the animated gif animates properly
setTimeout(disableForm, 50);
// Check every 5 seconds whether the cookie has been set.
// When we spot the cookie has been set, the download has completed
// turn off the poller and remove the cookie.
downloadChecker = window.setInterval(function() {
if ($.cookie("mySpecialCookie") != null) {
window.clearInterval(downloadChecker);
$.cookie("mySpecialCookie", null, {path: '/myApp'});
}
}, 5000);
}
}
Remember to call clearInterval()
to stop the poller once you've got the PDF, otherwise it'll sit there checking cookies every 5 seconds until you leave the page.
Note that the animated gif is inside a div and is shown after a very short delay (so that it gets done in a separate thread).
This is purely to get the image to animate correctly in IE. #include "std_bloodyIE.h"
The Server Side
This is a Spring controller fragment, since that's what I'm usually using.
@RequestMapping(value="makeReport", method = RequestMethod.POST)
public String makeReport( /* whatever other params you need */, HttpServletResponse resp) {
/* do cleverness, validate stuff, get data, log things */
Cookie c = new Cookie("mySpecialCookie", "true");
c.setPath("/myApp");
resp.addCookie(c);
/* whatever other generation stuff needs doing */
}
Technically the cookie path setting and checking is only needed if the form page and report generation request have different paths, but it's safest to set it anyway.
Although this is a Spring controller method, the core Cookie and HttpServletResponse stuff is standard Java servlet, it's not Spring specific.
Actually this is all standard HTTP response protocol stuff, so any other webapp language/framework should be able to use the same approach with the right API.
Or, if you're hardcore, you can do the raw HTTP request/response socket fiddling to do the same thing too - you black tshirt wearing hero you!