14

I'm using an <iframe> (I know, I know, ...) in my app (single-page application with ExtJS 4.2) to do file downloads because they contain lots of data and can take a while to generate the Excel file (we're talking anything from 20 seconds to 20 minutes depending on the parameters).

The current state of things is : when the user clicks the download button, he is "redirected" by Javascript (window.location.href = xxx) to the page doing the export, but since it's done in PHP, and no headers are sent, the browser continuously loads the page, until the file is downloaded. But it's not very user-friendly, because nothing shows him whether it's still loading, done (except the file download), or failed (which causes the page to actually redirect, potentially making him lose the work he was doing).

So I created a small non-modal window docked in the bottom right corner that contains the iframe as well as a small message to reassure the user. What I need is to be able to detect when it's loaded and be able to differenciate 2 cases :

  • No data : OK => Close window
  • Text data : Error message => Display message to user + Close window

But I tried all 4 events (W3Schools doc) and none is ever fired. I could at least understand that if it's not HTML data returned, it may not be able to fire the event, but even if I force an error to return text data, it's not fired.

If anyone know of a solution for this, or an alternative system that may fit here, I'm all ears ! Thanks !

EDIT : Added iframe code. The idea is to get a better way to close it than a setTimeout.

var url = 'http://mywebsite.com/my_export_route';

var ifr = $('<iframe class="dl-frame" src="'+url+'" width="0" height="0" frameborder="0"></iframe>');
ifr.appendTo($('body'));

setTimeout(function() {
    $('.dl-frame').remove();
}, 3000);
3rgo
  • 3,115
  • 7
  • 31
  • 44

13 Answers13

7

I wonder if it would require some significant changes in both frontend and backend code, but have you considered using AJAX? The workflow would be something like this: user sends AJAX request to start file generating and frontend constantly polls it's status from the server, when it's done - show a download link to the user. I believe that workflow would be more straightforward.

Well, you could also try this trick. In parent window create a callback function for the iframe's complete loading myOnLoadCallback, then call it from the iframe with parent.myOnLoadCallback(). But you would still have to use setTimeout to handle server errors/connection timeouts.

And one last thing - how did you tried to catch iframe's events? Maybe it something browser-related. Have you tried setting event callbacks in HTML attributes directly? Like

<iframe onload="done()" onerror="fail()"></iframe>

That's a bad practice, I know, but sometimes job need to be done fast, eh?

UPDATE Well, I'm afraid you have to spend a long and painful day with a JS debugger. load event should work. I still have some suggestions, though:

1) Try to set event listener before setting element's src. Maybe onload event fires so fast that it slips between creating element and setting event's callback

2) At the same time try to check if your server code plays nicely with iframes. I have made a simple test which attempts to download a PDF from Dropbox, try to replace my URL with your backed route's.

<script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
<iframe id="book"></iframe>
<button id="go">Request downloads!</button>

<script>
    var bookUrl = 'https://www.dropbox.com/s/j4o7tw09lwncqa6/thinkpython.pdf';

    $('#book').on('load', function(){
        console.log('WOOT!', arguments);
    });

    $('#go').on('click', function(){
        $('#book').attr('src', bookUrl);
    });
</script>

UPDATE 2

3) Also, look at the Network tab of your browser's debugger, what happens when you set src to the iframe, it should show request and server's response with headers.

Anton Melnikov
  • 1,048
  • 7
  • 21
  • The long-poll is kind of like what we already had before : user clicks on link, it sends an AJAX request to the server, who generated the file and returns the link to that file for the AJAX call. Then on the client side we open a small window with that link. It's all good, but the window does not close itself, so I thought it could be better with an iframe. I'll check out your tips and get back to you – 3rgo May 26 '14 at 16:31
  • I tried putting simple `alert('test');` in the `onload` and `onerror` attributes, but nothing went through... As for calling a callback function from within the iframe, it's not possible since it's not HTML content I'm loading, it's a document (Excel, Word, PDF) – 3rgo May 26 '14 at 16:45
  • Coudn't you just append the link to your UI (after ajax is finished) instead of opening a new window (like stack overflow does for alerts)? You could also use web sockets or `eventsource` instead of long polling. – Will May 26 '14 at 19:06
  • You don't need to let the user click the link. You can make a hidden form or link that autosubmits/autoclicks once Ajax has done generating the file through the url sent as response. Here I use that method and works very well. – Roberto Maldonado May 26 '14 at 19:11
  • I tried putting the onload listener before setting the `src`, but the event is fired immediately after I set that, even though the "page" takes around 3 seconds to load – 3rgo May 27 '14 at 08:53
  • 1
    @Squ36 Yep, there may be two of those events, I've seen that myself when tested my example, though it's browser dependent, Firefox behaves like that while Chrome doesn't. The first one fires when loading empty src, the second one fires when loading document. You can filter them by event data or simply by iframe's src at that moment. What about your backend? Did console.log show something when you pressed the button? – Anton Melnikov May 27 '14 at 09:22
