React Grid - Editing in Detail Row

The detail row is an expandable pane that displays data row details. The detail row does not allow users to edit data out of the box, but you can create a custom plugin to implement this functionality. Before you start, review the following help topics:

The implementation consists of the following steps:

  1. Add a Custom Plugin
  2. Handle the Detail Row Expand and Collapse Events
  3. Handle Data Edits
  4. Add the Edit Form
  5. Replace the Expand Icon

Add a Custom Plugin

For information on custom plugins, refer to the following help topic: Custom Plugin Development. In the following code, the plugin is called DetailEditCell, but you can rename it if required.

const DetailEditCell = () => (
  <Plugin name="DetailEditCell">
  </Plugin>
)

Handle the Detail Row Expand and Collapse Events

When a user expands or collapses a detail row, the TableRowDetail plugin executes the toggleDetailRowExpanded action. Override this action to switch an expanded row to the edit state or switch a collapsed row back to the normal state.

The IDs of expanded rows are included in the expandedDetailRowIds array. To find whether a row is expanded or collapsed, check if this array includes the row ID.

const DetailEditCell = () => (
  <Plugin name="DetailEditCell">
    <Action
      name="toggleDetailRowExpanded"
      action={({ rowId }, { expandedDetailRowIds }, { startEditRows, stopEditRows }) => {
        const rowIds = [rowId];
        const isCollapsing = expandedDetailRowIds.indexOf(rowId) > -1;
        if (isCollapsing) {
          stopEditRows({ rowIds });
        } else {
          startEditRows({ rowIds });
        }
      }}
    />
  </Plugin>
)

Handle Data Edits

To handle data edits, add the following functions:

  • processValueChange - processes user input
  • applyChanges - applies data edits
  • cancelChanges - cancels data edits

The detail row and the edit form in it will use these functions later. Implement the functions in the tableCell template and pass them as params to the TemplatePlaceholder along with the row being edited.

const DetailEditCell = () => (
  <Plugin name="DetailEditCell">
    {/* ... */}
    <Template
      name="tableCell"
      predicate={({ tableRow }) => tableRow.type === TableRowDetail.ROW_TYPE}
    >
      {(params) => (
        <TemplateConnector>
          {({
           tableColumns,
            createRowChange,
            rowChanges,
          }, {
            changeRow,
            commitChangedRows,
            cancelChangedRows,
            toggleDetailRowExpanded,
          }) => {
            if (tableColumns.indexOf(params.tableColumn) !== 0) {
              return null;
            }
            const { tableRow: { rowId } } = params;
            const row = { ...params.tableRow.row, ...rowChanges[rowId] };

            const processValueChange = ({ target: { name, value }}) => {
              const changeArgs = {
                rowId,
                change: createRowChange(row, value, name)
              };
              changeRow(changeArgs);
            };

            const applyChanges = () => {
              toggleDetailRowExpanded({ rowId });
              commitChangedRows({ rowIds: [rowId] });
            };
            const cancelChanges = () => {
              toggleDetailRowExpanded({ rowId });
              cancelChangedRows({ rowIds: [rowId] });
            };

            return (
              <TemplatePlaceholder params={{
                ...params,
                row,
                tableRow: {
                  ...params.tableRow,
                  row,
                },
                changeRow,
                processValueChange,
                applyChanges,
                cancelChanges,
              }} />
            );
          }}
        </TemplateConnector>
      )}
    </Template>
  </Plugin>
)

The functions and the row being edited are then passed on to the child elements of TableRowDetail.Cell. These elements are edit form elements.

const DetailCell = ({
  children, changeRow, editingRowIds, addedRows, processValueChange,
  applyChanges, cancelChanges,
  ...restProps
}) => {
  const { row } = restProps;

  return (
    <TableRowDetail.Cell {...restProps}>
      {React.cloneElement(children, {
        row, changeRow, processValueChange, applyChanges, cancelChanges,
      })}
    </TableRowDetail.Cell>
  );
};

Add the Edit Form

Implement a component that renders an edit form with multiple text fields and Save and Cancel buttons. Each text field should execute the processValueChange function when the onChange event is raised. The buttons should execute the applyChanges and cancelChanges functions.

const DetailContent = ({ row, ...rest }) => {
  const {
    processValueChange,
    applyChanges,
    cancelChanges,
  } = rest;
  return (
    ...
      <TextField
        margin="normal"
        name="prefix"
        label="Title"
        value={row.prefix}
        onChange={processValueChange}
      />
    ... // other editors
      <Button onClick={applyChanges} variant="text" color="primary">
          Save
      </Button>
      <Button onClick={cancelChanges} color="secondary">
        Cancel
      </Button>
      ...
  )
}

Replace the Expand Icon

Replace the Expand icon with a text label: Edit for collapsed rows, Cancel for expanded rows.

const styles = theme => ({
  toggleCell: {
    textAlign: 'center',
    textOverflow: 'initial',
    paddingTop: 0,
    paddingBottom: 0,
    paddingLeft: theme.spacing(1),
  },
  toggleCellButton: {
    verticalAlign: 'middle',
    display: 'inline-block',
    padding: theme.spacing(1),
  },
});

const ToggleCellBase = ({
  style, expanded, classes, onToggle,
  tableColumn, tableRow, row,
  className,
  ...restProps
}) => {
  const handleClick = (e) => {
    e.stopPropagation();
    onToggle();
  };
  return (
    <TableCell
      className={classNames(classes.toggleCell, className)}
      style={style}
      {...restProps}
    >
      <IconButton
        className={classes.toggleCellButton}
        onClick={handleClick}
      >
        {
          expanded
            ? <Cancel />
            : <Edit />
        }
      </IconButton>
    </TableCell>
  );
};

const ToggleCell = withStyles(styles, { name: 'ToggleCell' })(ToggleCellBase);

You can view the demo and the full code below: