React.js: using HTML Table as DataGrid for CRUD Operations

 

React.js is a powerful and popular JavaScript Library for building interactive User Interfaces. We use React to build a lightweight user interface using components. A component is an autonomous object that has data properties, behaviors (methods), and User Interface. One of the major advantages of component-based libraries and frameworks is that they can be used to build composable User Interfaces using multiple components loaded at a time on the browser this helps to build a complex UI easily for the application. One of the advantages of the component is the re-usability. 

When you are building a UI where a similar HTML UI is used repetitively in various components then consider creating a custom re-usable component that can be re-used as a child of various parent components. In this case, a parent component can pass data to this reusable component and manage its rendering based on the data.
In a typical web application UI designed using React, we may use (or generally use) an HTML table UI across various web pages to show a collection of data received from the server. In some cases, we may edit or delete the data directly from the table (remember good old ASP.NET Days with DataGrid Programming, I really miss those). Of course, there are several third-party DataTables (like DataGrid) available that we can use in React.js but can we develop our own? In this article, we will be having a look at it. We will see an approach of building an Editable HTML table to show data and we will see CRUD operations with it.
To implement the code for this article you must have good knowledge of React.js with practical hands-on on components, hooks, state, props, etc. The code for this article is implemented using Microsoft Visual Code and Node.js.

Step 1: Open the command prompt and run the following command to install React CLI
npm install - -global create-react-app

Step 2: To create a react application we need to run the following command
Create-react-app react-data-grid
Once this command is executed successfully, the react-data-grid folder will be created with the necessary react dependencies.

Step 3: Open Visual Code and open the project folder named react-data-grid in it. Now navigate to this project folder and install Bootstrap and Axios packages in it using the following command.

npm install bootstrap Axios

Step 4: In the project, add a new folder and name it as services. In this folder add a new JavaScript named catservice.js. In this file add the code as shown in Listing 1
import axios from "axios";
export class CategoryService {
     constructor(){
        this.url = "https://catprdapi.azurewebsites.net/api/Category";
     }

      getCategories(){
         const response =  axios.get(this.url);
        
         return response;
     }
      getCategory(id){
        const response =  axios.get(`${this.url}/${id}`);
        return response;
     }
      postCategory(cat){
        const response =  axios.post(`${this.url}`, cat, {
            headers: {
                'Content-Type':'application/json'
            }
        });
        return response;
     }
      putCategory(id,cat){
        const response =  axios.put(`${this.url}/${id}`, cat, {
            headers: {
                'Content-Type':'application/json'
            }
        });
        return response;
     }
      deleteCategory(id){
        const response =  axios.delete(`${this.url}/${id}`);
        return response;
     }
}



Listing 1: The CategoryService

The code in Listing 1, uses the axios package and its HTTP methods to perform Read/Write operations with REST API.

Step 5: In the project folder, add a new folder named Components. In this folder add a new file and name it tablecomponent.js. In this file, we will add code to create EditTableComponent. This component will accept array data from its parent using props and based on it the HTML table code will be generated. The component will have the following features:

Props

o dataSource: This is a collection input that will be received from the parent component. The EditTableComponent will generate an HTML Table based on the dataSource.

o saveRecord: This is a callback method props type this will be invoked to save either a new record or edit an existing record.

o columns: This props type will be used to pass column names from the parent to child component so that table columns will be generated for these column properties.

o delete: This props type will be used to delete the record.

o canAdd, canEdit, and canDelete : These properties when set to true will be used to generate Add, Edit, and Delete buttons in the EditTableComponent.  

State Properties

o editIndex: This will be used to decide the row to be edited from the HTML table. The value for this state property will be set using the setEditIndex action. The default value for this state property is -1. This means that none of the rows from the table is editable initially when the component is loaded.

o recData: This will be used to add a new empty row in the table. The value for this property will be set using the setRecData action. The default value of this property will be the 0th record from the input collection received from the parent component.

o editRecData: Since the table component will be used to edit the record, we need to read the edited values for all cells for the editing record. This state property will be used to store all edited values for the row in the HTML table. The data for this property will be set using the setEditRecData action. The default value of this property is an empty JSON object ({}).

o tableData: This state property will be used to add a new empty row in the HTML table so that the new record can be added. The data for this property will be set using the setTableData action. 

o operationType: This state property is used to decide what type of operation is requested by the components means will it be the new record created or edited an existing record. The value for this property will be set using the setOperationType action.

