mbrn / material-table

Datatable for React based on material-ui's table with additional features
https://material-table.com
MIT License
3.5k stars 1.03k forks source link

Displaying Error State for Editable TextFields #602

Closed HanaaAbbas closed 5 years ago

HanaaAbbas commented 5 years ago

Hi,

Thank you very much for the well-developed component and for your support! Great work!

I am currently making use of the table's editable feature.

When the user edits a cell and clicks on "check" icon button, in onRowUpdate, some validations checks are applied and if the validations fail, all I could do is revert newData with oldData.

However, what I ideally want is to show the user that an error has occurred if the validations have failed. Option 1: The MaterialUI's TextField error's prop to be set to {true} which will render it as error state (i.e. becomes underlined in red) and not to allow the row update. Option 2: Revert the data but display an error message somewhere below the footer of the table for example.

I personally prefer Option 1.

Here is what I have tried to do so far but without avail.

          <EditableTable
            enableSearch={false}
            showToolbar={true}
            showTitle={false}
            maxBodyHeight={250}
            title="Coordinates"
            enableRowDelete={true}
            enableRowAdd={true}
            columns={[
              {
                title: 'Count',
                field: 'counter',
                editable: 'onAdd',
                sorting: true,
                type: 'numeric',
                cellStyle: materialTableStyle.centerAlignedCellStyle
              }, {
                title: 'Coordinate',
                field: 'coordinate',
                sorting: false
              }
            ]}
            validationError={this.state.validationError}
            data={this.state.rows}
            editable={{
              onRowAdd: newData => new Promise((resolve, reject) => {
                setTimeout(() => {
                  {
                    this.handleRowAdd(newData, resolve);
                  }
                  resolve()
                }, 1000)
              }),
              onRowUpdate: (newData, oldData) => new Promise((resolve, reject) => {
                setTimeout(() => {
                  {
                    const data = this.state.rows;
                    const index = data.indexOf(oldData);

                    let isValidated = this.validateCoordinates(newData.coordinate);

                    if (!isValidated) {
                      this.setState({validationError: true});
                      return;
                    } else {
                      data[index] = newData;
                      this.setState({
                        data, validationError: false
                      }, () => resolve());
                    }
                  }
                   resolve()
                }, 1000)
              }),
              onRowDelete: oldData => new Promise((resolve, reject) => {
                setTimeout(() => {
                  {
                    this.handleRowDelete(oldData, resolve);
                  }
                  resolve()
                }, 1000)
              })
            }}/>
import React, {Component} from "react";
import {defineMessages, injectIntl} from 'react-intl';
import PropTypes from 'prop-types';
// Material Table imports
import MaterialTable, { MTableToolbar } from 'material-table';
// Material UI
import Paper from '@material-ui/core/Paper';
import TextField from '@material-ui/core/TextField';
// Material UI icons
import AddIcon from '@material-ui/icons/AddBox';
import DeleteIcon from '@material-ui/icons/Delete';
import EditIcon from '@material-ui/icons/Edit';
import CancelIcon from '@material-ui/icons/Save';
import CheckIcon from '@material-ui/icons/Check';
import SearchIcon from '@material-ui/icons/Search';
import ClearIcon from '@material-ui/icons/Clear';

import materialTableStyle, {toolbarStyle} from './EditableTableJSS';
import { withStyles } from '@material-ui/core/styles';

class EditableTable extends Component {

  constructor(props) {
    super(props);
  }

  render() {
    const props = this.props;
    let intl = this.props.intl;
    return(
      <MaterialTable
        title={props.title}
        options={{
          search: props.enableSearch,
          actionsColumnIndex: -1, // aligning table actions on the right-hand side
          toolbar: props.showToolbar,
          showTitle: props.showTitle,
          title: props.title,
          toolbarButtonAlignment: props.toolbarButtonAlignment ? props.toolbarButtonAlignment : 'right',
          doubleHorizontalScroll: false,
          paging: false,
          sorting: true,
          headerStyle: materialTableStyle.headerStyle,
          addRowPosition: 'last',
          maxBodyHeight: props.maxBodyHeight
        }}
        components={{
          // providing class names for the components in order to be able to override their default Material UI CSS
          Container: props => (<Paper {...props} className="material-table-paper" />),
          Toolbar: props => (<StyledMTableToolbar {...props}/>),
          // overriding EditField
          EditField: efProps => {
            return(
            <TextField
              style={efProps.columnDef.type === 'numeric' ? { float: 'right' } : {}}
              type={efProps.columnDef.type === 'numeric' ? 'number' : 'text'}
              placeholder={efProps.columnDef.title}
              value={efProps.value === undefined ? '' : efProps.value}
              onChange={event => efProps.onChange(event.target.value)}
              error={props.validationError}
            />);
          },
        }}
        icons={{
          Add: props => (<AddIcon {...props} color="primary" className="icon-small" />),
          Edit: props => (<EditIcon {...props} className="icon-small" />),
          Search: props => (<SearchIcon {...props} color="primary" className="icon-small" />),
          Delete: props => (<DeleteIcon {...props} className="icon-small" />),
          Check: props => (<CheckIcon {...props} className="icon-small" />),
          Clear: props => (<ClearIcon {...props} className="icon-small" />),
        }}
        localization={{
          body: {
            editTooltip: intl.formatMessage(messages.editTooltip),
            deleteTooltip: intl.formatMessage(messages.deleteTooltip),
            addTooltip: intl.formatMessage(messages.addTooltip),
            emptyDataSourceMessage: intl.formatMessage(messages.noData),
            editRow: {
              deleteText: intl.formatMessage(messages.deleteText),
              cancelTooltip: intl.formatMessage(messages.cancelTooltip),
              saveTooltip: intl.formatMessage(messages.saveTooltip),
            }
          }
        }}
        columns={props.columns}
        data={props.data}
        editable={props.editable}
      />
    );
  }

}

EditableTable.propTypes = {
 /**
 * If true, search field is displayed. *Required
 */
  enableSearch: PropTypes.bool.isRequired,
  /**
  *Display Title
  */
  showTitle: PropTypes.bool,
  /**
  * Title of the table, displayed in the toolbar
  */
  title: PropTypes.string,
  /**
  *By default, toolbar is visible. Set to false to hide the toolbar
  */
  showToolbar: PropTypes.bool,
  /**
  * Alignment by default is right. Can either be right or left
  */
  toolbarButtonAlignment: PropTypes.string,
  /**
  * Allow row deletion. *Required
  */
  enableRowDelete: PropTypes.bool.isRequired,
  /**
  *Allow new row addition. *Required
  */
  enableRowAdd: PropTypes.bool.isRequired,
  /**
  *Table columns. Array. *Required
  */
  columns: PropTypes.array.isRequired,
  /**
  * Table Data/Rows. *Required
  */
  data: PropTypes.array.isRequired,
  /**
  * Editable Data
  */
  editable: PropTypes.object,
  /**
  *Localization
  */
  localization: PropTypes.object,
  /**
  * Max height at which the table becomes scrollable
  */
  maxBodyHeight: PropTypes.number,
  /**
  * Validation Error for all editable TextFields
  */
  validationError: PropTypes.bool,
};

export default injectIntl(EditableTable);

/**
*@function StyledToolbar
* using JSS to apply a className to a component.
*/
function StyledToolbar(props) {
  return (
    <MTableToolbar {...props} />
  );
}
const StyledMTableToolbar = withStyles(toolbarStyle)(StyledToolbar);

This is the result: image

I can see that the textfield is underlined, however the promise is not resolved and the spinner remains on.

How can I fix this?

Thanks again!

HanaaAbbas commented 5 years ago

I figured out a workaround: In onRowUpdate, I am calling this function:

handleRowEdit = (newData, oldData, resolve, reject) => { const data = this.state.rows; const index = data.indexOf(oldData); const {intl} = this.props; let msg = intl.formatMessage(messages.invalidCoordinate);

let isValidated = this.validateCoordinates(newData.coordinate);
if (!isValidated) {
  newData = oldData;
  data[index] = newData;
  this.setState({ data, validationError: true, validationErrorMsg: msg}, () => {
    reject();
    setTimeout(() => {this.setState({validationError: false, validationErrorMsg: ''})}, 3000);
  });
} else {
  data[index] = newData;
  this.setState({ data, validationError: false, validationErrorMsg: ''}, () => resolve());
}

}

and I updated columns data to use:

{
                title: 'Coordinate',
                field: 'coordinate',
                sorting: false,
                editComponent: props => {
                  return(
                  <TextField
                    type={'text'}
                    placeholder={props.columnDef.title}
                    required={true}
                    autoFocus={true}
                    margin="dense"
                    value={props.value === props ? '' : props.value}
                    onChange={event => {
                      props.onChange(event.target.value)
                    }}
                    error={this.state.validationError}
                  />);
                },
              }

The reason why I am doing this: setTimeout(() => {this.setState({validationError: false, validationErrorMsg: ''})}, 3000);

clearing the error state after 3 seconds because if I cancel the edit, the next time I tried to edit any row, the editable textfield was showing in error state.

Is there any way I can extend the functionality of the edit cancellation button to also change the state of the validation error?

mbrn commented 5 years ago

You can call reject before return when validation fails. So spinner will be disappeared.

d1sd41n commented 5 years ago

Hi,

Thank you very much for the well-developed component and for your support! Great work!

I am currently making use of the table's editable feature.

When the user edits a cell and clicks on "check" icon button, in onRowUpdate, some validations checks are applied and if the validations fail, all I could do is revert newData with oldData.

However, what I ideally want is to show the user that an error has occurred if the validations have failed. Option 1: The MaterialUI's TextField error's prop to be set to {true} which will render it as error state (i.e. becomes underlined in red) and not to allow the row update. Option 2: Revert the data but display an error message somewhere below the footer of the table for example.

I personally prefer Option 1.

Here is what I have tried to do so far but without avail.

      <EditableTable
        enableSearch={false}
        showToolbar={true}
        showTitle={false}
        maxBodyHeight={250}
        title="Coordinates"
        enableRowDelete={true}
        enableRowAdd={true}
        columns={[
          {
            title: 'Count',
            field: 'counter',
            editable: 'onAdd',
            sorting: true,
            type: 'numeric',
            cellStyle: materialTableStyle.centerAlignedCellStyle
          }, {
            title: 'Coordinate',
            field: 'coordinate',
            sorting: false
          }
        ]}
        validationError={this.state.validationError}
        data={this.state.rows}
        editable={{
          onRowAdd: newData => new Promise((resolve, reject) => {
            setTimeout(() => {
              {
                this.handleRowAdd(newData, resolve);
              }
              resolve()
            }, 1000)
          }),
          onRowUpdate: (newData, oldData) => new Promise((resolve, reject) => {
            setTimeout(() => {
              {
                const data = this.state.rows;
                const index = data.indexOf(oldData);

                let isValidated = this.validateCoordinates(newData.coordinate);

                if (!isValidated) {
                  this.setState({validationError: true});
                  return;
                } else {
                  data[index] = newData;
                  this.setState({
                    data, validationError: false
                  }, () => resolve());
                }
              }
               resolve()
            }, 1000)
          }),
          onRowDelete: oldData => new Promise((resolve, reject) => {
            setTimeout(() => {
              {
                this.handleRowDelete(oldData, resolve);
              }
              resolve()
            }, 1000)
          })
        }}/>

import React, {Component} from "react"; import {defineMessages, injectIntl} from 'react-intl'; import PropTypes from 'prop-types'; // Material Table imports import MaterialTable, { MTableToolbar } from 'material-table'; // Material UI import Paper from '@material-ui/core/Paper'; import TextField from '@material-ui/core/TextField'; // Material UI icons import AddIcon from '@material-ui/icons/AddBox'; import DeleteIcon from '@material-ui/icons/Delete'; import EditIcon from '@material-ui/icons/Edit'; import CancelIcon from '@material-ui/icons/Save'; import CheckIcon from '@material-ui/icons/Check'; import SearchIcon from '@material-ui/icons/Search'; import ClearIcon from '@material-ui/icons/Clear';

import materialTableStyle, {toolbarStyle} from './EditableTableJSS'; import { withStyles } from '@material-ui/core/styles';

