Creating a dynamic inline editable table in React — Part 3

Vanu Protap Verma
7 min readMar 30, 2020

This is the last part of my series on creating a dynamic table in React. So far, we have created GenericCustomTable component to render some data as dynamically generated table (part 1) and then updated this component to render cell value in a custom way (part 2).

Goal:

In this post, our goal is to display an Edit button for each row, and when that button is clicked, we will make the first_name field as Inline Editable input field.

Let’s start

From our previous posts, we know that we can show/hide columns in the dynamic table as per our requirement by defining the columns in getDisplayColumns function. To display an Actions column in our table, we can do the same and our newly added column setting will look like this:

Users component (Users.js)

...
{
fieldName: 'action_buttons',
displayText: 'Actions',
visible: true,
columnSize: 2,
renderer: this.editRenderer
}
...

Note that the fieldName we have defined here does not match with any of our data field. That is okay since we are not going to display any value in this column. Also, we have assigned editRenderer function to the renderer property. So, the GenericCustomTable will now call this function when it renders the Actions column.

Let’s add the editRenderer function:

...
// we will not be using fieldName here, still keeping it same with
// other renderer function
editRenderer(data, fieldName) {
return (
<div>
<button
onClick={() => console.log(`EDIT CLICKED ${data.email}`)}
>
Edit
</button>
</div>
);
}
...

We are doing a console.log() so that we can see the Edit button is clicked for individual records. Here how it looks in the browser now:

screenshot 5: Edit button for each row

Sweet, Edit button is rendered and working. But our intension was to make the First name column Inline Editable. Before we start making this change, we need to consider:

  1. Only to make the First name editable for corresponding Edit button click.
  2. Show the updated value in the field when we save any change for the field.
  3. Show the initial value in case we decide to cancel our change.

As you are getting the idea, this is a bit tricky and we need some state variables plus couple of new functions. Let’s proceed:

First, let’s make the First name value as Editable. To do that, we need to grab the corresponding row id (data id) in a state variable. So update the constructor function of Users component as below:

// leave the users array unchanged
this.state = {
users: [...],
id: ''
};

We will be saving the row id into this state variable when we click on the Edit button. For that, we need to create a new editButtonClickHandler function as below and attach it to the onClick event of the Edit button.

...
editButtonClickHandler(data) {
this.setState({
id: data.id
});
}
...

and we update our editRenderer function like below:

...
editRenderer(data, fieldName) {
return (
<div>
<button
onClick={() => this.editButtonClickHandler(data)}
>
Edit
</button>
</div>
);
}
...

notice how we are passing data through to the editButtonClickHandler function and then accessing the data properties inside that function.

Now that we have the corresponding record id saved in a state variable, let’s add another renderer function named firstNameRenderer to make First name editable. The new function will look like this:

...
firstNameRenderer(data, fieldName) {
if (this.state.id && this.state.id === data.id) {
const inputId = `${fieldName}-${data.id}`;
return (
<div>
<input type="text"
id={inputId}
className="input-textbox"
value={data[fieldName]}
onChange={() => console.log('VALUE CHANGED')}
/>
</div>
);
}

return (
<div>
<span>
{data[fieldName]}
</span>
</div>
);
}
...

Here, we are checking if the state variable id has any value and if it has, checking whether the value is same as the current data.id . If both conditions met, then we are returning the value inside an input element.

Let us attach this function to the renderer property for First name column settings.

...
{
fieldName: 'first_name',
displayText: 'First name',
visible: true,
columnSize: 2,
renderer: this.firstNameRenderer
},
...

When Edit button is clicked for any row, we can add some logic to change the Edit button of that row to Save and Cancel button. For this, we need to update the editRenderer function like below:

...
editRenderer(data, fieldName) {
if (this.state.id && this.state.id === data.id) {
const saveButtonId = `save-${data.id}`;
const cancelButtonId = `cancel-${data.id}`;
return (
<div>
<button
id={saveButtonId}
onClick={() => console.log('SAVE CLICKED')}
>
Save
</button>
<button
id={cancelButtonId}
onClick={() => console.log('CANCEL CLICKED')}
>
Cancel
</button>
</div>
);
}

return (
<div>
<button
onClick={() => this.editButtonClickHandler(data)}
>
Edit
</button>
</div>
);
}
...

Time to check the changes in the browser.

screenshot 6: on “Edit” button, First name field became editable, and Edit button changed to Save and Cancel

Cool! Great job so far. We have converted our generic table into an inline editable table. But, still there are few things we need to fix:

  1. When clicking on the Cancel button, we want restore the initial value.
  2. When typing into the text box, updated value is not showing in the text box [a classic React issue :) ]
  3. When clicking on the Save button, we want to save the updated value (of course we are not saving to datastore, rather I will only show how to get the updated value and then the place to make backend api call).

