2

I'm working on a project where we have an AJAX call which responds with a blob containing an excel file. I want the code to open the file as a download when I get the response. Here is the callback:

var blob = new Blob([response.data], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});
var objectUrl = URL.createObjectURL(blob);
var a = document.createElement("a");
var header = response.headers("Content-Disposition");
a.download = header.substring(header.indexOf("filename=") + "filename=".length);
a.href = objectUrl;
document.body.appendChild(a);
console.debug("Clicking a tag");
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(objectUrl);

This code works fine in chrome but in firefox nothing happens when a.click() fires. The debug statement prints so I know that the callback is happening. Also, for some reason, if I put a breakpoint on a.click() it works perfectly.

Can someone explain why the click only works in debug mode?

Thomas
  • 750
  • 2
  • 6
  • 19

1 Answers1

3

Firefox has a couple of safeguards or just odd behaviors around things like this. I don't know the derivation, but yielding back to the browser for a moment before doing the click usually clears it up:

// ...
a.href = objectUrl;
document.body.appendChild(a);
setTimeout(function() {
    a.click();
    document.body.removeChild(a);
    window.URL.revokeObjectURL(objectUrl);
}, 0);
// ...

Note that code following the setTimeout will run before the content of the setTimeout does.

You might even need two:

// ...
a.href = objectUrl;
document.body.appendChild(a);
setTimeout(function() {
    a.click();
    setTimeout(function() {
        document.body.removeChild(a);
        window.URL.revokeObjectURL(objectUrl);
    }, 0);
}, 0);
// ...
T.J. Crowder
  • 879,024
  • 165
  • 1,615
  • 1,639
  • @user2752635: I may be misremembering, you may need it *after* the `click`. I've added an example with it in both places above. I also remember having something in Firefox about a year ago where a `0` timeout didn't work but a `10` timeout didn't. (At which point I decided to rethink my entire approach, as I wasn't comfortable with that... :-) ) – T.J. Crowder Sep 13 '16 at 15:29
  • 1
    This fixes the problem for me. I don't like have `setTimeout(fn, 0)`'s in my code (especially nested) but if it works it works. – Thomas Sep 13 '16 at 15:32
  • Also, I tried without the first and it didn't work. It needs both. – Thomas Sep 13 '16 at 15:33
  • Wow. Yeah, sometimes you need to yield back for it to work properly. Seems like a bug to me, in this particular case. Other cases it's reasonable, but... – T.J. Crowder Sep 13 '16 at 15:38
  • 1
    @user2752635: I probably don't have to say this, but: Do be sure to test thoroughly in case it only works intermittently. That's what happened to me with the case I mentioned above where it only became reliable at 10ms and I went another way. – T.J. Crowder Sep 13 '16 at 15:42
  • I had to add two setTimeouts as you wrote. FF v 45.2 – Ernesto Jun 08 '18 at 08:03