class EditableTable extends Component {

constructor(props) { super(props); }

render() { const props = this.props; let intl = this.props.intl; return( <MaterialTable title={props.title} options={{ search: props.enableSearch, actionsColumnIndex: -1, // aligning table actions on the right-hand side toolbar: props.showToolbar, showTitle: props.showTitle, title: props.title, toolbarButtonAlignment: props.toolbarButtonAlignment ? props.toolbarButtonAlignment : 'right', doubleHorizontalScroll: false, paging: false, sorting: true, headerStyle: materialTableStyle.headerStyle, addRowPosition: 'last', maxBodyHeight: props.maxBodyHeight }} components={{ // providing class names for the components in order to be able to override their default Material UI CSS Container: props => (<Paper {...props} className="material-table-paper" />), Toolbar: props => (<StyledMTableToolbar {...props}/>), // overriding EditField EditField: efProps => { return( <TextField style={efProps.columnDef.type === 'numeric' ? { float: 'right' } : {}} type={efProps.columnDef.type === 'numeric' ? 'number' : 'text'} placeholder={efProps.columnDef.title} value={efProps.value === undefined ? '' : efProps.value} onChange={event => efProps.onChange(event.target.value)} error={props.validationError} />); }, }} icons={{ Add: props => (<AddIcon {...props} color="primary" className="icon-small" />), Edit: props => (<EditIcon {...props} className="icon-small" />), Search: props => (<SearchIcon {...props} color="primary" className="icon-small" />), Delete: props => (<DeleteIcon {...props} className="icon-small" />), Check: props => (<CheckIcon {...props} className="icon-small" />), Clear: props => (<ClearIcon {...props} className="icon-small" />), }} localization={{ body: { editTooltip: intl.formatMessage(messages.editTooltip), deleteTooltip: intl.formatMessage(messages.deleteTooltip), addTooltip: intl.formatMessage(messages.addTooltip), emptyDataSourceMessage: intl.formatMessage(messages.noData), editRow: { deleteText: intl.formatMessage(messages.deleteText), cancelTooltip: intl.formatMessage(messages.cancelTooltip), saveTooltip: intl.formatMessage(messages.saveTooltip), } } }} columns={props.columns} data={props.data} editable={props.editable} /> ); }

}

EditableTable.propTypes = { /**

  • If true, search field is displayed. Required / enableSearch: PropTypes.bool.isRequired, / Display Title / showTitle: PropTypes.bool, /
  • Title of the table, displayed in the toolbar / title: PropTypes.string, /* By default, toolbar is visible. Set to false to hide the toolbar / showToolbar: PropTypes.bool, /__
  • Alignment by default is right. Can either be right or left / toolbarButtonAlignment: PropTypes.string, /*
  • Allow row deletion. Required / enableRowDelete: PropTypes.bool.isRequired, / *Allow new row addition. Required / enableRowAdd: PropTypes.bool.isRequired, / *Table columns. Array. Required / columns: PropTypes.array.isRequired, /__
  • Table Data/Rows. Required / data: PropTypes.array.isRequired, /__
  • Editable Data / editable: PropTypes.object, /* Localization / localization: PropTypes.object, /__
  • Max height at which the table becomes scrollable / maxBodyHeight: PropTypes.number, /*
  • Validation Error for all editable TextFields */ validationError: PropTypes.bool, };

export default injectIntl(EditableTable);

/* @function StyledToolbar

  • using JSS to apply a className to a component. */ function StyledToolbar(props) { return ( <MTableToolbar {...props} /> ); } const StyledMTableToolbar = withStyles(toolbarStyle)(StyledToolbar);

This is the result: image

I can see that the textfield is underlined, however the promise is not resolved and the spinner remains on.

How can I fix this?

Thanks again!

how did you do yo underlined the text?

vincentp commented 5 years ago

@d1sd41n a bit late but it might help others.

I added a 'required' field to the columns I wanted to be required.

Then, I did override EditField to return the initial components and adding props to it when needed (you can change your logic there). The fields are re-rendered on each change (to update the state). The extra props are passed to the rendered components (TextField, Select etc.)

components={{
            EditField: (props) => {
              if (props.columnDef.required && props.value.length === 0) {
                return (<MTableEditField {...props} error label="Required" />);
              }
              return (<MTableEditField {...props} />);
            },
          }}
chitgoks commented 5 years ago

@vincentp sorry, which library is MTableEditField? how do i import it. i cant find it in the documentation

HanaaAbbas commented 5 years ago

Hi,

You will have to override the EditField component of the table. You can use Material UI's TextField component which accepts error as props.

import MaterialTable from 'material-table';

      <MaterialTable
         validationError={this.state.validationError}
         columns={[
          {
            title: 'Count',
            field: 'counter',
            editable: 'onAdd',
            sorting: true,
            type: 'numeric',
          }, {
            title: 'Coordinate',
            field: 'coordinate',
            sorting: false
          }
        ]}
       components={{
// overriding EditField
EditField: props=> {
return(
<TextField
style={props.columnDef.type === 'numeric' ? { float: 'right' } : {}}
type={props.columnDef.type === 'numeric' ? 'number' : 'text'}
placeholder={props.columnDef.title}
value={props.value === undefined ? '' : props.value}
onChange={event => props.onChange(event.target.value)}
error={props.validationError}
/>);
}
}}

In onRowUpdate props function, you reject the promise if validation fails.

onRowUpdate: (newData, oldData) => new Promise((resolve, reject) => {
            setTimeout(() => {
              {
                const data = this.state.rows;
                const index = data.indexOf(oldData);

                if (!isValidated) {
                  this.setState({validationError: true});
                  return;
                } else {
                  data[index] = newData;
                  this.setState({
                    data, validationError: false
                  }, () => resolve());
                }
              }
               resolve()
            }, 1000)
          }),

Whenever there is a validation error, you will call setState of the component which will re-render it showing the underlined error.

HanaaAbbas commented 5 years ago

The import statement is:

import MaterialTable, { MTableEditField } from 'material-table';

chitgoks commented 5 years ago

@vincentp i wanted to try that MTableEditField since the block of code is short and i figure i only need to add required: true in the column object { }. but it doesnt work. it does not show the field as having validation error. the row add entry just disappears instead of showing them as like red borders

chitgoks commented 5 years ago

@CatLover94 im not sure what else could be wrong. i tried your approach and it never showed any errors and just accepted the entry and showed a circle progress like the one above.

HanaaAbbas commented 5 years ago

What are you trying to do? Do you want to show an error if the field is left empty upon row update or row add or both?

You will have to reject the promise as I've mentioned in my comment above

chitgoks commented 5 years ago

both. add update, if textfeld is empty, border color to red.

i hope the library can add this option where user can just add required: true in the columns and then that simple validation kicks in. maybe adding a small error message below may be too much so the red border color should do.

chitgoks commented 5 years ago

i used @vincentp 's code and it works ok. however , when i click on the add button it already shows red. my workaround was to add a submitted property in the onRowAdd and then if validation checks out, remove it so it wont be included when the newData object is passed on to the backend for processing.

 components={{
          EditField: (props) => {
            if (props.columnDef.required && props.rowData.submitted && props.value === undefined) {
              return (<MTableEditField {...props} error/>);
            }
            return (<MTableEditField {...props} />);
          },
        }}

onRowAdd: newData =>
            new Promise((resolve, reject) => {
              newData.submitted = true;
              if (!newData.fieldname || newData.fieldname === '') 
              {
                return reject();
              }

              delete newData.submitted;
              resolve();
              // add code here.
            }),
chitgoks commented 5 years ago

@CatLover94 i have a question based on your code. while i have one of my columns as numeric, i do not want the float to be right. i wish it to be the same as the default (left). i tried to override cellStyle and headerStyle but the rendering is messed up a little and when in edit mode, the text field is right aligned.

could there be a property that negates this? i cant find one. rather than having to override it.

polnum polnumedit

ToanNguyen-SPCE commented 4 years ago

Hi guys, I got strange issue, please help. in onRowAdd, I add my custom validation in validatForm function, I set some state to show errors bellow textField.

function handleOnAdd(newData) {
    return new Promise((resolve, reject) => {
      console.log('newData', newData);
      if (!validateForm(newData)) {
        reject();
      } else {
        resolve();
        saveData(newData);
      }
    });
  }

It works fine, table re-rend show errors just fine.

But with onUpdateRow, still same validation code but table re-render and make all fields disappear even though I call reject

function handleOnUpdate(newData, oldData) {
    return new Promise((resolve, reject) => {
      console.log('oldData', oldData);
      console.log('newData', newData);
      if (!validateForm(newData)) {
        reject();
      } else {
        resolve();
        saveData(newData);
      }
    });
  }

Do you guys have any idea ?