Adding download link for downloading excel file retrieved from database

roger v
roger v used Ask the Experts™
on
I've got an excel file stored in base64, that is retrieved in a jquery ajax.get call. The base64 data is then converted back into .xlsx file, and attached to a download <a> tag, for the user to click and download the .xlsx file. I get 2 errors: the first error is: jquery.min.js:3 Uncaught TypeError: Cannot read property 'createDocumentFragment' of null.

The 2nd error happens when I click on the download link: Uncaught DOMException: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.
Not sure if 2nd error is related to the 1st one or not. My code is as following:

HTML code:

<a id="downloadlink" onClick="return DownloadExcel()">Download Excel File</a> 

Open in new window


JS code:

//download excel func
function DownloadExcel( file, filename) {
    //debugger;
    var blob = base64toBlob(file, "data:application/vnd.ms-excel;")
    if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
        return navigator.msSaveOrOpenBlob(blob, filename);
    }
    else{
        console.log("inside else of downloadexcel");
      var objectUrl = URL.createObjectURL(blob);
        var downloadLink = $("#downloadlink");
        downloadLink.href = objectUrl; //uri;
        downloadLink.download = filename;
       // document.body.appendChild(downloadLink);
       $(document).append(downloadLink);
    }
  }
  
  function base64toBlob(b64Data, contentType, sliceSize) {
    contentType = contentType || '';
    sliceSize = sliceSize || 512;
  
    var byteCharacters = atob(b64Data);
    var byteArrays = [];
  
    for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
      var slice = byteCharacters.slice(offset, offset + sliceSize);
  
      var byteNumbers = new Array(slice.length);
      for (var i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i);
      }
  
      var byteArray = new Uint8Array(byteNumbers);
  
      byteArrays.push(byteArray);
    }
    var blob = new Blob(byteArrays, {
      type: "application/vnd.ms-excel"
    });
    return blob;
  }
  // /download excel func

  $(document).ready(function(){ 

    $.ajax({
            url: "https://myserver.com/files/file.cfc?fileid=12",
            type: "GET",
            dataType: "json",
            cache: false,
            success: function(response){
                if(response.length != 0){
                    console.log("Successful file retrieval!");
                  
                   DownloadExcel( response[0].filecontent, response[0].filename  );    
                   
                }
                else{
                    console.log("Error in file retrieval!");
                    console.log( response );
                }
            },
            error: function( response ){
                console.log("There was an API server error!");
                console.log( response );
            }
        });
  });

Open in new window

Comment
Watch Question

Do more with

Expert Office
EXPERT OFFICE® is a registered trademark of EXPERTS EXCHANGE®

Author

Commented:
Update: The 2nd error that was being generated is related to the 1st error: I confirmed this by removing the code to attach the event to <a>, and instead did the conversion of the base64 file into .xlsx, and the conversion part works and the .xlsx file is downloaded without error to user computer. Now I need to figure out a way to actually trigger this download upon user clicking a <a href> tag.
Commented:
OK, I think I got it! I changed the way I'm building my <a href> tag. I did it this way and it worked:

 $(document).ready(function(){ 

    $.ajax({
            url: "https://myserver.com/files/file.cfc?fileid=12",
            type: "GET",
            dataType: "json",
            cache: false,
            success: function(response){
                if(response.length != 0){
                    console.log("Successful file retrieval!");
                  $("<a></a>").attr("href", "javascript:void(0)").html($("<span></span>").text(linkText)).appendTo($("#spanattachments"));
                    $("#spanattachments").on('click', 'a', function(event){
                        event.preventDefault();
                        DownloadExcel();
                        return false;
                    });
                   function DownloadExcel() {
                    //window.open(dataURI);
                    var blob = base64toBlob(response[0].filecontent, "data:application/vnd.ms-excel;")
                    if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
                        return navigator.msSaveOrOpenBlob(blob, response[0].filename);
                    }
                    else{
                      var objectUrl = URL.createObjectURL(blob);
                        var downloadLink = document.createElement("a");
                        downloadLink.href = objectUrl; //uri;
                        downloadLink.download = response[0].filename;
                        document.body.appendChild(downloadLink);
                        downloadLink.click();
                        document.body.removeChild(downloadLink);
                    }
                    //window.location.href = objectUrl;//"data:application/vnd.ms-excel;base64" + bindata
                  }
                  
                  function base64toBlob(b64Data, contentType, sliceSize) {
                    contentType = contentType || '';
                    sliceSize = sliceSize || 512;
                  
                    var byteCharacters = atob(b64Data);
                    var byteArrays = [];
                  
                    for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
                      var slice = byteCharacters.slice(offset, offset + sliceSize);
                  
                      var byteNumbers = new Array(slice.length);
                      for (var i = 0; i < slice.length; i++) {
                        byteNumbers[i] = slice.charCodeAt(i);
                      }
                  
                      var byteArray = new Uint8Array(byteNumbers);
                  
                      byteArrays.push(byteArray);
                    }
                    var blob = new Blob(byteArrays, {
                      type: "application/vnd.ms-excel"
                    });
                    return blob;
                  }          
       else{
                    console.log("Error in file retrieval!");
                    console.log( response );
                }
            },
            error: function( response ){
                console.log("There was an API server error!");
                console.log( response );
            }
        });
  });

Open in new window


I'm sure some of the code inside the else statement of DownloadExcel() might be redundant, but it works.
Most Valuable Expert 2017
Distinguished Expert 2018
Commented:
Ok a few things wrong with the script.
First a logic error

In your <a id="downloadlink"></a> you have linked onclick="DownloadExcel()"

You don't want to be doing this. Lets look at why.

In your AJAX (which is invoked at document load) You make an AJAX call to fetch your base64 encoded file - you then call the DownloadExcel function passing the name and content to the function.

The DownloadExcel() function sets up the object reference for the download - which will be invoked when you click the link - no need for a click handler so first step is to fix the link
<a id="downloadlink">Download Excel File</a> 

Open in new window


Next problem is in DownloadExcel() itself you have this
var downloadLink = $("#downloadlink");
downloadLink.href = objectUrl; //uri;

Open in new window

The first line is assigning downloadlink to the jQuery object associated with the element with id downloadlink
On the next line you are accessing the DOM object property href - which is not defined on the jQuery object. jQuery returns a Nodelist (of elements) - to access the native properties you have to index the nodelist to get the element you want - in this case 0 OR you need to use the jQuery equivalent.
So Either
var downloadLink = $("#downloadlink")[0];
downloadLink.href = objectUrl; //uri;

Open in new window

OR
var downloadLink = $("#downloadlink");
downloadLink.attr('href', objectUr)l; //uri;

Open in new window

Either is good.

The next bit is not a bug so much as being superfluous - the code above will now have primed the <a> tag for download so no need to do any appending - this can come out

      $(document).append(downloadLink);

Open in new window


So to summarise

a) Fix your <a> link - remove the onlick
b) Fix or you DownloadExcel - sample below
function DownloadExcel( file, filename) {
  var blob = base64toBlob(file, "data:application/vnd.ms-excel;")
  
  if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
    return navigator.msSaveOrOpenBlob(blob, filename);
  }
  else{
    var objectUrl = URL.createObjectURL(blob);
    var downloadLink = $("#downloadlink")[0];
    downloadLink.href = objectUrl; //uri;
    downloadLink.download = filename;
  }
}

Open in new window

Ensure you’re charging the right price for your IT

Do you wonder if your IT business is truly profitable or if you should raise your prices? Learn how to calculate your overhead burden using our free interactive tool and use it to determine the right price for your IT services. Start calculating Now!

Author

Commented:
Thanks for the great explanation! I've changed the way I'm building the <a href> tag and it's working now. But I'll go through your answer and try that out as well. thanks as always!

roger
Most Valuable Expert 2017
Distinguished Expert 2018

Commented:
I have put a working sample here
Most Valuable Expert 2017
Distinguished Expert 2018

Commented:
Your original approach was much better - the new way is a bit of a hack I would go with what you had and just make those small adjustments.

Why - your AJAX complete has become very "heavy" - the functions that are now local with respect to the complete handler are potentially functions you want to reuse. I always try to make my complete handlers very lightweight and put the supplementary code in a library or module.

Author

Commented:
Roger Wilco!

Do more with

Expert Office
Submit tech questions to Ask the Experts™ at any time to receive solutions, advice, and new ideas from leading industry professionals.

Start 7-Day Free Trial