Inline

The KendoReact Data Grid enables you to create, update, and delete data records inline.

Basic Concepts

The edit mode of the Grid rows is based on the value of the editField property.

import React from 'react';
import ReactDOM from 'react-dom';
import { Grid, GridColumn as Column, GridToolbar } from '@progress/kendo-react-grid';

import { sampleProducts } from './sample-products.jsx';
import MyCommandCell from './my-command-cell.jsx';

class App extends React.Component {

    CommandCell;

    constructor(props) {
        super(props);

        this.state = {
            data: sampleProducts.slice(0)
        };

        this.enterInsert = this.enterInsert.bind(this);
        this.itemChange = this.itemChange.bind(this);

        const enterEdit = this.enterEdit.bind(this);
        const save = this.save.bind(this);
        const cancel = this.cancel.bind(this);
        const remove = this.remove.bind(this);
        this.CommandCell = MyCommandCell(enterEdit,remove,save,cancel, "inEdit");
    }

    enterInsert() {
        const dataItem = { inEdit: true, Discontinued: false };
        const newproducts = this.state.data.slice();
        newproducts.unshift(dataItem);
        this.update(newproducts, dataItem);
        this.setState({
            data: newproducts
        });
    }

    enterEdit(dataItem) {
        this.update(this.state.data, dataItem).inEdit = true;
        this.setState({
            data: this.state.data.slice()
        });
    }

    save(dataItem) {
        dataItem.inEdit = undefined;
        dataItem.ProductID = this.update(sampleProducts, dataItem).ProductID;
        this.setState({
            data: this.state.data.slice()
        });
    }

    cancel(dataItem) {
        if (dataItem.ProductID) {
            let originalItem = sampleProducts.find(p => p.ProductID === dataItem.ProductID);
            this.update(this.state.data, originalItem);
        } else {
            this.update(this.state.data, dataItem, !dataItem.ProductID);
        }
        this.setState({
            data: this.state.data.slice()
        });
    }

    remove(dataItem) {
        dataItem.inEdit = undefined;
        this.update(this.state.data, dataItem, true);
        this.update(sampleProducts, dataItem, true);
        this.setState({
            data: this.state.data.slice()
        });
    }

    itemChange(event) {
        const value = event.value;
        const name = event.field;
        if (!name) {
            return;
        }
        const updatedData = this.state.data.slice();
        const item = this.update(updatedData, event.dataItem);
        item[name] = value;
        this.setState({
            data: updatedData
        });
    }

    update(data, item, remove) {
        let updated;
        let index = data.findIndex(p => p === item || item.ProductID && p.ProductID === item.ProductID);
        if (index >= 0) {
            updated = Object.assign({}, item);
            data[index] = updated;
        } else {
            let id = 1;
            data.forEach(p => { id = Math.max(p.ProductID + 1, id); });
            updated = Object.assign({}, item, { ProductID: id });
            data.unshift(updated);
            index = 0;
        }

        if (remove) {
            data = data.splice(index, 1);
        }

        return data[index];
    }

    render() {
        return (
            <div>
                <Grid
                    style={{ height: '420px' }}
                    data={this.state.data}
                    onItemChange={this.itemChange}
                    editField="inEdit"
                >
                    <GridToolbar>
                        <button
                            title="Add new"
                            className="k-button k-primary"
                            onClick={this.enterInsert}
                        >Add new
                        </button>

                        {this.state.data.filter(p => p.inEdit).length > 0 && (
                            <button
                                title="Cancel current changes"
                                className="k-button"
                                onClick={(e) => this.setState({ data: sampleProducts.slice() })}
                            >Cancel current changes
                            </button>
                        )}
                    </GridToolbar>
                    <Column field="ProductID" title="Id" width="50px" editable={false} />
                    <Column field="ProductName" title="Product Name" />
                    <Column field="FirstOrderedOn" title="First Ordered" editor="date" format="{0:d}" />
                    <Column field="UnitsInStock" title="Units" width="150px" editor="numeric" />
                    <Column field="Discontinued" title="Discontinued" editor="boolean" />
                    <Column cell={this.CommandCell} width="180px" />
                </Grid>
            </div>
        );
    }
}

ReactDOM.render(
    <App />,
    document.querySelector('my-app')
);
export const sampleProducts = [
    {
        "ProductID": 1,
        "ProductName": "Chai",
        "SupplierID": 1,
        "CategoryID": 1,
        "QuantityPerUnit": "10 boxes x 20 bags",
        "UnitPrice": 18,
        "UnitsInStock": 39,
        "UnitsOnOrder": 0,
        "ReorderLevel": 10,
        "Discontinued": false,
        "Category": {
            "CategoryID": 1,
            "CategoryName": "Beverages",
            "Description": "Soft drinks, coffees, teas, beers, and ales"
        },
        "FirstOrderedOn": new Date(1996, 8, 20)
    },
    {
        "ProductID": 2,
        "ProductName": "Chang",
        "SupplierID": 1,
        "CategoryID": 1,
        "QuantityPerUnit": "24 - 12 oz bottles",
        "UnitPrice": 19,
        "UnitsInStock": 17,
        "UnitsOnOrder": 40,
        "ReorderLevel": 25,
        "Discontinued": false,
        "Category": {
            "CategoryID": 1,
            "CategoryName": "Beverages",
            "Description": "Soft drinks, coffees, teas, beers, and ales"
        },
        "FirstOrderedOn": new Date(1996, 7, 12)
    },
    {
        "ProductID": 3,
        "ProductName": "Aniseed Syrup",
        "SupplierID": 1,
        "CategoryID": 2,
        "QuantityPerUnit": "12 - 550 ml bottles",
        "UnitPrice": 10,
        "UnitsInStock": 13,
        "UnitsOnOrder": 70,
        "ReorderLevel": 25,
        "Discontinued": false,
        "Category": {
            "CategoryID": 2,
            "CategoryName": "Condiments",
            "Description": "Sweet and savory sauces, relishes, spreads, and seasonings"
        },
        "FirstOrderedOn": new Date(1996, 8, 26)
    },
    {
        "ProductID": 4,
        "ProductName": "Chef Anton's Cajun Seasoning",
        "SupplierID": 2,
        "CategoryID": 2,
        "QuantityPerUnit": "48 - 6 oz jars",
        "UnitPrice": 22,
        "UnitsInStock": 53,
        "UnitsOnOrder": 0,
        "ReorderLevel": 0,
        "Discontinued": false,
        "Category": {
            "CategoryID": 2,
            "CategoryName": "Condiments",
            "Description": "Sweet and savory sauces, relishes, spreads, and seasonings"
        },
        "FirstOrderedOn": new Date(1996, 9, 19)
    },
    {
        "ProductID": 5,
        "ProductName": "Chef Anton's Gumbo Mix",
        "SupplierID": 2,
        "CategoryID": 2,
        "QuantityPerUnit": "36 boxes",
        "UnitPrice": 21.35,
        "UnitsInStock": 0,
        "UnitsOnOrder": 0,
        "ReorderLevel": 0,
        "Discontinued": true,
        "Category": {
            "CategoryID": 2,
            "CategoryName": "Condiments",
            "Description": "Sweet and savory sauces, relishes, spreads, and seasonings"
        },
        "FirstOrderedOn": new Date(1996, 7, 17)
    },
    {
        "ProductID": 6,
        "ProductName": "Grandma's Boysenberry Spread",
        "SupplierID": 3,
        "CategoryID": 2,
        "QuantityPerUnit": "12 - 8 oz jars",
        "UnitPrice": 25,
        "UnitsInStock": 120,
        "UnitsOnOrder": 0,
        "ReorderLevel": 25,
        "Discontinued": false,
        "Category": {
            "CategoryID": 2,
            "CategoryName": "Condiments",
            "Description": "Sweet and savory sauces, relishes, spreads, and seasonings"
        },
        "FirstOrderedOn": new Date(1996, 9, 19)
    },
    {
        "ProductID": 7,
        "ProductName": "Uncle Bob's Organic Dried Pears",
        "SupplierID": 3,
        "CategoryID": 7,
        "QuantityPerUnit": "12 - 1 lb pkgs.",
        "UnitPrice": 30,
        "UnitsInStock": 15,
        "UnitsOnOrder": 0,
        "ReorderLevel": 10,
        "Discontinued": false,
        "Category": {
            "CategoryID": 7,
            "CategoryName": "Produce",
            "Description": "Dried fruit and bean curd"
        },
        "FirstOrderedOn": new Date(1996, 7, 22)
    },
    {
        "ProductID": 8,
        "ProductName": "Northwoods Cranberry Sauce",
        "SupplierID": 3,
        "CategoryID": 2,
        "QuantityPerUnit": "12 - 12 oz jars",
        "UnitPrice": 40,
        "UnitsInStock": 6,
        "UnitsOnOrder": 0,
        "ReorderLevel": 0,
        "Discontinued": false,
        "Category": {
            "CategoryID": 2,
            "CategoryName": "Condiments",
            "Description": "Sweet and savory sauces, relishes, spreads, and seasonings"
        },
        "FirstOrderedOn": new Date(1996, 11, 1)
    },
    {
        "ProductID": 9,
        "ProductName": "Mishi Kobe Niku",
        "SupplierID": 4,
        "CategoryID": 6,
        "QuantityPerUnit": "18 - 500 g pkgs.",
        "UnitPrice": 97,
        "UnitsInStock": 29,
        "UnitsOnOrder": 0,
        "ReorderLevel": 0,
        "Discontinued": true,
        "Category": {
            "CategoryID": 6,
            "CategoryName": "Meat/Poultry",
            "Description": "Prepared meats"
        },
        "FirstOrderedOn": new Date(1997, 1, 21)
    },
    {
        "ProductID": 10,
        "ProductName": "Ikura",
        "SupplierID": 4,
        "CategoryID": 8,
        "QuantityPerUnit": "12 - 200 ml jars",
        "UnitPrice": 31,
        "UnitsInStock": 31,
        "UnitsOnOrder": 0,
        "ReorderLevel": 0,
        "Discontinued": false,
        "Category": {
            "CategoryID": 8,
            "CategoryName": "Seafood",
            "Description": "Seaweed and fish"
        },
        "FirstOrderedOn": new Date(1996, 8, 5)
    }
];
import React from 'react';
import { GridCell } from '@progress/kendo-react-grid';

export default function MyCommandCell(enterEdit, remove, save, cancel, editField) {
    return class extends GridCell {
        render() {
            return !this.props.dataItem[editField]
                ? (
                    <td>
                        <button
                            className="k-primary k-button k-grid-edit-command"
                            onClick={(e) => enterEdit(this.props.dataItem)}>
                            Edit
                        </button>
                        <button
                            className="k-button k-grid-remove-command"
                            onClick={(e) => confirm('Confirm deleting: ' + this.props.dataItem.ProductName) && remove(this.props.dataItem)}>
                            Remove
                        </button>
                    </td>
                )
                : (
                    <td>
                        <button
                            className="k-button k-grid-save-command"
                            onClick={(e) => save(this.props.dataItem)}>
                            {this.props.dataItem.ProductID
                                ? 'Update'
                                : 'Add'}
                        </button>
                        <button
                            className="k-button k-grid-cancel-command"
                            onClick={(e) => cancel(this.props.dataItem)}>{this.props.dataItem.ProductID
                                ? 'Cancel'
                                : 'Discard'}
                        </button>
                    </td>
                );
        }
    }
};

Setup

To enable the inline edit mode of the Grid, use a Higher-Order Component and two data collections.

  1. Set the field which will indicate the editable data items by using the editField property. This field is part of the temporary data items which are used during editing. To configure the edit state of the entire row or the field name that can be edited, you can use Boolean values to define that field.

    <Grid
           editField="inEdit"
  2. Configure the command column by defining the command buttons inside the GridCell component. To render the command cell, use a Higher-Order Component which has to receive all functions that will be executed from the command buttons.

    this.CommandCell = MyCommandCell(enterEdit,remove,save,cancel, "inEdit");
    export default function MyCommandCell(enterEdit, remove, save, cancel, editField) {
           return class extends GridCell {
           render() {
               return !this.props.dataItem[editField]
                   ? (
                       <td>
                           <button
                               className="k-primary k-button k-grid-edit-command"
                               onClick={(e) => enterEdit(this.props.dataItem)}>
                               Edit
                           </button>
                           <button
                               className="k-button k-grid-remove-command"
                               onClick={(e) => confirm('Confirm deleting: ' + this.props.dataItem.ProductName) && remove(this.props.dataItem)}>
                               Remove
                           </button>
                       </td>
                   )
                   : (
                       <td>
                           <button
                               className="k-button k-grid-save-command"
                               onClick={(e) => save(this.props.dataItem)}>
                               {this.props.dataItem.ProductID
                                   ? 'Update'
                                   : 'Add'}
                           </button>
                           <button
                               className="k-button k-grid-cancel-command"
                               onClick={(e) => cancel(this.props.dataItem)}>{this.props.dataItem.ProductID
                                   ? 'Cancel'
                                   : 'Discard'}
                           </button>
                       </td>
                   );
               }
           }
       };  
  3. Define a function for the onItemChange event which will handle any input changes during editing. Inside the event, all relevant data, such as the edited data item, the newly entered value, or the edited field, will be available as itemChange parameters.

    <Grid itemChange={this.itemChange}>
    itemChange(event) {
           const value = event.value;
           const name = event.field;
           if (!name) {
               return;
           }
           const updatedData = this.state.data.slice();
           const item = this.update(updatedData, event.dataItem);
           item[name] = value;
           this.setState({
               data: updatedData
           });
       }
  4. Define the functions which will set the item in edit mode or create a new item in edit mode. You can call these functions from the command buttons in the command cell template.

    enterEdit(dataItem) {
           this.update(this.state.data, dataItem).inEdit = true;
           this.setState({
           data: this.state.data.slice()
       });
    }
    enterInsert() {
           const dataItem = { inEdit: true, Discontinued: false };
           const newproducts = this.state.data.slice();
           newproducts.unshift(dataItem);
           this.update(newproducts, dataItem);
           this.setState({
               data: newproducts
           });
       }
  5. Define the functions which will handle the save, cancel, and remove actions. You can call these functions from the command buttons, the toolbar template, or a button which is outside of the Grid.

    save(dataItem) {
           dataItem.inEdit = undefined;
           dataItem.ProductID = this.update(sampleProducts, dataItem).ProductID;
           this.setState({
               data: this.state.data.slice()
           });
       }
    cancel(dataItem) {
           if (dataItem.ProductID) {
               let originalItem = sampleProducts.find(p => p.ProductID === dataItem.ProductID);
               this.update(this.state.data, originalItem);
           } else {
               this.update(this.state.data, dataItem, !dataItem.ProductID);
           }
           his.setState({
               data: this.state.data.slice()
           });
       }
    remove(dataItem) {
           dataItem.inEdit = undefined;
           this.update(this.state.data, dataItem, true);
           this.update(sampleProducts, dataItem, true);
           this.setState({
               data: this.state.data.slice()
           });
       }
  6. Send the requests to update the data base to the server or use an internal function which will locally keep track of all changes.

    update(data, item, remove) {
           let updated;
           let index = data.findIndex(p => p === item || item.ProductID && p.ProductID === item.ProductID);
           if (index >= 0) {
               updated = Object.assign({}, item);
               data[index] = updated;
           } else {
               let id = 1;
               data.forEach(p => { id = Math.max(p.ProductID + 1, id); });
               updated = Object.assign({}, item, { ProductID: id });
               data.unshift(updated);
               index = 0;
    
           if (remove) {
               data = data.splice(index, 1);
           }
           return data[index];
       }
  7. Per column, set the options that are related to editing:

    • editable—Determines if the column is editable.
    • editor—Specifies the data type of the column and, based on that, sets the appropriate editor.

In this article