So let’s fix issue 1 first.

For this, we need to save the updated First name value into a state variable. So let’s add a variable into state inside constructor function.

// code can be refactored, but I am skipping that part
...
this.state = {
...
initialUsers: [
{
"id": 1,
"first_name": "Shara",
"last_name": "Weeds",
"email": "sweeds0@barnesandnoble.com",
"gender": "Female"
},
{
"id": 2,
"first_name": "Conant",
"last_name": "Puddan",
"email": "cpuddan1@ihg.com",
"gender": "Male"
},
{
"id": 3,
"first_name": "Mehetabel",
"last_name": "Mawtus",
"email": "mmawtus2@sakura.ne.jp",
"gender": "Female"
}
],
...
};

The initialUsers state variable will contain the same data as users state variable and will not be updated anywhere in the code (it is important to restore the initial value when Cancel button is clicked). To make the Cancel button work, we need to add a new function cancelButtonClickHandler and attach it to the onClick event of the Cancel button. The function will look like this:

...
cancelButtonClickHandler() {
const initialUsers = clonedeep(this.state.initialUsers);
this.setState({
id: '',
users: initialUsers
});
}
...

We have also attached the cancelButtonClickHandler function to the Cancel button onClick event handler.

...
editRenderer(data, fieldName) {
if (this.state.id && this.state.id === data.id) {
...
<button
id={cancelButtonId}
onClick={this.cancelButtonClickHandler}
>
Cancel
</button>
...
}
...
}

We are taking a copy of initialUsers state variable and assigning to users state variable to reset the value when Cancel button is clicked.

Great! We have fixed the Cancel button issue. Let’s move forward and fix issue 2.

To fix issue 2, we need to add a new firstNameChangeHandler function and attach it to the onChange event handler for the input text box. The new function will look like this:

...
firstNameChangeHandler(event) {
const fieldAndId = event.target.id.split('-');
const fieldName = fieldAndId[0];
const updatedUsers = clonedeep(this.state.users);
updatedUsers.forEach(user => {
if (user.id === fieldAndId[1]) {
user[fieldName] = event.target.value;
}
});
this.setState({
users: updatedUsers
});
}
...

and we need to update our firstNameRenderer function like below (highlighted in bold):

...
firstNameRenderer(data, fieldName) {
...
<input type="text"
id={inputId}
className="input-textbox"
value={data[fieldName]}
onChange={this.firstNameChangeHandler}
/>
...
}
...

Now, at this moment, if we test our changes in browser, we will notice that the issue isn’t fixed. The reason for this is setState function is NOT guaranteed to be immediate (classic React issue). For this reason, we need add a componentWillReceiveProps function inside our GenericCustomTable component. The function will look like this:

GenericCustomTable component (GenericCustomTable.js)

...
componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.props.data) {
this.setState({
currentTableData: clonedeep(nextProps.data)
});
}
}
...

And that’s it. With this change, we should be able to change the value of the input text box. Let’s check it in browser.

screenshot 7: First name value is changing (see the console log)

Now, it’s time to fix the last issue, saving the data. For this, I will only show how to get the updated data inside a save function and how to call the backend from there. We will not be calling actual backend rather point out the place where we can put our backend calling logic. Let’s do that:

For this, we need to create a new function saveButtonClickHandler inside our Users component and then attach it to the onClick event for Save button.

Users component (Users.js)

...
saveButtonClickHandler(event) {
const fieldAndId = event.target.id.split('-');
if (this.state.id && this.state.id === fieldAndId[1]) {
const updatedUser = this.state.users.find(user => user.id === this.state.id);
// TODO: call backend service/api to save the data; POST request
console.log(`USER SAVED ${JSON.stringify(updatedUser)}`);
this.setState({
id: '',
// below line just gives an impression that the data is saved
initialUsers: clonedeep(this.state.users)
}, () => {
// TODO: refresh data by calling backend; GET request
});
}
}
...

and our updated Save button will look like this:

...
editRenderer(data, fieldName) {
if (this.state.id && this.state.id === data.id) {
...
<button
id={saveButtonId}
onClick={this.saveButtonClickHandler}
>
Save
</button>
...
}
...
}

Let’s test it in the browser:

screenshot 8: We are handling save button click.

That’s it. We have achieved a lot during this series: We have created a component to render data as a table dynamically, then we have modified the component to render some values in a custom way and lastly, we made our dynamic table to be inline editable.

Note:

If you have any feedback on improving the quality of the posts or face any issue, please feel free to contact me.

Big thank you for sticking with me so far.

Stay fine and enjoy coding :)

Links:

Part 1 of this series is here

Part 2 of this series is here

--

--