React
April 11, 2022

Осваиваем react-beautiful-dnd (часть 1)

Всем привет!
По итогом опроса в телеграм канале я поняла, что вам будет полезна статья по настройке и использованию библиотеки react-beautiful-dnd. Именно поэтому в этой статье мы начнем с вами создавать свой небольшой проект канбан-доски, а в ходе работы и узнаем тонкости этой библиотеки!

Давайте начнем наше знакомство с библиотекой react-beautiful-dnd.

Создание проекта

Так как мы будем разбирать эту библиотеку на практике, то давайте для начала создадим наше приложение:

npx create-react-app dnd-project
cd dnd-project

Теперь можем запустить наше приложение и увидеть стандартную заставку react:

npm start

Теперь давайте почистим наш проект, оставив в файле index.js:

import React from 'react';
import ReactDOM from 'react-dom';

const App = () => <p>Hello, world!</p>

ReactDOM.render(<App />, document.getElementById('root'));

Остальные файлы в папке src можно удалить, они нам не нужны.

Создадим данные для отображения

Давайте создадим файл initial-data.js и заполним его информацией:

export const initialData = {
    tasks: {
        'task-1': { id: 'task-1', content: 'Some text 1'},
        'task-2': { id: 'task-2', content: 'Some text 2'},
        'task-3': { id: 'task-3', content: 'Some text 3'},
        'task-4': { id: 'task-4', content: 'Some text 4'},
        'task-5': { id: 'task-5', content: 'Some text 5'},
        'task-6': { id: 'task-6', content: 'Some text 6'}
    },
    columns: {
        'column-1': {
            id: 'column-1',
            title: 'Надо сделать',
            taskIds: ['task-1', 'task-4', 'task-5']
        },
    },

    columnOrder: ['column-1']
}

Дальше мы создадим больше колонок, но пока нам нужна только одна.

Теперь нам нужно добавить эти данные в начальный стейт нашего компонента приложения:

import React from 'react';
import ReactDOM from 'react-dom';
import { initialData } from './initial-data';

