React-dnd
npm i react-dnd react-dnd-html5-backend
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
const DragAndDropContainer = () => {
return (
<section className={styles.app}>
<DndProvider backend={HTML5Backend}>
<article className={styles.element}>
<div className={styles.animals}>
{
elements.map( animal => <DraggableAnimal key={animal.id} data={animal}/>)
}
</div>
<DropTarget onDropHandler={handleDrop} >
{
draggedElements.map(item => (
<div key={item.id} className={styles.item}>
<span className={styles.animalItem}>
{item.content}
</span>
<p>
{item.description}
</p>
</div>
))
}
</DropTarget>
</article>
</DndProvider>
</section>
)
}
export default DragAndDropContainer
Работа с компонентами в react-dnd
происходит через два кастомных хука:
useDrag
— для перетаскиваемого элемента,useDrop
— для целевого элемента.
Перетаскивание элемента. Хук useDrag
useDrag
позволяет добавлять элементам функциональность перетаскивания. Хук возвращает массив из трёх значений:
CollectedProps
— объект, который предоставляет другим частям компонента доступ к вычислениям функцииcollect
внутри хукаuseDrag
;dragRef
— реф, который добавляется перетаскиваемому элементу или компоненту;dragPreviewRef
— реф, который указывается для элемента, используемого в качестве превью, или компонента во время перетаскивания.dragPreviewRef
использовать необязательно.
Сам хук принимает два параметра:
- объект или функцию;
- массив
deps
, который используется для мемоизации вычислений.
первый параметр содержит объект с уникальными для react-dnd
полями:
- Обязательное свойство
type
. Это строка, благодаря которой целевой элемент понимает, какие элементы в него можно перетащить. Это поле мы укажем и в хукеuseDrag
, и вuseDrop
. - Обязательное свойство
item
. Это данные о перетаскиваемом элементе. Они используются в обоих хуках —useDrag
иuseDrop
. Посколькуreact-dnd
используетHTML5 dnd api
в качестве провайдера — важно помещать вitem
только ключевую информацию о перетаскиваемом объекте. Почти всегда можно использовать только id конкретного элемента. - Необязательный метод
collect
. Об этой функции мы говорили выше — это набор вычислений для работы с пропсами. Метод принимает два параметра:monitor
и пропсы.
import React from "react";
import styles from "./draggable-animal.module.css";
import { useDrag } from "react-dnd";
const DraggableAnimal = ({data}) => {
const {id, content} = data;
const [, dragRef] = useDrag({
type: "animal",
item: {id}
});
return (
<div
ref={dragRef}
className={styles.animalElement}
>
{content}
</div>
)
;
};
export default DraggableAnimal
Для подсвечивания перетаскиваемого объекта можно использовать метод collect
с параметром monitor
. Параметр monitor
— собственная реализация хранилища в React dnd. В объекте monitor
11 методов, но мы разберём только основные из них:
canDrag
— возвращает булевое значениеtrue
в случае, если в этот момент никакой элемент не перетаскивается.isDragging
— возвращаетtrue
, если происходит перетаскивание.didDrop
— возвращаетtrue
, если на целевом элементе срабатывает событиеdrop
.getItemType
— возвращаетtype
перетаскиваемого элемента.getItem
— возвращает объектitem
. Это те данные, которые мы указали в ключеitem
хукаuseDrag
.
Например, мы можем полностью скрыть перетаскиваемый элемент из основного контейнера:
import React from "react";
import styles from "./draggable-animal.module.css";
import { useDrag } from "react-dnd";
const DraggableAnimal = ({data}) => {
const {id, content} = data;
const [{isDrag}, dragRef] = useDrag({
type: "animal",
item: {id},
collect: monitor => ({
isDrag: monitor.isDragging()
})
});
return (
!isDrag &&
<div
ref={dragRef}
className={styles.animalElement}
>
{content}
</div>
)
;
};
export default DraggableAnimal
Работа с целевым элементом. Хук useDrop
Этот хук похож на useDrag
структурой и методами объекта monitor
, но сам объект внутри хука несколько отличается. Разберём основные параметры и возвращаемые значения этого хука.
Хук возвращает массив из двух значений:
CollectedProps
— объект, который предоставляет другим частям компонента доступ к вычислениям функцииcollect
внутри хукаuseDrop
;DropTargetRef
— реф, который указывает на целевой элемент.
Внутри объекта хука useDrop
наиболее важны:
- Обязательное свойство
accept
. Его значение — строка, которая должна быть аналогична свойствуtype
перетаскиваемого компонента. - Необязательные методы
hover
иdrop
. Оба принимают в качестве параметраitem
перетаскиваемого компонента иmonitor
. Первый метод срабатывает, когда перетаскиваемый элемент попадает в зону целевого. Второй метод — при «броске» перетаскиваемого элемента в целевой. - Необязательный метод
collect
. Аналогичен методуcollect
хукаuseDrag
. Единственная разница — наличие методаisOver
и отсутствие методаisDragging
.
По аналогии с компонентом DraggableAnimal
напишем новый код в компоненте DragTarget
:
import React from 'react';
import styles from './drop-target.module.css'
import { useDrop } from "react-dnd";
const DropTarget = ({children, onDropHandler}) => {
const [, dropTarget] = useDrop({
accept: "animal",
drop(itemId) {
onDropHandler(itemId);
},
});
return (
<div
ref={dropTarget}
className={styles.target}
>
{children}
</div>
);
};
export default DropTarget;
В этом компоненте впервые появились наши вычисления. React-dnd берёт на себя всё, что касается событий dnd, но работу с вычислениями и изменением состояния должен описывать сам разработчик. Опишем изменения состояния в компоненте DragAndDropContainer
и передадим их компоненту DropTarget
в качестве пропса:
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
const DragAndDropContainer = () => {
const [elements, setElements] = React.useState([]);
const [draggedElements, setDraggedElements] = React.useState([]);
const handleDrop = (itemId) => {
setElements([
...elements.filter(element => element.id !== itemId.id)
]);
setDraggedElements([
...draggedElements,
...elements.filter(element => element.id === itemId.id)
]);
};
return (
// Другая возвращаемая разметка и компоненты
<DropTarget onDropHandler={handleDrop} />
)
}
export default DragAndDropContainer
Стоит подсказать пользователю, что элемент интерфейса можно перетащить в целевой элемент. Например, окрашивая рамку целевого элемента. Для этого можно пойти двумя путями: использовать метод hover
хука useDrop
или написать собственный метод внутри collect
. Воспользуемся вторым подходом: мы хотим получать значения из useDrop
в других местах. Обратимся к методу isOver
объекта monitor
, он ведёт себя схожим образом с hover
:
import React from 'react';
import styles from './drop-target.module.css'
import { useDrop } from "react-dnd";
const DropTarget = ({children, onDropHandler}) => {
const [{isHover}, dropTarget] = useDrop({
accept: "animal",
drop(itemId) {
onDropHandler(itemId);
},
collect: monitor => ({
isHover: monitor.isOver(),
})
});
const borderColor = isHover ? 'lightgreen' : 'transparent';
return (
<div
ref={dropTarget}
className={styles.target}
style={{borderColor}}
>
{children}
</div>
);
};
export default DropTarget;
EXAMPLE:
const [{opacity}, dragRef] = useDrag({
type: 'items',
item: {id},
collect: monitor => ({
opacity: monitor.isDragging() ? 0.5 : 1
})
});
return (
<div style={{opacity}} className={`${styles.product}`}>
<div ref={dragRef} className={styles.productBox}>
<img className={styles.img} src={src} alt="фото товара." />
<p className={styles.text}>{text}</p>
</div>
<div className={styles.amountbox}>
<AmountButton onClick={decrease}>-</AmountButton>
<p className={styles.amount}>{qty}</p>
<AmountButton onClick={increase}>+</AmountButton>
</div>
<div className={styles.price}>
<p className={`${styles.price} ${discount && styles.exPrice}`}>
{priceFormat(price * qty)}
</p>
{discount && <p className={styles.price}>{priceFormat(discountedPrice)}</p>}
</div>
<DeleteButton onDelete={onDelete} />
</div>
);
const [{isHover}, dropTarget] = useDrop({
accept: (tabName === 'items') ? 'items' : 'postponed',
collect: monitor => ({
isHover: monitor.isOver()
})
})
const className = `${styles.tab} ${currentTab === tabName ? styles.tab_type_current : ''} ${isHover && styles.onHover}`;
return (
<div ref={dropTarget} className={className} onClick={switchTab}>
{text}
</div>
);
};
#react