Уроки по веб-разработке

Редактируемая таблица на ReactJS

В этом посте мы сделаем простую редактируемую таблицу (editable table). За основу возьмем библиотеку React Bootstrap v2.0.2, чтобы не тратить время на оформление.

Таблица будет выглядеть вот так:

Custom Editable Table ReactJS

Чтобы увидеть «живой» пример, можете перейти на 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();

Далее идут обработчики событий:

Дальше мы рендерим таблицу. Столбцы берутся из 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 для каждого элемента.