5

I've tried with jQuery and it worked just fine as you can see in this post.

I made a working example here.

It's basically this:

<iframe src="http://www.example.com" id="myFrame"></iframe>

And the code:

function test() {
    alert('iframe loaded');
}

$('#myFrame').load(test);

Tested on IE11.

Vitor Canova
  • 3,918
  • 5
  • 31
  • 55
  • This obviously works with a simple HTML content, but as I said in my OP, the url I'm loading is not HTML content, it's a document (mostly Excel files, but it can be PDF or Word) – 3rgo May 26 '14 at 16:42
  • Pdf documents trigger allert as well! http://jsfiddle.net/Yz6NP/1/ After 6 seconds the alert will trigger! – Mr.TK May 26 '14 at 20:54
  • @Squ36 You said you are generation this files. Do you have an URL where we can test this behavior? As far as I know iframes trigger load events because the html it contains. If you have no html to load you question title is plain wrong. Should be "how to silently start to download a file in browser" or something like this. I think a popup window can reach closer to what you want than an iframe. – Vitor Canova May 27 '14 at 00:45
  • 1
    @VitorCanova : Sorry but I cannot provide you with an URL to test, because it's an intranet application. I know there are other ways to do a silent file download, but the iframe is the best we have found, because the popup window cannot be closed, and a simple redirection actually redirects the user if something went wrong – 3rgo May 27 '14 at 08:39
3

I guess I'll give a more hacky alternative to the more proper ways of doing it that the others have posted. If you have control over the PHP download script, perhaps you can just simply output javascript when the download is complete. Or perhaps redirect to a html page that runs javascript. The javascript run, can then try to call something in the parent frame. What will work depends if your app runs in the same domain or not

 

Same domain

Same domain frame can just use frame javascript objects to reference each other. so it could be something like, in your single page application you can have something like

window.downloadHasFinished=function(str){ //Global pollution. More unique name?
    //code to be run when download has finished
}

And for your download php script, you can have it output this html+javascript when it's done

<script>
if(parent && parent.downloadHasFinished)
    parent.downloadHasFinished("if you want to pass a data. maybe export url?")
</script>

 

Different Domains

For different domains, We can use postMessage. So in your single page application it will be something like

$(window).on("message",function(e){
    var e=e.originalEvent
    if(e.origin=="http://downloadphp.anotherdomain.com"){ //for security
      var message=e.data //data passed if any
      //code to be run when download has finished
    }
});

and in your php download script you can have it output this html+javascript

<script>
parent.postMessage("if you want to pass data",
   "http://downloadphp.anotherdomain.com");
</script>
  1. Parent Demo
  2. Child jsfiddle

 

Conclusion

Honestly, if the other answers work, you should probably use those. I just thought this was an interesting alternative so I posted it up.

mfirdaus
  • 4,574
  • 1
  • 25
  • 26
2

Maybe you should use

$($('.dl-frame')[0].contentWindow.document).ready(function () {...})
hichris123
  • 10,145
  • 15
  • 56
  • 70
