Предисловие.
В этой серии статей мы рассмотрим задачу разработки и тестирования сортируемых компонентов Drag-and-Drop. Существует множество сценариев использования drag-and-drop поведения, вот некоторые из них:
Загрузка или удаление файлов и изображений (самое банальное).
Сортируемые таблицы.
Сортируемые заметки или стикеры.
Переупорядочиваемые вкладки. Посмотрите на открытые вкладки вашего браузера, вы можете изменить их порядок с помощью перетаскивания.
Проверка капчи (соберите пазл).
Игры (шахматы и шашки).
В первой части статьи мы создадим небольшое подобие ресторана в стиле культового ситкома ALF, с функционалом перетаскивания блюд между столами посетителей. Вы можете попробовать работающее демо по следующей ссылке Demo.
Представим, что в нашем ресторане начинающий младший официант перепутал заказы посетителей и неправильно расставил все блюда, а наша задача — расставить все по своим местам. Для этого нам нужно переставить нужное блюдо и поставить его на соответствующий стол. Давайте поможем нашему официанту и наведем здесь порядок.
Используемые технологии и библиотеки:
Примечание:
Хотелось бы объяснить значение нескольких терминов, так как они могут использоваться в чистом виде:
drag
- Тащить, перетаскивать. То есть это перетаскиваемый элемент, что-то, что вы перетаскиваете из одного места в другое.
drop
- Бросить, падение. Это область куда перетаскиваемый (drag) элемент будет размещён. То есть область перетаскивания/падения.
Структура проекта.
Структура максимально простая и понятная, с абстрактными именами, чтобы не привязываться к какому-то конкретному варианту использования. Все используемые данные имитируемые, чтобы не отвлекаться на лишнюю реализацию. Стили используются для того, чтобы сделать вещи визуально более приятными, но они никак не влияют на проект, поэтому не сосредотачивайтесь на них. В некоторых примерах, необязательные части кода будут опущены с соответствующими комментариями:
// Omitted pieces of code.
Drag-and-Drop Provider.
Чтобы добавить функционал drag-and-drop в нашем приложении, мы должны обернуть необходимый вам компонент в DnD Provider вместе с переданным ему backend
props.
File: src/components/Container/Container.tsx
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { DropBoxContainer } from "../DnD/DropBox/DropBoxContainer";
// Omitted pieces of code.
<DndProvider backend={HTML5Backend}>
<DropBoxContainer selectionsData={MOCK_DATA} />
</DndProvider>
// Omitted pieces of code.
Drop Box container.
Следующим шагом будет создание DropBoxContainer
. Он отвечает за хранение данных в правильном порядке, сортировку и отображение компонентов DropBox
. В реальном проекте, хранение данных можно вынести на другой уровень, например React context или Redux store, так же как и функционал сортировки, который можно вынести в отдельные utils
файлы, но для текущей демонстрации, этого будет достаточно.
Внутри этого файла мы будем перебирать все данные (в нашем случае это столы с едой посетителей) и отображать каждый элемент как отдельный компонент DropBox
.
File: src/components/DnD/DropBox/DropBoxContainer.tsx
// Omitted pieces of code.
<div className={styles.container}>
{selections.map((item, index) => {
return (
<div className={styles.itemContainer}>
<DropBox
key={index}
index={index}
selection={selections[index]}
updateSelectionsOrder={updateSelectionsOrder}
/>
<Table />
</div>
);
})}
</div>
// Omitted pieces of code.
Drop Box item.
Двигаемся дальше, создайте файл DropBox
, который будет действовать как отдельный контейнер/коробка для каждого перетаскиваемого файла. Для более точного понимания попробую объяснить простыми словами на примере нашего приложения:
File: src/components/DnD/DropBox/DropBoxContainer.tsx
- Это как фуд-корт, место, где все посетители сидят за своими столиками. Это своего рода контейнер, внутри которого все происходит.File: src/components/DnD/DropBox/DropBox.tsx
- Это конкретный столик, за которым размещается посетитель и его еда. Это место, куда размещаются перетаскиваемые элементы.File: src/components/DnD/DragBox/DragBox.tsx
- Это тарелка с едой, или, другими словами, перетаскиваемый предмет, который нам нужно перетаскивать (перемещать) из одного столика на другой.
Компонент DropBox
принимает несколько props:
index
- который мы берем из функцииmap()
.selection
- это элемент данных, в нашем случае это блюдо.updateSelectionsOrder
- функция для обновления порядка элементов.
А вот тут начинается первая «магия», связанная с поведением перетаскивания.
File: src/components/DnD/DropBox/DropBox.tsx
// Omitted pieces of code.
const [isHovered, setIsHovered] = useState(false);
const [_, drop] = useDrop({
accept: [DragTypes.Card],
drop(item: DragItem) {
updateSelectionsOrder(item.index, index);
},
collect: (monitor) => {
if (monitor.isOver()) {
setIsHovered(true);
} else {
setIsHovered(false);
}
},
});
// Omitted pieces of code.
Как говорит нам документация о хуке useDrop:
useDrophook предоставляет вам возможность подключить ваш компонент к системе DnD в качестве цели для перетаскивания (drop target). Передав спецификацию в useDrophook, вы можете указать, какие типы элементов будет принимать данная цель, какие props собирать и многое другое. Эта функция возвращает массив, содержащий ref для присоединения к узлу Drop Target и собранные props.
Давайте рассмотрим всё по частям:
accept
: Обязательный. Строка, символ или массив любого из них. Указывает тип, на который будет реагировать конечная drop цель, она будет реагировать на элементы, созданные источниками drag, только указанного типа или типов. Проще говоря, если вы попытаетесь перетащить на drop элемент, тип который не указан в свойстве accept
, конечная drop цель не отреагирует на него и проигнорирует. Это предотвращает взаимодействие с нежелательными и не указанными элементами.
drop(item, monitor)
: Необязательный. Вызывается, когда совместимый drag элемент перетаскивается на drop цель. В нашем случае, когда мы берём блюдо и отпускаем его на стол, вызывается эта функция, и мы запускаем предоставленную функцию обратного вызова updateSelectionsOrder
, чтобы обновить расположение блюд.
collect
: Необязательный. Функция сбора. Она должна возвращать обычный объект props для инъекции в ваш компонент. Она получает два параметра, monitor
и props
. Мы используем ее с DropTargetMonitor, чтобы получить информацию о том, находится ли drag
операция в процессе выполнения. Это помогает применить некоторые визуальные эффекты, когда мы наводим тарелку на стол и рисуем пунктирную линию вокруг элементов.
Чтобы обозначить drop цель, мы присоединяем возвращаемое значение drop из хука useDrop
к участку drop цели в DOM.
File: src/components/DnD/DropBox/DropBox.tsx
// Omitted pieces of code.
return (
<div
className={clsx(styles.dropContainer, {
[styles.hovered]: isHovered,
})}
ref={drop}
>
<DragBox dragItem={selection} index={index} />
</div>
);
// Omitted pieces of code.
Drag Box item.
Наконец, мы достигли нашей цели и самой аппетитной части (по вкусу ALF). Рассмотрим наше блюдо DragBox
. Оно получает следующие props:
dragItem
: Данные перетаскиваемого элемента. Для данного приложения они были упрощены до свойств id
, name
и icon
.
index
: Индекс перетаскиваемого элемента, который будет использоваться для работы с поведением перетаскивания, которое мы рассмотрим далее.
File: src/components/DnD/DragBox/DragBox.tsx
// Omitted pieces of code.
const [{ isDragging }, drag] = useDrag({
type: DragTypes.Card,
item: { type: DragTypes.Card, id, index, name, icon },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
// Omitted pieces of code.
Чтобы подключить наш компонент в drag источника, мы используем хук useDrag. Здесь мы указываем type
, который мы обсуждали выше в хуке useDrop, item
- данные перетаскиваемого элемента, которые будут переданы в функцию drop
хука useDrop в компоненте DropBox
, и функцию collect
для получения информации о том, перетаскивается ли наш элемент в данный момент, для визуальных эффектов.
Чтобы обозначить перетаскиваемый элемент, мы присоединяем возвращаемое значение drag
из хука useDrag к перетаскиваемому участку DOM.
File: src/components/DnD/DragBox/DragBox.tsx
// Omitted pieces of code.
return (
<div
className={clsx(styles.container, {
[styles.dragging]: isDragging,
})}
ref={drag}
>
<img src={image} className={styles.icon} />
<p className={styles.name}>{name}</p>
</div>
);
// Omitted pieces of code.
Заключение.
Наконец, у нас есть рабочая версия приложения с сортируемыми перетаскиваемыми компонентами, которое, при необходимой адаптации, наверняка найдет свое применение в реальном мире. В этом примере мы рассмотрели лишь малую часть возможностей этой библиотеки, которых будет достаточно, чтобы начать создавать свою собственную реализацию. Для более глубокого понимания ознакомьтесь с документацией, в которой также есть отличный раздел Примеры, где можно найти полезные примеры использования. В следующей статье мы разработаем юнит тесты для этих компонентов и покроем ими основные случаи использования.