В этом посте мы сделаем простую редактируемую таблицу (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
. Когда таблица рендерится мы проверяем если этот стейт не пустой — выводим из него данные, если же пустой тогда данные изrowsState
defaultValue={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 для каждого элемента.
Добавить комментарий