Creating a dynamic inline editable table in React — Part 3
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:
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:
- Only to make the First name editable for corresponding Edit button click.
- Show the updated value in the field when we save any change for the field.
- 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.
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:
- When clicking on the Cancel button, we want restore the initial value.
- When typing into the text box, updated value is not showing in the text box [a classic React issue :) ]
- 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.
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:
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