react-tabulator icon indicating copy to clipboard operation
react-tabulator copied to clipboard

Downloading doesn't work without callbacks

Open FabriceReynolds opened this issue 6 years ago • 7 comments

Title Downloading doesn't work out of the box.

Short Description: You have to add dummy downloadDataFormatter and downloadReady callbacks to the options to make it work. I don't know if this is by design or not. If you do not add these callbacks then when tabulator-tables calls these functions react-tabulator calls a NOOPS dummy function that does nothing and therefore causes errors.

Environment Details

  • OS: Linux Debian 8
  • Node.js version: 9.11.2
  • npm version: 5.6.0
  • react version: 16.5.2
  • tabulator-tables version: 4.2.3 (included with react-tabulator)
  • react-tabulator version: 0.9.1

Long Description I'm trying to download a csv of the table data which uses grouped columns. My first attempt was to simply create a reference to the ReactTabulator component and call the following function as per tabulator's instructions:

this.tableRef.current.table.download("csv", "data.csv")

This causes the following error:

TypeError: Cannot read property 'forEach' of undefined parseRows

node_modules/react-tabulator/node_modules/tabulator-tables/dist/js/tabulator.js:9608 9605 | 9606 | function parseRows(data) { 9607 | //generate each row of the table 9608 | data.forEach(function (row) { | ^ 9609 | var rowData = []; 9610 | fields.forEach(function (field) { 9611 | var value = self.getFieldValue(field, row);

What is happening is that the data is undefined as it is being overwritten by the following code:

Download.download node_modules/react-tabulator/node_modules/tabulator-tables/dist/js/tabulator.js:9336 9333 | this.processColumns(); 9334 | 9335 | if (downloadFunc) { 9336 | downloadFunc.call(this, self.processDefinitions(), self.processData(), options || {}, buildLink, this.config); | ^ 9337 | } 9338 | };

As we have not set a downloadDataFormatter option which would be stored in downloadFunc, react-tabulator provides a NOOPS function that returns undefined.

var NOOPS = function NOOPS() {}; // get callbacks from props (default: NOOP) to set them to Tabulator options later.

Ok so we can fix this by implementing a downloadDataFormatter as shown in the workaround and then this error goes away. However we then have a new issues and the download fails silently. This is now due to tabulator-tables calling a downloadReady callback which also does not exist and therefore calls the NOOPs dummy function that returns undefined. The downloadReady expects to either return a blob which then triggers the actual download or if undefined then stops further processing. see docs

9514 | Download.prototype.triggerDownload = function (data, mime, type, filename, newTab) { 9515 | var element = document.createElement('a'), 9516 | blob = new Blob([data], { 9517 | type: mime 9518 | }), 9519 | filename = filename || "Tabulator." + (typeof type === "function" ? "txt" : type); 9520 | blob = this.table.options.downloadReady.call(this.table, data, blob); 9521 | ^ fails here

Workaround To make the download work correctly you simply need to provide the two missing callbacks in the tabulator options:

<ReactTabulator 
  ref={this.tableRef} 
  columns={columns} 
  data={items} 
  options={options}
/>

...

options  = {
    downloadDataFormatter: (data) => data,
    downloadReady: (fileContents, blob) => blob,
}

Possible solutions would be for react-tabulator to provide these required options instead of calling a NOOPS function which would therefore work the same was as the tabulator docs or provide information in the react-tabulator docs to state that these callbacks are necessary.

I hope this is useful to someone :)

FabriceReynolds avatar Apr 27 '19 13:04 FabriceReynolds

Thank you so much @FabriceReynolds this was making me crazy!

NikkiMeganMoore avatar Jun 20 '19 21:06 NikkiMeganMoore

I am also having troubles downloading table data to a file.

Below is my .js file. I've added the dummy downloadDataFormatter and downloadReady callbacks to the options. I have an onClick to call a downloadData function but I receive:

Uncaught TypeError: _this.download is not a function

As per the Tabulator documentation I added the following to my index.html but did not help:

<script type="text/javascript" src="https://oss.sheetjs.com/sheetjs/xlsx.full.min.js"></script>

I also tried installing:

$ npm install xlsx

and added to my .js:

import * as XLSX from 'xlsx';

with no luck either. Honestly I'm not even sure this is the correct node module I would need. I just took a stab at it based on https://oss.sheetjs.com/sheetjs/xlsx.full.min.js