Methods for the component

o addNewRecord: This method will be used to add a new Empty row to the HTML table. This method will be bound to the Add New Record HTML button. This method will call the setTableData action to add a new empty object in the tableData state property. This method will set the setOperationType state property to ‘New’.

o editRow: This method will be invoked on the Edit button click that will be shown for each row in the HTML table. This method accepts the id property. This property is an index key from the input received collection based on which the table is generated. Once the Edit button is clicked, the editIndex state property value will be set by invoking the setEditIndex action and then further the record will be filtered from the input received collection based on the id.  This filtered record will be set to the editRecData state property by invoking the setEditRectData action. This method will set the setOperationType state property to ‘Edit.

o handleChanges: This method will be used to read values for each editable row. When the Edit button is clicked, that specific row will show Text Elements to edit values. These Text elements will be bound to the handleChanges method so that edited values can be read. One important behavior of this method is that the same method will be used for adding new records as well as editing an existing record. As discussed in the editRow method when the Edit button is clicked the row will show Text elements at the same time when the Add New Record button is clicked the new empty row will be appended to the HTML table with the Edit button in it. This means that we need to click on the Edit button to add a new record as well as edit an existing record, we will do this by toggling across recData and editRecData state properties. When editRecData is undefined it means that the new record will be created else an existing record will be updated. The handleChanges method will set values in these two state properties based on either adding a new record or editing an existing record.  

o saveRow: This is the most important method. This method will be invoked on the Save button click. This method will be used to either create a new record or edit an existing record based on the recData or editRecData respectively. This method will use the saveRecord props type to emit a save request to the parent component. This method will emit the operation type either as Save to Edit to the parent component using the saveRecord props type to either perform an operation to create a New record or Edit an existing record.  

o deleteRow: This method will be invoked on the Delete button. This will be used to emit the delete request to the parent component by invoking the saveRecord props type. 

The code in Listing 2 shows the EditTableComponent



import { useEffect, useState } from "react";

export const EditTableComponent=(props)=> {
      // The edit index to -1
     const [editIndex, setEditIndex] = useState(-1);
     // The record that will be bound with the Table row
    //  const [recData, setRecData] = useState(props.dataSource[0]);
    const [recData, setRecData] = useState(Object.create(props.dataSource[0])); 
    // The State Object for the Edit
     const [editRecData, setEditRecData] = useState({}); 
     // Data to be displayed in the HTML Table   
     const [tableData, setTableData] = useState(props.dataSource);
     const [operationType, setOperationType] = useState('');

    
     
     const editRow = (id)=>{       
        setEditIndex(id);
     
        // Read Old Valeus for the Edit Clicked Rows
        let rec = props.dataSource.filter((r,i)=>{
 
            return Object.values(r)[0] === id;
        })[0];
        setEditRecData(rec);
        // Set the OPerationType as 'Edit'
        setOperationType('Edit');
     }
     const cancelEdit = ()=>{
        setEditIndex(-1);
     }
     const saveRow=()=>{
     
        if(editRecData === undefined){
            alert('Save New');
            props.saveRecord(recData,'Save');
        } else {
 
            props.saveRecord(editRecData,'Edit');
        }
        setEditIndex(-1);
        setOperationType('');
     };
     const deleteRow = (rec)=>{
        props.delete(rec);  
     }
     const handleChanges =(evt)=>{
        if(editRecData === undefined) {
 
            setRecData({...recData, [evt.target.id]:evt.target.value});    
        } else {
 
            setEditRecData({...editRecData, [evt.target.id]:evt.target.value});
        }
 
     };
     const addNewRecord=()=>{
        //tableData.push({});
        setTableData([...tableData, {}]);
        setOperationType('New');
     };

     if(props.dataSource === undefined || props.dataSource.length === 0){
        return(
            <div className="alert alert-danger">
                 <strong>No Data to Show In Table Component</strong>    
            </div>
        );
     } else {
     return (
        <div className="container">
            <div className="container" hidden={!props.canAdd}>
                <button className="btn btn-primary"
                onClick={addNewRecord}
                >Add New Record</button>
            </div>
            <table className="table table-bordered table-striped">
                <thead>
                    <tr>
                        {
                            props.columns.map((column,index)=>(
                                <th key={index}>{column}</th>
                            ))
                        }
                    </tr>
                </thead>
                <tbody>
                    {
                        tableData.map((record,idx)=>(
                            Object.values(record)[0] === editIndex ? 
                            <tr key={idx}>
                               {
                                props.columns.map((column,index)=>(
                                    <td key={index}>
                                        <input value={record.column}
                                            placeholder={record[column]}
                                            id={column}  
                                            onChange={handleChanges}
                                        />
                                    </td>
                                ))
                              }   
                               <td>
                                    <button className="btn btn-success"
                                     onClick={saveRow}
                                    >Save</button>
                                </td> 
                                <td>
                                    <button className="btn btn-primary"
                                    onClick={cancelEdit}
                                    >Cancel</button>
                                </td>   
                            </tr>
                            :
                            <tr key={idx}>
                            {
                                props.columns.map((column,index)=>(
                                    <td key={index}>{record[column]}</td>
                                ))
                               
                            } 
                             <td hidden={!props.canEdit}>
                                    <button className="btn btn-warning"
                                    onClick={()=>editRow(Object.values(record)[0])}>Edit</button>
                                </td>
                                <td hidden={!props.canDelete}>
                                    <button className="btn btn-danger"
                                    onClick={()=>deleteRow(Object.values(record)[0])}>Delete</button>
                                </td>
                            </tr>
                        ))
                    }        
                </tbody>
            </table>
        </div>
     );
    }
}

