53

I've created an application in Angular JS for downloading an Excel workbook through $http post.

In the below code I'm passing the information in the form of JSON , and send it to the server REST web service (java) through an angular $http post. The web service uses the information from the JSON and produces an Excel workbook. In the response within the success body of $http post, I'm getting binary data within that data variable, but don't know how to convert it and download as an Excel file.

Can anyone please tell me some solution for this for converting the binary to Excel file and download?

My code is as given below:

$http({
        url: 'myweb.com/myrestService',
        method: "POST",
        data: json, //this is your json data string
        headers: {
           'Content-type': 'application/json'
        }
    }).success(function (data, status, headers, config) {

        // Here i'm getting excel sheet binary datas in 'data' 

    }).error(function (data, status, headers, config) {

    });
Edward Brey
  • 40,302
  • 20
  • 199
  • 253
Alex Man
  • 4,746
  • 17
  • 93
  • 178
  • Hey. I actually thought about your problem some more... What browsers are you supporting? I might have a solution using blobs, but that doesn't work in IE 8 and 9: http://caniuse.com/#feat=bloburls – Jorg Mar 17 '14 at 06:56
  • :( .... I'm Working with IE8 and IE9 – Alex Man Mar 17 '14 at 07:03
  • Bummer. Had my answer finished so it's there for anyone else – Jorg Mar 17 '14 at 07:12
  • 1
    It's not a specific angular issue btw, it's the same in vanilla js or jquery, only the xmlhttprequest is wrapped differently – Jorg Mar 17 '14 at 07:41

9 Answers9

82

Just noticed you can't use it because of IE8/9 but I'll push submit anyway... maybe someone finds it useful

This can actually be done through the browser, using blob. Notice the responseType and the code in the success promise.

$http({
    url: 'your/webservice',
    method: "POST",
    data: json, //this is your json data string
    headers: {
       'Content-type': 'application/json'
    },
    responseType: 'arraybuffer'
}).success(function (data, status, headers, config) {
    var blob = new Blob([data], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});
    var objectUrl = URL.createObjectURL(blob);
    window.open(objectUrl);
}).error(function (data, status, headers, config) {
    //upload failed
});

There are some problems with it though like:

  1. It doesn't support IE 8 and 9:
  2. It opens a pop up window to open the objectUrl which people might have blocked
  3. Generates weird filenames

It did work!

blob The server side code in PHP I tested this with looks like this. I'm sure you can set similar headers in Java:

$file = "file.xlsx";
header('Content-disposition: attachment; filename='.$file);
header('Content-Length: ' . filesize($file));
header('Content-Transfer-Encoding: binary');
header('Cache-Control: must-revalidate');
header('Pragma: public');
echo json_encode(readfile($file));

Edit 20.04.2016

Browsers are making it harder to save data this way. One good option is to use filesaver.js. It provides a cross browser implementation for saveAs, and it should replace some of the code in the success promise above.

