От автора: в этом посте я объясню основы использования библиотеки с открытым исходным кодом react-dnd (версия 14.0.2 на данный момент) и покажу несколько актуальных примеров кода.
Подготовка
Установите библиотеки react-dnd и react-dnd-html5-backend с помощью npm/yarn.
Оберните компонент-контейнер (тот, который отображает компоненты с помощью перетаскивания) DndProvider следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 |
import { DndProvider } from 'react-dnd' import { HTML5Backend } from 'react-dnd-html5-backend' export const App = () => { return ( <DndProvider backend={HTML5Backend}> {/* Here, render a component that uses DND inside it */} </DndProvider> ) } export default App |
Нам необходимо предоставить react-dnd бэкэнд, который инкапсулирует фактическое использование drag-and-drop API в браузере.
В основном вам понадобится только HTML5Backend для начинающих, хотя можно использовать другие drag-and-drop бэкэнды (например, touch для мобильных устройств).
Пример №1 — Перетаскивание элементов в единую область
Почти везде, где я смотрел, первый пример реализации перетаскивания, показывали, как сделать много элементов перетаскиваемыми в одну назначенную область.
Хотя это не то требование, которое я имел в виду, я быстро понял, что это самое основное и легко объяснимое для новичков в DND. Вот почему я счел важным объяснить это:
Drag
Это код для компонента PetCard (перетаскиваемого элемента):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import React from 'react' import { useDrag } from 'react-dnd' export const PetCard = ({ id, name }) => { const [{ isDragging }, dragRef] = useDrag({ type: 'pet', item: { id, name }, collect: (monitor) => ({ isDragging: monitor.isDragging() }) }) return ( <div className='pet-card' ref={dragRef}> {name} {isDragging && 'Oops'} </div> ) } |
Давайте рассмотрим этот пример. Чтобы любой элемент React можно было перетаскивать, нам нужно установить ему свойство ref, которое будет возвращать dragRef из хука useDrag (который мы импортировали из react-dnd).
Это важная часть того, как мы обеспечиваем библиотеку элементом, который нужно перетащить.
useDrag получает входной объект. Объект должен включать обязательное свойство type — строку, которую мы определяем. Позже это станет актуальным, когда мы создадим область перетаскивания (чтобы принять тип перетаскивания).
Следующее свойство — item. Мы используем его для передачи данных, которые нам понадобятся позже для области перетаскивания. Нам нужны только необходимые данные для визуализации представления элемента в области перетаскивания (в данном случае это объект Pet: id и name).
Затем у нас есть необязательное свойство с именем collect. Это полезно для многих целей. Это функция, которая получает объект monitor, предоставленный нам react-dnd, который содержит состояние и метаданные действия перетаскивания.
В нашем случае мы используем его для извлечения isDragging — логического значения, которое мы можем использовать для рендеринга симпатичного смайлика на перетаскиваемой карте. Для получения полной информации об API monitor я рекомендую ознакомиться с документацией.
Drop
Это код компонента Basket (область перетаскивания):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import React, { useState } from 'react' import { useDrop } from 'react-dnd'; import { PetCard } from './PetCard'; const PETS = [ { id: 1, name: 'dog' }, { id: 2, name: 'cat' }, { id: 3, name: 'fish' }, { id: 4, name: 'hamster' }, ] export const Basket = () => { const [basket, setBasket] = useState([]) const [{ isOver }, dropRef] = useDrop({ accept: 'pet', drop: (item) => setBasket((basket) => !basket.includes(item) ? [...basket, item] : basket), collect: (monitor) => ({ isOver: monitor.isOver() }) }) return ( <React.Fragment> <div className='pets'> {PETS.map(pet => <PetCard draggable id={pet.id} name={pet.name} />)} </div> <div className='basket' ref={dropRef}> {basket.map(pet => <PetCard id={pet.id} name={pet.name} />)} {isOver && <div>Drop Here!</div>} </div> </React.Fragment> ) } |
Прежде всего, нам нужен useState чтобы сохранить массив элементов, брошенных в область перетаскивания (корзину). Мы сопоставляем каждый объект Pet в корзине с элементом PetCard, который будет отображаться внутри корзины.
Вдобавок, как и раньше, чтобы сказать react-dnd, что элемент является областью перетаскивания, нам нужно снабдить его свойством ref. На этот раз dropRef возвращается из хука useDrop (в нашем примере это второй div, который мы рендерим внизу).
Хук useDrop получает объект с обязательным свойством accept, соответствующее свойство type мы передаем в useDrag заранее. Область перетаскивания будет реагировать только на перетаскивание этого типа.
Тогда у нас есть свойство drop. Это функция обратного вызова, которая срабатывает при каждом нажатии. Она получает данные, которые мы передали в свойстве item от useDrag. В нашем примере мы берем данные Pet и добавляем их в массив basket (только если его еще нет). Обратите внимание, что мы не изменяем массив basket. Мы возвращаем его новый экземпляр с добавленным элементом Pet.
Наконец, у нас есть еще одна функция collect, аналогичная той, которую мы использовали раньше. На этот раз мы используем ее для извлечения логического значения isOver, которое указывает, находится ли перетаскиваемый элемент в настоящее время над нашей областью перетаскивания. С его помощью мы визуализируем уведомление «Drop Here» внутри.
Пример # 2 — Перетаскивание элементов списка и изменения их порядка
Этот пример немного сложнее. Но на этот раз это действительно соответствует требованию, которое я имел в виду при изучении данной библиотеки:
Обратите внимание на то, что каждый элемент списка является одновременно перетаскиваемым и местом размещения. Это очень важно, потому что это означает, что каждый компонент элемента должен реализовывать useDrag и useDrop хуки, и объединять их в один ref. Это код для ListItem:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
import React, { useRef } from 'react' import { useDrag, useDrop } from 'react-dnd' import { listItemStyle as style } from './style' export const ListItem = ({ text, index, moveListItem }) => { // useDrag - the list item is draggable const [{ isDragging }, dragRef] = useDrag({ type: 'item', item: { index }, collect: (monitor) => ({ isDragging: monitor.isDragging(), }), }) // useDrop - the list item is also a drop area const [spec, dropRef] = useDrop({ accept: 'item', hover: (item, monitor) => { const dragIndex = item.index const hoverIndex = index const hoverBoundingRect = ref.current?.getBoundingClientRect() const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 const hoverActualY = monitor.getClientOffset().y - hoverBoundingRect.top // if dragging down, continue only when hover is smaller than middle Y if (dragIndex < hoverIndex && hoverActualY < hoverMiddleY) return // if dragging up, continue only when hover is bigger than middle Y if (dragIndex > hoverIndex && hoverActualY > hoverMiddleY) return moveListItem(dragIndex, hoverIndex) item.index = hoverIndex }, }) // Join the 2 refs together into one (both draggable and can be dropped on) const ref = useRef(null) const dragDropRef = dragRef(dropRef(ref)) // Make items being dragged transparent, so it's easier to see where we drop them const opacity = isDragging ? 0 : 1 return ( <div ref={dragDropRef} style={{ ...style, opacity }}> {text} </div> ) } |
Этот компонент получает следующие свойства:
text — Текст, отображаемый элементом списка.
index — Индекс элемента в списке.
moveListItem — Обратный вызов, который будет выполняться, когда нам нужно, чтобы этот элемент переместился в другой индекс (что происходит, когда мы проводим курсор мимо него, как показано на гифке).
Для useDragу нас в тот же входной объект, что и в первом примере, только на этот раз данные, которые мы передаем (свойство item), являются просто индексом.
Интересная часть — это входной объект, который мы передаем в useDrop. В частности – функция hover. Функция hover срабатывает каждый раз, когда перетаскиваемость парит над зоной падения. Мы используем ее в примере, чтобы перемещать элементы вверх и вниз, когда перетаскиваемый курсор перемещается вокруг элементов списка, всегда оставляя место для бросания элемента.
Реализация может показаться сложной на первый взгляд, но все, что она делает, это перемещает элемент вверх или вниз, когда курсор мыши выходит за среднюю ось Y элемента (как видно на гифке выше).
Затем все, что осталось, — это объединить две ссылки в одну и отрендерить элемент так, как нам заблагорассудится. Это код для компонента List, который отображает все элементы списка:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import React, { useState, useCallback } from 'react' import { ListItem } from './ListItem' import { listStyle as style } from './style' const PETS = [ { id: 1, name: 'dog' }, { id: 2, name: 'cat' }, { id: 3, name: 'fish' }, { id: 4, name: 'hamster' }, ] export const List = () => { const [pets, setPets] = useState(PETS) const movePetListItem = useCallback( (dragIndex, hoverIndex) => { const dragItem = pets[dragIndex] const hoverItem = pets[hoverIndex] // Swap places of dragItem and hoverItem in the pets array setPets(pets => { const updatedPets = [...pets] updatedPets[dragIndex] = hoverItem updatedPets[hoverIndex] = dragItem return updatedPets }) }, [pets], ) return ( <div style={style}>{pets.map((pet, index) => ( <ListItem key={pet.id} index={index} text={pet.name} moveListItem={movePetListItem} /> ))} </div> ) } |
По большей части это просто контейнер, и мы не делаем здесь никаких вещей, связанных с DND. Единственное, что стоит упомянуть, — это обратный вызов movePetListItem, который мы передаем ListItem. Он получает два индекса и меняет позицию элемента в массиве pets с одного на другой (с dragIndex на hoverIndex).
Еще примеры
Есть еще много вариантов использования drag-and-drop, которые могут вас заинтересовать, но я не буду упоминать о них здесь. Вы можете найти много примеров и реализаций в коде и документации react-dnd.
Резюме
Я решил написать этот пост после того, как узнал, что пользоваться библиотекой react-dnd оказалось не так просто, как я ожидал вначале. Я надеюсь, что с помощью этого поста я облегчил вам задачу самостоятельно заняться изучением этой библиотеки.
Автор: Liad Shiran
Источник: medium.com
Редакция: Команда webformyself.
Читайте нас в Telegram, VK, Яндекс.Дзен