How to display a loading animation while file is generated for download?
To understand what needs to be done here, let's see what normally happens on this kind of request.
User clicks the button to request the file.
The file takes time to generate (the user gets no feedback).
The file is finished and starts to be sent to user.
What we would like to add is a feedback for the user to know what we are doing... Between step 1 and 2 we need to react to the click, and we need to find a way to detect when step 3 occurred to remove the visual feedback. We will not keep the user informed of the download status, their browser will do it as with any other download, we just want to tell the user that we are working on their request.
For the file-generation script to communicate with our requester page's script we will be using cookies, this will assure that we are not browser dependent on events, iframes or the like. After testing multiple solutions this seemed to be the most stable from IE7 to latest mobiles.
Step 1.5: Display graphical feedback.
We will use javascript to display a notification on-screen. I've opted for a simple transparent black overlay on the whole page to prevent the user to interact with other elements of the page as following a link might make him lose the possibility to receive the file.
$('#downloadLink').click(function() {
$('#fader').css('display', 'block');
});
#fader {
opacity: 0.5;
background: black;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
<div id="fader"></div>
<a href="#path-to-file-generator" id="downloadLink">Click me to receive file!</a>
</body>
Step 3.5: Removing the graphical display.
The easy part is done, now we need to notify JavaScript that the file is being downloaded. When a file is sent to the browser, it is sent with the usual HTTP headers, this allows us to update the client cookies. We will leverage this feature to provide the proper visual feedback. Let's modify the code above, we will need to set the cookie's starting value, and listen to its modifications.
var setCookie = function(name, value, expiracy) {
var exdate = new Date();
exdate.setTime(exdate.getTime() + expiracy * 1000);
var c_value = escape(value) + ((expiracy == null) ? "" : "; expires=" + exdate.toUTCString());
document.cookie = name + "=" + c_value + '; path=/';
};
var getCookie = function(name) {
var i, x, y, ARRcookies = document.cookie.split(";");
for (i = 0; i < ARRcookies.length; i++) {
x = ARRcookies[i].substr(0, ARRcookies[i].indexOf("="));
y = ARRcookies[i].substr(ARRcookies[i].indexOf("=") + 1);
x = x.replace(/^\s+|\s+$/g, "");
if (x == name) {
return y ? decodeURI(unescape(y.replace(/\+/g, ' '))) : y; //;//unescape(decodeURI(y));
}
}
};
$('#downloadLink').click(function() {
$('#fader').css('display', 'block');
setCookie('downloadStarted', 0, 100); //Expiration could be anything... As long as we reset the value
setTimeout(checkDownloadCookie, 1000); //Initiate the loop to check the cookie.
});
var downloadTimeout;
var checkDownloadCookie = function() {
if (getCookie("downloadStarted") == 1) {
setCookie("downloadStarted", "false", 100); //Expiration could be anything... As long as we reset the value
$('#fader').css('display', 'none');
} else {
downloadTimeout = setTimeout(checkDownloadCookie, 1000); //Re-run this function in 1 second.
}
};
#fader {
opacity: 0.5;
background: black;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
<div id="fader"></div>
<a href="#path-to-file-generator" id="downloadLink">Click me to receive file!</a>
</body>
Ok, what have we added here. I've put the set/getCookie functions I use, I don't know if they are the best, but they work very well. We set the cookie value to 0 when we initiate the download, this will make sure that any other past executions will not interfere. We also initiate a "timeout loop" to check the value of the cookie every second. This is the most arguable part of the code, using a timeout to loop function calls waiting for the cookie change to happen may not be the best, but it has been the easiest way to implement this on all browsers. So, every second we check the cookie value and, if the value is set to 1 we hide the faded visual effect.
Changing the cookie server side
In PHP, one would do like so:
setCookie("downloadStarted", 1, time() + 20, '/', "", false, false);
In ASP.Net
Response.Cookies.Add(new HttpCookie("downloadStarted", "1") { Expires = DateTime.Now.AddSeconds(20) });
Name of the cookie is downloadStarted
, its value is 1
, it expires in NOW + 20seconds
(we check every second so 20 is more than enough for that, change this value if you change the timeout value in the javascript), its path is on the whole domain (change this to your liking), its not secured as it contains no sensitive data and it is NOT HTTP only so our JavaScript can see it.
Voilà! That sums it up. Please note that the code provided works perfectly on a production application I am working with but might not suit your exact needs, correct it to your taste.
This is a simplified version of Salketer's excellent answer. It simply checks for the existence of a cookie, without regard for its value.
Upon form submit it will poll for the cookie's presence every second. If the cookie exists, the download is still being processed. If it doesn't, the download is complete. There is a 2 minute timeout.
The HTML/JS page:
var downloadTimer; // reference to timer object
function startDownloadChecker(buttonId, imageId, timeout) {
var cookieName = "DownloadCompleteChecker";
var downloadTimerAttempts = timeout; // seconds
setCookie(cookieName, 0, downloadTimerAttempts);
// set timer to check for cookie every second
downloadTimer = window.setInterval(function () {
var cookie = getCookie(cookieName);
// if cookie doesn't exist, or attempts have expired, re-enable form
if ((typeof cookie === 'undefined') || (downloadTimerAttempts == 0)) {
$("#" + buttonId).removeAttr("disabled");
$("#" + imageId).hide();
window.clearInterval(downloadTimer);
expireCookie(cookieName);
}
downloadTimerAttempts--;
}, 1000);
}
// form submit event
$("#btnSubmit").click(function () {
$(this).attr("disabled", "disabled"); // disable form submit button
$("#imgLoading").show(); // show loading animation
startDownloadChecker("btnSubmit", "imgLoading", 120);
});
<form method="post">
...fields...
<button id="btnSubmit">Submit</button>
<img id="imgLoading" src="spinner.gif" style="display:none" />
</form>
Supporting Javascript to set/get/delete cookies:
function setCookie(name, value, expiresInSeconds) {
var exdate = new Date();
exdate.setTime(exdate.getTime() + expiresInSeconds * 1000);
var c_value = escape(value) + ((expiresInSeconds == null) ? "" : "; expires=" + exdate.toUTCString());
document.cookie = name + "=" + c_value + '; path=/';
};
function getCookie(name) {
var parts = document.cookie.split(name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
function expireCookie(name) {
document.cookie = encodeURIComponent(name) + "=; path=/; expires=" + new Date(0).toUTCString();
}
Server side code in ASP.Net:
...generate big document...
// attach expired cookie to response to signal download is complete
var cookie = new HttpCookie("DownloadCompleteChecker"); // same cookie name as above!
cookie.Expires = DateTime.Now.AddDays(-1d); // expires yesterday
HttpContext.Current.Response.Cookies.Add(cookie); // Add cookie to response headers
HttpContext.Current.Response.Flush(); // send response
Hope that helps! :)
You can fetch the file using ajax add indicator then create a tag with dataURI and click on it using JavaScript:
You will need help from this lib: https://github.com/henrya/js-jquery/tree/master/BinaryTransport
var link = document.createElement('a');
if (link.download != undefined) {
$('.download').each(function() {
var self = $(this);
self.click(function() {
$('.indicator').show();
var href = self.attr('href');
$.get(href, function(file) {
var dataURI = 'data:application/octet-stream;base64,' + btoa(file);
var fname = self.data('filename');
$('<a>' + fname +'</a>').attr({
download: fname,
href: dataURI
})[0].click();
$('.indicator').hide();
}, 'binary');
return false;
});
});
}
You can see download attribute support on caniuse
and in your html put this:
<a href="somescript.php" class="download" data-filename="foo.pdf">generate</a>