Listing 2: The EditTableComponent

Step 6: Modify the code of App.js where we will be invoking the CategoeyService and then whatever the Categores data received will be passed to the EditTableComponent using the dataSource props type. We will also pass the columns props types to pass the names of the columns to be shown in the EditTableComponent. The App.js uses the useEffect() hooks to call the getCategories() method of the CategoryService class to fetch the Categories data. The code in useEffect() hooks will read the 0th record from the response received from the REST API so that column names can be extracted so that the table will show those columns.  The save() method will perform either ‘Save’ new record or an ‘Edit’ existing record operation based on the value emitted from the EditaTableComponent. The deleteRecord() method will perform the delete operation. The code for App.js is shown in Listing 3


import { Employees } from './models/data';
import { EditTableComponent } from './components/tablecomponent';
import { CategoryService } from './catservice';
import { useEffect, useState } from 'react';
function App() {
  
  const [categories, setCategories] = useState([]);
  const [columns, setColumns] = useState([]);
  const [category, setCategory] = useState({
    CategoryId: 0,
    CategoryName: '',
    BasePrice: 0,
  });
  const serv= new CategoryService();

  useEffect(()=>{
     serv.getCategories().then(resp=>{
       setCategories(resp.data);
       setColumns(Object.keys(resp.data[0]));
     }).catch(error=>{
      console.log(`Error : ${error}`);
     })
  } ,[]);

  const save=(rec, operation)=>{
    if(operation === 'Save'){
      alert(`In Save New ${JSON.stringify(rec)}`);
     serv.postCategory(rec)
         .then(response=>{
           return response.data;
         }).then(data=>{
            setCategories(data);
            window.location.reload(true);
         })
         .catch(error=>{
           console.log(`Error occurred while saving record: ${error}`);
         }); 
    }
    if(operation === 'Edit'){
      alert(`In Edit ${JSON.stringify(rec)}`);
      serv.putCategory(rec.CategoryId,rec)
          .then(response=>{
            return response.data;
          }).then(data=>{
             setCategories(data);
             window.location.reload(true);
          })
          .catch(error=>{
            console.log(`Error occurred while saving record: ${error}`);
          }); 
     }

  };
  const deleteRecord=(rec)=>{
    alert(`Record to be deleted :${JSON.stringify(rec)}`);
     serv.deleteCategory(rec).then(response=>{
      return response.data;
    }).then(data=>{
       window.location.reload(true);
    })
    .catch(error=>{
      console.log(`Error occurred while saving record: ${error}`);
    }); 
  };
  if(categories === undefined || categories.length === 0){
    return (
      <div className='alert alert-danger'>
        <strong>No data to show in Main</strong>
      </div>
    );
  } else {
  return (
    <div className="container">
       <h1>React Editable Grid</h1>
       <EditTableComponent dataSource={categories} saveRecord={save}
         delete={deleteRecord} canDelete={true} canEdit={true}
         columns = {columns}
          canAdd={true}
         />
    </div>
  );
}
}

export default App;



Listing 3: App.js

Now run the application, you can see the HTML Table as DataGrid as shown in the following Video


The code for this article can be downloaded from this link

Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach