Search code examples
reactjsreactjs-flux

React - require some aid with saving data to a table


I have a table with dynamically generated column headers and rows, a user can enter in data to the rows and when they click save the data should be saved into that row but at the moment it isn't saving the values to the table but it is saving them on my database. Ideally I would like it so that when the save button is clicked the data will be saved to the row and is then viewable in that row (if that makes any sense).

Here is the code I am working with (I know it is a mess at the moment!):

Code for data input form:

import React from 'react';
import AppStore from '../../stores/AppStore';

export default class RowForm extends React.Component {
    state = {dataEntries: []};

    onChange = (event, element) => {
        let dataEntries = this.state.dataEntries;
        dataEntries[element] = event.target.value;
        this.setState({dataEntries});
    };

    editStop = () => {
        this.props.editStop();
    };

    handleSubmit = (e) => {
        e.preventDefault();

        let access_token = AppStore.getToken();
        let id = AppStore.getTable().id;
        let dataEntries = this.state.dataEntries;
        let dataEntriesArray = [];
        for (let key in dataEntries) {
            if (dataEntries.hasOwnProperty(key)) {
                dataEntriesArray.push({contents: dataEntries[key]});
            }
        }

        this.props.handleSubmit(access_token, id, dataEntriesArray);

    };

    componentDidMount() {
        let nameArray = AppStore.getTable().columns.map((obj) => {
            return obj.name;
        });

        let dataEntries = nameArray.reduce((obj, name) => {
            obj[name] = null;
            return obj;
        }, {});
        this.setState({dataEntries});
    }

    render() {

        let {dataEntries} = this.state;

        return (
            <tr>
                {Object.keys(dataEntries).map((element) => {
                    return (
                        <td key={element}><input type="text" className="form-control" id={element} placeholder="enter data" value={dataEntries[element]} onChange={event => this.onChange(event, element)} /></td>
                    );
                })}
                <td>
                    <button className="btn btn-default" onClick={this.editStop}><i className="fa fa-ban"></i>Cancel</button>
                    <button className="btn btn-success" onClick={this.handleSubmit}><i className="fa fa-check"></i>Save</button>
                </td>
            </tr>
        );
    }

After the data has been entered and submitted (it is an array of objects like dataEntriesArray = [{contents: "value"}, {contents: "value"}, {contents: "value"}, {contents: "value"}].

And here is how I am rendering the table (this is where the problem is I think):

import React from 'react';
import TableHeader from './TableHeader.jsx';
import RowForm from './RowForm.jsx';
import {createRow} from '../../actions/DALIActions';
import AppStore from '../../stores/AppStore';

export default class Table extends React.Component {
    state = {rows: [], isNew: false, isEditing: false};

    handleAddRowClickEvent = () => {
        let rows = this.state.rows;
        rows.push({isNew: true});
        this.setState({rows: rows, isEditing: false});
    };

    handleEdit = (row) => {
        this.setState({isEditing: true});
    };

    editStop = () => {
        this.setState({isEditing: false});
    };

    handleSubmit = (access_token, id, dataEntriesArray) => {
        createRow(access_token, id, dataEntriesArray);
    };

    render() {

        let {rows, isNew, isEditing} = this.state;

        let headerArray = AppStore.getTable().columns;

        return (
            <div>
                <div className="row" id="table-row">
                    <table className="table table-striped">
                        <thead>
                            <TableHeader />
                        </thead>
                        <tbody>
                            {rows.map((row, index) => this.state.isEditing ?
                                <RowForm formKey={index} key={index} editStop={this.editStop} handleSubmit={this.handleSubmit} /> :
                                <tr key={index}>
                                    {headerArray.map((element, index) => {
                                        return (
                                            <td key={index} id={element.id}></td>
                                        );
                                    })}
                                    <td>
                                        <button className="btn btn-primary" onClick={this.handleEdit.bind(this, row)}><i className="fa fa-pencil"></i>Edit</button>
                                    </td>
                                </tr>)}
                        </tbody>
                    </table>
                </div>
                <div className="row">
                    <div className="col-xs-12 de-button">
                        <button type="button" className="btn btn-success" onClick={this.handleAddRowClickEvent}>Add Row</button>
                    </div>
                </div>
            </div>
        );
    }
}

I am using flux at the moment, and would ideally like to keep using it for now (I know about redux but I would ideally like to get it working in flux before I start refactoring my code). I suspect it is a problem with the way I am rendering my table.

Any help would be much appreciated, especially examples!

Thanks for you time!


Solution

  • It looks like you probably want to extract your table data into your store, your UI child elements trigger change events, your store then updates its data and trigger change events which your parent component can listen for and update.

    Something like the following simplified example, which mutates array elements:

    class Store extends EventEmitter {
      constructor() {
        super()
    
        this.data = [ 'a', 'b', 'c' ]
      }
    
      onChange() {
        this.emit( 'update', this.data )
      }
    
      mutate( index, value ) {
        this.data[ index ] = value
        this.onChange()
      }
    }
    
    var store = new Store()
    
    class ChildComponent extends React.Component {
      constructor( props ) {
        super( props )
      }
    
      // You probably want to use a dispatcher rather than directly accessing the store
      onClick = event => {
        store.mutate( this.props.index, this.props.value + 'Z' )
      }
    
      render() {
        return <button onClick={ this.onClick }>{ this.props.value }</button>
      }
    }
    
    class ParentComponent extends React.Component {
      constructor( props ) {
        super( props )
    
        // You probably want to be smarter about initially populating state
        this.state = {
          data: store.data
        }
      }
    
      componentWillMount() {
        store.on( 'update', data => this.setState({ data: data }) )
      }
    
      render() {
        let cells = this.state.data.map( ( value, index ) => <ChildComponent index={ index } value={ value } /> )
    
        return (
          <div>
            { cells }
          </div>
        )
      }
    }
    

    For brevity child components here directly tell the store to change values, you'd probably want to dispatch messages/actions and have the store decide how to respond, the key is simply that the store data is passed to the parent component, which updates its state and triggers a re-render.

    The flow here is that essentially the UI is dumb, it simply renders the data that it gathers from the store and dispatches a message to tell the store to update/mutate when a user action is detected (in this case a button press but it sound like you'll need an input of some sort), when the data in the store changes it emits (or could use a dispatcher also) a change event which forces the UI to re-render the new state. As child components are re-rendered at this stage they are populated with the new data state, ensuring that your UI remains consistent.