Hello developer, here I'm going to share how to make editable cells using Ant design (antd). Antd was developed by the Alibaba Group. It's one of the most popular UI frameworks for web applications and React-based applications. Antd provides many UI components that developers can use when building web applications. It helps developers to make the UI they create consistent and interesting. We can read the documentation on the official website of ant.design.
Prerequisite
There are necessary requirements to create this application.
- ReactJS (here I am using version 18.2.0)
- Antd (when writing this, I was using version 4) Defining Dummy Data Code below is an example of the data to display
Defining Dummy Data
Code below is an example of the data to be displayed
const data = {
content: [
{
id: 1,
name: 'John Sr',
age: 60,
address: 'Sudirman No 1, Jakarta Selatan',
},
{
id: 2,
name: 'Lennon Jr',
age: 32,
address: 'Kemanggisan No 3, Jakarta Barat',
},
]
}
*Defining Column *
After knowing the response data to be displayed, then we need to define the column according to the data parameters.
const columns = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Age',
dataIndex: 'age',
},
{
title: 'Address',
dataIndex: 'address',
},
]
Implement EditableCell & EditableContext
Next we can take advantage of some hooks in ReactJs on the Context.tsx
file.
// src/components/home/table/Context.tsx
import { createContext, useContext, useEffect, useRef, useState } from 'react';
import { validateInput } from '../../../utils/validate';
import { Form, Input } from 'antd';
export const EditableContext: any = createContext(null);
export const EditableCell = (props: any) => {
const {
title,
editable,
children,
dataIndex,
record,
index,
suffix,
validate,
handleSave,
success,
setSuccess,
handleValidate,
...restProps
} = props
const [editing, setEditing] = useState(false);
const inputRef: any = useRef(null);
const form: any = useContext(EditableContext);
const content = restProps?.data?.content || [];
useEffect(() => {
if (editing) {
inputRef.current.focus();
}
}, [editing]);
const toggleEdit = (val: any) => {
setEditing(!editing);
const value = val.target.value;
form.setFieldsValue({
[dataIndex]: value,
});
};
const save = async (e: any) => {
try {
toggleEdit(e);
if (e.target.value) {
const val = await form.validateFields();
await handleSave({
...record,
...val,
});
}
const suc = success;
const bool = await handleValidate(
index,
record,
dataIndex,
e.target.value
);
if (index !== undefined) {
suc[index] = bool;
setSuccess(suc);
}
} catch (errInfo) {
//
}
};
let errorMsg = null;
let validateStatus: '' | 'success' | 'warning' | 'error' | 'validating' = '';
if (
validate &&
validate.length &&
validate[index] &&
validate[index]?.[dataIndex]?.errorMsg
) {
errorMsg = validate[index]?.[dataIndex]?.errorMsg;
validateStatus = validate[index]?.[dataIndex]?.validateStatus;
}
let childNode = children;
const value =
children && Array.isArray(children) && children.length
? children[children.length - 1]
: '';
if (editable) {
childNode = editing ? (
<Form.Item
style={{
margin: 0,
}}
name={dataIndex}
rules={[
{
required: true,
message: `${title} is required.`,
},
]}
>
<Input
style={{
width: '100%',
}}
max={suffix === 'th' ? 100 : undefined}
placeholder="0"
suffix={suffix}
ref={inputRef}
disabled={restProps?.disabled}
onBlur={save}
onKeyDown={(e) =>
validateInput(e, () => {}, {
minLength: 1,
maxLength: suffix === 'th' ? 2 : undefined,
allowDecimal: suffix === 'th',
isNumber: true,
})
}
/>
</Form.Item>
) : (
<Form.Item
style={{
margin: 0,
}}
help={errorMsg}
validateStatus={validateStatus}
>
<Input
className="editable-cell-value-wrap"
style={{
paddingRight: 24,
}}
onFocus={toggleEdit}
value={value}
disabled={restProps?.isDisabled}
/>
</Form.Item>
);
}
if (dataIndex === 'rangeOmzetUpper' && index === content.length - 1) {
childNode = <></>;
}
return <td {...restProps}>{childNode}</td>;
};
There are 5 hooks that I use to make this application, namely:
-
createContext
is used to create a context object that allows components in the React application to share data without having to pass the prop from component to component. In the above code, I use it on the variableEditableContext
. -
useContext
is used for accessing the data provided by the context object created withcreateContext
. It allows the components to take values from the context. I place it on a variable form to access the values ofEditableContext
. -
useEffect
is used in dealing with side effects in the component React. In my example, it's used to detect changes in the variable editing. -
useRef
is used to make references to the DOM element or other values in the component. I use useRef to access and manipulate the input element directly, triggering it if there are changes to the editing variable. -
useState
is used to create state variables in functional components. WithuseState
, developers can store and modify data that affects the display of components, and any change will trigger the component rerender.
Implementation of the HomeTableContainer
Now I'm going to create a HomeTableContainer
, here's the logic code that will be used in the HomeTableView
. I also called the Functions EditableContext
and EditableCell
from the Context.tsx
file. On the function handleValidate
I created an age limit condition that can be entered by the user, if the age exceeds 60 years then there will be error.
// src/components/home/table/index.tsx
import { useEffect, useState } from 'react';
import HomeTableView from './Main'
import { Form } from 'antd';
import { cloneDeep, isEmpty } from 'lodash';
import { EditableContext, EditableCell } from './Context';
const HomeTableContainer = ({
data,
isLoading,
isDisabled,
}: any) => {
const [validate, setValidate]: any = useState([]);
const [mutableData, setMutableData] = useState(data)
const [success, setSuccess] = useState<any>([]);
const handleSave = async (row: any) => {
if (isDisabled) {
return;
}
const newData = cloneDeep(data);
const index = newData.content.findIndex(
(item: any) => Number(row.id) === Number(item.id)
);
if (newData.content[index]) {
newData.content[index] = row;
setMutableData({ ...newData });
}
};
const handleValidate: any = (
index: number,
record: any,
dataIndex: string,
value: string | number
) => {
let bool = true;
if (isDisabled) {
return false;
}
const val = cloneDeep(validate);
const obj: any = {}
if ((dataIndex === 'age' &&
Number(value) > 60)) {
obj.age = {
validateStatus: 'error',
errorMsg: `Maksimal usia ${
record.name
} adalah 60th`,
};
bool = false;
}
if (!val[index]) {
val[index] = {
[dataIndex]: {}
}
}
if (obj && !isEmpty(obj) && val[index]) {
val[index] = {...obj}
} else {
val[index] = {}
}
setValidate([...val])
return bool;
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const EditableRow: any = ({ index, ...obj }: any) => {
const [form] = Form.useForm();
return (
<Form initialValues={data.content} form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...obj} />
</EditableContext.Provider>
</Form>
);
};
const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};
const sharedOnCell = () => {
return {};
};
const handleSetSuccess = (val: any) => {
setSuccess(val);
};
useEffect(() => {
return () => {
setSuccess([]);
};
}, []);
return (
<HomeTableView
data={mutableData}
components={components}
isLoading={isLoading}
rowKey={'id'}
handleSave={handleSave}
success={success}
setSuccess={handleSetSuccess}
validate={validate !== undefined ? validate : []}
isDisabled={false}
handleValidate={handleValidate}
sharedOnCell={sharedOnCell}
/>
)
}
export default HomeTableContainer
Implement HomeTableView
After creating logic on HomeTableContainer
, in the HomeTableView
file I defined and modified the column by placing the function that we have created in the HomeTableContainers
file so that the rows on column age can be edited.
// src/components/home/table/Main.tsx
import { Col, Row, Table } from 'antd';
const HomeTableView = ({
data,
components,
isLoading,
onTableChange,
rowKey,
handleSave,
handleValidate,
sharedOnCell,
validate,
success,
setSuccess,
isDisabled,
}: any) => {
const columns = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
width: '100px',
},
{
title: 'Age',
dataIndex: 'age',
key: 'age',
onCell: sharedOnCell,
editable: true,
suffix: 'th',
width: '50px',
},
{
title: 'Address',
dataIndex: 'address',
width: '100px',
key: 'address',
},
];
const mutableColumns = columns.map((col: any) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record: any, index: number) => ({
record,
suffix: col.suffix,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
handleSave,
data,
disabled: isDisabled,
handleValidate,
validate,
success,
setSuccess,
index,
}),
};
});
return (
<>
<Row style={{ marginTop: '12px' }}>
<Col flex={'auto'}>
<Row style={{ marginBottom: '12px', marginTop: '12px' }}>
<Table
bordered
columns={mutableColumns}
components={components}
dataSource={data?.content || []}
loading={isLoading}
rowClassName={() => 'editable-row'}
onChange={onTableChange}
pagination={false}
rowKey={rowKey}
sticky
scroll={{ x: 1500 }}
style={{ width: '100%' }}
/>
</Row>
</Col>
</Row>
</>
);
};
export default HomeTableView
Output
Below this is the result of the code above, the input form will display an error if the age is over 60 years.
I hope my sharing this time can be useful for the developer friends to create editable cells and tables using Antd-ReactJs. If you're inspired and think this can be implemented in your projects, please do it. For article with Indonesian Language you can read in this article.
Feel free to ask questions
Thank You
Top comments (2)
Hello! Interesting tutorial.
Welcome to the DEV community!
One little advice, you can highlight your code by placing the name of the code language after placing the initial first 3 backticks when opening a code block. e.g. ` ` `js, ` ` `css
Hope this helps!
Hello, thank you for your advice.