Chrysi
  • 121
  • 3
  • Hooray, this actually worked for me! Still fires if the request was for a downloading file (i.e it works when `onload` wouldn't work.) – Redzarf Oct 02 '17 at 20:34
2

You can use the following script. It comes from a project of mine.

$("#reportContent").html("<iframe id='reportFrame' sandbox='allow-same-origin allow-scripts' width='100%' height='300' scrolling='yes' onload='onReportFrameLoad();'\></iframe>");
BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
Ross Bush
  • 14,648
  • 2
  • 32
  • 55
2

Try this (pattern)

    $(function () {
        var session = function (url, filename) {
           // `url` : URL of resource
           // `filename` : `filename` for resource (optional)
            var iframe = $("<iframe>", {
                "class": "dl-frame",
                    "width": "150px",
                    "height": "150px",
                    "target": "_top"
            })
            // `iframe` `load` `event`
            .one("load", function (e) {
                $(e.target)
                    .contents()
                    .find("html")
                    .html("<html><body><div>" 
                          + $(e.target)[0].nodeName 
                          + " loaded" + "</div><br /></body></html>");
                alert($(e.target)[0].nodeName 
                        + " loaded" + "\nClick link to download file");
                return false
            });

            var _session = $.when($(iframe).appendTo("body"));
            _session.then(function (data) {
                var link = $("<a>", {
                        "id": "file",
                        "target": "_top",
                        "tabindex": "1",
                        "href": url,
                        "download": url,
                        "html": "Click to start {filename} download"
                });
                $(data)
                    .contents()
                    .find("body")
                    .append($(link))
                    .addBack()
                    .find("#file")
                    .attr("download", function (_, o) {
                      return (filename || o)
                    })
                    .html(function (_, o) {
                      return o.replace(/{filename}/, 
                      (filename || $(this).attr("download")))
                })

            });
            _session.always(function (data) {
                $(data)
                    .contents()
                    .find("a#file")
                    .focus()
                    // start 6 second `download` `session`,
                    // on `link` `click`
                    .one("click", function (e) {
                    var timer = 6;
                    var t = setInterval(function () {
                        $(data)
                            .contents()
                            .find("div")
                             // `session` notifications
                            .html("Download session started at " 
                                  + new Date() + "\n" + --timer);
                    }, 1000);
                    setTimeout(function () {
                        clearInterval(t);
                        $(data).replaceWith("<span class=session-notification>"    
                          + "Download session complete at\n" 
                          + new Date() 
                          + "</span><br class=session-notification />"
                          + "<a class=session-restart href=#>"
                          + "Restart download session</a>");
                        if ($("body *").is(".session-restart")) {
                            // start new `session`,
                            // on `.session-restart` `click`
                            $(".session-restart")
                            .on("click", function () {
                                $(".session-restart, .session-notification")
                                .remove() 
                                // restart `session` (optional),
                                // or, other `session` `complete` `callback` 
                                && session(url, filename ? filename : null)
                            })
                        };
                    }, 6000);
                });
            });
        };
        // usage
        session("http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf", "ECMA_JS.pdf")
    });

jsfiddle http://jsfiddle.net/guest271314/frc82/

guest271314
  • 1
  • 15
  • 104
  • 177
1

You can use this library. The code snippet for you purpose would be something like:

window.onload = function () {
  rajax_obj = new Rajax('',
    {
        action : 'http://mywebsite.com/my_export_route', 
        onComplete : function(response) {
                //This will only called if you have returned any response 
               // instead of file from your export script
               // In your case 2
               // Text data : Error message => Display message to user
        }
    });
}

Then you can call rajax_obj.post() on your download link click.

<a href="javascript:rajax_obj.post()">Download</a>

NB: You should add some header to your PHP script so it force file download

header('Content-Disposition: attachment; filename="'.$file.'"');
header('Content-Transfer-Encoding: binary');
BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
xiidea
  • 3,344
  • 20
  • 24
1

In regards to your comment about to get a better way to close it instead of setTimeout. You could use jQuery fadeOut option or any of the transitions and in the 'complete' callback remove the element. Below is an example you can dump right into a fiddle and only need to reference jQuery.

I also wrapped inside listener for 'load' event to not do the fade until the iFrame has been loaded as question originally was asking.

// plugin your URL here 
var url = 'http://jquery.com';

// create the iFrame, set attrs, and append to body 
var ifr = $("<iframe>") 
    .attr({
        "src": url, 
        "width": 300,
        "height": 100,
        "frameborder": 0 
    }) 
    .addClass("dl-frame")
    .appendTo($('body'))
;

// log to show its part of DOM 
console.log($(".dl-frame").length + " items found"); 

// create listener for load 
ifr.one('load', function() {
    console.log('iframe is loaded'); 

    // call $ fadeOut to fade the iframe 
    ifr.fadeOut(3000, function() {
        // remove iframe when fadeout is complete
        ifr.remove();  
        // log after, should no longer exist in DOM
        console.log($(".dl-frame").length + " items found");
    });  
}); 
Scott
  • 729
  • 1
  • 11
  • 30
  • BTW, in addition to the code I posted, from reading what you are trying to accomplish you could always use a library such as [alertify](http://fabien-d.github.io/alertify.js/). I've had great success with it and it sounds like what you are trying to accomplish would work well with alertify? Look at the Default notifications section. Just another suggestion. – Scott May 29 '14 at 19:24
  • Thx for the tip on alertify. In this particular case, it's not needed because this project uses ExtJS, so making a notification-style window is easy, and its design stays consistent. As for your code suggestion, I do not use jQuery, and ExtJS's load event doesn't seem to fire. I'll check it out on Monday more extensively, and let you know – 3rgo May 30 '14 at 00:26
  • What version of ExtJS are you using? You can accomplish the same thing without jQuery. Of course as you've already seen setTimeout works as well. Depending on version of ExtJS I once had problems with iFrame load event in ExtJS but its been some time ago. – Scott May 30 '14 at 14:06
1

Try this:
Note: You should be on the same domain.

var url = 'http://mywebsite.com/my_export_route',
    iFrameElem = $('body')
        .append('<iframe class="dl-frame" src="' + url + '" width="0" height="0" frameborder="0"></iframe>')
        .find('.dl-frame').get(0),
    iDoc = iFrameElem.contentDocument || iFrameElem.contentWindow.document;

$(iDoc).ready(function (event) {
    console.log('iframe ready!');
    // do stuff here
});
Onur Yıldırım
  • 32,327
  • 12
  • 84
  • 98
1

If you are doing a file download from a iframe the load event wont fire :) I was doing this a week ago. The only solution to this problem is to call a download proxy script with a tag and then return that tag trough a cookie then the file is loaded. min while yo need to have a setInterval on the page witch will watch for that specific cookie.

// Jst to clearyfy

var token = new Date().getTime(); // ticks
$('<iframe>',{src:"yourproxy?file=somefile.file&token="+token}).appendTo('body');

var timers = [];
timers[timers.length+1] = setInterval(function(){
var _index = timers.length+1;
 var cookie = $.cooke(token);
 if(typeof cookie != "undefined"){
  // File has been downloaded
   $.removeCookie(token);
   clearInterval(_index);
 }
},400);

in your proxy script add the cookie with the name set to the string sent bay the token url parameter.

Erik Simonic
  • 457
  • 3
  • 13
1

If you control the script in server that generates excel or whatever you are sending to iframe why don't you put a UID flag and store it in session with value 0, so... when iframe is created and server script is called just set UID flag to 1 and when script is finished (the iframe will be loaded) just put it to 2.

Then you only need a timer and a periodic AJAX call to the server to check the UID flag... if it's set to 0 the process doesn't started, if it's 1 the file is creating, and finally if it's 2 the process has been ended.

What do you think? If you need more information about this approach just ask.

ZeroWorks
  • 1,618
  • 1
  • 18
  • 22
1

What you are saying could be done for images and other media formats using $(iframe).load(function() {...});

For PDF files or other rich media, you can use the following Library: http://johnculviner.com/jquery-file-download-plugin-for-ajax-like-feature-rich-file-downloads/

Note: You will need JQuery UI

Simcha Khabinsky
  • 1,970
  • 2
  • 19
  • 34
1

There is two solutions that i can think of. Either you have PHP post it's progress to a MySQL table where from frontend will be pulling information from using AJAX calls to check up on the progress of the generation. Using somekind of unique key that is being generated when accessing the page would be ideal for multiple people generating excel files at the same time.

Another solution would be to use nodejs & then in PHP post the progress of the excel file using cURL or a socket to a nodejs service. Then when receiving updates from PHP in nodejs you simply write the progress of the excel file for the right socket. This will cut off some browser support though. Unless you go through with it using external libraries to bring websocket support for pretty much all browsers & versions.

Hope this answer helped. I was having the same issue previous year. Ended up doing AJAX polling having PHP post progress on the fly.

danniehansenweb
  • 465
  • 4
  • 14