Question

I have the following situation:

I have set up a series of Cron jobs on an internal company server to run various PHP scripts designed to check data integrity. Each PHP script queries a company database, formats the returned query data into an HTML file containing one or more <tables>, and then mails the HTML file to several client emails as an attachment. From my experience, most of the PHP scripts generate HTML files with only a few tables, however there are a few PHP scripts the create HTML files with around 30 tables. HTML files have been chosen as the distribution format of these scans because HTML makes it easy to view many tables at once in a browser window.

I would like to add the functionality for the clients to download a table in the HTML file as a CSV file. I anticipate clients using this feature when they suspect a data integrity issue based on the table data. It would be ideal for them to be able to take the table in question, export the data out to a CSV file, and then study it further.

Because need for exporting the data to CSV format is at the discretion of the client, unpredictable as to what table will be under scrutiny, and intermittently used I do not want to create CSV files for every table.

Normally creating a CSV file wouldn't be too difficult, using JavaScript/jQuery to preform DOM traversal and generate the CSV file data into a string utilizing a server call or flash library to facilitate the download process; but I have one limiting constraint: The HTML file needs to be "portable."

I would like the clients to be able to take their HTML file and preform analysis of the data outside the company intranet. Also it is likely these HTML files will be archived, so making the export functionality "self contained" in the HTML files is a highly desirable feature for the two previous reasons.

The "portable" constraint of CSV file generation from a HTML file means:

  1. I cannot make a server call. This means ALL the file generation must be done client-side.

  2. I want the single HTML file attached to the email to contain all the resources to generate the CSV file. This means I cannot use jQuery or flash libraries to generate the file.

I understand, for obvious security reasons, that writing out files to disk using JavaScript isn't supported by any browser. I don't want to create a file without the user knowledge; I would like to generate the file using JavaScript in memory and then prompt the user the "download" the file from memory.

I have looked into generating the CSV file as a URI however, according to my research and testing, this approach has a few problems:

  • URIs for files are not supported by IE (See Here)

  • URIs in FireFox saves the file with a random file name and as a .part file

As much as it pains me, I can accept the fact the IE<=v9 won't create a URI for files. I would like to create a semi-cross-browser solution in which Chrome, Firefox, and Safari create a URI to download the CSV file after JavaScript DOM traversal compiles the data.

My Example Table:

<table>
    <thead class="resulttitle">
        <tr>
            <th style="text-align:center;" colspan="3">
            NameOfTheTable</th>
        </tr>
    </thead>
    <tbody>
        <tr class="resultheader">
            <td>VEN_PK</td>
            <td>VEN_CompanyName</td>
            <td>VEN_Order</td>
        </tr>
        <tr>
            <td class='resultfield'>1</td>
            <td class='resultfield'>Brander Ranch</td>
            <td class='resultfield'>Beef</td>
        </tr>
        <tr>
            <td class='resultfield'>2</td>
            <td class='resultfield'>Super Tree Produce</td>
            <td class='resultfield'>Apples</td>
        </tr>
        <tr>
            <td class='resultfield'>3</td>
            <td class='resultfield'>John's Distilery</td>
            <td class='resultfield'>Beer</td>
        </tr>
    </tbody>
    <tfoot>
        <tr>
          <td colspan="3" style="text-align:right;">
          <button onclick="doSomething(this);">Export to CSV File</button></td>
        </tr>
    </tfoot>
</table>

My Example JavaScript:

<script type="text/javascript">
  function doSomething(inButton) {

    /* locate elements */
    var table = inButton.parentNode.parentNode.parentNode.parentNode;
    var name  = table.rows[0].cells[0].textContent;
    var tbody = table.tBodies[0];

    /* create CSV String through DOM traversal */
    var rows  = tbody.rows;
    var csvStr = "";
    for (var i=0; i < rows.length; i++) {
      for (var j=0; j < rows[i].cells.length; j++) {
        csvStr += rows[i].cells[j].textContent +",";
      }
      csvStr += "\n";
    }

    /* temporary proof DOM traversal was successful */
    alert("Table Name:\t" + name + "\nCSV String:\n" + csvStr);

    /* Create URI Here!
     * (code I am missing)
     */

    /* Approach #1 : Auto-Download From Browser
     * Attempts to redirect the browser to the URI
     * and have the browser download the data as a file
     *
     * Approach does downloads CSV data but:
     * In FireFox downloads as randomCharacers.part instead of name.csv
     * In Chrome downloads without prompting the user and without correct file name
     * In Safari opens the files in browser (textfile)
     */
    /* Approach #1 Code:
       var hrefData = "data:text/csv;charset=US-ASCII," + encodeURIComponent(csvStr);
       document.location.href = hrefData;
     */

    /* Approach #2 : Right-Click Save As Link...
     * Attempts to remove "Download Button"
     * and replace the it with an anchor tag <a>
     * that the user can use to download the data
     *
     * Approach sort of works better:
     * When clicking on the link:
     * In FireFox downloads as randomCharacers.part instead of name.csv
     * In Chrome downloads without prompting the user (uses correct name) 
     * In Safari opens the files in browser (textfile)
     * 
     * When right-click "Save As" on the link:
     * In FireFox prompts the user with filename: randomCharacers.part 
     * In Chrome  prompts the user with filename: "download"
     * In Safari  prompts the user with filename: "Unknown"
     */
    /* Approach #2 Code:
       var hrefData = "data:text/csv;charset=US-ASCII," + encodeURIComponent(csvStr);
       var fileLink = document.createElement("a");
       fileLink.href = hrefData;
       fileLink.type  = "text/csv";
       fileLink.innerHTML = "CSV Download Link";
       fileLink.setAttribute("download",name+".csv"); // for Chrome
       parentTD = inButton.parentNode;
       parentTD.appendChild(fileLink);
       parentTD.removeChild(inButton);
     */
  }
