4

I am trying to implement these seemingly simple requirements but can't find a way :

  • Single Page App using Angular JS
  • REST(ish) back end
  • Back end resource exposed via POST request
  • Resource parameters passed as JSON in the request body
  • Resource produces a CSV file
  • When a user clicks a button, generate a request with the right JSON parameters in the body, send it, and allow user to download the response as a file (prompts the browser's "open / save as" dialog)

The problem is mainly, how to pass the JSON as request body? The most common technique seems to be the hidden HTML form to trigger the download, but an HTML form cannot send JSON data in the body. And I can't find any way to trigger a download dialog using an XMLHttpRequest...

Any ideas?

I specified Angular but any generic JS solution is very welcome too!

Rob
  • 14,746
  • 28
  • 47
  • 65
Pierre Henry
  • 16,658
  • 22
  • 85
  • 105
  • Does this help?: http://stackoverflow.com/questions/14551194/how-are-parameters-sent-in-an-http-post-request And can you post a snippet, how you make the JS HTTP call? If it's about the download dialog, you need to specify an additional header. – Markus Aug 22 '16 at 11:46
  • 3
    Response must have "Content-Disposition: attachment" set in the headers for the browser to display "open / save as" dialog. JSON won't do it, unless you post-process the JSON and redirect to the URL – Dimitri Aug 22 '16 at 11:46
  • @Dimitri : yes, this is already the case. The request contains JSON in the body, the response contains CSV with the right headers including "Content-Disposition" – Pierre Henry Aug 22 '16 at 11:53
  • @Makus : thanks but this doesn't help much. How the HTTP call should be made is kind of the question. It could be using Angular's $http.post or any other way that works... – Pierre Henry Aug 22 '16 at 11:55
  • @Pierre: What is the content type of the response? For the save/open dialog it must be text/csv, and the headers must be set on response, not CSV. – Dimitri Aug 22 '16 at 11:57
  • @Dimitri : the headers are correct on the response. The thing is, even with the correct headers, if the request was made using XMLHttpRequest it will not trigger the save/open dialog. – Pierre Henry Aug 22 '16 at 12:06
  • @Pierre got it. The solution I've found for those cases is to save the file in a temp location and pass the link to the file in the response. Then using javascript do a redirect to that location. – Dimitri Aug 22 '16 at 12:11
  • @Dimitri: yes I know this is possible but I really want to avoid it. I want the data generated and streamed on the fly to the client. Never mind, I am on the right track to working solution, it involves JS and the Blob API. Will post an answer when it finally works. – Pierre Henry Aug 22 '16 at 12:35
  • have a look at http://blog.davidjs.com/2015/07/download-files-via-post-request-in-angularjs/ he uses the standard saveas from html and post to request a zip file. – Raphael Müller Aug 22 '16 at 12:58
  • @Raphael: interesting, but I could not find much info about browser support for this FileSaver API... only polyfills. Hopefully it will get better ! – Pierre Henry Aug 22 '16 at 13:23

1 Answers1

0

I finally found a solution that satisfies all my requirements, and works in IE11, FF and Chrome (and degrades kind of OK in Safari...).

The idea is to create a Blob object containing the data from the response, then force the browser to open it as a file. It is slightly different for IE (proprietary API) and Chrome/FF (using a link element).

Here is the implementation, as a small Angular service:

myApp.factory('Download', [function() {
    return {
        openAsFile : function(response){

            // parse content type header
            var contentTypeStr = response.headers('Content-Type');
            var tokens = contentTypeStr.split('/');
            var subtype = tokens[1].split(';')[0];
            var contentType = {
                type : tokens[0],
                subtype : subtype
            };

            // parse content disposition header, attempt to get file name
            var contentDispStr = response.headers('Content-Disposition');
            var proposedFileName = contentDispStr ? contentDispStr.split('"')[1] : 'data.'+contentType.subtype;

            // build blob containing response data
            var blob = new Blob([response.data], {type : contentTypeStr});

            if (typeof window.navigator.msSaveBlob !== 'undefined'){
                // IE : use proprietary API
                window.navigator.msSaveBlob(blob, proposedFileName);
            }else{
                var downloadUrl = URL.createObjectURL(blob);

                // build and open link - use HTML5 a[download] attribute to specify filename
                var a = document.createElement("a");

                // safari doesn't support this yet
                if (typeof a.download === 'undefined') {
                    window.open(downloadUrl);
                }

                var link = document.createElement('a');
                link.href = downloadUrl;
                link.download = proposedFileName;
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
            }
        }
    }
}]);

The response argument expects a $http response object. Here is an example of use with a POST request:

$http.post(url, {property : 'value'},  {responseType: 'blob'}).then(function(response){
    Download.openAsFile(response);
});

Note the responseType parameter. Without this, my CSV data was being read as text and stored in memory as UTF-8 (or 16), and subsequently the file was saved in the same encoding, causing Excel to not recognize special characters such as éè etc. Since my CSVs are intended to be opened by Excel, the server encodes them Windows 1252, I wanted to keep them that way. Setting the responseType parameter to blob achieves this.

Disclaimer: It should work with any file type. But I tested it only with CSV files ! Binary files might behave somehow differently !

Pierre Henry
  • 16,658
  • 22
  • 85
  • 105