В этом посте мы сделаем простую редактируемую таблицу (editable table). За основу возьмем библиотеку React Bootstrap v2.0.2, чтобы не тратить время на оформление.
Таблица будет выглядеть вот так:
Чтобы увидеть «живой» пример, можете перейти на CodeSandBox.
Чтобы установить все зависимости из скачанного проекта, зайдите в папку с проектом и из командной строки запустите:
npm install
Теперь давайте разберем, что у нас есть в этом проекте. У нас есть папка components, в которой как раз и будет компонент нашей таблицы. В файле App.jsx мы будем выводить таблицу и передавать нужные props.
Содержимое файла App.js:
import React from 'react';
import EditableTable from "./components/EditableTable";
function App() {
const columns = [
{ field: 'id', fieldName: '#' },
{ field: 'firstName', fieldName: 'First Name' },
{ field: 'lastName', fieldName: 'Last Name' },
{ field: 'role', fieldName: 'User\'s role' },
];
const data = [
{ id: 1, firstName: 'John', lastName: 'Doe', role: 'Admin' },
{ id: 2, firstName: 'John', lastName: 'Smith', role: 'Editor' }
];
return (
<>
<EditableTable columns={columns} rows={data} actions />
</>
);
}
export default App;
Здесь мы определяем columns и data, чтобы потом передать в наш компонент. Также эти данные вы могли бы получать с сервера.
Дальше в папке components -> EditableTable у нас есть 3 файла:
- Файл со стилями
- Файл самого компонента
- И индексный файл. В нем мы просто делаем экспорт нашего компонента.
Содержимое файла EditableTable.jsx:
import React, { useState } from 'react';
import { Form, Table } from "react-bootstrap";
import { PencilFill, Save, Trash, XSquare } from 'react-bootstrap-icons';
import './EditableTable.styles.scss';
const EditableTable = ({ columns, rows, actions }) => {
const [isEditMode, setIsEditMode] = useState(false);
const [rowIDToEdit, setRowIDToEdit] = useState(undefined);
const [rowsState, setRowsState] = useState(rows);
const [editedRow, setEditedRow] = useState();
const handleEdit = (rowID) => {
setIsEditMode(true);
setEditedRow(undefined);
setRowIDToEdit(rowID);
}
const handleRemoveRow = (rowID) => {
const newData = rowsState.filter(row => {
return row.id !== rowID ? row : null
});
setRowsState(newData);
}
const handleOnChangeField = (e, rowID) => {
const { name: fieldName, value } = e.target;
setEditedRow({
id: rowID,
[fieldName]: value
})
}
const handleCancelEditing = () => {
setIsEditMode(false);
setEditedRow(undefined);
}
const handleSaveRowChanges = () => {
setTimeout(() => {
setIsEditMode(false);
const newData = rowsState.map(row => {
if (row.id === editedRow.id) {
if (editedRow.firstName) row.firstName = editedRow.firstName;
if (editedRow.lastName) row.lastName = editedRow.lastName;
if (editedRow.role) row.role = editedRow.role;
}
return row;
})
setRowsState(newData);
setEditedRow(undefined)
}, 1000)
}
return (
<Table striped bordered hover>
<thead>
<tr>
{columns.map((column) => {
return <th key={column.field}>{ column.fieldName }</th>
})}
</tr>
</thead>
<tbody>
{rowsState.map((row) => {
return <tr key={row.id}>
<td>
{row.id}
</td>
<td>
{ isEditMode && rowIDToEdit === row.id
? <Form.Control
type='text'
defaultValue={editedRow ? editedRow.firstName : row.firstName}
id={row.id}
name='firstName'
onChange={ (e) => handleOnChangeField(e, row.id) }
/>
: row.firstName
}
</td>
<td>
{ isEditMode && rowIDToEdit === row.id
? <Form.Control
type='text'
defaultValue={editedRow ? editedRow.lastName : row.lastName}
id={row.id}
name='lastName'
onChange={ (e) => handleOnChangeField(e, row.id) }
/>
: row.lastName
}
</td>
<td>
{ isEditMode && rowIDToEdit === row.id
? <Form.Select onChange={e => handleOnChangeField(e, row.id)} name="role" defaultValue={row.role}>
<option value='Admin'>Admin</option>
<option value='Editor'>Editor</option>
<option value='Subscriber'>Subscriber</option>
</Form.Select>
: row.role
}
</td>
{actions &&
<td>
{ isEditMode && rowIDToEdit === row.id
? <button onClick={ () => handleSaveRowChanges() } className='custom-table__action-btn' disabled={!editedRow}>
<Save />
</button>
: <button onClick={ () => handleEdit(row.id) } className='custom-table__action-btn'>
<PencilFill />
</button>
}
{ isEditMode && rowIDToEdit === row.id
? <button onClick={() => handleCancelEditing()} className='custom-table__action-btn'>
<XSquare />
</button>
: <button onClick={() => handleRemoveRow(row.id)} className='custom-table__action-btn'>
<Trash />
</button>
}
</td>
}
</tr>
})}
</tbody>
</Table>
);
};
export default EditableTable;
Первое что есть в компоненте, это определение состояния
const [isEditMode, setIsEditMode] = useState(false); const [rowIDToEdit, setRowIDToEdit] = useState(undefined); const [rowsState, setRowsState] = useState(rows); const [editedRow, setEditedRow] = useState();
- isEditMode — нужно для определения находимся ли мы сейчас в режиме редактирования.
- rowIDToEdit — тут хранится id ряда, который мы редактируем
- rowState — все данные нашей таблицы, которые мы получаем через пропсы. Нужны для того, чтобы во время редактирования мы смогли обновлять их через
setRowsState - editedRow — это новые данные, после того как пользователь начал менять что-то в таблице.
Далее идут обработчики событий:
- handleEdit — после клика на кнопку редактирования, при помощи этого обработчика мы меняем переменную состояния
isEditModeнаtrue, сбрасываем данные вeditedRowдля того, чтобы во время редактирования одной строки, если пользователь нажмет на другую строку, данные не перепутались. - handleRemoveRow — обработчик удаления строки. Тут мы методом
filterпроходимся по нашему массиву данных и просто удаляем ненужную строку - handleOnChangeField — эта функция отвечает за обработку данных, когда пользователь меняет что-то в инпутах. Мы получаем новые значения и сохраняем их в state
editedRow. Когда таблица рендерится мы проверяем если этот стейт не пустой — выводим из него данные, если же пустой тогда данные изrowsStatedefaultValue={editedRow ? editedRow.firstName : row.firstName} - handleCancelEditing — отменяет edit mode и сбрасывает данные в
editedRow - handleSaveRowChanges — при клике на кнопку сохранить, нам нужно забрать данные из
editedRowи поменять их вrowsState. С помощью методаmapмы проходимся по массиву и еслиidстрок в обоих переменных совпадают, тогда меняем данные. Затем обновляем state c помощьюsetRowsStateи сбрасываем значение дляeditedRow
Дальше мы рендерим таблицу. Столбцы берутся из columns — это props, который мы получали в компоненте App.
Для отрисовки строк, мы пробегаемся по rowsState методом map. В зависимости от режима, т.е. isEditMode === true нам нужно отрисовывать или просто ячейки таблицы, или инпуты
<td>
{ isEditMode && rowIDToEdit === row.id
? <Form.Control
type='text'
defaultValue={editedRow ? editedRow.firstName : row.firstName}
id={row.id}
name='firstName'
onChange={ (e) => handleOnChangeField(e, row.id) }
/>
: row.firstName
}
</td>
Вот собственно и всё… Один только момент. Не забывайте, что когда отрисовываете какие-то повторяющиеся данные в цикле (в нашем случае строки и столбцы с помощью метода map) вам нужно обязательно указывать key для каждого элемента.