Jorg
  • 7,219
  • 3
  • 44
  • 65
  • I think in many cases it wont work. U need to set content type of $http to 'blob'. – user2171669 Nov 10 '14 at 12:28
  • @jorg Is it possible to add similar headers in django python. – nosdalg Jun 01 '15 at 09:25
  • @Explore-X yes I'm positive you can. Not familiar with django though, so I can't tell you how exactly. – Jorg Jun 03 '15 at 23:43
  • @Jorg in the screenshot of the Excel you have shared above, the text in the field, i.e. "my content", it is exceeding the width of the cell, is there an option through which I can set the options like default column-width or word wrap? The issue I am facing is that while printing a file like your above, the data gets truncated if there are multiple columns and the width of the data is more than the width of the column. Much thanks! – Mitaksh Gupta Aug 11 '15 at 11:43
  • @MitakshGupta the file is a copy of what you saved. If you generated it with the proper column width it would come through that way I imagine. I can try if you like. – Jorg Aug 11 '15 at 11:47
  • @Jorg Please have a look at this question I posted a couple of days back http://stackoverflow.com/questions/31918248/set-column-width-and-word-wrap-properties-while-exporting-data-to-excel-csv-angu This gives the exact description of the problem I am facing – Mitaksh Gupta Aug 11 '15 at 11:49
  • 5
    Excellent! **The `responseType: 'arraybuffer'` was the key for me**. I had to fight with [connect-livereload issues causing zip file corruption](http://stackoverflow.com/questions/29562954/downloaded-pdf-files-are-corrupted-when-using-expressjs?answertab=active#tab-top) on the server, so this angular tweak was much faster-found welcome fix! Thanks! – treejanitor Dec 17 '15 at 16:25
  • Try filesaver.js instead of window.open – Jorg Apr 19 '16 at 12:26
  • Just going to piggyback on the most upvoted comment to add that you don't need to use a `responsetype: arraybuffer` because you then have to manually convert it to a blob using `new Blob()`. Unless you need to modify the incoming blob from the server, then just use a response type of blob directly. – puiu Mar 17 '17 at 17:09
  • can you answer this question http://stackoverflow.com/questions/43894768/how-to-set-the-no-of-column-name-while-export-the-excel-using-angularjs – Vinoth May 10 '17 at 14:15
  • getting this error in safari. "'[object BlobConstructor]' is not a constructor (evaluating 'new Blob([data], { type: contentType })')" – Wella Aug 08 '17 at 10:46
  • Is there a good way to control the filename using this approach? – Sledge Oct 16 '17 at 18:20
  • No longer works after Chrome 65 update. It's blocking cross-origin https://www.chromestatus.com/feature/4969697975992320 – Hardik Sondagar Apr 04 '18 at 06:06
  • @HardikSondagar FileSaver.js is working on this: https://github.com/eligrey/FileSaver.js/issues/428 – Jorg Apr 04 '18 at 06:07
  • @Jorg Can we create a dynamic form with blob URL as an action and submit using `js`? – Hardik Sondagar Apr 04 '18 at 06:24
25

This is how you do it:

  1. Forget IE8/IE9, it is not worth the effort and does not pay the money back.
  2. You need to use the right HTTP header,use Accept to 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' and also you need to put responseType to 'arraybuffer'(ArrayBuffer but set with lowercase).
  3. HTML5 saveAs is used to save the actual data to your wanted format. Note it will still work without adding type in this case.
$http({
    url: 'your/webservice',
    method: 'POST',
    responseType: 'arraybuffer',
    data: json, //this is your json data string
    headers: {
        'Content-type': 'application/json',
        'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    }
}).success(function(data){
    var blob = new Blob([data], {
        type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    });
    saveAs(blob, 'File_Name_With_Some_Unique_Id_Time' + '.xlsx');
}).error(function(){
    //Some error log
});

Tip! Don't mix " and ', stick to always use ', in a professional environment you will have to pass js validation for example jshint, same goes for using === and not ==, and so on, but that is another topic :)

I would put the save excel in another service, so you have clean structure and the post is in a proper service of its own. I can make a JS fiddle for you, if you don't get my example working. Then I would also need some json data from you that you use for a full example.

Happy coding.. Eduardo

  • 7
    Worked from me.. but depends on http://github.com/eligrey/FileSaver.js/blob/master/FileSaver.min.js FileSaver for saveAs – Simon H Apr 02 '15 at 10:18
  • 1
    This worked for me as well. Take a look at https://github.com/alferov/angular-file-saver if you're using AngularJS. And if you won't be supported IE8/9, it may be nice to let the user know, for that I use this: https://github.com/burocratik/outdated-browser. Also, setting `responseType: 'arraybuffer'` is important. – chipit24 Sep 09 '15 at 23:50
  • can you answer this question http://stackoverflow.com/questions/43894768/how-to-set-the-no-of-column-name-while-export-the-excel-using-angularjs – Vinoth May 10 '17 at 14:15
  • is there a way to get the filename in the header with this solution? – dierre Aug 07 '19 at 20:32
11

Download the server response as an array buffer. Store it as a Blob using the content type from the server (which should be application/vnd.openxmlformats-officedocument.spreadsheetml.sheet):

var httpPromise = this.$http.post(server, postData, { responseType: 'arraybuffer' });
httpPromise.then(response => this.save(new Blob([response.data],
    { type: response.headers('Content-Type') }), fileName));

Save the blob to the user's device:

save(blob, fileName) {
    if (window.navigator.msSaveOrOpenBlob) { // For IE:
        navigator.msSaveBlob(blob, fileName);
    } else { // For other browsers:
        var link = document.createElement('a');
        link.href = window.URL.createObjectURL(blob);
        link.download = fileName;
        link.click();
        window.URL.revokeObjectURL(link.href);
    }
}
Edward Brey
  • 40,302
  • 20
  • 199
  • 253
5

Worked for me -

$scope.downloadFile = function () {
        Resource.downloadFile().then(function (response) {
            var blob = new Blob([response.data], { type: "application/pdf" });
            var objectUrl = URL.createObjectURL(blob);
            window.open(objectUrl);
        },
        function (error) {
            debugger;
        });
    };

Which calls the following from my resource factory-

  downloadFile: function () {
           var downloadRequst = {
                method: 'GET',
                url: 'http://localhost/api/downloadFile?fileId=dfckn4niudsifdh.pdf',
                headers: {
                    'Content-Type': "application/pdf",
                    'Accept': "application/pdf"
                },
                responseType: 'arraybuffer'
            }

            return $http(downloadRequst);
        }

Make sure your API sets the header content type too -

        response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf");
        response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
4

There is no way (to my knowledge) to trigger the download window in your browser from Javascript. The only way to do it is to redirect the browser to a url that streams the file to the browser.

If you can modify your REST service, you might be able to solve it by changing so the POST request doesn't respond with the binary file, but with a url to that file. That'll get you the url in Javascript instead of the binary data, and you can redirect the browser to that url, which should prompt the download without leaving the original page.

Anders Ekdahl
  • 22,685
  • 4
  • 70
  • 59
1

Answer No 5 worked for me ,Suggestion to developer who are facing similar issue.

//////////////////////////////////////////////////////////
//Server side 
//////////////////////////////////////////////////////////
imports ***
public class AgentExcelBuilder extends AbstractExcelView {

protected void buildExcelDocument(Map<String, Object> model,
            HSSFWorkbook workbook, HttpServletRequest request,
            HttpServletResponse response) throws Exception {

        //poi code goes here ....

        response.setHeader("Cache-Control","must-revalidate");
        response.setHeader("Pragma", "public");
        response.setHeader("Content-Transfer-Encoding","binary");
        response.setHeader("Content-disposition", "attachment; filename=test.xls");

        OutputStream output = response.getOutputStream();

        workbook.write(output);
        System.out.println(workbook.getActiveSheetIndex());
        System.out.println(workbook.getNumberOfSheets());
        System.out.println(workbook.getNumberOfNames());
        output.flush();
        output.close(); 
}//method buildExcelDocument ENDS

//service.js at angular JS code
function getAgentInfoExcel(workgroup,callback){
        $http({
            url: CONTEXT_PATH+'/rest/getADInfoExcel',
            method: "POST",
            data: workgroup, //this is your json data string
            headers: {
               'Content-type': 'application/json'
            },
            responseType: 'arraybuffer'
        }).success(function (data, status, headers, config) {
            var blob = new Blob([data], {type: "application/vnd.ms-excel"});
            var objectUrl = URL.createObjectURL(blob);
            window.open(objectUrl);
        }).error(function (data, status, headers, config) {
            console.log('Failed to download Excel')
        });
    }
////////////////////////////////in .html 

<div class="form-group">`enter code here`
                                <a href="javascript:void(0)" class="fa fa-file-excel-o"
                                    ng-click="exportToExcel();"> Agent Export</a>
                            </div>
Mayank Patel
  • 3,868
  • 10
  • 36
  • 59
Vj Singh
  • 31
  • 2
0

You could as well take an alternative approach -- you don't have to use $http, you don't need any extra libraries, and it ought to work in any browser.

Just place an invisible form on your page.

<form name="downloadForm" action="/MyApp/MyFiles/Download" method="post" target="_self">
    <input type="hidden" name="value1" value="{{ctrl.value1}}" />
    <input type="hidden" name="value2" value="{{ctrl.value2}}" />
</form>

And place this code in your angular controller.

ctrl.value1 = 'some value 1';  
ctrl.value2 = 'some value 2';  
$timeout(function () {
    $window.document.forms['downloadForm'].submit();
});

This code will post your data to /MyApp/MyFiles/Download and you'll receive a file in your Downloads folder.
It works with Internet Explorer 10.

If a conventional HTML form doesn't let you post your complex object, then you have two options:

1. Stringify your object and put it into one of the form fields as a string.

<input type="hidden" name="myObjJson" value="{{ctrl.myObj | json:0}}" />


2. Consider HTML JSON forms: https://www.w3.org/TR/html-json-forms/

0

I created a service that will do this for you.

Pass in a standard $http object, and add some extra parameters.

1) A "type" parameter. Specifying the type of file you're retrieving. Defaults to: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
2) A "fileName" parameter. This is required, and should include the extension.

Example:

httpDownloader({
  method : 'POST',
  url : '--- enter the url that returns a file here ---',
  data : ifYouHaveDataEnterItHere,
  type : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // this is the default
  fileName : 'YourFileName.xlsx'
}).then(res => {}).catch(e => {});

That's all you need. The file will be downloaded to the user's device without a popup.

Here's the git repo: https://github.com/stephengardner/ngHttpDownloader

Augie Gardner
  • 2,749
  • 3
  • 25
  • 36
-2

I was facing this same problem. Let me tell you how I solved it and achieved everything you all seem to be wanting.

Requirements:

  1. Must have a button (or link) to a file - (or a generated memory stream)
  2. Must click the button and have the file download

In my service, (I'm using Asp.net Web API), I have a controller returning an "HttpResponseMessage". I add a "StreamContent" to the response.Content field, set the headers to "application/octet-stream" and add the data as an attachment. I even give it a name "myAwesomeFile.xlsx"

response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new StreamContent(memStream);
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = "myAwesomeFile.xlsx" };

Now here's the trick ;)

I am storing the base URL in a text file that I read into a variable in an Angular Value called "apiRoot". I do this by declaring it and then setting it on the "run" function of the Module, like so:

app.value('apiRoot', { url: '' });
app.run(function ($http, apiRoot) {
    $http.get('/api.txt').success(function (data) {
        apiRoot.url = data;
    });
});

That way I can set the URL in a text file on the server and not worry about "blowing it away" in an upload. (You can always change it later for security reasons - but this takes the frustration out of development ;) )

And NOW the magic:

All I'm doing is creating a link with a URL that directly hits my service endpoint and target's a "_blank".

<a ng-href="{{vm.getFileHref(FileId)}}" target="_blank" class="btn btn-default">&nbsp;Excel File</a>

the secret sauce is the function that sets the href. You ready for this?

vm.getFileHref = function (Id) {
    return apiRoot.url + "/datafiles/excel/" + Id;
}

Yep, that's it. ;)

Even in a situation where you are iterating over many records that have files to download, you simply feed the Id to the function and the function generates the url to the service endpoint that delivers the file.

Hope this helps!

MarmiK
  • 5,639
  • 6
  • 40
  • 49
user1628627
  • 447
  • 1
  • 6
  • 8