Menggunakan Antd dan React untuk Implement Editable Cells dan Table

We will be building an editable table with antd & reactjs.

bayusyaits
6 min readNov 2, 2023
image 1: react & antd

Halo developer, disini saya akan sharing bagaimana membuat Editable Cells menggunakan Ant design(antd). Antd dikembangkan oleh Alibaba Group. Ini salah satu framework UI yang cukup populer untuk aplikasi web dan aplikasi berbasis React. Antd menyediakan banyak komponen UI yang dapat digunakan developer saat membangun aplikasi web. Hal ini membantu developer agar UI yang dibuat dapat konsisten dan menarik. Kita dapat mempelajari dokumentasinya di situs resmi ant.design.

Prerequisite

Terdapat persyaratan yang diperlukan untuk membuat aplikasi ini.

  1. ReactJS (disini saya menggunakan versi 18.2.0)
  2. Antd (saat membuat tulisan ini, saya menggunakan versi 4)

Mendifinisikan Data Dummy

Kode dibawah merupakan contoh data yang akan ditampilkan

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',
},
]
}

Mendifinisikan Column

Setelah mengetahui response data yang akan ditampilkan, selanjutnya kita perlu definisikan kolom sesuai dengan parameter data tersebut.

const columns = [
{
title: 'Name',
dataIndex: 'name',
},
{
title: 'Age',
dataIndex: 'age',
},
{
title: 'Address',
dataIndex: 'address',
},
]

Implementasi EditableCell dan EditableContext

Selanjutnya kita dapat memanfaatkan beberapa hooks di ReactJs pada file Context.tsx.

// 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>;
};

Terdapat 5 hooks yang saya gunakan untuk membuat aplikasi ini, yaitu:

  1. createContext digunakan untuk membuat objek konteks yang memungkinkan komponen-komponen dalam aplikasi React berbagi data tanpa perlu melewati prop dari komponen ke komponen. Pada kode diatas, saya menggunakannya pada variable EditableContext.
  2. useContext digunakan untuk mengakses data yang disediakan oleh objek konteks yang dibuat dengan createContext. Ini memungkinkan komponen untuk mengambil nilai dari konteks. Saya menempatkannya pada variable form untuk mengakses nilai dari EditableContext.
  3. useEffect digunakan untuk menangani side effect dalam komponen React. Pada contoh saya, digunakan untuk mengetahui perubahan di variable editing.
  4. useRef digunakan untuk membuat referensi ke elemen DOM atau nilai lain dalam komponen. Saya menggunakan useRef untuk mengakses dan memanipulasi elemen input secara langsung, triggernya jika ada perubahan pada variable editing.
  5. useState digunakan untuk membuat variabel state dalam komponen fungsional. Dengan useState, developer dapat menyimpan dan mengubah data yang memengaruhi tampilan komponen, dan setiap perubahan akan memicu rerender komponen.

Implementasi HomeTableContainer

Sekarang saya akan membuat HomeTableContainer, disini adalah kode logic yang akan digunakan di HomeTableView . Saya juga memanggil function EditableContext dan EditableCell dari file Context.tsx . Pada function handleValidate saya membuat kondisi batas usia yang dapat diinput oleh user, jika usia melebih 60 thn maka akan 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

Implementasi HomeTableView

Setelah membuat logic pada HomeTableContainer, di file HomeTableView saya mendefinisikan serta memodifikasi column dengan menempatkan function yang telah kita buat di file HomeTableContainer sehingga, baris pada column age dapat di edit.

// 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

Dibawah ini adalah hasil dari kode diatas, form input akan menampilkan error jika usia lebih dari 60thn.

image 2: table user

Semoga sharing saya kali ini dapat bermanfaat bagi teman-teman developer untuk membuat editable cell dan table menggunakan Antd-ReactJs. Jika kalian terinspirasi dan berfikir ini dapat diimplement di projek kalian, silahkan lakukan.

Feel free to ask questions

Terima kasih

--

--

bayusyaits
bayusyaits

Written by bayusyaits

Bayu a developer & designer web based in Jakarta, Indonesia. He liked photography & travel. Say hi, do not hesitate to contact him at bayusyaitsacc@gmail.com

No responses yet