</script>

I am looking for an example solution in which the above example table can be downloaded as a CSV file:

  • using a URI
  • using a <button> or a <a>
  • code works as described in modern versions of FireFox, Safari, & Chrome
  • AND ( 1. OR 2. ):
      • the user is prompted to save the file
      • user does not need to "Right Click Save As"
      • automatically saves the file to default browser save directory
      • the default filename is the name of the table with the .csv file extension

I have added a <script> tag with the DOM traversal function doSomething(). The real help I need is with formatting the URI to what I want within the doSomething() function. Approach #2 (see my code) seem most promising.

I am willing to accept that my desired functionality is impossible to achieve using only HTML & JavaScript if the statement is presented with proof; such as browser documentation, DOM standards, JavaScript/ECMAscript documentation, or a logical counter example demonstrating the impossibility.

That being said, I think a solution is possible from someone with a deeper background/more experience with URIs, even if the solution is a little bit of a hack.

Was it helpful?

Solution

As long as you don't mind only supporting IE10+ when it comes to IE, use FileSaver.js. You can generate the CSV and append it to a Blob, and then saveAs() the blob. You will only be able to preserve the filename of the saved file in browsers that support <a download>, like Chrome.

OTHER TIPS

You state in the comments that you "shouldn't NEED to create separate HTML and CSV files," but in this case your fundamental need is to provide data in two formats - originating two copies of the data is not an unreasonable constraint. To repeat, what you're asking for is not feasible.

The closest solution I can imagine to your problem would be to package the CSV data in the HTML file (via a display: none div or similar) and populating it to HTML tables dynamically using JavaScript at loadtime. This is going to be slow if you have large recordsets, but it'd be simple to add a button to show and select the CSV text for copy-paste. Inelegant? Yeah, sorry. There's a reason why HTML isn't an interchange format.

That being said, you could package your tabular data in a PDF and include the CSVs inside it as attachments.

I want the single HTML file attached to the email to contain all the resources to generate the CSV file. This means I cannot use jQuery

This is a false statement. If you want to use the jQuery functionality in a single HTML file with no server call, simply copy/paste the minified jQuery library into the HTML itself. It will make each HTML file 32K bigger, which is not terrible, unless you're emailing hundreds at a time.

So if you're familiar with jQuery and are comfortable using it to refactor the data on the page, feel free to use it. I'd suggest your auto-created HTML have the data in a javascript array, and upon page load it creates the table(s). Then if the user clicks a CSV option, it can clear and refactor it.

var data = {
  title: 'NameOfTable',
  headers: ['VEN_PK', 'VEN_CompanyName', 'VEN_Order'],
  data: [
    ['1', 'Brander Ranch' 'Beef'],
    ['2', 'Super Tree Produce' 'Apples'],
    ['3', 'John\'s Distilery' 'Beer']
  ] 
}
$(document).ready(function() {
  var html = '<table>';
  html += '<thead>';
  html += '<tr><th style="text-align:center;" colspan="' + data.headers.length +'">' + data.title + '</th></tr>';
  html += '</thead><tbody>';
  html += '<tr class="resultheader">';
  for (i in data.headers) {
    html += '<td>'+data.headers[i]+'</td>';
  }
  html += '</tr>';
  for (i in data.data) {
    var row = data.data[i];
    html += '<tr>';
    for (j in row) {
      html += '<td class="resultfield">'+row[j]+'</td>';
    }
    html += '</tr>';
  }
  html += '</table>';
  $('#container').html(html);
});

function showCSV() {
   var csv = '';
   for (i in data.headers) {
     csv += data.headers[i] + ',';
   }
   csv += "\n";
   for (i in data.data) {
     var row = data.data[i];
     for (j in row) {
       csv += row[j] + ',';
     }
     csv += "\n";
   }
   $('#container').html(csv);
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top