iddan / react-spreadsheet

Simple, customizable yet performant spreadsheet for React
https://iddan.github.io/react-spreadsheet
MIT License
1.33k stars 155 forks source link

Custom DataViewer not updating on `useState` set function #84

Closed mgodf closed 4 years ago

mgodf commented 4 years ago

First of all, thanks for maintaining such a nice library.

I'm trying to react-spreadsheet with a custom DataViewer that will show a transformed version of the data entered into the cell. The problem is that the DataViewer does not update until it is selected again.

I tried to boil it down into a simplified version that shows my issue (this one just appends a ! after the text) but I'd be happy to share any details.

(ps. I understand that capturing a global ref to spreadsheet is probably not the right way to handle this. I'd be very grateful for a proper example!)

1.) Start

2.) Press a

3.) Press enter (Expect to see updated cell view)

4.) Press up-arrow to re-select cell (oh there, it is!)


// Custom Cell Viewer
const CellView = (c,r) => {
    const col = c; const row = r;
    return ({getValue, cell}) => 
    (   // Shows alternate 'display_value'
        <div> { cell.display_value } </div>
    )
  };

// Initialize empty data with custom cell viewer
const getInitialData = () =>{
  const initialData = [];
  const num_cols = 2;
  const num_rows = 2;

  for(let r=0; r<num_rows; r+=1) {
    var row = [];
    for(let c=0; c<num_cols; c+=1) {
      row.push ({ value: "" , DataViewer: CellView(c,r), display_value: "" });
    } 
    initialData.push(row);
  }
  return initialData;
};

//App with data bound by 'useState' hook
const simpleAppView = () =>{
    const [data, setData] = useState(getInitialData());

    return(<Spreadsheet 
        data={data} 
        ref={(spreadsheet) => {window.app_spreadsheet = spreadsheet}}
        onCellCommit={(prevCell, nextCell, coords) => { 
            nextCell.display_value = nextCell.value + "!";
            // Has same result with,
            setData(app_spreadsheet.prevState.data);
            // or,
            setData(data);
        }}
    />);
}
mgodf commented 4 years ago

Actually I was able to get the desired effect by passing an implementation of the IFormulaParser interface into Spreadsheet.

The problem is, that I am still unable to get a cell to update when it contains an expression that is dependent on another cell value. I can update the data property underneath, but it still does not update the displayed value until it is selected again.

// Custom Formula Parser
const getFormulaParser = () =>{
  const fp = {};
  fp.parse = (exp) => { return exp + "!!!"; }
  fp.on = (sig,cb) => {}
  return fp;
}

// Custom Cell Viewer
const CellView = () => {
    return ({getValue, cell, column, row, formulaParser}) => 
        (  <div> { formulaParser.parse(cell.value) +" ("+ column + ", " + row + ")!!"} </div> )
};

// Initialize empty data with custom cell viewer
const getInitialData = () =>{
  const initialData = [];
  const num_cols = 2;
  const num_rows = 2;

  for(let r=0; r<num_rows; r+=1) {
    var row = [];
    for(let c=0; c<num_cols; c+=1) {
      row.push ({ value: "" , DataViewer: CellView()});
    } 
    initialData.push(row);
  }
  return initialData;
};

//App with data bound by 'useState' hook
const simpleAppView = () =>{
    const [data, setData] = useState(getInitialData());
    return(<Spreadsheet data={data} formulaParser={getFormulaParser()} 
    onCellCommit={(prevCell, nextCell, coords) => {

        // Do some update with data and nextCell.value
        // ....

        setData(data);
    />);
}
iddan commented 4 years ago

Hey @mgodf, thank you for the detailed bug report. I'll give it a look this weekend and update here.

iddan commented 4 years ago

Hey @mgodf, I looked into your code and I found that once written correctly it actually works:

The fixes I made:

import React, { useState } from "react";
import Spreadsheet from "react-spreadsheet";

const COLUMNS = 2;
const ROWS = 2;

const DataViewer = ({ getValue, cell, row, column }) => {
  const value = getValue({ data: cell, row, column });
  // Shows alternate value
  return value && `${value}!`;
};

// Custom Cell Viewer
const getInitialData = () => {
  const initialData = [];

  for (let r = 0; r < ROWS; r += 1) {
    const row = [];
    for (let c = 0; c < COLUMNS; c += 1) {
      row.push({ value: "", DataViewer });
    }
    initialData.push(row);
  }
  return initialData;
};

// App with data bound by 'useState' hook
const SimpleAppView = () => {
  const [data, setData] = useState(getInitialData());

  return (
    <Spreadsheet
      // Provide data to spreadsheet
      data={data}
      // Update data state when spreadsheet changes
      onChange={setData}
    />
  );
};
iddan commented 4 years ago

Let me if that works for you if you have further questions you are welcome to ask here. If you encounter a new problem please open a new issue.

iddan commented 4 years ago

In case you have issues with dependant value try to use the getComputedValue utility:

import React, { useState } from "react";
import Spreadsheet from "react-spreadsheet";
import { getComputedValue } from "react-spreadsheet/dist/util"

const COLUMNS = 2;
const ROWS = 2;

const DataViewer = ({ getValue, cell, row, column, formulaParser }) => {
  const value = getComputedValue({ data: cell, row, column, formulaParser });
  // Shows alternate value
  return value && `${value}!`;
};

// Custom Cell Viewer
const getInitialData = () => {
  const initialData = [];

  for (let r = 0; r < ROWS; r += 1) {
    const row = [];
    for (let c = 0; c < COLUMNS; c += 1) {
      row.push({ value: "", DataViewer });
    }
    initialData.push(row);
  }
  return initialData;
};

// App with data bound by 'useState' hook
const SimpleAppView = () => {
  const [data, setData] = useState(getInitialData());

  return (
    <Spreadsheet
      // Provide data to spreadsheet
      data={data}
      // Update data state when spreadsheet changes
      onChange={setData}
    />
  );
};

export default createFixture({
  component: Test,
});