const App = () => {
  const [data, _] = React.useState(initialData);

  return (
    <React.Fragment>
      {
        data.columnOrder.map(item => {
          const column = data.columns[item];
          const tasks = column.taskIds.map(taskId => data.tasks[taskId]);

          return column.title;
        })
      }
    </React.Fragment>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

Как видите тут мы просто добавили наши данные в компонент App и вызвали его отрисовку, исходя из очередности колонок. initialData поместили в стейт компонента в качестве начального значения.

Структура приложения и простые компоненты

Давайте не будет создавать код-лапшу и выделим компонент колонки в отдельный файл Column.jsx. Даже с учетом того, что это приложение не будет особо большим предлагаю сделать ему нормальную структуру, поэтому я создам папку components/Column, а в него добавлю файл Column.jsx.

В файл Column.jsx поместим простой код отображения колонки:

import React from 'react';

export const Column = ({tasks, ...props}) => {
    const {id, title, taskIds} = props.columnData;

    return <React.Fragment>{title}</React.Fragment>;
}

А в файле index.js воспользуемся этим компонентом:

import React from 'react';
import ReactDOM from 'react-dom';
import { Column } from './components/Column/Column';
import { initialData } from './initial-data';

const App = () => {
  const [data, _] = React.useState(initialData);

  return (
    <React.Fragment>
      {
        data.columnOrder.map(item => {
          const column = data.columns[item];
          const tasks = column.taskIds.map(taskId => data.tasks[taskId]);

          return <Column key={column.id} data={column} tasks={tasks} />;
        })
      }
    </React.Fragment>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

Я не хочу разводить много css файлов, так что давайте подключим styled-components:

npm i styled-components --save-dev

Еще добавим небольшую утилитку для обнуления css стилей:

npm i reset-css --save-dev

Как только в index.js добавим строчку import 'reset-css'; можно будет заметить, что все лишние отступы на странице пропали.

Теперь нужно немного украсить наши колонки, поэтому давайте доработаем компонент Column:

import React from 'react';
import styled from 'styled-components';

const Container = styled.div`
    margin: 12px;
    padding: 4px;
    border: 1px solid lightgray;
    border-radius: 4px;
`;
const Title = styled.h2`
    padding: 8px;
`;
const TaskList = styled.div`
    padding: 8px;
`;

export const Column = ({tasks, ...props}) => {
    const {id, title, taskIds} = props.columnData;

    return (
        <Container>
            <Title>{title}</Title>
            <TaskList>TaskList</TaskList>
        </Container>
    );
}

Если откроете браузер это будет выглядеть примерно вот так:

Дальше давайте разберемся с отрисовкой задач в колонке, для этого тоже создадим отдельный компонент src/components/Task/Task.jsx:

import React from 'react';
import styled from 'styled-components';

const Container = styled.div`
    padding: 8px;
    border: 1px solid lightgray;
    border-radius: 4px;
    margin-bottom: 8px;
`;

export const Task = ({ ...props }) => {
    const { content } = props.data;

    return <Container>{content}</Container>;
}

Ну и в компонента Column вызовем создание компонентов Task:

import React from 'react';
import styled from 'styled-components';
import { Task } from '../Task/Task';

const Container = styled.div`
    margin: 12px;
    padding: 4px;
    border: 1px solid lightgray;
    border-radius: 4px;
`;

const Title = styled.h2`
    padding: 8px;
`;

const TaskList = styled.div`
    padding: 8px;
`;

export const Column = ({tasks, ...props}) => {
    const {id, title, taskIds} = props.columnData;

    return (
        <Container>
            <Title>{title}</Title>
            <TaskList>
                {tasks.map(task => <Task key={task.id} data={task} />)}
            </TaskList>
        </Container>
    );
}

После этого в браузере должен быть вот такой результат:

Добавим react-beautiful-dnd

Пришло время нам установить библиотеку react-beautiful-dnd:

npm i react-beautiful-dnd --save-dev

Давайте посмотрим как выглядят основные компоненты react-beautiful-dnd:

Если говорить просто, то эти компоненты отвечают за следующее:

  • DragDropContext - оборачиваем в этот компонент ту часть приложения, в которой хотим реализовать перетаскивания
  • Droppable - компонент, который определяет те места, в которые можно перетягивать и дропать другие элементы
  • Draggable - те элементы, которые можно перетягивать и дропать

Изменения порядка элементов списка

Начнем мы с того, что обернем в DragDropContext все то, что лежит в нашем компоненте App. Но для начала давайте поймем какие пропсы нужно и можно передавать в DragDropContext:

  • onDragStart - колбек на момент, когда перетаскивание только началось
  • onDragUpdate - колбек, на изменение при перетаскивании
  • onDragEnd - колбек на окончание перетаскивания

Обязательным из всех этих пропсов является тольк onDragEnd, поэтому давайте уже обернем наш компонент и напишем этот колбек:

import React from 'react';
import ReactDOM from 'react-dom';
import { Column } from './components/Column/Column';
import { initialData } from './initial-data';
import { DragDropContext } from 'react-beautiful-dnd';
import 'reset-css';

const App = () => {
  const [data, _] = React.useState(initialData);

  const handleDragEnd = result => {
    // TODO: обработать изменение порядка элементов в колонке
  };

  return (
    <DragDropContext
      onDragEnd={handleDragEnd}
    >
      {
        data.columnOrder.map(item => {
          const column = data.columns[item];
          const tasks = column.taskIds.map(taskId => data.tasks[taskId]);

          return <Column key={column.id} columnData={column} tasks={tasks} />;
        })
      }
    </DragDropContext>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

Теперь нам надо перескочить к компоненту Column и добавить в нем Droppable:

import React from 'react';
import styled from 'styled-components';
import { Task } from '../Task/Task';
import { Droppable } from 'react-beautiful-dnd';

const Container = styled.div`
    margin: 12px;
    padding: 4px;
    border: 1px solid lightgray;
    border-radius: 4px;
`;

const Title = styled.h2`
    padding: 8px;
`;

const TaskList = styled.div`
    padding: 8px;
`;

export const Column = ({tasks, ...props}) => {
    const {id, title} = props.columnData;

    return (
        <Container>
            <Title>{title}</Title>
            <Droppable droppableId={id}>
                {(provided) => (
                    <TaskList
                        ref={provided.innerRef}
                        {...provided.droppableProps}
                    >
                        {tasks.map((task, idx) => <Task key={task.id} taskData={task} index={idx} />)}
                        {provided.placeholder}
                    </TaskList>
                )}
            </Droppable>
        </Container>
    );
}

Как вы можете заметить, Droppable - принимает в качестве ребенка не просто реакт элемент, а функцию, которая возвращает реакт элемент, эта библиотека может себе позволить так делать, так как приложения не потеряют из-за этого в производительности, потому что react-beautiful-dnd не работает напрямую с DOM деревом.

В качестве параметра в эту функцию попадает provided, он дает нам возможность получить необходимые пропсы состояния для области драга, а так же такую штуку как placeholder - для добавления дополнительного пространства при драге в эту область.

Остался последний шаг, давайте добавим Draggable к нашему компоненту Task:

import React from 'react';
import styled from 'styled-components';
import { Draggable } from 'react-beautiful-dnd';

const Container = styled.div`
    padding: 8px;
    border: 1px solid lightgray;
    border-radius: 4px;
    margin-bottom: 8px;
    backgroud-color: white;
`;

export const Task = ({ index, ...props }) => {
    const { id, content } = props.taskData;

    return (
        <Draggable draggableId={id} index={index}>
            {(provided) => (
                <Container
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                    ref={provided.innerRef}
                >
                    {content}
                </Container>
            )}
        </Draggable>
    );
}

В качестве дочернего элемент Draggable так же принимает функцию.

  • draggableProps - дает нам необходимые пропсы для элемента, который будут перетягивать.
  • dragHandleProps - дает нам необходимые пропсы для того кусочка элемента, за который можно тянуть (в нашем случае это весь элемент).

Давайте посмотрим что у нас получилось:

Как видите красивый драг у нас работает, только вот состояние не запоминается.

Запоминаем состояние драга

Для того, чтобы сохранять состояние при драге нам надо доделать функцию handleDragEnd в нашем файле index.js, в качестве параметра в эту функцию прилетать объект вот такого типа:

{
    draggableId: 'task-1',
    type: 'TYPE',
    reason: 'DROP',
    source: {
        droppableId: 'column-1',
        index: 0
    },
    destination: {
        droppableId: 'column-1',
        index: 1
    },
}

На основе этих данных давайте доделаем нашу функцию:

  const handleDragEnd = result => {
    const {destination, source, draggableId } = result;

    // Если объект перетащиши за область, в которую можно дропать
    if (!destination) {
      return;
    }

    // Если объект перетащили в то же самое место
    if (destination.droppableId === source.droppableId 
        && destination.index === source.index) {
          return;
    }

    const column = data.columns[source.droppableId];
    const newTaskIds = Array.from(column.taskIds);

    newTaskIds.splice(source.index, 1);
    newTaskIds.splice(destination.index, 0, draggableId);

    const newColumn = {
      ...column,
      taskIds: newTaskIds,
    };


    setData((prevData) => ({
      ...prevData, 
      columns: {
        ...prevData.columns,
        [newColumn.id]: newColumn,
      }
    }));
  };

После добавление такой обработки конца драга, у нас получается работающее приложение:

Итоги

Вот мы с вами и разобрали создание самого простого перетаскивания при помощи библиотеки react-beautiful-dnd, надеюсь вам это было полезно, я планирую дальше выпустить еще пару статей, где мы разберем более сложные кейсы использования этой библиотеки!

Подпишитесь на телеграм канал, чтобы не пропустить выход новых интересных заметок