Руководство по Drag And Drop в React
Интерфейс перетаскивания стал неотъемлемой частью большинства современных приложений. Он обеспечивает богатство пользовательского интерфейса, не затрагивая UX.
Есть много вариантов использования перетаскивания пользовательского интерфейса. Наиболее распространенные из них:
- Использование перетаскивания в браузере для загрузки файлов. Такие продукты, как Gmail, WordPress, Invision и т. д., имеют это в качестве одной из своих основных функций.
- Перемещение элементов между несколькими списками. Trello, Asana и многие другие продукты для повышения производительности имеют эту функцию.
- Перестановка изображений или ресурсов. Эта функция есть в большинстве видеоредакторов, а также в таких продуктах, как Invision, для перемещения элементов дизайна между разделами.
Сегодня мы увидим некоторые из этих вариантов использования перетаскивания, создав простой проект в React.
Наше простое приложение будет иметь следующие функции:
- Загрузка файлов изображений, перетаскивая файлы в браузере
- Показать предварительный просмотр этих изображений в виде сетки
- Измените порядок этих изображений, перетащив их
Давайте начнем с начальной загрузки приложения React, используя create-react-app
, например:
npx create-react-app logrocket-drag-and-drop cd logrocket-drag-and-drop yarn start
Загрузка файлов с помощью перетаскивания
Мы не собираемся изобретать велосипед, создавая всю логику и компоненты самостоятельно. Вместо этого мы будем использовать самые стандартные и известные библиотеки в нашем проекте.
Для функции загрузки с помощью drag and drop мы будем использовать одну из самых известных библиотек в React под названием react-dropzone
. Он имеет более 6 тысяч звезд на Github и поддерживает React Hooks. Вы можете прочитать документацию здесь . Это очень мощная библиотека, которая помогает создавать собственные компоненты в React.
Сначала установим его:
yarn add react-dropzone
После того, как мы установим это, давайте создадим новый файл с именем Dropzone.js
. Этот компонент отвечает за преобразование простой области содержимого в область перетаскивания, куда вы можете перетаскивать свои файлы.
Как react-dropzone
работает:
react-dropzone
скрывает ввод файла и показывает красивую пользовательскую область dropzone- Когда мы удаляем файлы,
react-dropzone
используютсяonDrag
события HTML и захватываются файлы из события в зависимости от того, были ли файлы удалены в области зоны сброса. - Если мы нажмем на область,
react-dropzone
библиотека инициирует диалог выбора файла через скрытый ввод с использованием Reactref
и позволит нам выбирать файлы и загружать их.
Давайте создадим наш компонент с именем Dropzone
:
/* filename: Dropzone.js */ import React from "react"; // Import the useDropzone hooks from react-dropzone import { useDropzone } from "react-dropzone"; const Dropzone = ({ onDrop, accept }) => { // Initializing useDropzone hooks with options const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, accept }); /* useDropzone hooks exposes two functions called getRootProps and getInputProps and also exposes isDragActive boolean */ return ( <div {...getRootProps()}> <input className="dropzone-input" {...getInputProps()} /> <div className="text-center"> {isDragActive ? ( <p className="dropzone-content">Release to drop the files here</p> ) : ( <p className="dropzone-content"> Drag 'n' drop some files here, or click to select files </p> )} </div> </div> ); }; export default Dropzone;
Давайте подробнее рассмотрим этот код.
useDropzone
предоставляет нам несколько методов и переменных для создания пользовательской области dropzone. Для нашего проекта нас в основном интересуют три разных свойства:
getRootProps
— это props, который будет установлен на основе родительского элемента области дропзоны. Таким образом, этот элемент определяет ширину и высоту области дропзоны.getInputProps
— это props, переданный элементу ввода. И это необходимо для того, чтобы мы могли поддерживать события щелчка вместе с событиями перетаскивания для получения файлов.- Все параметры, связанные с файлами, которые мы передаем,
useDropzone
будут установлены для этого элемента ввода. Например, если вы хотите поддерживать только отдельные файлы, вы можете передатьmultiple: false
. Это автоматически потребует,dropzone
чтобы только один файл был принят isDragActive
будет установлено, если файлы будут перетаскиваться над областью дропзоны. Будет очень полезно сделать стиль на основе этой переменной
Вот пример того, как установить имена стилей/классов на основе значения isDragActive
:
const getClassName = (className, isActive) => { if (!isActive) return className; return `${className} ${className}-active`; }; ... <div className={getClassName("dropzone", isDragActive)} {...getRootProps()}> ...
В нашем примере мы использовали только два props. Библиотека поддерживает множество props для настройки dropzone
области в соответствии с вашими потребностями.
Мы использовали accept
props, чтобы разрешать только файлы изображений. Наши App.js
должны выглядеть так:
/* filename: App.js */ import React, { useCallback } from "react"; // Import the dropzone component import Dropzone from "./Dropzone"; import "./App.css"; function App() { // onDrop function const onDrop = useCallback(acceptedFiles => { // this callback will be called after files get dropped, we will get the acceptedFiles. If you want, you can even access the rejected files too console.log(acceptedFiles); }, []); // We pass onDrop function and accept prop to the component. It will be used as initial params for useDropzone hook return ( <main className="App"> <h1 className="text-center">Drag and Drop Example</h1> <Dropzone onDrop={onDrop} accept={"image/*"} /> </main> ); } export default App;
Мы добавили dropzone
компонент на главную страницу. Теперь, если вы перетащите файлы, он будет утешать удаленные файлы изображений.
acceptedFiles
представляет собой массивFile
значений. Вы можете прочитать файл или отправить файл на сервер и загрузить. Какой бы процесс вы ни хотели сделать, вы можете сделать это там- Даже когда вы нажмете область и загрузите,
onDrop
будет вызван тот же обратный вызов accept
props принимает MIME-типы. Вы можете проверить документ для всех поддерживаемых типов mime. Он поддерживает все стандартные типы пантомимы, а также шаблоны сопоставления. Если вы хотите разрешить только pdf, тоaccept={'application/pdf'}
. Если вам нужен как тип изображения, так и pdf, то он поддерживаетaccept={'application/pdf, image/*'}
onDrop
функция заключена вuseCallback
. На данный момент мы не проводили никаких сложных вычислений и не отправляли файлы на сервер. Мы просто утешаемacceptedFiles
. Но позже мы прочитаем файлы и установим состояние для отображения изображений в браузере. Рекомендуется дляuseCallback
дорогих функций и во избежание ненужных повторных рендеров. В нашем примере это совершенно необязательно
Давайте прочитаем файлы изображений и добавим их в состояние в App.js
:
/* filename: App.js */ import React, { useCallback, useState } from "react"; // cuid is a simple library to generate unique IDs import cuid from "cuid"; function App() { // Create a state called images using useState hooks and pass the initial value as empty array const [images, setImages] = useState([]); const onDrop = useCallback(acceptedFiles => { // Loop through accepted files acceptedFiles.map(file => { // Initialize FileReader browser API const reader = new FileReader(); // onload callback gets called after the reader reads the file data reader.onload = function(e) { // add the image into the state. Since FileReader reading process is asynchronous, its better to get the latest snapshot state (i.e., prevState) and update it. setImages(prevState => [ ...prevState, { id: cuid(), src: e.target.result } ]); }; // Read the file as Data URL (since we accept only images) reader.readAsDataURL(file); return file; }); }, []); ... }
Структура данных нашего images
состояния:
const images = [ { id: 'abcd123', src: 'data:image/png;dkjds...', }, { id: 'zxy123456', src: 'data:image/png;sldklskd...', } ]
Давайте покажем предварительный просмотр изображений в виде сетки. Для этого мы собираемся создать еще один компонент с именем ImageList
.
import React from "react"; // Rendering individual images const Image = ({ image }) => { return ( <div className="file-item"> <img alt={`img - ${image.id}`} src={image.src} className="file-img" /> </div> ); }; // ImageList Component const ImageList = ({ images }) => { // render each image by calling Image component const renderImage = (image, index) => { return ( <Image image={image} key={`${image.id}-image`} /> ); }; // Return the list of files return <section className="file-list">{images.map(renderImage)}</section>; }; export default ImageList;
Теперь мы можем добавить этот компонент ImageList в App.js и показать предварительный просмотр изображений.
function App() { ... // Pass the images state to the ImageList component and the component will render the images return ( <main className="App"> <h1 className="text-center">Drag and Drop Example</h1> <Dropzone onDrop={onDrop} accept={"image/*"} /> <ImageList images={images} /> </main> ); }
Мы успешно выполнили половину нашей заявки. Мы сможем загружать файлы с помощью перетаскивания, а также сможем просматривать изображения в предварительном просмотре.
Далее мы разрешим переупорядочивать просматриваемые изображения с помощью функции перетаскивания. Прежде чем сделать это, мы увидим некоторые из различных библиотек, используемых для такого решения, и как выбрать одну из них в зависимости от потребностей нашего приложения.
Существует три различных пакета React, которые очень популярны для перетаскивания:
react-beautiful-dnd
, 30 тысяч звезд на Github (при поддержке Atlasssian)react-dnd
, 19 тысяч звезд на Githubreact-grid-layout
, 18 тысяч звезд на Github
Все они одинаково популярны среди разработчиков React, а также имеют активных участников, но у каждой библиотеки есть свои плюсы и минусы.
Мы составили список с указанием плюсов и минусов каждой библиотеки:
React beautiful DND
Плюсы
- Он очень хорошо работает для одномерного макета (например, списков), и если для перетаскивания требуется либо горизонтальное, либо вертикальное перемещение.
- Например, макет, похожий на Trello, список дел и т. д. будут работать из коробки с
react-beautiful-dnd
- API простой, любой может легко во всем разобраться. Опыт разработчиков действительно хорош и приятен с добавлением сложности в кодовую базу.
Минусы
react-beautiful-dnd
не работает для сеток, потому что вы перемещаете элементы во всех направлениях иreact-beautiful-dnd
не сможете вычислить позиции для осей x и y одновременно. Таким образом, при перетаскивании элементов в сетке ваш контент будет смещаться случайным образом, пока вы не перетащите элемент.
React grid layout
Плюсы
- Это работает для сеток. Сама сетка покрывает все, поэтому технически она работает и для одномерных движений.
- Он хорошо работает для сложных макетов сетки, которые требуют перетаскивания.
- Например, информационные панели с полной настройкой и изменением размера (т. е. средство просмотра, продукты для визуализации данных и т. д.).
- Это стоит сложности для крупномасштабных приложений
Минусы
- У него очень уродливый API — много вычислений приходится делать самостоятельно
- Вся структура макета должна быть определена в пользовательском интерфейсе через их компонентный API, и это создает дополнительный уровень сложности при создании динамических элементов на лету.
React DND
Плюсы
- Он работает почти для всех случаев использования (сетка, одномерные списки и т. д.).
- Он имеет очень мощный API для любой настройки с помощью перетаскивания.
Минусы
- API легко запустить для небольших примеров. Становится очень сложно добиться чего-то, если вашему приложению нужно что-то нестандартное. Кривая обучения выше и сложнее, чем у react-beautiful-dnd
- Нам нужно сделать много хаков для поддержки как веб-устройств, так и сенсорных устройств.
Для нашего варианта использования я выбираю react-dnd
. Я бы выбрал react-beautiful-dnd
, если бы наш макет включал только список элементов. Но в нашем примере у нас есть сетка изображений. Итак, следующий самый простой API для перетаскивания — это react-dnd
.
Перетаскивание списков с помощью React
Прежде чем мы углубимся в код перетаскивания, нам нужно сначала понять, как react-dnd
работает.
React DND может сделать любой элемент перетаскиваемым, а также сделать любой элемент перетаскиваемым. Чтобы добиться этого, у react dnd есть несколько допущений:
- Он должен иметь ссылки на все выпадающие предметы.
- Он должен иметь ссылки на все перетаскиваемые элементы.
- Все элементы, которые можно перетаскивать и удалять, должны быть заключены в
react-dnd
контекстный провайдер . Этот провайдер используется для инициализации, а также для управления внутренним состоянием.
Нам не нужно слишком беспокоиться о том, как он управляет состоянием. У него есть хороший и простой API для отображения этих состояний, мы можем вычислять и обновлять наши локальные состояния, используя его.
Давайте начнем с кода. Установите пакет:
yarn add react-dnd
Во-первых, мы поместим наш компонент ImageList внутри поставщика контекста DND, например:
/* filename: App.js */ import { DndProvider } from "react-dnd"; import HTML5Backend from "react-dnd-html5-backend"; function App() { ... return ( <main className="App"> ... <DndProvider backend={HTML5Backend}> <ImageList images={images} onUpdate={onUpdate} /> </DndProvider> </main> ); }
Это просто, мы просто импортируем DNDProvider
и инициализируем его с помощью внутренних props.
backend
– Как я упоминал ранее, это переменная, которая помогает выбрать, какой API использовать для перетаскивания.
Он поддерживает:
- API перетаскивания HTML5 (поддерживается только в Интернете, а не на сенсорных устройствах)
- Touch drag and drop API (поддерживается на сенсорных устройствах)
В настоящее время мы используем API HTML5 для начала работы, и как только функциональность будет реализована, мы напишем простую утилиту, которая также обеспечит базовую поддержку сенсорных устройств.
Теперь нам нужно добавить элементы как перетаскиваемые и сбрасываемые. В нашем приложении перетаскиваемые и выпадающие элементы одинаковы. Мы перетащим Image
компонент и поместим его на другой Image
компонент. Так что это немного облегчает нашу работу.
Давайте реализуем это, например:
import React, { useRef } from "react"; // import useDrag and useDrop hooks from react-dnd import { useDrag, useDrop } from "react-dnd"; const type = "Image"; // Need to pass which type element can be draggable, its a simple string or Symbol. This is like an Unique ID so that the library know what type of element is dragged or dropped on. const Image = ({ image, index }) => { const ref = useRef(null); // Initialize the reference // useDrop hook is responsible for handling whether any item gets hovered or dropped on the element const [, drop] = useDrop({ // Accept will make sure only these element type can be droppable on this element accept: type, hover(item) { ... } }); // useDrag will be responsible for making an element draggable. It also expose, isDragging method to add any styles while dragging const [{ isDragging }, drag] = useDrag({ // item denotes the element type, unique identifier (id) and the index (position) item: { type, id: image.id, index }, // collect method is like an event listener, it monitors whether the element is dragged and expose that information collect: monitor => ({ isDragging: monitor.isDragging() }) }); /* Initialize drag and drop into the element using its reference. Here we initialize both drag and drop on the same element (i.e., Image component) */ drag(drop(ref)); // Add the reference to the element return ( <div ref={ref} style={{ opacity: isDragging ? 0 : 1 }} className="file-item" > <img alt={`img - ${image.id}`} src={image.src} className="file-img" /> </div> ); }; const ImageList = ({ images }) => { ... }; export default ImageList;
Теперь наши изображения уже можно перетаскивать. Но если мы его уроним, то снова изображение вернется в исходное положение. Потому useDrag
и useDrop
будет справляться, пока не уроним. Если мы не изменим наше локальное состояние, оно снова вернется в исходное положение.
Чтобы обновить локальное состояние, нам нужно знать две вещи:
- перетаскиваемый элемент
- hovered element (элемент, на котором находится перетаскиваемый элемент)
useDrag
предоставляет эту информацию через hover
метод. Давайте посмотрим на это в нашем коде:
const [, drop] = useDrop({ accept: type, // This method is called when we hover over an element while dragging hover(item) { // item is the dragged element if (!ref.current) { return; } const dragIndex = item.index; // current element where the dragged element is hovered on const hoverIndex = index; // If the dragged element is hovered in the same place, then do nothing if (dragIndex === hoverIndex) { return; } // If it is dragged around other elements, then move the image and set the state with position changes moveImage(dragIndex, hoverIndex); /* Update the index for dragged item directly to avoid flickering when the image was half dragged into the next */ item.index = hoverIndex; } });
hover
метод будет запускаться всякий раз, когда элемент перетаскивается и наводится на этот элемент. Таким образом, когда мы начинаем перетаскивать элемент, мы получаем индекс этого элемента, а также элемент, на который мы наводим курсор. Мы пропустим это dragIndex
и hoverIndex
обновим состояние наших изображений.
Теперь у вас может возникнуть два вопроса:
- Зачем нам нужно обновлять состояние при наведении?
- Почему бы не обновить его при сбросе?
Можно просто обновить при сбросе. Тогда также будет работать перетаскивание и перестановка позиций. Но UX не будет хорошим.
Например, если вы перетащите одно изображение поверх другого изображения, если мы немедленно изменим положение, то это даст хорошую обратную связь пользователям, которые его перетаскивают. В противном случае они могут не знать, работает ли функция перетаскивания, пока не перетащите изображение в какое-либо положение.
Вот почему мы обновляем состояние при каждом наведении. Наводя курсор на другое изображение, мы устанавливаем состояние и меняем позиции. Пользователь увидит красивую анимацию. Вы можете проверить это на нашей демонстрационной странице.
Пока что мы просто показываем код для обновления состояния как moveImage
. Посмотрим на реализацию:
/* filename: App.js */ import update from "immutability-helper"; const moveImage = (dragIndex, hoverIndex) => { // Get the dragged element const draggedImage = images[dragIndex]; /* - copy the dragged image before hovered element (i.e., [hoverIndex, 0, draggedImage]) - remove the previous reference of dragged element (i.e., [dragIndex, 1]) - here we are using this update helper method from immutability-helper package */ setImages( update(images, { $splice: [[dragIndex, 1], [hoverIndex, 0, draggedImage]] }) ); }; // We will pass this function to ImageList and then to Image -> Quiet a bit of props drilling, the code can be refactored and place all the state management in ImageList itself to avoid props drilling. It's an exercise for you :)
Теперь наше приложение полностью функционально на onDrag
устройствах с поддержкой событий HTML5. Но, к сожалению, это не будет работать на сенсорных устройствах.
Как я уже говорил, мы можем поддерживать сенсорные устройства, а также использовать служебную функцию. Это не лучшее решение, но оно все еще работает. Тем не менее, опыт перетаскивания не будет хорошим на сенсорном устройстве. Он просто обновляется, но вы не почувствуете, что перетаскиваете. Также возможно сделать его чистым.
import HTML5Backend from "react-dnd-html5-backend"; import TouchBackend from "react-dnd-touch-backend"; // simple way to check whether the device support touch (it doesn't check all fallback, it supports only modern browsers) const isTouchDevice = () => { if ("ontouchstart" in window) { return true; } return false; }; // Assigning backend based on touch support on the device const backendForDND = isTouchDevice() ? TouchBackend : HTML5Backend; ... return ( ... <DndProvider backend={backendForDND}> <ImageList images={images} moveImage={moveImage} /> </DndProvider> ) ...
Заключение
Мы успешно создали небольшую и мощную демонстрацию для перетаскивания файлов, загрузки файлов, а также изменения порядка этих файлов.
Мы только что коснулись того, на что способен React с точки зрения функциональности перетаскивания. Мы можем создавать очень исчерпывающие функции, используя библиотеки перетаскивания. Надеемся, что это поможет вам быстрее и увереннее создать следующую функцию перетаскивания.