Any help is appreciated. Thank you.

import React from 'react';
import {Row, Col, Card} from 'react-bootstrap';
import Aux from "../../hoc/_Aux";
import 'react-tabulator/lib/styles.css';
import { ReactTabulator } from 'react-tabulator';
import 'react-tabulator/lib/css/tabulator.min.css'; // theme

const columns = [
   { title: "Area", field: "Area", align: "center" }, 
   { title: "Cash Flow (Mo)", field: "Cash Flow (Mo)", align: "right" },
   { title: "ROI", field: "ROI", align: "right" },
   { title: "Total Rent", field: "Total Rent", align: "right" },
   { title: "Downpayment", field: "Downpayment", align: "right" },
   { title: "Loan Balance", field: "Loan Balance", align: "right" },
   { title: "P&I", field: "P&I", align: "right" }, 
   { title: "PITI", field: "PITI", align: "right" },
   { title: "Taxes (Mo)", field: "Taxes (Mo)", align: "right" },
   { title: "Ins (Mo)", field: "Ins (Mo)", align: "right" }
];

class TabulatorTable extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            error: null,
            isLoaded: false,
            items: [],
        errorMsg: ""
        };
    }
 
    componentDidMount() {
        this.fetchData();
    }

    fetchData = () => {
    fetch("http://127.0.0.1:5000/properties?apikey=abc123xyz&rate=" + document.getElementById('rate').value + "&downpayment=" + document.getElementById('downpayment').value + "&insurance=" + document.getElementById('insurance').value + "&closingcosts=" + document.getElementById('closing-costs').value + "&managementfee=" + document.getElementById('management-fee').value )      
    .then(res => res.json())
      .then(
        (result) => {
          this.setState({
            isLoaded: true,
            items: result.properties
          });
        },
        (error) => {
          this.setState({
            isLoaded: true,
            error
          });
        }
      )
    }

    downloadData = () => {
      this.download("csv", "data.csv");
    };

    render() {
        const { items, error } = this.state;
        const options = {
            pagination: 'local', 
            paginationSize:20, 
            paginationSizeSelector:[20, 50, 100],
            layout:'fitDataFill',
            movableColumns: true,
            selectable:true,
            downloadDataFormatter: (data) => data,
            downloadReady: (fileContents, blob) => blob
        };
        return (
            <Aux>
                <Row>
                    <Col>
                        <Card>
                            <Card.Header>
                                <Card.Title as="h5">Available Properties</Card.Title>
                            </Card.Header>
                            <Card.Body>
                            <button onClick={this.downloadData}>Download</button> 
                                <ReactTabulator 
                                  data={items}
                                  columns={columns}
                                  tooltips={true}
                                  layout={"fitData"}
                                  options={options}
                                />  
                            </Card.Body>
                        </Card>
                    </Col>
                </Row>
            </Aux>
        );
    }
}
export default TabulatorTable;

psaban20 avatar Feb 14 '20 18:02 psaban20

Thanks a lot @FabriceReynolds for the workaround ! Saved my day !

jmazier-j2d avatar Jun 24 '20 10:06 jmazier-j2d

@psaban20 I just managed to get this to work with the button you've got by adding a reference to the table, the reason you're getting this.download doesn't exist is that "this" was referring to the instance of the button in the jsx..

Add the ref below along with the external js file and you should be good to go. p.s. I only figured it out from using you code, which helped me get unstuck, so thank you !.

class TabulatorTable extends React.Component {
     ref = null;
....
    downloadData = () => {
        this.ref.table.download("xlsx", "data.xlsx");
    }; 

.....
 return (
            <div>
            <button type="button" onClick={this.downloadData} className="btn btn-success">Download XLSX</button>
            <ReactTabulator
                ref={ref => (this.ref = ref)}
                data={data}
                columns={columns}
                ........
            />
            </div>
        );

pjaol avatar Jul 07 '20 19:07 pjaol

WHAT A LIFE SAVIOR. THANK YOU! @FabriceReynolds ❤

doraemonxxx avatar Jan 06 '22 19:01 doraemonxxx

Thanks @FabriceReynolds I added your workaround here: https://github.com/ngduc/react-tabulator/blob/master/docs/examples.md I'll investigate and find a proper solution.

ngduc avatar Jan 21 '22 07:01 ngduc